From f89486f5fe364e493845c09abac5e5a206b32b3c Mon Sep 17 00:00:00 2001 From: Jack Date: Fri, 24 Jun 2016 13:12:27 -0400 Subject: [PATCH 01/14] port and import name --- lbrynet/core/LBRYcrdWallet.py | 4 ++-- lbrynet/lbrynet_daemon/LBRYDaemon.py | 2 +- lbrynet/lbrynet_daemon/LBRYUIManager.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lbrynet/core/LBRYcrdWallet.py b/lbrynet/core/LBRYcrdWallet.py index 17fd0e229..8807e757c 100644 --- a/lbrynet/core/LBRYcrdWallet.py +++ b/lbrynet/core/LBRYcrdWallet.py @@ -634,7 +634,7 @@ class LBRYcrdWallet(LBRYWallet): def _get_rpc_conf(self): settings = {"username": "rpcuser", "password": "rpcpassword", - "rpc_port": 8332} + "rpc_port": 9245} if os.path.exists(self.wallet_conf): conf = open(self.wallet_conf) for l in conf: @@ -846,7 +846,7 @@ class LBRYcrdWallet(LBRYWallet): @_catch_connection_error def _get_nametrie_rpc(self): rpc_conn = self._get_rpc_conn() - return rpc_conn.getnametrie() + return rpc_conn.getclaimtrie() @_catch_connection_error def _get_wallet_balance_rpc(self): diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index ac157bd71..aa47f021d 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -24,7 +24,7 @@ from appdirs import user_data_dir from urllib2 import urlopen from lbrynet import __version__ as lbrynet_version -from lbryum.version import ELECTRUM_VERSION as lbryum_version +from lbryum.version import LBRYUM_VERSION as lbryum_version from lbrynet.core.PaymentRateManager import PaymentRateManager from lbrynet.core.server.BlobAvailabilityHandler import BlobAvailabilityHandlerFactory from lbrynet.core.server.BlobRequestHandler import BlobRequestHandlerFactory diff --git a/lbrynet/lbrynet_daemon/LBRYUIManager.py b/lbrynet/lbrynet_daemon/LBRYUIManager.py index e7c405352..ce36aaf01 100644 --- a/lbrynet/lbrynet_daemon/LBRYUIManager.py +++ b/lbrynet/lbrynet_daemon/LBRYUIManager.py @@ -10,7 +10,7 @@ from twisted.web import static from twisted.internet import defer from lbrynet.conf import DEFAULT_UI_BRANCH, LOG_FILE_NAME from lbrynet import __version__ as lbrynet_version -from lbryum.version import ELECTRUM_VERSION as lbryum_version +from lbryum.version import LBRYUM_VERSION as lbryum_version from zipfile import ZipFile from appdirs import user_data_dir From 7cc3e9d5eff93765395b26e2a19bad7e00b4226e Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 27 Jun 2016 17:07:59 -0400 Subject: [PATCH 02/14] publish updates -adds a base set of metadata fields required for results to be rendered in conf.py, including language and content-type -dont support old style claims on the new blockchain --- lbrynet/__init__.py | 2 +- lbrynet/conf.py | 12 ++-- lbrynet/core/LBRYcrdWallet.py | 88 ++++++++++-------------- lbrynet/lbrynet_daemon/LBRYDaemon.py | 62 ++++++----------- lbrynet/lbrynet_daemon/LBRYDownloader.py | 31 ++++----- lbrynet/lbrynet_daemon/LBRYPublisher.py | 53 ++++++-------- 6 files changed, 98 insertions(+), 150 deletions(-) diff --git a/lbrynet/__init__.py b/lbrynet/__init__.py index 7bb1777c8..655afaa08 100644 --- a/lbrynet/__init__.py +++ b/lbrynet/__init__.py @@ -4,5 +4,5 @@ log = logging.getLogger(__name__) logging.getLogger(__name__).addHandler(logging.NullHandler()) log.setLevel(logging.ERROR) -version = (0, 2, 5) +version = (0, 2, 6) __version__ = ".".join([str(x) for x in version]) diff --git a/lbrynet/conf.py b/lbrynet/conf.py index 116a395f2..9ead15f4d 100644 --- a/lbrynet/conf.py +++ b/lbrynet/conf.py @@ -17,9 +17,9 @@ MIN_VALUABLE_BLOB_HASH_PAYMENT_RATE = .05 # points/1000 infos MAX_CONNECTIONS_PER_STREAM = 5 KNOWN_DHT_NODES = [('104.236.42.182', 4000), - ('lbryum1.lbry.io', 4444), - ('lbryum2.lbry.io', 4444), - ('lbryum3.lbry.io', 4444)] + ('lbrynet1.lbry.io', 4444), + ('lbrynet2.lbry.io', 4444), + ('lbrynet3.lbry.io', 4444)] POINTTRADER_SERVER = 'http://ec2-54-187-192-68.us-west-2.compute.amazonaws.com:2424' #POINTTRADER_SERVER = 'http://127.0.0.1:2424' @@ -35,7 +35,7 @@ API_PORT = 5279 ICON_PATH = "app.icns" APP_NAME = "LBRY" API_CONNECTION_STRING = "http://%s:%i/%s" % (API_INTERFACE, API_PORT, API_ADDRESS) -UI_ADDRESS = "http://" + API_INTERFACE + ":" + str(API_PORT) +UI_ADDRESS = "http://%s:%i" % (API_INTERFACE, API_PORT) PROTOCOL_PREFIX = "lbry" DEFAULT_WALLET = "lbryum" @@ -46,3 +46,7 @@ DEFAULT_MAX_KEY_FEE = 100.0 DEFAULT_SEARCH_TIMEOUT = 3.0 DEFAULT_CACHE_TIME = 3600 DEFAULT_UI_BRANCH = "master" + +SOURCE_TYPES = ['lbry_sd_hash', 'url', 'btih'] +BASE_METADATA_FIELDS = ['title', 'description', 'author', 'language', 'license', 'content-type'] +OPTIONAL_METADATA_FIELDS = ['thumbnail', 'preview', 'fee', 'contact', 'pubkey'] \ No newline at end of file diff --git a/lbrynet/core/LBRYcrdWallet.py b/lbrynet/core/LBRYcrdWallet.py index 8807e757c..92f8d1fdf 100644 --- a/lbrynet/core/LBRYcrdWallet.py +++ b/lbrynet/core/LBRYcrdWallet.py @@ -4,6 +4,7 @@ from lbrynet.core.client.ClientRequest import ClientRequest from lbrynet.core.Error import UnknownNameError, InvalidStreamInfoError, RequestCanceledError from lbrynet.core.Error import InsufficientFundsError from lbrynet.core.sqlite_helpers import rerun_if_locked +from lbrynet.conf import BASE_METADATA_FIELDS, SOURCE_TYPES, OPTIONAL_METADATA_FIELDS from lbryum import SimpleConfig, Network from lbryum.bitcoin import COIN, TYPE_ADDRESS @@ -318,68 +319,51 @@ class LBRYWallet(object): r_dict = {} if 'value' in result: value = result['value'] + try: value_dict = json.loads(value) except (ValueError, TypeError): return Failure(InvalidStreamInfoError(name)) - known_fields = ['stream_hash', 'name', 'description', 'key_fee', 'key_fee_address', 'thumbnail', - 'content_license', 'sources', 'fee', 'author'] - known_sources = ['lbry_sd_hash', 'btih', 'url'] - known_fee_types = {'LBC': ['amount', 'address']} - for field in known_fields: - if field in value_dict: - if field == 'sources': - for source in known_sources: - if source in value_dict[field]: - if source == 'lbry_sd_hash': - r_dict['stream_hash'] = value_dict[field][source] - else: - r_dict[source] = value_dict[field][source] - elif field == 'fee': - fee = value_dict['fee'] - if 'type' in fee: - if fee['type'] in known_fee_types: - fee_fields = known_fee_types[fee['type']] - if all([f in fee for f in fee_fields]): - r_dict['key_fee'] = fee['amount'] - r_dict['key_fee_address'] = fee['address'] - else: - for f in ['key_fee', 'key_fee_address']: - if f in r_dict: - del r_dict[f] - else: - r_dict[field] = value_dict[field] - if 'stream_hash' in r_dict and 'txid' in result: - d = self._save_name_metadata(name, r_dict['stream_hash'], str(result['txid'])) - else: - d = defer.succeed(True) + r_dict['sources'] = value_dict['sources'] + for field in BASE_METADATA_FIELDS: + r_dict[field] = value_dict[field] + for field in value_dict: + if field in OPTIONAL_METADATA_FIELDS: + r_dict[field] = value_dict[field] + + if 'txid' in result: + d = self._save_name_metadata(name, r_dict['sources']['lbry_sd_hash'], str(result['txid'])) d.addCallback(lambda _: r_dict) return d elif 'error' in result: log.warning("Got an error looking up a name: %s", result['error']) - return Failure(UnknownNameError(name)) + return Failure(UnknownNameError(name)) + else: + log.warning("Got an error looking up a name: %s", json.dumps(result)) + return Failure(UnknownNameError(name)) - def claim_name(self, name, sd_hash, amount, description=None, key_fee=None, - key_fee_address=None, thumbnail=None, content_license=None, author=None, sources=None): - value = {"sources": {'lbry_sd_hash': sd_hash}} - if description is not None: - value['description'] = description - if key_fee is not None and key_fee_address is not None: - value['fee'] = {'type': 'LBC', 'amount': key_fee, 'address': key_fee_address} - if thumbnail is not None: - value['thumbnail'] = thumbnail - if content_license is not None: - value['content_license'] = content_license - if author is not None: - value['author'] = author - if isinstance(sources, dict): - sources['lbry_sd_hash'] = sd_hash - value['sources'] = sources + def claim_name(self, name, bid, sources, metadata, fee=None): + value = {'sources': {}} + for k in SOURCE_TYPES: + if k in sources: + value['sources'][k] = sources[k] + if value['sources'] == {}: + return defer.fail("No source given") + for k in BASE_METADATA_FIELDS: + if k not in metadata: + return defer.fail("Missing required field '%s'" % k) + value[k] = metadata[k] + for k in metadata: + if k not in BASE_METADATA_FIELDS: + value[k] = metadata[k] + if fee is not None: + if "LBC" in fee: + value['fee'] = {'LBC': {'amount': fee['LBC']['amount'], 'address': fee['LBC']['address']}} - d = self._send_name_claim(name, json.dumps(value), amount) + d = self._send_name_claim(name, json.dumps(value), bid) def _save_metadata(txid): - d = self._save_name_metadata(name, sd_hash, txid) + d = self._save_name_metadata(name, value['sources']['lbry_sd_hash'], txid) d.addCallback(lambda _: txid) return d @@ -409,7 +393,7 @@ class LBRYWallet(object): def abandon(results): if results[0][0] and results[1][0]: address = results[0][1] - amount = results[1][1] + amount = float(results[1][1]) return self._send_abandon(txid, address, amount) elif results[0][0] is False: return defer.fail(Failure(ValueError("Couldn't get a new address"))) @@ -826,7 +810,7 @@ class LBRYcrdWallet(LBRYWallet): @_catch_connection_error def _send_abandon_rpc(self, txid, address, amount): rpc_conn = self._get_rpc_conn() - return rpc_conn.abandonname(txid, address, amount) + return rpc_conn.abandonclaim(txid, address, amount) @_catch_connection_error def _get_blockchain_info_rpc(self): diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index aa47f021d..0a90bd92b 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -1151,12 +1151,11 @@ class LBRYDaemon(jsonrpc.JSONRPC): def _add_key_fee(data_cost): d = self._resolve_name(name) - d.addCallback(lambda info: data_cost + info['key_fee'] if 'key_fee' in info.keys() else data_cost) + d.addCallback(lambda info: data_cost if 'fee' not in info else data_cost + info['fee']['LBC']['amount']) return d d = self._resolve_name(name) - d.addCallback(lambda info: info['stream_hash'] if isinstance(info['stream_hash'], str) - else info['stream_hash']['sd_hash']) + d.addCallback(lambda info: info['sources']['lbry_sd_hash']) d.addCallback(lambda sd_hash: download_sd_blob(self.session, sd_hash, self.blob_request_payment_rate_manager)) d.addCallback(self.sd_identifier.get_metadata_for_sd_blob) @@ -1170,10 +1169,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): def _get_lbry_file_by_uri(self, name): def _get_file(stream_info): - if isinstance(stream_info['stream_hash'], str) or isinstance(stream_info['stream_hash'], unicode): - sd = stream_info['stream_hash'] - elif isinstance(stream_info['stream_hash'], dict): - sd = stream_info['stream_hash']['sd_hash'] + sd = stream_info['sources']['lbry_sd_hash'] for l in self.lbry_file_manager.lbry_files: if l.sd_hash == sd: @@ -1791,15 +1787,13 @@ class LBRYDaemon(jsonrpc.JSONRPC): for r in results: t = {} t.update(r[0]) - if 'name' in r[1].keys(): - r[1]['stream_name'] = r[1]['name'] - del r[1]['name'] + if not 'thumbnail' in r[1].keys(): + r[1]['thumbnail'] = "img/Free-speech-flag.svg" t.update(r[1]) t['cost_est'] = r[2] - if not 'thumbnail' in t.keys(): - t['thumbnail'] = "img/Free-speech-flag.svg" consolidated_results.append(t) # log.info(str(t)) + return consolidated_results log.info('[' + str(datetime.now()) + '] Search nametrie: ' + search) @@ -1845,46 +1839,30 @@ class LBRYDaemon(jsonrpc.JSONRPC): def jsonrpc_publish(self, p): """ - Make a new name claim + Make a new name claim and publish associated data to lbrynet Args: 'name': name to be claimed, string 'file_path': path to file to be associated with name, string 'bid': amount of credits to commit in this claim, float - optional 'author': author, string - optional 'title': title, description - optional 'description': content description, string - optional 'thumbnail': thumbnail image url - optional 'key_fee': key fee to be paid to publisher, float (default 0.0) - optional 'key_fee_address': address for key fee to be sent to, string (defaults on new address) - optional 'content_license': content license string - optional 'sources': alternative sources dict, keys 'lbry_sd_hash', 'btih', 'url' + 'metadata': metadata dictionary + optional 'fee' Returns: - Confirmation message + Claim txid """ - - metadata_fields = ["name", "file_path", "bid", "author", "title", - "description", "thumbnail", "key_fee", "key_fee_address", - "content_license", "sources"] - - for k in metadata_fields: - if k not in p.keys(): - p[k] = None + # start(self, name, file_path, bid, metadata, fee=None, sources=None): + name = p['name'] + bid = p['bid'] + file_path = p['file_path'] + metadata = p['metadata'] + if 'fee' in p: + fee = p['fee'] + else: + fee = None pub = Publisher(self.session, self.lbry_file_manager, self.session.wallet) - d = pub.start(p['name'], - p['file_path'], - p['bid'], - title=p['title'], - description=p['description'], - thumbnail=p['thumbnail'], - key_fee=p['key_fee'], - key_fee_address=p['key_fee_address'], - content_license=p['content_license'], - author=p['author'], - sources=p['sources']) - + d = pub.start(name, file_path, bid, metadata, fee) d.addCallbacks(lambda msg: self._render_response(msg, OK_CODE), lambda err: self._render_response(err.getTraceback(), BAD_REQUEST)) diff --git a/lbrynet/lbrynet_daemon/LBRYDownloader.py b/lbrynet/lbrynet_daemon/LBRYDownloader.py index a134c44e5..ba59227c6 100644 --- a/lbrynet/lbrynet_daemon/LBRYDownloader.py +++ b/lbrynet/lbrynet_daemon/LBRYDownloader.py @@ -42,15 +42,14 @@ log.addHandler(handler) log.setLevel(logging.INFO) class GetStream(object): - def __init__(self, sd_identifier, session, wallet, lbry_file_manager, max_key_fee, pay_key=True, data_rate=0.5, - timeout=DEFAULT_TIMEOUT, download_directory=None, file_name=None): + def __init__(self, sd_identifier, session, wallet, lbry_file_manager, max_key_fee, data_rate=0.5, + timeout=DEFAULT_TIMEOUT, download_directory=None, file_name=None): self.wallet = wallet self.resolved_name = None self.description = None self.key_fee = None self.key_fee_address = None self.data_rate = data_rate - self.pay_key = pay_key self.name = None self.file_name = file_name self.session = session @@ -79,7 +78,7 @@ class GetStream(object): self.finished.callback((self.stream_hash, self.download_path)) elif self.timeout_counter >= self.timeout: - log.info("Timeout downloading lbry://" + self.resolved_name + ", " + str(self.stream_info)) + log.info("Timeout downloading lbry://%s" % self.resolved_name) self.checker.stop() self.d.cancel() self.code = STREAM_STAGES[4] @@ -88,28 +87,24 @@ class GetStream(object): def start(self, stream_info, name): self.resolved_name = name self.stream_info = stream_info - if 'stream_hash' in self.stream_info.keys(): - self.stream_hash = self.stream_info['stream_hash'] - elif 'sources' in self.stream_info.keys(): + if 'sources' in self.stream_info: self.stream_hash = self.stream_info['sources']['lbry_sd_hash'] else: raise InvalidStreamInfoError(self.stream_info) - if 'description' in self.stream_info.keys(): + if 'description' in self.stream_info: self.description = self.stream_info['description'] - if 'key_fee' in self.stream_info.keys(): - self.key_fee = float(self.stream_info['key_fee']) - if 'key_fee_address' in self.stream_info.keys(): - self.key_fee_address = self.stream_info['key_fee_address'] + if 'fee' in self.stream_info: + if 'LBC' in self.stream_info['fee']: + self.key_fee = float(self.stream_info['fee']['LBC']['amount']) + self.key_fee_address = self.stream_info['fee']['LBC']['address'] else: self.key_fee_address = None else: self.key_fee = None self.key_fee_address = None if self.key_fee > self.max_key_fee: - if self.pay_key: - log.info("Key fee (" + str(self.key_fee) + ") above limit of " + str( - self.max_key_fee) + ", didn't download lbry://" + str(self.resolved_name)) - return defer.fail(None) + log.info("Key fee %f above limit of %f didn't download lbry://%s" % (self.key_fee, self.max_key_fee, self.resolved_name)) + return defer.fail(None) else: pass @@ -145,7 +140,7 @@ class GetStream(object): reserved_points = self.wallet.reserve_points(self.key_fee_address, self.key_fee) if reserved_points is None: return defer.fail(InsufficientFundsError()) - log.info("Key fee: " + str(self.key_fee) + " | " + str(self.key_fee_address)) + log.info("Key fee: %f --> %s" % (self.key_fee, self.key_fee_address)) return self.wallet.send_points_to_address(reserved_points, self.key_fee) return defer.succeed(None) @@ -155,5 +150,5 @@ class GetStream(object): d = defer.Deferred() self.downloader = downloader self.download_path = os.path.join(downloader.download_directory, downloader.file_name) - d.addCallback(lambda _: log.info("[" + str(datetime.now()) + "] Downloading " + str(self.stream_hash) + " --> " + str(self.download_path))) + d.addCallback(lambda _: log.info("[%s] Downloading %s --> %s" % (datetime.now(), self.stream_hash, self.file_name))) d.addCallback(lambda _: self.downloader.start()) diff --git a/lbrynet/lbrynet_daemon/LBRYPublisher.py b/lbrynet/lbrynet_daemon/LBRYPublisher.py index f7fd64f57..084731a87 100644 --- a/lbrynet/lbrynet_daemon/LBRYPublisher.py +++ b/lbrynet/lbrynet_daemon/LBRYPublisher.py @@ -1,4 +1,5 @@ import logging +import mimetypes import os import sys @@ -36,42 +37,27 @@ class Publisher(object): self.received_file_name = False self.file_path = None self.file_name = None - self.thumbnail = None - self.title = None self.publish_name = None self.bid_amount = None - self.key_fee = None - self.key_fee_address = None - self.key_fee_address_chosen = False - self.description = None self.verified = False self.lbry_file = None - self.sd_hash = None - self.tx_hash = None - self.content_license = None - self.author = None - self.sources = None + self.txid = None + self.sources = {} + self.fee = None - def start(self, name, file_path, bid, title=None, description=None, thumbnail=None, - key_fee=None, key_fee_address=None, content_license=None, author=None, sources=None): + def start(self, name, file_path, bid, metadata, fee=None, sources={}): def _show_result(): - message = "[" + str(datetime.now()) + "] Published " + self.file_name + " --> lbry://" + \ - str(self.publish_name) + " with txid: " + str(self.tx_hash) + + message = "[%s] Published %s --> lbry://%s txid: %s" % (datetime.now(), self.file_name, self.publish_name, self.txid) log.info(message) - return defer.succeed(self.tx_hash) + return defer.succeed(self.txid) self.publish_name = name self.file_path = file_path self.bid_amount = bid - self.title = title - self.description = description - self.thumbnail = thumbnail - self.key_fee = key_fee - self.key_fee_address = key_fee_address - self.content_license = content_license - self.author = author - self.sources = sources + self.fee = fee + self.metadata = metadata d = self._check_file_path(self.file_path) d.addCallback(lambda _: create_lbry_file(self.session, self.lbry_file_manager, @@ -118,20 +104,21 @@ class Publisher(object): self.lbry_file.stream_hash) def set_sd_hash(sd_hash): - self.sd_hash = sd_hash + self.sources['lbry_sd_hash'] = sd_hash d.addCallback(set_sd_hash) return d def _claim_name(self): - d = self.wallet.claim_name(self.publish_name, self.sd_hash, self.bid_amount, - description=self.description, key_fee=self.key_fee, - key_fee_address=self.key_fee_address, thumbnail=self.thumbnail, - content_license=self.content_license, author=self.author, - sources=self.sources) - - def set_tx_hash(tx_hash): - self.tx_hash = tx_hash + self.metadata['content-type'] = mimetypes.guess_type(os.path.join(self.lbry_file.download_directory, + self.lbry_file.file_name))[0] + d = self.wallet.claim_name(self.publish_name, + self.bid_amount, + self.sources, + self.metadata, + fee=self.fee) + def set_tx_hash(txid): + self.txid = txid d.addCallback(set_tx_hash) return d From cc5d917c722adfead1056e16e8512ab6c1244501 Mon Sep 17 00:00:00 2001 From: Jack Date: Tue, 28 Jun 2016 01:51:05 -0400 Subject: [PATCH 03/14] download fix --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 21 ++++++++++++--------- lbrynet/lbrynet_daemon/LBRYDownloader.py | 14 ++++++-------- lbrynet/lbrynet_daemon/LBRYUIManager.py | 2 +- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index 0a90bd92b..6bb957ff0 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -24,7 +24,7 @@ from appdirs import user_data_dir from urllib2 import urlopen from lbrynet import __version__ as lbrynet_version -from lbryum.version import LBRYUM_VERSION as lbryum_version +from lbryum.version import ELECTRUM_VERSION as lbryum_version from lbrynet.core.PaymentRateManager import PaymentRateManager from lbrynet.core.server.BlobAvailabilityHandler import BlobAvailabilityHandlerFactory from lbrynet.core.server.BlobRequestHandler import BlobRequestHandlerFactory @@ -398,7 +398,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): def setup(self, branch=DEFAULT_UI_BRANCH, user_specified=False, branch_specified=False): def _log_starting_vals(): d = self._get_lbry_files() - d.addCallback(lambda r: json.dumps([d[1] for d in r])) + d.addCallback(lambda r: json.dumps([d[1] if not isinstance(d[1], UnknownNameError) else {'error': 'Pending claim'} for d in r])) d.addCallback(lambda r: log.info("LBRY Files: " + r)) d.addCallback(lambda _: log.info("Starting balance: " + str(self.session.wallet.wallet_balance))) return d @@ -1038,12 +1038,15 @@ class LBRYDaemon(jsonrpc.JSONRPC): def _get_stream(stream_info): def _wait_for_write(): - if os.path.isfile(os.path.join(self.download_directory, self.streams[name].downloader.file_name)): - written_file = file(os.path.join(self.download_directory, self.streams[name].downloader.file_name)) - written_file.seek(0, os.SEEK_END) - written_bytes = written_file.tell() - written_file.close() - else: + try: + if os.path.isfile(os.path.join(self.download_directory, self.streams[name].downloader.file_name)): + written_file = file(os.path.join(self.download_directory, self.streams[name].downloader.file_name)) + written_file.seek(0, os.SEEK_END) + written_bytes = written_file.tell() + written_file.close() + else: + written_bytes = False + except: written_bytes = False if not written_bytes: @@ -2238,4 +2241,4 @@ class LBRYDaemon(jsonrpc.JSONRPC): d = threads.deferToThread(subprocess.Popen, ['xdg-open', '-R', path]) d.addCallback(lambda _: self._render_response(True, OK_CODE)) - return d \ No newline at end of file + return d diff --git a/lbrynet/lbrynet_daemon/LBRYDownloader.py b/lbrynet/lbrynet_daemon/LBRYDownloader.py index ba59227c6..c72444c70 100644 --- a/lbrynet/lbrynet_daemon/LBRYDownloader.py +++ b/lbrynet/lbrynet_daemon/LBRYDownloader.py @@ -112,6 +112,7 @@ class GetStream(object): self.timeout_counter = self.timeout * 2 def _set_status(x, status): + log.info("Download lbry://%s status changed to %s" % (self.resolved_name, status)) self.code = next(s for s in STREAM_STAGES if s[0] == status) return x @@ -121,9 +122,8 @@ class GetStream(object): self.d.addCallback(lambda _: download_sd_blob(self.session, self.stream_hash, self.payment_rate_manager)) self.d.addCallback(self.sd_identifier.get_metadata_for_sd_blob) self.d.addCallback(lambda r: _set_status(r, DOWNLOAD_RUNNING_CODE)) - self.d.addCallback(lambda metadata: ( - next(factory for factory in metadata.factories if isinstance(factory, ManagedLBRYFileDownloaderFactory)), - metadata)) + self.d.addCallback(lambda metadata: (next(factory for factory in metadata.factories if isinstance(factory, ManagedLBRYFileDownloaderFactory)), + metadata)) self.d.addCallback(lambda (factory, metadata): factory.make_downloader(metadata, [self.data_rate, True], self.payment_rate_manager, @@ -144,11 +144,9 @@ class GetStream(object): return self.wallet.send_points_to_address(reserved_points, self.key_fee) return defer.succeed(None) - if self.pay_key: - d = _pay_key_fee() - else: - d = defer.Deferred() + d = _pay_key_fee() self.downloader = downloader self.download_path = os.path.join(downloader.download_directory, downloader.file_name) - d.addCallback(lambda _: log.info("[%s] Downloading %s --> %s" % (datetime.now(), self.stream_hash, self.file_name))) + d.addCallback(lambda _: log.info("[%s] Downloading %s --> %s" % (datetime.now(), self.stream_hash, self.downloader.file_name))) d.addCallback(lambda _: self.downloader.start()) + diff --git a/lbrynet/lbrynet_daemon/LBRYUIManager.py b/lbrynet/lbrynet_daemon/LBRYUIManager.py index ce36aaf01..e7c405352 100644 --- a/lbrynet/lbrynet_daemon/LBRYUIManager.py +++ b/lbrynet/lbrynet_daemon/LBRYUIManager.py @@ -10,7 +10,7 @@ from twisted.web import static from twisted.internet import defer from lbrynet.conf import DEFAULT_UI_BRANCH, LOG_FILE_NAME from lbrynet import __version__ as lbrynet_version -from lbryum.version import LBRYUM_VERSION as lbryum_version +from lbryum.version import ELECTRUM_VERSION as lbryum_version from zipfile import ZipFile from appdirs import user_data_dir From 09d8a86e30b16b7722a1aeefafa97bd400a86bae Mon Sep 17 00:00:00 2001 From: Jack Date: Tue, 28 Jun 2016 02:06:04 -0400 Subject: [PATCH 04/14] rename variable --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 2 +- lbrynet/lbrynet_daemon/LBRYUIManager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index 6bb957ff0..f35b5f089 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -24,7 +24,7 @@ from appdirs import user_data_dir from urllib2 import urlopen from lbrynet import __version__ as lbrynet_version -from lbryum.version import ELECTRUM_VERSION as lbryum_version +from lbryum.version import LBRYUM_VERSION as lbryum_version from lbrynet.core.PaymentRateManager import PaymentRateManager from lbrynet.core.server.BlobAvailabilityHandler import BlobAvailabilityHandlerFactory from lbrynet.core.server.BlobRequestHandler import BlobRequestHandlerFactory diff --git a/lbrynet/lbrynet_daemon/LBRYUIManager.py b/lbrynet/lbrynet_daemon/LBRYUIManager.py index e7c405352..ce36aaf01 100644 --- a/lbrynet/lbrynet_daemon/LBRYUIManager.py +++ b/lbrynet/lbrynet_daemon/LBRYUIManager.py @@ -10,7 +10,7 @@ from twisted.web import static from twisted.internet import defer from lbrynet.conf import DEFAULT_UI_BRANCH, LOG_FILE_NAME from lbrynet import __version__ as lbrynet_version -from lbryum.version import ELECTRUM_VERSION as lbryum_version +from lbryum.version import LBRYUM_VERSION as lbryum_version from zipfile import ZipFile from appdirs import user_data_dir From b3b804bf2ddc9fa9de1db96b6beacc55a4562ee2 Mon Sep 17 00:00:00 2001 From: Jack Date: Tue, 28 Jun 2016 14:28:59 -0400 Subject: [PATCH 05/14] add update_name --- lbrynet/core/LBRYcrdWallet.py | 18 +++++--- lbrynet/lbrynet_daemon/LBRYDaemon.py | 61 +++++++++++++++------------- 2 files changed, 45 insertions(+), 34 deletions(-) diff --git a/lbrynet/core/LBRYcrdWallet.py b/lbrynet/core/LBRYcrdWallet.py index 92f8d1fdf..0d1d2dc65 100644 --- a/lbrynet/core/LBRYcrdWallet.py +++ b/lbrynet/core/LBRYcrdWallet.py @@ -408,8 +408,10 @@ class LBRYWallet(object): d.addCallback(self._get_decoded_tx) return d - # def update_name(self, name_value): - # return self._update_name(name_value) + def update_name(self, name, value, amount): + d = self._get_value_for_name(name) + d.addCallback(lambda r: self._update_name(r['txid'], json.dumps(value), amount)) + return d def get_name_and_validity_for_sd_hash(self, sd_hash): d = self._get_claim_metadata_for_sd_hash(sd_hash) @@ -565,6 +567,9 @@ class LBRYWallet(object): def _send_abandon(self, txid, address, amount): return defer.fail(NotImplementedError()) + def _update_name(self, txid, value, amount): + return defer.fail(NotImplementedError()) + def _do_send_many(self, payments_to_send): return defer.fail(NotImplementedError()) @@ -697,6 +702,9 @@ class LBRYcrdWallet(LBRYWallet): def _send_abandon(self, txid, address, amount): return threads.deferToThread(self._send_abandon_rpc, txid, address, amount) + def _update_name(self, txid, value, amount): + return threads.deferToThread(self._update_name_rpc, txid, value, amount) + def get_claims_from_tx(self, txid): return threads.deferToThread(self._get_claims_from_tx_rpc, txid) @@ -847,9 +855,9 @@ class LBRYcrdWallet(LBRYWallet): rpc_conn = self._get_rpc_conn() return rpc_conn.getvalueforname(name) - # def _update_name_rpc(self, name_value): - # rpc_conn = self._get_rpc_conn() - # return rpc_conn.updatename(name_value) + def _update_name_rpc(self, txid, value, amount): + rpc_conn = self._get_rpc_conn() + return rpc_conn.updatename(txid, value, amount) @_catch_connection_error def _send_name_claim_rpc(self, name, value, amount): diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index f35b5f089..89e650eb0 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -39,7 +39,8 @@ from lbrynet.lbrynet_daemon.LBRYPublisher import Publisher from lbrynet.core.utils import generate_id from lbrynet.lbrynet_console.LBRYSettings import LBRYSettings from lbrynet.conf import MIN_BLOB_DATA_PAYMENT_RATE, DEFAULT_MAX_SEARCH_RESULTS, KNOWN_DHT_NODES, DEFAULT_MAX_KEY_FEE, \ - DEFAULT_WALLET, DEFAULT_SEARCH_TIMEOUT, DEFAULT_CACHE_TIME, DEFAULT_UI_BRANCH, LOG_POST_URL, LOG_FILE_NAME + DEFAULT_WALLET, DEFAULT_SEARCH_TIMEOUT, DEFAULT_CACHE_TIME, DEFAULT_UI_BRANCH, LOG_POST_URL, LOG_FILE_NAME, \ + BASE_METADATA_FIELDS, OPTIONAL_METADATA_FIELDS, SOURCE_TYPES from lbrynet.conf import DEFAULT_TIMEOUT, WALLET_TYPES from lbrynet.core.StreamDescriptor import StreamDescriptorIdentifier, download_sd_blob from lbrynet.core.Session import LBRYSession @@ -47,7 +48,7 @@ from lbrynet.core.PTCWallet import PTCWallet from lbrynet.core.LBRYcrdWallet import LBRYcrdWallet, LBRYumWallet from lbrynet.lbryfilemanager.LBRYFileManager import LBRYFileManager from lbrynet.lbryfile.LBRYFileMetadataManager import DBLBRYFileMetadataManager, TempLBRYFileMetadataManager -from lbryum import LOG_PATH as lbryum_log +# from lbryum import LOG_PATH as lbryum_log log = logging.getLogger(__name__) @@ -67,12 +68,13 @@ handler = logging.handlers.RotatingFileHandler(lbrynet_log, maxBytes=2097152, ba log.addHandler(handler) log.setLevel(logging.INFO) -if os.path.isfile(lbryum_log): - f = open(lbryum_log, 'r') - PREVIOUS_LBRYUM_LOG = len(f.read()) - f.close() -else: - PREVIOUS_LBRYUM_LOG = 0 +# if os.path.isfile(lbryum_log): +# f = open(lbryum_log, 'r') +# PREVIOUS_LBRYUM_LOG = len(f.read()) +# f.close() +# else: +# PREVIOUS_LBRYUM_LOG = 0 + if os.path.isfile(lbrynet_log): f = open(lbrynet_log, 'r') PREVIOUS_LBRYNET_LOG = len(f.read()) @@ -634,11 +636,11 @@ class LBRYDaemon(jsonrpc.JSONRPC): def _upload_log(self, log_type=None, exclude_previous=False, force=False): if self.upload_log or force: - for lm, lp in [('lbrynet', lbrynet_log), ('lbryum', lbryum_log)]: + for lm, lp in [('lbrynet', lbrynet_log)]: #, ('lbryum', lbryum_log)]: if os.path.isfile(lp): if exclude_previous: f = open(lp, "r") - f.seek(PREVIOUS_LBRYNET_LOG if lm == 'lbrynet' else PREVIOUS_LBRYUM_LOG) + f.seek(PREVIOUS_LBRYNET_LOG) # if lm == 'lbrynet' else PREVIOUS_LBRYUM_LOG) log_contents = f.read() f.close() else: @@ -2130,25 +2132,26 @@ class LBRYDaemon(jsonrpc.JSONRPC): d.addCallback(lambda r: self._render_response(r, OK_CODE)) return d - # def jsonrpc_update_name(self, metadata): - # def _disp(x): - # print x - # return x - # - # metadata = json.loads(metadata) - # - # required = ['name', 'file_path', 'bid'] - # - # for r in required: - # if not r in metadata.keys(): - # return defer.fail() - # - # d = defer.Deferred() - # d.addCallback(lambda _: self.session.wallet.update_name(metadata)) - # d.addCallback(_disp) - # d.callback(None) - # - # return d + def jsonrpc_update_name(self, p): + """ + Update name claim + + Args: + 'name': the uri of the claim to be updated + 'value': new metadata dict + 'amount': bid amount of updated claim + Returns: + txid + """ + + name = p['name'] + value = p['value'] if isinstance(p['value'], dict) else json.loads(p['value']) + amount = p['amount'] + + d = self.session.wallet.update_name(name, value, amount) + d.addCallback(lambda r: self._render_response(r, OK_CODE)) + + return d def jsonrpc_log(self, p): """ From 08b612575ce53e8a649bca5950fd1081ce50e889 Mon Sep 17 00:00:00 2001 From: Jack Date: Tue, 28 Jun 2016 23:20:28 -0400 Subject: [PATCH 06/14] update_claim fix and clean up publish keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -replace ‘value’ key with ‘metadata’ -update claim metadata in db when an update is published --- lbrynet/core/LBRYcrdWallet.py | 9 ++++++++- lbrynet/lbrynet_daemon/LBRYDaemon.py | 15 ++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lbrynet/core/LBRYcrdWallet.py b/lbrynet/core/LBRYcrdWallet.py index 0d1d2dc65..970072f77 100644 --- a/lbrynet/core/LBRYcrdWallet.py +++ b/lbrynet/core/LBRYcrdWallet.py @@ -410,7 +410,8 @@ class LBRYWallet(object): def update_name(self, name, value, amount): d = self._get_value_for_name(name) - d.addCallback(lambda r: self._update_name(r['txid'], json.dumps(value), amount)) + d.addCallback(lambda r: (self._update_name(json.loads(r)['txid'], json.dumps(value), amount), json.loads(r)['txid'])) + d.addCallback(lambda new_txid, old_txid: self._update_name_metadata(name, value['sources']['lbry_sd_hash'], old_txid, new_txid)) return d def get_name_and_validity_for_sd_hash(self, sd_hash): @@ -527,6 +528,12 @@ class LBRYWallet(object): return d + def _update_name_metadata(self, name, sd_hash, old_txid, new_txid): + d = self.db.runQuery("delete from name_metadata where txid=? and sd_hash=?", (old_txid, sd_hash)) + d.addCallback(lambda _: self.db.runQuery("insert into name_metadata values (?, ?, ?)", (name, new_txid, sd_hash))) + d.addCallback(lambda _: new_txid) + return d + def _get_claim_metadata_for_sd_hash(self, sd_hash): d = self.db.runQuery("select name, txid from name_metadata where sd_hash=?", (sd_hash,)) d.addCallback(lambda r: r[0] if len(r) else None) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index 89e650eb0..acd13939f 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -399,11 +399,8 @@ class LBRYDaemon(jsonrpc.JSONRPC): def setup(self, branch=DEFAULT_UI_BRANCH, user_specified=False, branch_specified=False): def _log_starting_vals(): - d = self._get_lbry_files() - d.addCallback(lambda r: json.dumps([d[1] if not isinstance(d[1], UnknownNameError) else {'error': 'Pending claim'} for d in r])) - d.addCallback(lambda r: log.info("LBRY Files: " + r)) - d.addCallback(lambda _: log.info("Starting balance: " + str(self.session.wallet.wallet_balance))) - return d + log.info("Starting balance: " + str(self.session.wallet.wallet_balance)) + return defer.succeed(None) def _announce_startup(): def _wait_for_credits(): @@ -2138,17 +2135,17 @@ class LBRYDaemon(jsonrpc.JSONRPC): Args: 'name': the uri of the claim to be updated - 'value': new metadata dict + 'metadata': new metadata dict 'amount': bid amount of updated claim Returns: txid """ - + name = p['name'] - value = p['value'] if isinstance(p['value'], dict) else json.loads(p['value']) + metadata = p['metadata'] if isinstance(p['metadata'], dict) else json.loads(p['metadata']) amount = p['amount'] - d = self.session.wallet.update_name(name, value, amount) + d = self.session.wallet.update_name(name, metadata, amount) d.addCallback(lambda r: self._render_response(r, OK_CODE)) return d From 62b80c13cc870372e06514463a02c190973d2af5 Mon Sep 17 00:00:00 2001 From: Jack Date: Thu, 30 Jun 2016 01:26:33 -0400 Subject: [PATCH 07/14] version bump --- lbrynet/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lbrynet/__init__.py b/lbrynet/__init__.py index 655afaa08..7027afdd4 100644 --- a/lbrynet/__init__.py +++ b/lbrynet/__init__.py @@ -4,5 +4,5 @@ log = logging.getLogger(__name__) logging.getLogger(__name__).addHandler(logging.NullHandler()) log.setLevel(logging.ERROR) -version = (0, 2, 6) +version = (0, 3, 0) __version__ = ".".join([str(x) for x in version]) From 3cea41a85494aab06be455cc3122ccd703a759e6 Mon Sep 17 00:00:00 2001 From: Jack Date: Fri, 1 Jul 2016 02:42:34 -0400 Subject: [PATCH 08/14] remove lbrynet_gui --- lbrynet/lbrynet_gui/GuiApp.py | 352 ----------- .../Ic_arrow_drop_down_48px.svg.LICENSE | 393 ------------ lbrynet/lbrynet_gui/LBRYGui.py | 571 ------------------ lbrynet/lbrynet_gui/StreamFrame.py | 463 -------------- lbrynet/lbrynet_gui/__init__.py | 1 - lbrynet/lbrynet_gui/close.gif | Bin 188 -> 0 bytes lbrynet/lbrynet_gui/close1.png | Bin 1369 -> 0 bytes lbrynet/lbrynet_gui/close2.gif | Bin 195 -> 0 bytes lbrynet/lbrynet_gui/drop_down.gif | Bin 72 -> 0 bytes lbrynet/lbrynet_gui/gui.py | 33 - lbrynet/lbrynet_gui/hide_options.gif | Bin 174 -> 0 bytes lbrynet/lbrynet_gui/lbry-dark-242x80.gif | Bin 3647 -> 0 bytes lbrynet/lbrynet_gui/lbry-dark-icon.ico | Bin 370070 -> 0 bytes lbrynet/lbrynet_gui/lbry-dark-icon.xbm | 14 - lbrynet/lbrynet_gui/lbry.conf | 108 ---- lbrynet/lbrynet_gui/lbry.png | Bin 18944 -> 0 bytes lbrynet/lbrynet_gui/show_options.gif | Bin 155 -> 0 bytes 17 files changed, 1935 deletions(-) delete mode 100644 lbrynet/lbrynet_gui/GuiApp.py delete mode 100644 lbrynet/lbrynet_gui/Ic_arrow_drop_down_48px.svg.LICENSE delete mode 100644 lbrynet/lbrynet_gui/LBRYGui.py delete mode 100644 lbrynet/lbrynet_gui/StreamFrame.py delete mode 100644 lbrynet/lbrynet_gui/__init__.py delete mode 100644 lbrynet/lbrynet_gui/close.gif delete mode 100644 lbrynet/lbrynet_gui/close1.png delete mode 100644 lbrynet/lbrynet_gui/close2.gif delete mode 100644 lbrynet/lbrynet_gui/drop_down.gif delete mode 100644 lbrynet/lbrynet_gui/gui.py delete mode 100644 lbrynet/lbrynet_gui/hide_options.gif delete mode 100755 lbrynet/lbrynet_gui/lbry-dark-242x80.gif delete mode 100755 lbrynet/lbrynet_gui/lbry-dark-icon.ico delete mode 100644 lbrynet/lbrynet_gui/lbry-dark-icon.xbm delete mode 100644 lbrynet/lbrynet_gui/lbry.conf delete mode 100644 lbrynet/lbrynet_gui/lbry.png delete mode 100644 lbrynet/lbrynet_gui/show_options.gif diff --git a/lbrynet/lbrynet_gui/GuiApp.py b/lbrynet/lbrynet_gui/GuiApp.py deleted file mode 100644 index 176cce9d7..000000000 --- a/lbrynet/lbrynet_gui/GuiApp.py +++ /dev/null @@ -1,352 +0,0 @@ -import Tkinter as tk -import logging -import sys -import tkFont -import tkMessageBox -import ttk -from lbrynet.lbrynet_gui.LBRYGui import LBRYDownloader -from lbrynet.lbrynet_gui.StreamFrame import StreamFrame -import locale -import os -from twisted.internet import defer, reactor, tksupport, task - - -log = logging.getLogger(__name__) - - -class DownloaderApp(object): - def __init__(self): - self.master = None - self.downloader = None - self.wallet_balance_check = None - self.streams_frame = None - - def start(self): - - d = defer.maybeDeferred(self._start_root) - d.addCallback(lambda _: self._draw_main()) - d.addCallback(lambda _: self._start_downloader()) - d.addCallback(lambda _: self._start_checking_wallet_balance()) - d.addCallback(lambda _: self._enable_lookup()) - - def show_error_and_stop(err): - log.error(err.getErrorMessage()) - tkMessageBox.showerror(title="Start Error", message=err.getErrorMessage()) - return self.stop() - - d.addErrback(show_error_and_stop) - return d - - def stop(self): - - def log_error(err): - log.error(err.getErrorMessage()) - - if self.downloader is not None: - d = self.downloader.stop() - else: - d = defer.succeed(True) - d.addErrback(log_error) - d.addCallback(lambda _: self._stop_checking_wallet_balance()) - d.addErrback(log_error) - d.addCallback(lambda _: reactor.stop()) - d.addErrback(log_error) - return d - - def _start_root(self): - if os.name == "nt": - button_foreground = "#104639" - lookup_button_padding = 10 - else: - button_foreground = "#FFFFFF" - lookup_button_padding = 11 - - root = tk.Tk() - root.resizable(0, 0) - root.wm_title("LBRY") - - tksupport.install(root) - - if os.name == "nt": - root.iconbitmap(os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), - "lbry-dark-icon.ico")) - else: - root.wm_iconbitmap("@" + os.path.join(os.path.dirname(__file__), "lbry-dark-icon.xbm")) - - root.button_font = tkFont.Font(size=9) - - ttk.Style().configure(".", background="#FFFFFF") - ttk.Style().configure("LBRY.TButton", background="#104639", foreground=button_foreground, - borderwidth=1, relief="solid", font=root.button_font) - ttk.Style().map("LBRY.TButton", - background=[('pressed', "#104639"), - ('active', "#104639")]) - #ttk.Style().configure("LBRY.TButton.border", background="#808080") - ttk.Style().configure("Lookup.LBRY.TButton", padding=lookup_button_padding) - ttk.Style().configure("Stop.TButton", padding=1, background="#FFFFFF", relief="flat", borderwidth=0) - ttk.Style().configure("TEntry", padding=11) - ttk.Style().configure("Float.TEntry", padding=2) - #ttk.Style().configure("A.TFrame", background="red") - #ttk.Style().configure("B.TFrame", background="green") - #ttk.Style().configure("B2.TFrame", background="#80FF80") - #ttk.Style().configure("C.TFrame", background="orange") - #ttk.Style().configure("D.TFrame", background="blue") - #ttk.Style().configure("E.TFrame", background="yellow") - #ttk.Style().configure("F.TFrame", background="#808080") - #ttk.Style().configure("G.TFrame", background="#FF80FF") - #ttk.Style().configure("H.TFrame", background="#0080FF") - #ttk.Style().configure("LBRY.TProgressbar", background="#104639", orient="horizontal", thickness=5) - #ttk.Style().configure("LBRY.TProgressbar") - #ttk.Style().layout("Horizontal.LBRY.TProgressbar", ttk.Style().layout("Horizontal.TProgressbar")) - - root.configure(background="#FFFFFF") - - root.protocol("WM_DELETE_WINDOW", self.stop) - - self.master = root - - def _draw_main(self): - self.frame = ttk.Frame(self.master, style="A.TFrame") - self.frame.grid(padx=20, pady=20) - - logo_file_name = "lbry-dark-242x80.gif" - if os.name == "nt": - logo_file = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), logo_file_name) - else: - logo_file = os.path.join(os.path.dirname(__file__), logo_file_name) - - self.logo_picture = tk.PhotoImage(file=logo_file) - - self.logo_frame = ttk.Frame(self.frame, style="B.TFrame") - self.logo_frame.grid(pady=5, sticky=tk.W + tk.E) - - self.dummy_frame = ttk.Frame(self.logo_frame, style="C.TFrame") # keeps the logo in the middle - self.dummy_frame.grid(row=0, column=1, padx=5) - - self.logo_label = ttk.Label(self.logo_frame, image=self.logo_picture) - self.logo_label.grid(row=0, column=1, padx=5) - - self.wallet_balance_frame = ttk.Frame(self.logo_frame, style="C.TFrame") - self.wallet_balance_frame.grid(sticky=tk.E + tk.N, row=0, column=2) - - self.logo_frame.grid_columnconfigure(0, weight=1, uniform="a") - self.logo_frame.grid_columnconfigure(1, weight=2, uniform="b") - self.logo_frame.grid_columnconfigure(2, weight=1, uniform="a") - - self.wallet_balance = ttk.Label( - self.wallet_balance_frame, - text=" -- LBC" - ) - self.wallet_balance.grid(row=0, column=0) - - dropdown_file_name = "drop_down.gif" - if os.name == "nt": - dropdown_file = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), - dropdown_file_name) - else: - dropdown_file = os.path.join(os.path.dirname(__file__), dropdown_file_name) - - self.dropdown_picture = tk.PhotoImage( - file=dropdown_file - ) - - def get_new_address(): - def show_address(address): - w = AddressWindow(self.master, address) - w.show() - d = defer.maybeDeferred(self.downloader.get_new_address) - d.addCallback(show_address) - - def show_error(err): - tkMessageBox.showerror(title="Failed to get new address", message=err.getErrorMessage()) - - d.addErrback(show_error) - - self.wallet_menu = tk.Menu( - self.master, tearoff=0 - ) - self.wallet_menu.add_command(label="Get new LBRYcrd address", command=get_new_address) - - if os.name == "nt": - button_cursor = "" - else: - button_cursor = "hand1" - - self.wallet_menu_button = ttk.Button(self.wallet_balance_frame, image=self.dropdown_picture, - style="Stop.TButton", cursor=button_cursor) - self.wallet_menu_button.grid(row=0, column=1, padx=(5, 0)) - - def popup_wallet(event): - self.wallet_menu.tk_popup(event.x_root, event.y_root) - - self.wallet_menu_button.bind("", popup_wallet) - - self.uri_frame = ttk.Frame(self.frame, style="B.TFrame") - self.uri_frame.grid() - - self.uri_label = ttk.Label( - self.uri_frame, text="lbry://" - ) - self.uri_label.grid(row=0, column=0, sticky=tk.E, pady=2) - - self.entry_font = tkFont.Font(size=11) - - self.uri_entry = ttk.Entry(self.uri_frame, width=50, foreground="#222222", font=self.entry_font, - state=tk.DISABLED) - self.uri_entry.grid(row=0, column=1, padx=2, pady=2) - - def copy_command(): - self.uri_entry.event_generate('') - - def cut_command(): - self.uri_entry.event_generate('') - - def paste_command(): - self.uri_entry.event_generate('') - - def popup_uri(event): - selection_menu = tk.Menu( - self.master, tearoff=0 - ) - if self.uri_entry.selection_present(): - selection_menu.add_command(label=" Cut ", command=cut_command) - selection_menu.add_command(label=" Copy ", command=copy_command) - selection_menu.add_command(label=" Paste ", command=paste_command) - selection_menu.tk_popup(event.x_root, event.y_root) - - self.uri_entry.bind("", popup_uri) - - self.uri_button = ttk.Button( - self.uri_frame, text="Go", command=self._open_stream, - style='Lookup.LBRY.TButton', cursor=button_cursor - ) - self.uri_button.grid(row=0, column=2, pady=2, padx=0) - - def _start_downloader(self): - self.downloader = LBRYDownloader() - d = self.downloader.start() - d.addCallback(lambda _: self.downloader.check_first_run()) - d.addCallback(self._show_welcome_message) - return d - - def _show_welcome_message(self, points_sent): - if points_sent != 0.0: - w = WelcomeWindow(self.master, points_sent) - w.show() - - def stream_removed(self): - if self.streams_frame is not None: - if len(self.streams_frame.winfo_children()) == 0: - self.streams_frame.destroy() - self.streams_frame = None - - def _start_checking_wallet_balance(self): - - def set_balance(balance): - self.wallet_balance.configure(text=locale.format_string("%.2f LBC", (round(balance, 2),), - grouping=True)) - - def update_balance(): - balance = self.downloader.session.wallet.get_available_balance() - set_balance(balance) - - def start_looping_call(): - self.wallet_balance_check = task.LoopingCall(update_balance) - self.wallet_balance_check.start(5) - - d = self.downloader.session.wallet.get_balance() - d.addCallback(set_balance) - d.addCallback(lambda _: start_looping_call()) - - def _stop_checking_wallet_balance(self): - if self.wallet_balance_check is not None: - self.wallet_balance_check.stop() - - def _enable_lookup(self): - self.uri_entry.bind('', self._open_stream) - self.uri_entry.config(state=tk.NORMAL) - - def _open_stream(self, event=None): - if self.streams_frame is None: - self.streams_frame = ttk.Frame(self.frame, style="B2.TFrame") - self.streams_frame.grid(sticky=tk.E + tk.W) - uri = self.uri_entry.get() - self.uri_entry.delete(0, tk.END) - stream_frame = StreamFrame(self, "lbry://" + uri) - - self.downloader.download_stream(stream_frame, uri) - - -class WelcomeWindow(object): - def __init__(self, root, points_sent): - self.root = root - self.points_sent = points_sent - - def show(self): - window = tk.Toplevel(self.root, background="#FFFFFF") - window.transient(self.root) - window.wm_title("Welcome to LBRY") - window.protocol("WM_DELETE_WINDOW", window.destroy) - window.resizable(0, 0) - - text_box = tk.Text(window, width=45, height=3, relief=tk.FLAT, borderwidth=0, - highlightthickness=0) - - points_string = locale.format_string("%.2f LBC", (round(self.points_sent, 2),), - grouping=True) - - text_box.insert(tk.END, "Thank you for using LBRY! You have been\n" - "given %s for free because we love\n" - "you. Please give them 60 seconds to show up." % points_string) - text_box.grid(row=0, padx=10, pady=5, columnspan=2) - text_box.config(state='normal') - - window.focus_set() - - -class AddressWindow(object): - def __init__(self, root, address): - self.root = root - self.address = address - - def show(self): - window = tk.Toplevel(self.root, background="#FFFFFF") - window.transient(self.root) - window.wm_title("New address") - window.protocol("WM_DELETE_WINDOW", window.destroy) - window.resizable(0, 0) - - text_box = tk.Text(window, width=35, height=1, relief=tk.FLAT, borderwidth=0, - highlightthickness=0) - text_box.insert(tk.END, self.address) - text_box.grid(row=0, padx=10, pady=5, columnspan=2) - text_box.config(state='normal') - - def copy_to_clipboard(): - self.root.clipboard_clear() - self.root.clipboard_append(text_box.get('1.0', 'end-1c')) - - def copy_command(): - text_box.event_generate("") - - copy_menu = tk.Menu( - self.root, tearoff=0 - ) - copy_menu.add_command(label=" Copy ", command=copy_command) - - def popup(event): - if text_box.tag_ranges("sel"): - copy_menu.tk_popup(event.x_root, event.y_root) - - text_box.bind("", popup) - - copy_button = ttk.Button( - window, text="Copy", command=copy_to_clipboard, style="LBRY.TButton" - ) - copy_button.grid(row=1, column=0, pady=(0, 5), padx=5, sticky=tk.E) - - done_button = ttk.Button( - window, text="OK", command=window.destroy, style="LBRY.TButton" - ) - done_button.grid(row=1, column=1, pady=(0, 5), padx=5, sticky=tk.W) - window.focus_set() diff --git a/lbrynet/lbrynet_gui/Ic_arrow_drop_down_48px.svg.LICENSE b/lbrynet/lbrynet_gui/Ic_arrow_drop_down_48px.svg.LICENSE deleted file mode 100644 index 69face7d0..000000000 --- a/lbrynet/lbrynet_gui/Ic_arrow_drop_down_48px.svg.LICENSE +++ /dev/null @@ -1,393 +0,0 @@ -Attribution 4.0 International - -======================================================================= - -Creative Commons Corporation ("Creative Commons") is not a law firm and -does not provide legal services or legal advice. Distribution of -Creative Commons public licenses does not create a lawyer-client or -other relationship. Creative Commons makes its licenses and related -information available on an "as-is" basis. Creative Commons gives no -warranties regarding its licenses, any material licensed under their -terms and conditions, or any related information. Creative Commons -disclaims all liability for damages resulting from their use to the -fullest extent possible. - -Using Creative Commons Public Licenses - -Creative Commons public licenses provide a standard set of terms and -conditions that creators and other rights holders may use to share -original works of authorship and other material subject to copyright -and certain other rights specified in the public license below. The -following considerations are for informational purposes only, are not -exhaustive, and do not form part of our licenses. - - Considerations for licensors: Our public licenses are - intended for use by those authorized to give the public - permission to use material in ways otherwise restricted by - copyright and certain other rights. Our licenses are - irrevocable. Licensors should read and understand the terms - and conditions of the license they choose before applying it. - Licensors should also secure all rights necessary before - applying our licenses so that the public can reuse the - material as expected. Licensors should clearly mark any - material not subject to the license. This includes other CC- - licensed material, or material used under an exception or - limitation to copyright. More considerations for licensors: - wiki.creativecommons.org/Considerations_for_licensors - - Considerations for the public: By using one of our public - licenses, a licensor grants the public permission to use the - licensed material under specified terms and conditions. If - the licensor's permission is not necessary for any reason--for - example, because of any applicable exception or limitation to - copyright--then that use is not regulated by the license. Our - licenses grant only permissions under copyright and certain - other rights that a licensor has authority to grant. Use of - the licensed material may still be restricted for other - reasons, including because others have copyright or other - rights in the material. A licensor may make special requests, - such as asking that all changes be marked or described. - Although not required by our licenses, you are encouraged to - respect those requests where reasonable. More_considerations - for the public: - wiki.creativecommons.org/Considerations_for_licensees - -======================================================================= - -Creative Commons Attribution 4.0 International Public License - -By exercising the Licensed Rights (defined below), You accept and agree -to be bound by the terms and conditions of this Creative Commons -Attribution 4.0 International Public License ("Public License"). To the -extent this Public License may be interpreted as a contract, You are -granted the Licensed Rights in consideration of Your acceptance of -these terms and conditions, and the Licensor grants You such rights in -consideration of benefits the Licensor receives from making the -Licensed Material available under these terms and conditions. - - -Section 1 -- Definitions. - - a. Adapted Material means material subject to Copyright and Similar - Rights that is derived from or based upon the Licensed Material - and in which the Licensed Material is translated, altered, - arranged, transformed, or otherwise modified in a manner requiring - permission under the Copyright and Similar Rights held by the - Licensor. For purposes of this Public License, where the Licensed - Material is a musical work, performance, or sound recording, - Adapted Material is always produced where the Licensed Material is - synched in timed relation with a moving image. - - b. Adapter's License means the license You apply to Your Copyright - and Similar Rights in Your contributions to Adapted Material in - accordance with the terms and conditions of this Public License. - - c. Copyright and Similar Rights means copyright and/or similar rights - closely related to copyright including, without limitation, - performance, broadcast, sound recording, and Sui Generis Database - Rights, without regard to how the rights are labeled or - categorized. For purposes of this Public License, the rights - specified in Section 2(b)(1)-(2) are not Copyright and Similar - Rights. - - d. Effective Technological Measures means those measures that, in the - absence of proper authority, may not be circumvented under laws - fulfilling obligations under Article 11 of the WIPO Copyright - Treaty adopted on December 20, 1996, and/or similar international - agreements. - - e. Exceptions and Limitations means fair use, fair dealing, and/or - any other exception or limitation to Copyright and Similar Rights - that applies to Your use of the Licensed Material. - - f. Licensed Material means the artistic or literary work, database, - or other material to which the Licensor applied this Public - License. - - g. Licensed Rights means the rights granted to You subject to the - terms and conditions of this Public License, which are limited to - all Copyright and Similar Rights that apply to Your use of the - Licensed Material and that the Licensor has authority to license. - - h. Licensor means the individual(s) or entity(ies) granting rights - under this Public License. - - i. Share means to provide material to the public by any means or - process that requires permission under the Licensed Rights, such - as reproduction, public display, public performance, distribution, - dissemination, communication, or importation, and to make material - available to the public including in ways that members of the - public may access the material from a place and at a time - individually chosen by them. - - j. Sui Generis Database Rights means rights other than copyright - resulting from Directive 96/9/EC of the European Parliament and of - the Council of 11 March 1996 on the legal protection of databases, - as amended and/or succeeded, as well as other essentially - equivalent rights anywhere in the world. - - k. You means the individual or entity exercising the Licensed Rights - under this Public License. Your has a corresponding meaning. - - -Section 2 -- Scope. - - a. License grant. - - 1. Subject to the terms and conditions of this Public License, - the Licensor hereby grants You a worldwide, royalty-free, - non-sublicensable, non-exclusive, irrevocable license to - exercise the Licensed Rights in the Licensed Material to: - - a. reproduce and Share the Licensed Material, in whole or - in part; and - - b. produce, reproduce, and Share Adapted Material. - - 2. Exceptions and Limitations. For the avoidance of doubt, where - Exceptions and Limitations apply to Your use, this Public - License does not apply, and You do not need to comply with - its terms and conditions. - - 3. Term. The term of this Public License is specified in Section - 6(a). - - 4. Media and formats; technical modifications allowed. The - Licensor authorizes You to exercise the Licensed Rights in - all media and formats whether now known or hereafter created, - and to make technical modifications necessary to do so. The - Licensor waives and/or agrees not to assert any right or - authority to forbid You from making technical modifications - necessary to exercise the Licensed Rights, including - technical modifications necessary to circumvent Effective - Technological Measures. For purposes of this Public License, - simply making modifications authorized by this Section 2(a) - (4) never produces Adapted Material. - - 5. Downstream recipients. - - a. Offer from the Licensor -- Licensed Material. Every - recipient of the Licensed Material automatically - receives an offer from the Licensor to exercise the - Licensed Rights under the terms and conditions of this - Public License. - - b. No downstream restrictions. You may not offer or impose - any additional or different terms or conditions on, or - apply any Effective Technological Measures to, the - Licensed Material if doing so restricts exercise of the - Licensed Rights by any recipient of the Licensed - Material. - - 6. No endorsement. Nothing in this Public License constitutes or - may be construed as permission to assert or imply that You - are, or that Your use of the Licensed Material is, connected - with, or sponsored, endorsed, or granted official status by, - the Licensor or others designated to receive attribution as - provided in Section 3(a)(1)(A)(i). - - b. Other rights. - - 1. Moral rights, such as the right of integrity, are not - licensed under this Public License, nor are publicity, - privacy, and/or other similar personality rights; however, to - the extent possible, the Licensor waives and/or agrees not to - assert any such rights held by the Licensor to the limited - extent necessary to allow You to exercise the Licensed - Rights, but not otherwise. - - 2. Patent and trademark rights are not licensed under this - Public License. - - 3. To the extent possible, the Licensor waives any right to - collect royalties from You for the exercise of the Licensed - Rights, whether directly or through a collecting society - under any voluntary or waivable statutory or compulsory - licensing scheme. In all other cases the Licensor expressly - reserves any right to collect such royalties. - - -Section 3 -- License Conditions. - -Your exercise of the Licensed Rights is expressly made subject to the -following conditions. - - a. Attribution. - - 1. If You Share the Licensed Material (including in modified - form), You must: - - a. retain the following if it is supplied by the Licensor - with the Licensed Material: - - i. identification of the creator(s) of the Licensed - Material and any others designated to receive - attribution, in any reasonable manner requested by - the Licensor (including by pseudonym if - designated); - - ii. a copyright notice; - - iii. a notice that refers to this Public License; - - iv. a notice that refers to the disclaimer of - warranties; - - v. a URI or hyperlink to the Licensed Material to the - extent reasonably practicable; - - b. indicate if You modified the Licensed Material and - retain an indication of any previous modifications; and - - c. indicate the Licensed Material is licensed under this - Public License, and include the text of, or the URI or - hyperlink to, this Public License. - - 2. You may satisfy the conditions in Section 3(a)(1) in any - reasonable manner based on the medium, means, and context in - which You Share the Licensed Material. For example, it may be - reasonable to satisfy the conditions by providing a URI or - hyperlink to a resource that includes the required - information. - - 3. If requested by the Licensor, You must remove any of the - information required by Section 3(a)(1)(A) to the extent - reasonably practicable. - - 4. If You Share Adapted Material You produce, the Adapter's - License You apply must not prevent recipients of the Adapted - Material from complying with this Public License. - - -Section 4 -- Sui Generis Database Rights. - -Where the Licensed Rights include Sui Generis Database Rights that -apply to Your use of the Licensed Material: - - a. for the avoidance of doubt, Section 2(a)(1) grants You the right - to extract, reuse, reproduce, and Share all or a substantial - portion of the contents of the database; - - b. if You include all or a substantial portion of the database - contents in a database in which You have Sui Generis Database - Rights, then the database in which You have Sui Generis Database - Rights (but not its individual contents) is Adapted Material; and - - c. You must comply with the conditions in Section 3(a) if You Share - all or a substantial portion of the contents of the database. - -For the avoidance of doubt, this Section 4 supplements and does not -replace Your obligations under this Public License where the Licensed -Rights include other Copyright and Similar Rights. - - -Section 5 -- Disclaimer of Warranties and Limitation of Liability. - - a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE - EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS - AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF - ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, - IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, - WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR - PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, - ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT - KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT - ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. - - b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE - TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, - NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, - INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, - COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR - USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN - ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR - DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR - IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. - - c. The disclaimer of warranties and limitation of liability provided - above shall be interpreted in a manner that, to the extent - possible, most closely approximates an absolute disclaimer and - waiver of all liability. - - -Section 6 -- Term and Termination. - - a. This Public License applies for the term of the Copyright and - Similar Rights licensed here. However, if You fail to comply with - this Public License, then Your rights under this Public License - terminate automatically. - - b. Where Your right to use the Licensed Material has terminated under - Section 6(a), it reinstates: - - 1. automatically as of the date the violation is cured, provided - it is cured within 30 days of Your discovery of the - violation; or - - 2. upon express reinstatement by the Licensor. - - For the avoidance of doubt, this Section 6(b) does not affect any - right the Licensor may have to seek remedies for Your violations - of this Public License. - - c. For the avoidance of doubt, the Licensor may also offer the - Licensed Material under separate terms or conditions or stop - distributing the Licensed Material at any time; however, doing so - will not terminate this Public License. - - d. Sections 1, 5, 6, 7, and 8 survive termination of this Public - License. - - -Section 7 -- Other Terms and Conditions. - - a. The Licensor shall not be bound by any additional or different - terms or conditions communicated by You unless expressly agreed. - - b. Any arrangements, understandings, or agreements regarding the - Licensed Material not stated herein are separate from and - independent of the terms and conditions of this Public License. - - -Section 8 -- Interpretation. - - a. For the avoidance of doubt, this Public License does not, and - shall not be interpreted to, reduce, limit, restrict, or impose - conditions on any use of the Licensed Material that could lawfully - be made without permission under this Public License. - - b. To the extent possible, if any provision of this Public License is - deemed unenforceable, it shall be automatically reformed to the - minimum extent necessary to make it enforceable. If the provision - cannot be reformed, it shall be severed from this Public License - without affecting the enforceability of the remaining terms and - conditions. - - c. No term or condition of this Public License will be waived and no - failure to comply consented to unless expressly agreed to by the - Licensor. - - d. Nothing in this Public License constitutes or may be interpreted - as a limitation upon, or waiver of, any privileges and immunities - that apply to the Licensor or You, including from the legal - processes of any jurisdiction or authority. - - -======================================================================= - -Creative Commons is not a party to its public licenses. -Notwithstanding, Creative Commons may elect to apply one of its public -licenses to material it publishes and in those instances will be -considered the "Licensor." Except for the limited purpose of indicating -that material is shared under a Creative Commons public license or as -otherwise permitted by the Creative Commons policies published at -creativecommons.org/policies, Creative Commons does not authorize the -use of the trademark "Creative Commons" or any other trademark or logo -of Creative Commons without its prior written consent including, -without limitation, in connection with any unauthorized modifications -to any of its public licenses or any other arrangements, -understandings, or agreements concerning use of licensed material. For -the avoidance of doubt, this paragraph does not form part of the public -licenses. - -Creative Commons may be contacted at creativecommons.org. \ No newline at end of file diff --git a/lbrynet/lbrynet_gui/LBRYGui.py b/lbrynet/lbrynet_gui/LBRYGui.py deleted file mode 100644 index 3c83308a8..000000000 --- a/lbrynet/lbrynet_gui/LBRYGui.py +++ /dev/null @@ -1,571 +0,0 @@ -import binascii -import logging -import tkMessageBox -from Crypto import Random -from lbrynet.conf import MIN_BLOB_DATA_PAYMENT_RATE -from lbrynet.core import StreamDescriptor -from lbrynet.core.Error import UnknownNameError, UnknownStreamTypeError, InvalidStreamDescriptorError -from lbrynet.core.Error import InvalidStreamInfoError -from lbrynet.core.LBRYcrdWallet import LBRYcrdWallet -from lbrynet.core.PaymentRateManager import PaymentRateManager -from lbrynet.core.Session import LBRYSession -from lbrynet.core.StreamDescriptor import StreamDescriptorIdentifier -from lbrynet.core.server.BlobAvailabilityHandler import BlobAvailabilityHandlerFactory -from lbrynet.core.server.BlobRequestHandler import BlobRequestHandlerFactory -from lbrynet.core.server.ServerProtocol import ServerProtocolFactory -from lbrynet.lbryfile.LBRYFileMetadataManager import TempLBRYFileMetadataManager -from lbrynet.lbryfile.StreamDescriptor import LBRYFileStreamType -from lbrynet.lbryfile.client.LBRYFileDownloader import LBRYFileSaverFactory, LBRYFileOpenerFactory -from lbrynet.lbryfile.client.LBRYFileOptions import add_lbry_file_to_sd_identifier -import os -import requests -import shutil -import sys -from twisted.internet import threads, defer, task - - -log = logging.getLogger(__name__) - - -class LBRYDownloader(object): - def __init__(self): - self.session = None - self.known_dht_nodes = [('104.236.42.182', 4000)] - self.db_dir = os.path.join(os.path.expanduser("~"), ".lbrydownloader") - self.blobfile_dir = os.path.join(self.db_dir, "blobfiles") - self.peer_port = 3333 - self.dht_node_port = 4444 - self.run_server = True - self.first_run = False - self.current_db_revision = 1 - if os.name == "nt": - from lbrynet.winhelpers.knownpaths import get_path, FOLDERID, UserHandle - self.download_directory = get_path(FOLDERID.Downloads, UserHandle.current) - self.wallet_dir = os.path.join(get_path(FOLDERID.RoamingAppData, UserHandle.current), "lbrycrd") - else: - if sys.platform == 'darwin': - self.download_directory = os.path.join(os.path.expanduser("~"), "Downloads") - self.wallet_dir = os.path.join(os.path.expanduser("~"), "Library/Application Support/lbrycrd") - else: - self.download_directory = os.getcwd() - self.wallet_dir = os.path.join(os.path.expanduser("~"), ".lbrycrd") - self.wallet_conf = os.path.join(self.wallet_dir, "lbrycrd.conf") - self.wallet_user = None - self.wallet_password = None - self.sd_identifier = StreamDescriptorIdentifier() - self.wallet_rpc_port = 8332 - self.download_deferreds = [] - self.stream_frames = [] - self.default_blob_data_payment_rate = MIN_BLOB_DATA_PAYMENT_RATE - self.use_upnp = True - self.start_lbrycrdd = True - if os.name == "nt": - self.lbrycrdd_path = "lbrycrdd.exe" - else: - self.lbrycrdd_path = None - self.default_lbrycrdd_path = "./lbrycrdd" - self.delete_blobs_on_remove = True - self.blob_request_payment_rate_manager = None - - def start(self): - d = self._load_conf_options() - d.addCallback(lambda _: threads.deferToThread(self._create_directory)) - d.addCallback(lambda _: self._check_db_migration()) - d.addCallback(lambda _: self._get_session()) - d.addCallback(lambda _: self._setup_stream_info_manager()) - d.addCallback(lambda _: self._setup_stream_identifier()) - d.addCallback(lambda _: self.start_server()) - return d - - def stop(self): - dl = defer.DeferredList(self.download_deferreds) - for stream_frame in self.stream_frames: - stream_frame.cancel_func() - if self.session is not None: - dl.addBoth(lambda _: self.stop_server()) - dl.addBoth(lambda _: self.session.shut_down()) - return dl - - def get_new_address(self): - return self.session.wallet.get_new_address() - - def _check_db_migration(self): - old_revision = 0 - db_revision_file = os.path.join(self.db_dir, "db_revision") - if os.path.exists(db_revision_file): - old_revision = int(open(db_revision_file).read().strip()) - if old_revision < self.current_db_revision: - if os.name == "nt": - import subprocess - import sys - - def run_migrator(): - migrator_exe = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), - "dbmigrator", "migrator.exe") - print "trying to find the migrator at", migrator_exe - si = subprocess.STARTUPINFO - si.dwFlags = subprocess.STARTF_USESHOWWINDOW - si.wShowWindow = subprocess.SW_HIDE - print "trying to run the migrator" - migrator_proc = subprocess.Popen([migrator_exe, self.db_dir, str(old_revision), - str(self.current_db_revision)], startupinfo=si) - print "started the migrator" - migrator_proc.wait() - print "migrator has returned" - - return threads.deferToThread(run_migrator) - else: - from lbrynet.db_migrator import dbmigrator - return threads.deferToThread(dbmigrator.migrate_db, self.db_dir, old_revision, - self.current_db_revision) - return defer.succeed(True) - - def _load_conf_options(self): - - def get_lbrycrdd_path_conf_file(): - if os.name == "nt": - return "" - lbrycrdd_path_conf_path = os.path.join(os.path.expanduser("~"), ".lbrycrddpath.conf") - if not os.path.exists(lbrycrdd_path_conf_path): - return "" - lbrycrdd_path_conf = open(lbrycrdd_path_conf_path) - lines = lbrycrdd_path_conf.readlines() - return lines - - d = threads.deferToThread(get_lbrycrdd_path_conf_file) - - def load_lbrycrdd_path(conf): - for line in conf: - if len(line.strip()) and line.strip()[0] != "#": - self.lbrycrdd_path = line.strip() - - d.addCallback(load_lbrycrdd_path) - - def get_configuration_file(): - if os.name == "nt": - lbry_conf_path = "lbry.conf" - if not os.path.exists(lbry_conf_path): - log.debug("Could not read lbry.conf") - return "" - else: - lbry_conf_path = os.path.join(os.path.expanduser("~"), ".lbrynetgui.conf") - if not os.path.exists(lbry_conf_path): - clean_conf_path = os.path.join(os.path.dirname(__file__), "lbry.conf") - shutil.copy(clean_conf_path, lbry_conf_path) - lbry_conf = open(lbry_conf_path) - log.debug("Loading configuration options from %s", lbry_conf_path) - lines = lbry_conf.readlines() - log.debug("%s file contents:\n%s", lbry_conf_path, str(lines)) - return lines - - d.addCallback(lambda _: threads.deferToThread(get_configuration_file)) - - def load_configuration_file(conf): - for line in conf: - if len(line.strip()) and line.strip()[0] != "#": - try: - field_name, field_value = map(lambda x: x.strip(), line.strip().split("=", 1)) - field_name = field_name.lower() - except ValueError: - raise ValueError("Invalid configuration line: %s" % line) - if field_name == "known_dht_nodes": - known_nodes = [] - nodes = field_value.split(",") - for n in nodes: - if n.strip(): - try: - ip_address, port_string = map(lambda x: x.strip(), n.split(":")) - ip_numbers = ip_address.split(".") - assert len(ip_numbers) == 4 - for ip_num in ip_numbers: - num = int(ip_num) - assert 0 <= num <= 255 - known_nodes.append((ip_address, int(port_string))) - except (ValueError, AssertionError): - raise ValueError("Expected known nodes in format 192.168.1.1:4000,192.168.1.2:4001. Got %s" % str(field_value)) - log.debug("Setting known_dht_nodes to %s", str(known_nodes)) - self.known_dht_nodes = known_nodes - elif field_name == "run_server": - if field_value.lower() == "true": - run_server = True - elif field_value.lower() == "false": - run_server = False - else: - raise ValueError("run_server must be set to True or False. Got %s" % field_value) - log.debug("Setting run_server to %s", str(run_server)) - self.run_server = run_server - elif field_name == "data_dir": - log.debug("Setting data_dir to %s", str(field_value)) - self.db_dir = field_value - self.blobfile_dir = os.path.join(self.db_dir, "blobfiles") - elif field_name == "wallet_dir": - log.debug("Setting wallet_dir to %s", str(field_value)) - self.wallet_dir = field_value - elif field_name == "wallet_conf": - log.debug("Setting wallet_conf to %s", str(field_value)) - self.wallet_conf = field_value - elif field_name == "peer_port": - try: - peer_port = int(field_value) - assert 0 <= peer_port <= 65535 - log.debug("Setting peer_port to %s", str(peer_port)) - self.peer_port = peer_port - except (ValueError, AssertionError): - raise ValueError("peer_port must be set to an integer between 1 and 65535. Got %s" % field_value) - elif field_name == "dht_port": - try: - dht_port = int(field_value) - assert 0 <= dht_port <= 65535 - log.debug("Setting dht_node_port to %s", str(dht_port)) - self.dht_node_port = dht_port - except (ValueError, AssertionError): - raise ValueError("dht_port must be set to an integer between 1 and 65535. Got %s" % field_value) - elif field_name == "use_upnp": - if field_value.lower() == "true": - use_upnp = True - elif field_value.lower() == "false": - use_upnp = False - else: - raise ValueError("use_upnp must be set to True or False. Got %s" % str(field_value)) - log.debug("Setting use_upnp to %s", str(use_upnp)) - self.use_upnp = use_upnp - elif field_name == "default_blob_data_payment_rate": - try: - rate = float(field_value) - assert rate >= 0.0 - log.debug("Setting default_blob_data_payment_rate to %s", str(rate)) - self.default_blob_data_payment_rate = rate - except (ValueError, AssertionError): - raise ValueError("default_blob_data_payment_rate must be a positive floating point number, e.g. 0.5. Got %s" % str(field_value)) - elif field_name == "start_lbrycrdd": - if field_value.lower() == "true": - start_lbrycrdd = True - elif field_value.lower() == "false": - start_lbrycrdd = False - else: - raise ValueError("start_lbrycrdd must be set to True or False. Got %s" % field_value) - log.debug("Setting start_lbrycrdd to %s", str(start_lbrycrdd)) - self.start_lbrycrdd = start_lbrycrdd - elif field_name == "lbrycrdd_path": - self.lbrycrdd_path = field_value - elif field_name == "download_directory": - log.debug("Setting download_directory to %s", str(field_value)) - self.download_directory = field_value - elif field_name == "delete_blobs_on_stream_remove": - if field_value.lower() == "true": - self.delete_blobs_on_remove = True - elif field_value.lower() == "false": - self.delete_blobs_on_remove = False - else: - raise ValueError("delete_blobs_on_stream_remove must be set to True or False") - else: - log.warning("Got unknown configuration field: %s", field_name) - - d.addCallback(load_configuration_file) - return d - - def _create_directory(self): - if not os.path.exists(self.db_dir): - os.makedirs(self.db_dir) - db_revision = open(os.path.join(self.db_dir, "db_revision"), mode='w') - db_revision.write(str(self.current_db_revision)) - db_revision.close() - log.debug("Created the configuration directory: %s", str(self.db_dir)) - if not os.path.exists(self.blobfile_dir): - os.makedirs(self.blobfile_dir) - log.debug("Created the data directory: %s", str(self.blobfile_dir)) - if os.name == "nt": - if not os.path.exists(self.wallet_dir): - os.makedirs(self.wallet_dir) - if not os.path.exists(self.wallet_conf): - lbrycrd_conf = open(self.wallet_conf, mode='w') - self.wallet_user = "rpcuser" - lbrycrd_conf.write("rpcuser=%s\n" % self.wallet_user) - self.wallet_password = binascii.hexlify(Random.new().read(20)) - lbrycrd_conf.write("rpcpassword=%s\n" % self.wallet_password) - lbrycrd_conf.write("server=1\n") - lbrycrd_conf.close() - else: - lbrycrd_conf = open(self.wallet_conf) - for l in lbrycrd_conf: - if l.startswith("rpcuser="): - self.wallet_user = l[8:].rstrip('\n') - if l.startswith("rpcpassword="): - self.wallet_password = l[12:].rstrip('\n') - if l.startswith("rpcport="): - self.wallet_rpc_port = int(l[8:-1].rstrip('\n')) - - def _get_session(self): - lbrycrdd_path = None - if self.start_lbrycrdd is True: - lbrycrdd_path = self.lbrycrdd_path - if not lbrycrdd_path: - lbrycrdd_path = self.default_lbrycrdd_path - - if sys.platform == 'darwin': - os.chdir("/Applications/LBRY.app/Contents/Resources") - - wallet = LBRYcrdWallet(self.db_dir, wallet_dir=self.wallet_dir, wallet_conf=self.wallet_conf, - lbrycrdd_path=lbrycrdd_path) - - peer_port = None - if self.run_server: - peer_port = self.peer_port - self.session = LBRYSession(self.default_blob_data_payment_rate, db_dir=self.db_dir, - blob_dir=self.blobfile_dir, use_upnp=self.use_upnp, wallet=wallet, - known_dht_nodes=self.known_dht_nodes, dht_node_port=self.dht_node_port, - peer_port=peer_port) - return self.session.setup() - - def _setup_stream_info_manager(self): - self.stream_info_manager = TempLBRYFileMetadataManager() - return defer.succeed(True) - - def start_server(self): - - if self.run_server: - self.blob_request_payment_rate_manager = PaymentRateManager( - self.session.base_payment_rate_manager, - self.default_blob_data_payment_rate - ) - handlers = [ - BlobAvailabilityHandlerFactory(self.session.blob_manager), - self.session.wallet.get_wallet_info_query_handler_factory(), - BlobRequestHandlerFactory(self.session.blob_manager, self.session.wallet, - self.blob_request_payment_rate_manager) - ] - - server_factory = ServerProtocolFactory(self.session.rate_limiter, - handlers, - self.session.peer_manager) - from twisted.internet import reactor - self.lbry_server_port = reactor.listenTCP(self.peer_port, server_factory) - - return defer.succeed(True) - - def stop_server(self): - if self.lbry_server_port is not None: - self.lbry_server_port, p = None, self.lbry_server_port - return defer.maybeDeferred(p.stopListening) - else: - return defer.succeed(True) - - def _setup_stream_identifier(self): - add_lbry_file_to_sd_identifier(self.sd_identifier) - file_saver_factory = LBRYFileSaverFactory(self.session.peer_finder, self.session.rate_limiter, - self.session.blob_manager, self.stream_info_manager, - self.session.wallet, self.download_directory) - self.sd_identifier.add_stream_downloader_factory(LBRYFileStreamType, file_saver_factory) - file_opener_factory = LBRYFileOpenerFactory(self.session.peer_finder, self.session.rate_limiter, - self.session.blob_manager, self.stream_info_manager, - self.session.wallet) - self.sd_identifier.add_stream_downloader_factory(LBRYFileStreamType, file_opener_factory) - - def check_first_run(self): - d = self.session.wallet.is_first_run() - d.addCallback(lambda is_first_run: self._do_first_run() if is_first_run else 0.0) - return d - - def _do_first_run(self): - d = self.session.wallet.get_new_address() - - def send_request(url, data): - r = requests.post(url, json=data) - if r.status_code == 200: - return r.json()['credits_sent'] - return 0.0 - - def log_error(err): - log.warning("unable to request free credits. %s", err.getErrorMessage()) - return 0.0 - - def request_credits(address): - url = "http://credreq.lbry.io/requestcredits" - data = {"address": address} - d = threads.deferToThread(send_request, url, data) - d.addErrback(log_error) - return d - - d.addCallback(request_credits) - return d - - def _resolve_name(self, uri): - return self.session.wallet.get_stream_info_for_name(uri) - - def download_stream(self, stream_frame, uri): - resolve_d = self._resolve_name(uri) - - stream_frame.show_metadata_status("resolving name...") - - stream_frame.cancel_func = resolve_d.cancel - payment_rate_manager = PaymentRateManager(self.session.base_payment_rate_manager) - - def update_stream_name(value): - if 'name' in value: - stream_frame.show_name(value['name']) - if 'description' in value: - stream_frame.show_description(value['description']) - if 'thumbnail' in value: - stream_frame.show_thumbnail(value['thumbnail']) - return value - - def get_sd_hash(value): - if 'stream_hash' in value: - return value['stream_hash'] - raise UnknownNameError(uri) - - def get_sd_blob(sd_hash): - stream_frame.show_metadata_status("name resolved, fetching metadata...") - get_sd_d = StreamDescriptor.download_sd_blob(self.session, sd_hash, - payment_rate_manager) - get_sd_d.addCallback(self.sd_identifier.get_metadata_for_sd_blob) - get_sd_d.addCallbacks(choose_download_factory, bad_sd_blob) - return get_sd_d - - def get_info_from_validator(info_validator): - stream_name = None - stream_size = None - for field, val in info_validator.info_to_show(): - if field == "suggested_file_name": - stream_name = val - elif field == "stream_name" and stream_name is None: - stream_name = val - elif field == "stream_size": - stream_size = int(val) - if stream_size is None: - stream_size = "unknown" - if stream_name is None: - stream_name = "unknown" - return stream_name, stream_size - - def choose_download_factory(metadata): - #info_validator, options, factories = info_and_factories - stream_name, stream_size = get_info_from_validator(metadata.validator) - if isinstance(stream_size, (int, long)): - price = payment_rate_manager.get_effective_min_blob_data_payment_rate() - estimated_cost = stream_size * 1.0 / 2**20 * price - else: - estimated_cost = "unknown" - - stream_frame.show_stream_metadata(stream_name, stream_size, estimated_cost) - - available_options = metadata.options.get_downloader_options(metadata.validator, - payment_rate_manager) - - stream_frame.show_download_options(available_options) - - get_downloader_d = defer.Deferred() - - def create_downloader(f, chosen_options): - - def fire_get_downloader_d(downloader): - if not get_downloader_d.called: - get_downloader_d.callback(downloader) - - stream_frame.disable_download_buttons() - d = f.make_downloader(metadata, chosen_options, - payment_rate_manager) - d.addCallback(fire_get_downloader_d) - - for factory in metadata.factories: - - def choose_factory(f=factory): - chosen_options = stream_frame.get_chosen_options() - create_downloader(f, chosen_options) - - stream_frame.add_download_factory(factory, choose_factory) - - get_downloader_d.addCallback(start_download) - - return get_downloader_d - - def show_stream_status(downloader): - total_bytes = downloader.get_total_bytes_cached() - bytes_left_to_download = downloader.get_bytes_left_to_download() - points_paid = payment_rate_manager.points_paid - payment_rate = payment_rate_manager.get_effective_min_blob_data_payment_rate() - points_remaining = 1.0 * bytes_left_to_download * payment_rate / 2**20 - stream_frame.show_progress(total_bytes, bytes_left_to_download, - points_paid, points_remaining) - - def show_finished(arg, downloader): - show_stream_status(downloader) - stream_frame.show_download_done(payment_rate_manager.points_paid) - return arg - - def start_download(downloader): - stream_frame.stream_hash = downloader.stream_hash - l = task.LoopingCall(show_stream_status, downloader) - l.start(1) - d = downloader.start() - stream_frame.cancel_func = downloader.stop - - def stop_looping_call(arg): - l.stop() - stream_frame.cancel_func = resolve_d.cancel - return arg - - d.addBoth(stop_looping_call) - d.addCallback(show_finished, downloader) - return d - - def lookup_failed(err): - stream_frame.show_metadata_status("name lookup failed") - return err - - def bad_sd_blob(err): - stream_frame.show_metadata_status("Unknown type or badly formed metadata") - return err - - resolve_d.addCallback(update_stream_name) - resolve_d.addCallback(get_sd_hash) - resolve_d.addCallbacks(get_sd_blob, lookup_failed) - - def show_err(err): - tkMessageBox.showerror(title="Download Error", message=err.getErrorMessage()) - log.error(err.getErrorMessage()) - stream_frame.show_download_done(payment_rate_manager.points_paid) - - resolve_d.addErrback(lambda err: err.trap(defer.CancelledError, UnknownNameError, - UnknownStreamTypeError, InvalidStreamDescriptorError, - InvalidStreamInfoError)) - resolve_d.addErrback(show_err) - - def delete_associated_blobs(): - if stream_frame.stream_hash is None or self.delete_blobs_on_remove is False: - return defer.succeed(True) - d1 = self.stream_info_manager.get_blobs_for_stream(stream_frame.stream_hash) - - def get_blob_hashes(blob_infos): - return [b[0] for b in blob_infos if b[0] is not None] - - d1.addCallback(get_blob_hashes) - d2 = self.stream_info_manager.get_sd_blob_hashes_for_stream(stream_frame.stream_hash) - - def combine_blob_hashes(results): - blob_hashes = [] - for success, result in results: - if success is True: - blob_hashes.extend(result) - return blob_hashes - - def delete_blobs(blob_hashes): - return self.session.blob_manager.delete_blobs(blob_hashes) - - dl = defer.DeferredList([d1, d2], fireOnOneErrback=True) - dl.addCallback(combine_blob_hashes) - dl.addCallback(delete_blobs) - return dl - - resolve_d.addCallback(lambda _: delete_associated_blobs()) - self._add_download_deferred(resolve_d, stream_frame) - - def _add_download_deferred(self, d, stream_frame): - self.download_deferreds.append(d) - self.stream_frames.append(stream_frame) - - def remove_from_list(): - self.download_deferreds.remove(d) - self.stream_frames.remove(stream_frame) - - d.addBoth(lambda _: remove_from_list()) diff --git a/lbrynet/lbrynet_gui/StreamFrame.py b/lbrynet/lbrynet_gui/StreamFrame.py deleted file mode 100644 index de07c4d55..000000000 --- a/lbrynet/lbrynet_gui/StreamFrame.py +++ /dev/null @@ -1,463 +0,0 @@ -import Tkinter as tk -import sys -import tkFont -import ttk -import locale -import os - - -class StreamFrame(object): - def __init__(self, app, uri): - self.app = app - self.uri = uri - self.stream_hash = None - self.cancel_func = None - - self.stream_frame = ttk.Frame(self.app.streams_frame, style="B.TFrame") - - self.stream_frame.pack(fill=tk.X, side=tk.BOTTOM, pady=(30, 0)) - - self.stream_frame_header = ttk.Frame(self.stream_frame, style="C.TFrame") - self.stream_frame_header.grid(sticky=tk.E + tk.W) - - self.uri_font = tkFont.Font(size=8) - self.uri_label = ttk.Label( - self.stream_frame_header, text=self.uri, font=self.uri_font, foreground="#666666" - ) - self.uri_label.grid(row=0, column=0, sticky=tk.W) - - if os.name == "nt": - self.button_cursor = "" - else: - self.button_cursor = "hand1" - - close_file_name = "close2.gif" - if os.name == "nt": - close_file = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), close_file_name) - else: - close_file = os.path.join(os.path.dirname(__file__), close_file_name) - - self.close_picture = tk.PhotoImage( - file=close_file - ) - self.close_button = ttk.Button( - self.stream_frame_header, command=self.cancel, style="Stop.TButton", cursor=self.button_cursor - ) - self.close_button.config(image=self.close_picture) - self.close_button.grid(row=0, column=1, sticky=tk.E + tk.N) - - self.stream_frame_header.grid_columnconfigure(0, weight=1) - - self.stream_frame.grid_columnconfigure(0, weight=1) - - self.stream_frame_body = ttk.Frame(self.stream_frame, style="C.TFrame") - self.stream_frame_body.grid(row=1, column=0, sticky=tk.E + tk.W) - - self.name_frame = ttk.Frame(self.stream_frame_body, style="D.TFrame") - self.name_frame.grid(sticky=tk.W + tk.E) - self.name_frame.grid_columnconfigure(0, weight=16) - self.name_frame.grid_columnconfigure(1, weight=1) - - self.stream_frame_body.grid_columnconfigure(0, weight=1) - - self.metadata_frame = ttk.Frame(self.stream_frame_body, style="D.TFrame") - self.metadata_frame.grid(sticky=tk.W + tk.E, row=1) - self.metadata_frame.grid_columnconfigure(0, weight=1) - - self.options_frame = ttk.Frame(self.stream_frame_body, style="D.TFrame") - - self.outer_button_frame = ttk.Frame(self.stream_frame_body, style="D.TFrame") - self.outer_button_frame.grid(sticky=tk.W + tk.E, row=4) - - #show_options_picture_file_name = "show_options.gif" - #if os.name == "nt": - # show_options_picture_file = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), - # "lbrynet", "lbrynet_downloader_gui", - # show_options_picture_file_name) - #else: - # show_options_picture_file = os.path.join(os.path.dirname(__file__), - # show_options_picture_file_name) - - #self.show_options_picture = tk.PhotoImage( - # file=show_options_picture_file - #) - - #hide_options_picture_file_name = "hide_options.gif" - #if os.name == "nt": - # hide_options_picture_file = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), - # "lbrynet", "lbrynet_downloader_gui", - # hide_options_picture_file_name) - #else: - # hide_options_picture_file = os.path.join(os.path.dirname(__file__), - # hide_options_picture_file_name) - - #self.hide_options_picture = tk.PhotoImage( - # file=hide_options_picture_file - #) - - #self.show_options_button = None - - self.status_label = None - #self.name_label = None - self.name_text = None - self.estimated_cost_frame = None - self.bytes_downloaded_label = None - self.button_frame = None - self.download_buttons = [] - self.option_frames = [] - self.name_font = None - self.description_text = None - #self.description_label = None - self.file_name_frame = None - self.cost_frame = None - self.cost_description = None - self.remaining_cost_description = None - self.cost_label = None - self.remaining_cost_label = None - self.progress_frame = None - - def cancel(self): - if self.cancel_func is not None: - self.cancel_func() # pylint: disable=not-callable - self.stream_frame.destroy() - self.app.stream_removed() - - def _resize_text(self, text_widget): - actual_height = text_widget.tk.call(text_widget._w, "count", "-displaylines", "0.0", "end") - text_widget.config(height=int(actual_height)) - - def show_name(self, name): - self.name_font = tkFont.Font(size=16) - #self.name_label = ttk.Label( - # self.name_frame, text=name, font=self.name_font - #) - #self.name_label.grid(row=0, column=0, sticky=tk.W) - self.name_text = tk.Text( - self.name_frame, font=self.name_font, wrap=tk.WORD, relief=tk.FLAT, borderwidth=0, - highlightthickness=0, width=1, height=1 - ) - self.name_text.insert(tk.INSERT, name) - self.name_text.config(state=tk.DISABLED) - self.name_text.grid(row=0, column=0, sticky=tk.W+tk.E+tk.N+tk.S) - self.name_text.update() - self._resize_text(self.name_text) - - def show_description(self, description): - #if os.name == "nt": - # wraplength = 580 - #else: - # wraplength = 600 - #self.description_label = ttk.Label( - # self.name_frame, text=description, wraplength=wraplength - #) - #self.description_label.grid(row=1, column=0, sticky=tk.W) - self.description_text = tk.Text( - self.name_frame, wrap=tk.WORD, relief=tk.FLAT, borderwidth=0, - highlightthickness=0, width=1, height=1 - ) - self.description_text.insert(tk.INSERT, description) - self.description_text.config(state=tk.DISABLED) - self.description_text.grid(row=1, column=0, sticky=tk.W+tk.E+tk.N+tk.S) - self.description_text.update() - self._resize_text(self.description_text) - - def show_thumbnail(self, url=None): - thumbnail = None - if url is not None: - import urllib2 - import base64 - response = urllib2.urlopen(url) - thumbnail_data = base64.b64encode(response.read()) - thumbnail = tk.PhotoImage(data=thumbnail_data) - current_width = thumbnail.width() - current_height = thumbnail.height() - max_width = 130 - max_height = 90 - scale_ratio = max(1.0 * current_width / max_width, 1.0 * current_height / max_height) - if scale_ratio < 1: - scale_ratio = 1 - else: - scale_ratio = int(scale_ratio + 1) - thumbnail = thumbnail.subsample(scale_ratio) - if thumbnail is not None: - label = ttk.Label(self.name_frame, image=thumbnail) - label.safekeeping = thumbnail - label.grid(row=0, column=1, rowspan=2, sticky=tk.E+tk.N+tk.W+tk.S) - label.update() - self.description_text.update() - self.name_text.update() - self._resize_text(self.description_text) - self._resize_text(self.name_text) - - def show_metadata_status(self, value): - if self.status_label is None: - self.status_label = ttk.Label( - self.metadata_frame, text=value - ) - self.status_label.grid() - self.metadata_frame.grid_columnconfigure(0, weight=1) - else: - self.status_label.config(text=value) - - @staticmethod - def get_formatted_stream_size(stream_size): - if isinstance(stream_size, (int, long)): - if stream_size >= 2**40: - units = "TB" - factor = 2**40 - elif stream_size >= 2**30: - units = "GB" - factor = 2**30 - elif stream_size >= 2**20: - units = "MB" - factor = 2**20 - elif stream_size >= 2**10: - units = "KB" - factor = 2**10 - else: - return str(stream_size) + " B" - return "%.1f %s" % (round((stream_size * 1.0 / factor), 1), units) - return stream_size - - def show_stream_metadata(self, stream_name, stream_size, stream_cost): - if self.status_label is not None: - self.status_label.destroy() - - self.file_name_frame = ttk.Frame(self.metadata_frame, style="F.TFrame") - self.file_name_frame.grid(row=0, column=0, sticky=tk.W) - self.metadata_frame.grid_columnconfigure(0, weight=1, uniform="metadata") - - file_size_label = ttk.Label( - self.file_name_frame, - text=self.get_formatted_stream_size(stream_size) - ) - file_size_label.grid(row=0, column=2) - - file_name_label = ttk.Label( - self.file_name_frame, - text=" - " + stream_name, - ) - file_name_label.grid(row=0, column=3) - - self.estimated_cost_frame = ttk.Frame(self.metadata_frame, style="F.TFrame") - self.estimated_cost_frame.grid(row=1, column=0, sticky=tk.W) - - estimated_cost_label = ttk.Label( - self.estimated_cost_frame, - text=locale.format_string("%.2f LBC", - (round(stream_cost, 2)), grouping=True), - foreground="red" - ) - estimated_cost_label.grid(row=1, column=2) - - self.button_frame = ttk.Frame(self.outer_button_frame, style="E.TFrame") - self.button_frame.grid(row=0, column=1) - - self.outer_button_frame.grid_columnconfigure(0, weight=1, uniform="buttons") - self.outer_button_frame.grid_columnconfigure(1, weight=2, uniform="buttons1") - self.outer_button_frame.grid_columnconfigure(2, weight=1, uniform="buttons") - - def add_download_factory(self, factory, download_func): - download_button = ttk.Button( - self.button_frame, text=factory.get_description(), command=download_func, - style='LBRY.TButton', cursor=self.button_cursor - ) - self.download_buttons.append(download_button) - download_button.grid(row=0, column=len(self.download_buttons) - 1, padx=5, pady=(1, 2)) - - def disable_download_buttons(self): - for download_button in self.download_buttons: - download_button.config(state=tk.DISABLED) - - def remove_download_buttons(self): - for download_button in self.download_buttons: - download_button.destroy() - self.download_buttons = [] - - def get_option_widget(self, option_type, option_frame): - if option_type.value == float: - entry_frame = ttk.Frame( - option_frame, - style="H.TFrame" - ) - entry_frame.grid() - col = 0 - if option_type.short_description is not None: - entry_label = ttk.Label( - entry_frame, - #text=option_type.short_description - text="" - ) - entry_label.grid(row=0, column=0, sticky=tk.W) - col = 1 - entry = ttk.Entry( - entry_frame, - width=10, - style="Float.TEntry" - ) - entry_frame.entry = entry - entry.grid(row=0, column=col, sticky=tk.W) - return entry_frame - if option_type.value == bool: - bool_frame = ttk.Frame( - option_frame, - style="H.TFrame" - ) - bool_frame.chosen_value = tk.BooleanVar() - true_text = "True" - false_text = "False" - if option_type.bool_options_description is not None: - true_text, false_text = option_type.bool_options_description - true_radio_button = ttk.Radiobutton( - bool_frame, text=true_text, variable=bool_frame.chosen_value, value=True - ) - true_radio_button.grid(row=0, sticky=tk.W) - false_radio_button = ttk.Radiobutton( - bool_frame, text=false_text, variable=bool_frame.chosen_value, value=False - ) - false_radio_button.grid(row=1, sticky=tk.W) - return bool_frame - label = ttk.Label( - option_frame, - text="" - ) - return label - - def show_download_options(self, options): - left_padding = 20 - for option in options: - f = ttk.Frame( - self.options_frame, - style="E.TFrame" - ) - f.grid(sticky=tk.W + tk.E, padx=left_padding) - self.option_frames.append((option, f)) - description_label = ttk.Label( - f, - text=option.long_description - ) - description_label.grid(row=0, sticky=tk.W) - if len(option.option_types) > 1: - f.chosen_type = tk.IntVar() - choices_frame = ttk.Frame( - f, - style="F.TFrame" - ) - f.choices_frame = choices_frame - choices_frame.grid(row=1, sticky=tk.W, padx=left_padding) - choices_frame.choices = [] - for i, option_type in enumerate(option.option_types): - choice_frame = ttk.Frame( - choices_frame, - style="G.TFrame" - ) - choice_frame.grid(sticky=tk.W) - option_text = "" - if option_type.short_description is not None: - option_text = option_type.short_description - option_radio_button = ttk.Radiobutton( - choice_frame, text=option_text, variable=f.chosen_type, value=i - ) - option_radio_button.grid(row=0, column=0, sticky=tk.W) - option_widget = self.get_option_widget(option_type, choice_frame) - option_widget.grid(row=0, column=1, sticky=tk.W) - choices_frame.choices.append(option_widget) - if i == 0: - option_radio_button.invoke() - else: - choice_frame = ttk.Frame( - f, - style="F.TFrame" - ) - choice_frame.grid(sticky=tk.W, padx=left_padding) - option_widget = self.get_option_widget(option.option_types[0], choice_frame) - option_widget.grid(row=0, column=0, sticky=tk.W) - f.option_widget = option_widget - #self.show_options_button = ttk.Button( - # self.stream_frame_body, command=self._toggle_show_options, style="Stop.TButton", - # cursor=self.button_cursor - #) - #self.show_options_button.config(image=self.show_options_picture) - #self.show_options_button.grid(sticky=tk.W, row=2, column=0) - - def _get_chosen_option(self, option_type, option_widget): - if option_type.value == float: - return float(option_widget.entry.get()) - if option_type.value == bool: - return option_widget.chosen_value.get() - return option_type.value - - def get_chosen_options(self): - chosen_options = [] - for o, f in self.option_frames: - if len(o.option_types) > 1: - chosen_index = f.chosen_type.get() - option_type = o.option_types[chosen_index] - option_widget = f.choices_frame.choices[chosen_index] - chosen_options.append(self._get_chosen_option(option_type, option_widget)) - else: - option_type = o.option_types[0] - option_widget = f.option_widget - chosen_options.append(self._get_chosen_option(option_type, option_widget)) - return chosen_options - - #def _toggle_show_options(self): - # if self.options_frame.winfo_ismapped(): - # self.show_options_button.config(image=self.show_options_picture) - # self.options_frame.grid_forget() - # else: - # self.show_options_button.config(image=self.hide_options_picture) - # self.options_frame.grid(sticky=tk.W + tk.E, row=3) - - def show_progress(self, total_bytes, bytes_left_to_download, points_paid, - points_remaining): - if self.bytes_downloaded_label is None: - self.remove_download_buttons() - self.button_frame.destroy() - self.estimated_cost_frame.destroy() - for option, frame in self.option_frames: - frame.destroy() - self.options_frame.destroy() - #self.show_options_button.destroy() - - self.progress_frame = ttk.Frame(self.outer_button_frame, style="F.TFrame") - self.progress_frame.grid(row=0, column=0, sticky=tk.W, pady=(0, 8)) - - self.bytes_downloaded_label = ttk.Label( - self.progress_frame, - text="" - ) - self.bytes_downloaded_label.grid(row=0, column=0) - - self.cost_frame = ttk.Frame(self.outer_button_frame, style="F.TFrame") - self.cost_frame.grid(row=1, column=0, sticky=tk.W, pady=(0, 4)) - - self.cost_label = ttk.Label( - self.cost_frame, - text="", - foreground="red" - ) - self.cost_label.grid(row=0, column=1, padx=(1, 0)) - self.outer_button_frame.grid_columnconfigure(2, weight=0, uniform="") - - if self.bytes_downloaded_label.winfo_exists(): - percent_done = 0 - if total_bytes > 0: - percent_done = 100.0 * (total_bytes - bytes_left_to_download) / total_bytes - percent_done_string = locale.format_string(" (%.2f%%)", percent_done) - self.bytes_downloaded_label.config( - text=self.get_formatted_stream_size(total_bytes - bytes_left_to_download) + percent_done_string - ) - if self.cost_label.winfo_exists(): - total_points = points_remaining + points_paid - self.cost_label.config(text=locale.format_string("%.2f/%.2f LBC", - (round(points_paid, 2), round(total_points, 2)), - grouping=True)) - - def show_download_done(self, total_points_paid): - if self.bytes_downloaded_label is not None and self.bytes_downloaded_label.winfo_exists(): - self.bytes_downloaded_label.destroy() - if self.cost_label is not None and self.cost_label.winfo_exists(): - self.cost_label.config(text=locale.format_string("%.2f LBC", - (round(total_points_paid, 2),), - grouping=True)) diff --git a/lbrynet/lbrynet_gui/__init__.py b/lbrynet/lbrynet_gui/__init__.py deleted file mode 100644 index f58b75595..000000000 --- a/lbrynet/lbrynet_gui/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""A gui application for downloading LBRY files from LBRYnet""" \ No newline at end of file diff --git a/lbrynet/lbrynet_gui/close.gif b/lbrynet/lbrynet_gui/close.gif deleted file mode 100644 index 9c9e74a2f7245c16c9919dbca7d69f54d91ed555..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 188 zcmZ?wbhEHb6krfwXklRZ|NnnzXsCmO!=FEYe*XLkB!MEoe*FTHzP`SGetr-V$N+-$ z^z`!b@`i?ny1Kgh`uf`1+RDmGpa@V+R8$m@1nNQqia%KxxftXbbQpjDWG4fwD0S=G1A?g4C1m;OZ zK~zY`rIuNUTtyUyzf*O)Z{MEoS-LYxW|9mTV}g#8hzjOG#3ky3h>BtqM+q*VxWFWX z#JGzA#iu|-1W^!|2Q{E?g8HVQafwl*5{EcL49S>fdS*=DwT=(BdlrK)T5za3x2n#m z`p!pZ5wM^MVE&^>+sLSAb7S8Ql(}%?G`B$YnbqYjrJ;+~Iu)x8C{Xgv;hIat2vyyw<3f-euum7z4;EoNp+G)fZaEdDARumNU zpJS&$egIIiFr@j*uj$^WpWkznvKhbvyg5`G>3rz%Vd<)gCNWtU;-ZKw%KKcxTn|HB z6yl<0>szy>hlmJ;dbjE7t*`0(`nw-Lgdg#3{7Ks)}e! zce>zainjKk4iQ6PNq2EY!E7LLA9}rDqA2p>6UV_XLQohGQuFxH!gr)@0D&^U%d zA&06pug;N7Pm2@^MUn{50paP0nelOE5Bxwpc?#tHq=P2TwLk2hC zuDyZNFTX@b)NIWuwxdF^e*iK0TF(YBedGuy_8%aPQp7-(Kz;MA^o>4+UtdDxcQF3! zW0YMUVDFNR=sk}xojJ_ycQdu?MXF(h0IDaE-zE_CatHWDW=~C%Mj1{8GDs7p?~+UL z>q|)gn&q5(?aL1!mhlcRB?qD{KF(P>9p3gC_1DnNjH#7hyXIKtV21zP7ZPCsO zUyY7pmapKfO`FKFjQ-)f_;ad3`SL3`H8q6@5Jimb-c2xhlF-iO7#0ve*G;OBI8Zfn zB5DYtl&|i)k3*k)!fY6FbYgk)cIh+b5Yuj-Fkvw@eZp4j@Fx6;)K+*T9 zbXV!D1gv}RX(rdK<@YC^prX3y79#MMq`9*=%aJKrOW=6~o=3?T3hURCjf~KUBSfI6 z%GmZD*q?u9*~(RP2Ni}MewZ~cynvQUb9)2NqvTnPX$LuVs4d`EJ1KSMQVaszmMu)D zX70Ics5)OzrYZB+^- zeEV(u;bF91qxr!H42+G@yL2h4%8_TDrRbMfe%WPwwrv~bBq6BR=U`HoVve229aMmx zN{OIW!&+|+$K}pZ*t3TdS;op{liI3Ph~y?pS;o76ur^6@c>A@^;0MG%f0;Ie93@VlZmt~m#{xjxW5@A{ny+6Qpbrh4JRztkP8H?vA z+j7XY)Tc+Y5vAKUHm72J1dk{MZwLG>pGLJo(M`8QufJd3@J(F3R*)LHsXb b{)yP%K4@)8UH7Vx00000NkvXXu0mjf?%t3< diff --git a/lbrynet/lbrynet_gui/close2.gif b/lbrynet/lbrynet_gui/close2.gif deleted file mode 100644 index 5d1cf62e5bbee8325e87a5c875c09e095e6d0245..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 195 zcmZ?wbhEHbjEB+I7E=o--Nlj5G&n(GMaQE~LU{L(Y!pOzI$e;sK W1X9kx#MRQ@D7lMQGAB)p!5RR$IuU39 diff --git a/lbrynet/lbrynet_gui/gui.py b/lbrynet/lbrynet_gui/gui.py deleted file mode 100644 index 2457d52ea..000000000 --- a/lbrynet/lbrynet_gui/gui.py +++ /dev/null @@ -1,33 +0,0 @@ -import logging -import sys - -from lbrynet.lbrynet_gui.GuiApp import DownloaderApp -from twisted.internet import reactor, task -import locale - - -def start_gui(): - - log_format = "(%(asctime)s)[%(filename)s:%(lineno)s] %(funcName)s(): %(message)s" - formatter = logging.Formatter(log_format) - - logger = logging.getLogger() - logger.setLevel(logging.DEBUG) - file_handler = logging.FileHandler("gui.log") - file_handler.setFormatter(formatter) - file_handler.addFilter(logging.Filter("lbrynet")) - logger.addHandler(file_handler) - - sys.stdout = open("gui.out.log", 'w') - sys.stderr = open("gui.err.log", 'w') - - locale.setlocale(locale.LC_ALL, '') - - app = DownloaderApp() - - d = task.deferLater(reactor, 0, app.start) - - reactor.run() - -if __name__ == "__main__": - start_gui() \ No newline at end of file diff --git a/lbrynet/lbrynet_gui/hide_options.gif b/lbrynet/lbrynet_gui/hide_options.gif deleted file mode 100644 index 9c95b5a9d3692981617338438f7cec1564c41569..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 174 zcmZ?wbhEHb^k(2^XkcUjg8%>jEB+I7E=o--Nlj5G&n(GMaQE~LU{L(Y!pOzI$e;sK z1X9kxRM6AE^7LE&WEO{8)hninl=9fc8(5`wm_5Iq+jcB0^5&&=TH34M^nLR=^|iw3 z&daIJT8)nU6JJ($C7e_>kt~kdd^@`7Rc_YN)ABRD)2t3!e+{Tz=d!RsAXK{U{DwpS Z48Ml5T3GyWujokZ*m2@UM<4@(H2^-OMoa(z diff --git a/lbrynet/lbrynet_gui/lbry-dark-242x80.gif b/lbrynet/lbrynet_gui/lbry-dark-242x80.gif deleted file mode 100755 index 65ce976d432bff3f581ba5a0519ca31d18d6f63e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3647 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

U2Ho%8h$Z1A zG$&fKvf6nH2hia^<;@=ILde_^3Frd3||Y5KqREpNm?8O-^1K zA?vt8J5O22unClKuuzfXx?G2iqf7{pUuN$3d*E{depSut%?Av7kPox|%>m%lbcJ?2 zP`23f0t~;P?HV$nuB^F2eOzeBr7MvErX*fJYU*=%7^>0!Zd|S9;%)KYy(FCa<3+RM zsKi47*W}Gw4i9+cM3)Zp`p<0dQvl4PAnyV`Qg8ckAyD6;uI8`Lh9CUj(Gtn+&U}&a}CEH3gITc`lQH+#>h0=7-Ksu?j~8ND=Z_J9d_uN+ z3_jEmP~E}MZuCNV*!V`8zdq5!fnye$pO^2%Oa&FmwQ%cVl;J7xaJaG;@d9l1_<;dW zc{XVV+!Frt2vBlrz>%MA*msGs#EgV{*+lG1xgCsw9bU00)umi3UFaUqT$vqsERx~r z(N|h)GyffsjdxG;&^Pt@k%vjYxi4cgAz*>MTBlqNpY_p6y`*>fB@?pVw~_aU_&w~f zK~wt6<0Fc^yFPaBQU~cT6Y*4T`<~jW+&8F9BpCtu3BZ4K=4Fi&FP%jvAc^fX8aMr< z7>ALUry>l4dN5hzeX zMv64l=U0zm>AXJQg1baxkE_P5^T8ZxVf7*pxxZqpn>+;=>2_$x(3qpiMSannUhA%# zyViQ4mSZ^}8A5*IBy=kU#}jaeob}9+1~P-e38}Vv(sZnV|AKZ|ru=C+`EvKKxY?U1 zHQEog&b(**V2T+5dt|I;xaNE&zKX1JEGy7Jx~7Je*?{Z^1&^iAwYpb6_$(oioL*Io|yrC_lS zVv0jRmwdcRbI`7#LKZS40gWD-_i|YFDdea_;|qnX<)f4PQNm(zzvp+N4m!bMB7feb zqb$-m@WVp~_9C?2ARIQqNL!=_2}(F`E1UBGQ;V62IC{P;^5%OWEK8~!huz$N0J@k8 zS7wg6VP37z*g*5Tjv#vC{*Hl>NP!0X@$vS`=Y$35O7dJ+9^XO6hP{|IFQ@RJcO|{)R+JxGZEt&w z(tnNSy=VlYdGkZj#>P)~t97$QAkWsnq1$N19`JOp3;f}`?tT)Lr?YjAhcHO1j7?TP zuprSr9uW-33_Iqdla%zB^1dLNu^<5ryIZodU80}M?pgX>n_N8XcT6CSyFZh!D`xhP zhPscvs%Ntg*C>bX7!Zxg`#AA`tdkdc*K9HUIO;~MyJ5lFTe&mdqANo;avOx_Iy>hOcgTK4G2hk?TawhMV zMVhwYlpmou*;<$Q^W$Aodb8)mh94st0$3TzyyjRn1@eCMpGSwr`fa(AzC>V#LhMM~ zj$fEJn7y@$KbAg8eq-~va|RyE&vEPCeA?7sEf2O@ll8jX=B>OjpbI6GuBSF^&?MLR zz5?7wMMYXd8?s^9uQXx5 zT#x>%wVm8-Y)45N5*PWIc!JApKCxdXMW^ntjFT4Kp2ZAE+qMTuizW``7S%6RB=l!7 z1nPVaSX|gx-P|6+Z=d&?t|$$f==T11+;C&lsh!Fb1U5 zsw|IqM-7|gBs6}QkLB}>G{}adwQv~|irD>Aq9a|6tgx$_hX&0IM>7;bR_gYyAP>ohssYrPI$vYuNSgD$MfnY;u!WEF(KD)jyQ2odCm541l$j>_| z(!%%PSRm)7@0a5hJ_w#|Y#fz033eVkD&?zCfz{Q2S@n7E_BOXc@_oQtBWARj`~(S7 z{N0nF*7;D#vf}=e>@nd@*s5#IuCJ1`V;R(v2Mkdr|zza7(a#qRQ>)+#*O#W@d67?;!#=PhpP&E-Q4@?op z8&`;SJY0>-g@P2YCmlDo?c@xo!QKBe`RQ?q=ijOA}{VnBzaDqkqLI zX}yTv2wJFRxT1Rg^m{t?j#5`MqRSgJrthzO|?lI?|S zZ1%YI2Ee#IeZ4h;)&!dI_Uk)42TrX)&3B>kfjRr#Z6NUPeM$;9C_EK0*|DY4;Jkqd zSjT6e zTW=qByF*KmXq_tZ_f!1hPLLebOvt?^-Fgv^SBtZEt9beE-SXFjYH$kh@GKpv3`&0i z?$82gSGwL|}F=8#)UY<;Z z0KK5VY>GOQGWBE7XrL@oTy|oRpdFH;q140X?SQCj+J zH@Rff6Rh=>mDqjt5*An|Qj#8(@rmE$Sx%Wz2QVx>;rW`^Jjt=RhB>!Oz-+gLK1P?xzUj)Lylo^sS*pXXq#Ae^k5+nBDzSY_@$B#mLGzO=Oi050@rrx$yi34 zSCC)hJZ1i)Vnx{Azs-Dd0fQw?4neK#Pl7T<3YWQB%J@%)BeCCngitM+BQ{Bf z4O1Nknco^Y5K2T^B?GF6D6Pt%VvwPw(-Y9B|EEyOr&Z0{rV2WeAtb`XC$st!Vt#qo z`)^&J$-wB0b)|3j2rAj=dYlNbiXfBW%@uOD``-}JYa)NZym4dPqkeh1;_e|5T5E)q z=B#cUjQ~rqcHzZ}GPFS>6ZNGS((xI|7?lN)pZRR;&!EY5HFnuNMkEk;FVyw}u0w$l zwG|^BkB3hDB`eFJ(F=5x5I1GzVdn2g-erUd5GbD(@_iIZQT{wQF!XsI*z?+L!J=vM z@l)BEo%*-k;Uv~huJiIpPPgJLj~1dZR5!0>2q}nR7T}1p`przuE~dWcgXGP(8T)o- zw&F<5x_`xn^=J+XnPOU&%pl%Qiy54lA2ecbVP}d3OtMvla{)c3t*qk_BbHS=E_Sm+ zT0!5Vx@uPQw2b(j(f^qs;NVZ}U|clxxEKK5T@j}&Nz zaY=EVGO`Ph)|o1M&rbc7Do+&xj)yUFAGIReX=pfnGnG8_hsLbTPqW!rD=vrSu8F>F zOl58Vm!7&GH>(--_l_}*Ypr#!5@QkMVpOqiGa%~@??z{FQO6QTiRUPI3@&GhWLpr_ zH=02NQFhi2dcMGZrCIXlOw@$sbl#YuzX`eP=)Fwzz@I( zyY*g%14?=SSU)>D1{p#OK8KOg%JOhJ3vA82Yx@D1H*T<_i^ctOrN=Lp7>^3GBOzq`?Y|b}39d^*Efs2#}&JrpL@STWv zBpz_j)qZ}BqFLFx@?HO^1)QLqE>RBoPk|^!(4LSnmE+vh@*t3stcQlVX_i?P;gnr$ z9KoYbm7wEu#?Te-8?D@$y~%1ekgY|h?E#K%I;pH2pU@5S4~Y{1pc?DD@$V0)-f({_9| z8yK3cthpICHhI_YvU$1p5R9h-2^!i0N!0DPyE7r+v>w>^C`cZb1yp?0w_X(=4=I^TTrd2wYD#SbC3EU_cuD2*!3q% z-QYN~+n_T@g8~QA-0`LDEdh(Low`ZY*$e6)Y|+QDq1~&YSFn$7(cgYpAkix5ovU1w zT3_sqE{k61dv2h>`-&JFDU5?21d^9d-H4&C`+rvbpDb5Owq)6hLAKR~Ja-PSH3Pu< z<)pvolugK}Z_E$>>%2sJqv4{XG0(*1n^buU*4#og5(qovR4?gGSc92 zfZVcC_htixcKbILOoT>nghp!?GK$9S9z=FPjvtul zK4bu=qyc+v}bG6kxWn6lG&!7V8iTF~=@8zEcqjxJh7A5x=2j!UZx5 z{VPNazym84(}km{V}_nS?IcHnKOf)*{ks5^3r#-EHAzfc0FU3eFip3qHg_vrNW|Qp zUZp_YT`QU*?2eN_4l=O{`&(uREWd4J;1_R%-Y6dEnY*K4aj1M9KT$NG9dNc6(%vov zi18LU1qkxh%8fqT9^d2dPAc2hK*nVg7ps9LVc{80eDijDGZHXAew^suc+qV54$>P( zv2Vv$hK_Kason@tIiyY=eD~(da7Uoa?! z4I86?Xr&jG<4rF8CTg9$$WGV)W$l5(Zt>5Qg^wuG#GbLsr+ySdSpHBXju{Htw(IL; zt+veeK%f#^3swP$3Xb!hgX9N?{dl&V;Kqgp#w|1-YppW*@HWY8B2sk2~SVbtmwY4#A{3Vlt17Twzg6{0|fM-kha z^OLzdm*H62Sl=$u91)uQR~tv5tXw~NyeZm^Gxhli>C(Kp24`8F16Hke(P3=o8;8n! zTy~hy3RMZ*JN=%I?5bF51BA>K(gB&S%knB0sS*+I2ViaGWm94M%!s|#b6_glULQKs z5j+l82Tq+~av@!Y+`btw(~ux@Zv+^?C5$Fg-6)qDV`i+UdI+T7xQ7z~>q7$ds}YC| zP8Ta`dY1!yv`pe=0Ep|N0=L?xE90 zbwN@3d}DO9X2o}qrGSx-0B}1$xSa&(^v0VVEc-JR<=XYRAT9kZOM101f9Rv*CX%3_ zAUI78f^h$Af#~BHg88ODyVms{XYs_}+-Fy1JgKW}X zN(xd)r^`>kjdaS@%2vmAgZ_6mI);FYbUb9F7FZMWEiN<+-1a}m&3pgl*rzD_+UgWa zpn}X6@ZmRR)D9DS^o<%I>qW;`Zg8F&V%f#SBA`|LyXmdIStSF8`X_5|Uh4RPi{BPe z!6WuGK;H#|`50I>(Ch{*wk!$yVq=n=tuH@7;S_7@;`Fz7#r6cpt~_Tq7y0149XPiy z1^}l(V=Lt%-Lgv9<4H&X#-EEc-X{|t@M00a-fFpuLVZL!r}@`ynyR0zm+&KG`u zxjud=EaCmOaeL9t*|z75ob!F5YuYs`g$KegymKBCcyb&kYZ~-nM!xla>|ypjZrxeFIdB=> zNgvyZ&-^ZG?TqneZ0u>a^|8f(IX^LR6FW&s#XsS{{~+y>w~Rww=Xc3M7HBpaEpqqk z3BpXWI)VDaiLBI>FUc{qFvJ>=w{xH^+Hs8h3#If9qk$G zsbMiQ$r(ABkjs9@ICB{%*=OH|5tjOS{I%Z@aXLWBb0?X!E1Nw;Bcc ztO88gH1C@O>-v&?vD@H`SP(^&W+^PK++6M-KMvBD($YVK6>OW6cXz|TgB$9y(e4qG ztRfZ6tlczaOJeR``rG#$EPDPfQAOVcJ!xp`<*Ps?Zq}IOc6EN4pBdE~mksy~mO8Xu zmcGrBQ*4+J0i@t2Vv*>7BVj|Dv>#PQ6{&yUjR)T2KZ!uGGQVLsV?B`rGmKNH+> z`__(_Xm`g<$(={pcUHa@n)PHA8#cIyDcH^L=j(6Z z;@W*p;4eNmdTWOA0K%G3Vu9}*CI=b(@ zWxyw6+qm})@BJ&P+a9m_lA3e-gWBZe0BNE0antTk%4a zfvc%{xspGV8@o1#m$uf&D6wg&KSOgCE?jtK);Asbyz2Iw#(&~A-e~`9aK0}2%&&j* zqeXyvfNN8?mG4OiTJSb*?fU1FZL2MRy)LwwJNL_%FMpmFvuw*XPHO3XVVHb#&fKHB zOjk44N*_HoLlo%52Ac?h7)Bk12WOe|V%I+XTzkLp%@fVMt=lf$z3ba?y7anu?e9s7 zS#KnS;%6LZkG`*_rlmDu`r2cXH{;hG3TNo3Z~VYgAoHB_@Xf|;<^L|F?mKz%>5up6 zJMLX^*?zrSTZLtP>c=N50)6I79gogW*{GtaIbr(B12bFZy$53KjZGk zj~3bDY)^80d4GTAwk&+6Q~$4Y?#Ye9CnIK^dbZ~J-<#|k-{DRF*hykm@)RIXPiL z_kNilJQ~KwuB^Tu&T!%ZXA{pHYc}?Gmy_8K=Ua3a#sxprW=JVub-aJ@kVt|^fWQ)k z70hw0caF_i4GLMFUWFOFXBg@}7;{(^zBAj=V_5p?t81GF$CsbCQ*VD`xxH=sEq=9A zzS~XzuUR1st9;i O6N9I#pUXO@geCykjvm1P diff --git a/lbrynet/lbrynet_gui/show_options.gif b/lbrynet/lbrynet_gui/show_options.gif deleted file mode 100644 index c5ed0b6fd3e31d8c7aaa861a62e575434df31a2c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 155 zcmZ?wbhEHb^k(2^XkcUjg8%>jEB<6* Date: Fri, 1 Jul 2016 02:55:20 -0400 Subject: [PATCH 09/14] tanstaafl --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 6 +++--- lbrynet/lbrynet_daemon/LBRYPublisher.py | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index acd13939f..75203a9d0 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -427,9 +427,9 @@ class LBRYDaemon(jsonrpc.JSONRPC): else: d = defer.succeed(None) - if float(self.session.wallet.wallet_balance) == 0.0: - d.addCallback(lambda _: self._check_first_run()) - d.addCallback(self._show_first_run_result) + # if float(self.session.wallet.wallet_balance) == 0.0: + # d.addCallback(lambda _: self._check_first_run()) + # d.addCallback(self._show_first_run_result) d.addCallback(lambda _: _wait_for_credits() if self.requested_first_run_credits else _announce()) return d diff --git a/lbrynet/lbrynet_daemon/LBRYPublisher.py b/lbrynet/lbrynet_daemon/LBRYPublisher.py index 084731a87..c2ef94d1d 100644 --- a/lbrynet/lbrynet_daemon/LBRYPublisher.py +++ b/lbrynet/lbrynet_daemon/LBRYPublisher.py @@ -127,6 +127,7 @@ class Publisher(object): log.info(err.getTraceback()) message = "An error occurred publishing %s to %s. Error: %s." if err.check(InsufficientFundsError): + d = defer.succeed(True) error_message = "Insufficient funds" else: d = defer.succeed(True) From 1cedd8ee5bd62c3e387a7c534ad2cb241eb56016 Mon Sep 17 00:00:00 2001 From: Jack Date: Fri, 1 Jul 2016 03:16:08 -0400 Subject: [PATCH 10/14] update name --- lbrynet/core/LBRYcrdWallet.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lbrynet/core/LBRYcrdWallet.py b/lbrynet/core/LBRYcrdWallet.py index 970072f77..eea9ef915 100644 --- a/lbrynet/core/LBRYcrdWallet.py +++ b/lbrynet/core/LBRYcrdWallet.py @@ -410,8 +410,8 @@ class LBRYWallet(object): def update_name(self, name, value, amount): d = self._get_value_for_name(name) - d.addCallback(lambda r: (self._update_name(json.loads(r)['txid'], json.dumps(value), amount), json.loads(r)['txid'])) - d.addCallback(lambda new_txid, old_txid: self._update_name_metadata(name, value['sources']['lbry_sd_hash'], old_txid, new_txid)) + d.addCallback(lambda r: (self._update_name(r['txid'], json.dumps(value), amount), r['txid'])) + d.addCallback(lambda (new_txid, old_txid): self._update_name_metadata(name, value['sources']['lbry_sd_hash'], old_txid, new_txid)) return d def get_name_and_validity_for_sd_hash(self, sd_hash): @@ -529,7 +529,7 @@ class LBRYWallet(object): return d def _update_name_metadata(self, name, sd_hash, old_txid, new_txid): - d = self.db.runQuery("delete from name_metadata where txid=? and sd_hash=?", (old_txid, sd_hash)) + d = self.db.runQuery("delete * from name_metadata where txid=? and sd_hash=?", (old_txid, sd_hash)) d.addCallback(lambda _: self.db.runQuery("insert into name_metadata values (?, ?, ?)", (name, new_txid, sd_hash))) d.addCallback(lambda _: new_txid) return d @@ -864,7 +864,7 @@ class LBRYcrdWallet(LBRYWallet): def _update_name_rpc(self, txid, value, amount): rpc_conn = self._get_rpc_conn() - return rpc_conn.updatename(txid, value, amount) + return rpc_conn.updateclaim(txid, value, amount) @_catch_connection_error def _send_name_claim_rpc(self, name, value, amount): From e4f14d871f091e867a2b0f7c5defe809e6f73172 Mon Sep 17 00:00:00 2001 From: Jack Date: Fri, 1 Jul 2016 03:36:51 -0400 Subject: [PATCH 11/14] update setup.py --- setup.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/setup.py b/setup.py index baf069fa7..eb6a6095e 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,6 @@ console_scripts = ['lbrynet-console = lbrynet.lbrynet_console.LBRYConsole:launch 'lbrynet-launch-node = lbrynet.dht.node:main', 'lbrynet-launch-rpc-node = lbrynet.rpc_node:main', 'lbrynet-rpc-node-cli = lbrynet.node_rpc_cli:main', - 'lbrynet-gui = lbrynet.lbrynet_gui.gui:start_gui', 'lbrynet-lookup-hosts-for-hash = lbrynet.dht_scripts:get_hosts_for_hash_in_dht', 'lbrynet-announce_hash_to_dht = lbrynet.dht_scripts:announce_hash_to_dht', 'lbrynet-daemon = lbrynet.lbrynet_daemon.LBRYDaemonControl:start', @@ -28,10 +27,6 @@ requires = ['pycrypto', 'twisted', 'miniupnpc', 'yapsy', 'seccure', 'python-bitcoinrpc==0.1', 'txJSON-RPC', 'requests>=2.4.2', 'unqlite==0.2.0', 'leveldb', 'lbryum', 'jsonrpc', 'simplejson', 'appdirs', 'six==1.9.0', 'base58'] -gui_data_files = ['close2.gif', 'lbry-dark-242x80.gif', 'lbry-dark-icon.xbm', 'lbry-dark-icon.ico', - 'drop_down.gif', 'show_options.gif', 'hide_options.gif', 'lbry.conf', 'lbry.png'] -gui_data_paths = [os.path.join(base_dir, 'lbrynet', 'lbrynet_gui', f) for f in gui_data_files] - setup(name='lbrynet', description='A fully-decentralized content marketplace', version=__version__, @@ -47,7 +42,6 @@ setup(name='lbrynet', 'blindrepeater.yapsy-plugin') ] ), - ('lbrynet/lbrynet_gui', gui_data_paths) ], dependency_links=['https://github.com/lbryio/lbryum/tarball/master/#egg=lbryum'], ) From 808e3ea6e3673559c612154b95b72a8b06c33133 Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 4 Jul 2016 04:57:30 -0400 Subject: [PATCH 12/14] rename LBRYcrdWallet.py to LBRYWallet.py and change default wallet to lbrycrd --- lbrynet/conf.py | 2 +- lbrynet/core/{LBRYcrdWallet.py => LBRYWallet.py} | 0 lbrynet/core/PTCWallet.py | 2 +- lbrynet/lbrynet_console/LBRYConsole.py | 2 +- lbrynet/lbrynet_daemon/LBRYDaemon.py | 5 +++-- 5 files changed, 6 insertions(+), 5 deletions(-) rename lbrynet/core/{LBRYcrdWallet.py => LBRYWallet.py} (100%) diff --git a/lbrynet/conf.py b/lbrynet/conf.py index 9ead15f4d..d984f0454 100644 --- a/lbrynet/conf.py +++ b/lbrynet/conf.py @@ -38,7 +38,7 @@ API_CONNECTION_STRING = "http://%s:%i/%s" % (API_INTERFACE, API_PORT, API_ADDRES UI_ADDRESS = "http://%s:%i" % (API_INTERFACE, API_PORT) PROTOCOL_PREFIX = "lbry" -DEFAULT_WALLET = "lbryum" +DEFAULT_WALLET = "lbrycrd" WALLET_TYPES = ["lbryum", "lbrycrd"] DEFAULT_TIMEOUT = 30 DEFAULT_MAX_SEARCH_RESULTS = 25 diff --git a/lbrynet/core/LBRYcrdWallet.py b/lbrynet/core/LBRYWallet.py similarity index 100% rename from lbrynet/core/LBRYcrdWallet.py rename to lbrynet/core/LBRYWallet.py diff --git a/lbrynet/core/PTCWallet.py b/lbrynet/core/PTCWallet.py index bf02a3b7e..38f187034 100644 --- a/lbrynet/core/PTCWallet.py +++ b/lbrynet/core/PTCWallet.py @@ -12,7 +12,7 @@ from lbrynet.pointtraderclient import pointtraderclient from twisted.internet import defer, threads from zope.interface import implements from twisted.python.failure import Failure -from lbrynet.core.LBRYcrdWallet import ReservedPoints +from lbrynet.core.LBRYWallet import ReservedPoints log = logging.getLogger(__name__) diff --git a/lbrynet/lbrynet_console/LBRYConsole.py b/lbrynet/lbrynet_console/LBRYConsole.py index fee6c2faa..72e40d467 100644 --- a/lbrynet/lbrynet_console/LBRYConsole.py +++ b/lbrynet/lbrynet_console/LBRYConsole.py @@ -43,7 +43,7 @@ from lbrynet.lbrynet_console.ControlHandlers import ShowServerStatusFactory, Mod from lbrynet.lbrynet_console.ControlHandlers import ModifyLBRYFileOptionsChooserFactory, StatusFactory from lbrynet.lbrynet_console.ControlHandlers import PeerStatsAndSettingsChooserFactory, PublishFactory from lbrynet.lbrynet_console.ControlHandlers import BlockchainStatusFactory -from lbrynet.core.LBRYcrdWallet import LBRYcrdWallet, LBRYumWallet +from lbrynet.core.LBRYWallet import LBRYcrdWallet, LBRYumWallet log = logging.getLogger(__name__) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index 75203a9d0..4f4951876 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -45,7 +45,7 @@ from lbrynet.conf import DEFAULT_TIMEOUT, WALLET_TYPES from lbrynet.core.StreamDescriptor import StreamDescriptorIdentifier, download_sd_blob from lbrynet.core.Session import LBRYSession from lbrynet.core.PTCWallet import PTCWallet -from lbrynet.core.LBRYcrdWallet import LBRYcrdWallet, LBRYumWallet +from lbrynet.core.LBRYWallet import LBRYcrdWallet, LBRYumWallet from lbrynet.lbryfilemanager.LBRYFileManager import LBRYFileManager from lbrynet.lbryfile.LBRYFileMetadataManager import DBLBRYFileMetadataManager, TempLBRYFileMetadataManager # from lbryum import LOG_PATH as lbryum_log @@ -431,7 +431,8 @@ class LBRYDaemon(jsonrpc.JSONRPC): # d.addCallback(lambda _: self._check_first_run()) # d.addCallback(self._show_first_run_result) - d.addCallback(lambda _: _wait_for_credits() if self.requested_first_run_credits else _announce()) + # d.addCallback(lambda _: _wait_for_credits() if self.requested_first_run_credits else _announce()) + d.addCallback(lambda _: _announce()) return d log.info("[" + str(datetime.now()) + "] Starting lbrynet-daemon") From 568737e1234268a58270abfa15a6ad3d05f53d8b Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 4 Jul 2016 05:15:53 -0400 Subject: [PATCH 13/14] change lbryum version constant name --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index 4f4951876..f8262bfc3 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -516,7 +516,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): try: r = urlopen("https://raw.githubusercontent.com/lbryio/lbryum/master/lib/version.py").read().split('\n') version = next(line.split("=")[1].split("#")[0].replace(" ", "") - for line in r if "ELECTRUM_VERSION" in line) + for line in r if "LBRYUM_VERSION" in line) version = version.replace("'", "") log.info("remote lbryum " + str(version) + " > local lbryum " + str(lbryum_version) + " = " + str( version > lbryum_version)) From 7b9e3aace9fa471ec059be06ce62a77668c2c539 Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 4 Jul 2016 05:43:26 -0400 Subject: [PATCH 14/14] use lbrycrd --- lbrynet/lbrynet_daemon/LBRYDaemon.py | 2 +- lbrynet/lbrynet_daemon/LBRYDaemonServer.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemon.py b/lbrynet/lbrynet_daemon/LBRYDaemon.py index f8262bfc3..7549863ce 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemon.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemon.py @@ -143,7 +143,7 @@ class LBRYDaemon(jsonrpc.JSONRPC): isLeaf = True - def __init__(self, root, wallet_type=None): + def __init__(self, root, wallet_type="lbrycrd"): jsonrpc.JSONRPC.__init__(self) reactor.addSystemEventTrigger('before', 'shutdown', self._shutdown) diff --git a/lbrynet/lbrynet_daemon/LBRYDaemonServer.py b/lbrynet/lbrynet_daemon/LBRYDaemonServer.py index 74906da0b..0fae59410 100644 --- a/lbrynet/lbrynet_daemon/LBRYDaemonServer.py +++ b/lbrynet/lbrynet_daemon/LBRYDaemonServer.py @@ -183,7 +183,7 @@ class HostedLBRYFile(resource.Resource): class LBRYDaemonServer(object): def _setup_server(self, wallet): self.root = LBRYindex(os.path.join(os.path.join(data_dir, "lbry-ui"), "active")) - self._api = LBRYDaemon(self.root, wallet_type=wallet) + self._api = LBRYDaemon(self.root, wallet_type="lbrycrd") self.root.putChild("view", HostedLBRYFile(self._api)) self.root.putChild(API_ADDRESS, self._api) return defer.succeed(True)