From c8b2b7b279399ce9040428224eec8e2b10a4a1b3 Mon Sep 17 00:00:00 2001 From: Jimmy Kiselak Date: Thu, 27 Aug 2015 15:41:17 -0400 Subject: [PATCH] Downloader options in its own class, show options in gui downloader Put stream downloader options into its own class, and make stream downloader options global to the stream type rather than specific to each factory. Show downloader options in the lbrynet-downloader-gui. Make a class for downloader option choices, so that the descriptions can be displayed. In the console, if there are multiple choices for the download option, make it a list selected by its index. Make sure that the ConnectionManager closes properly when some of the connections fail to open (e.g. due to a host being down) --- lbrynet/core/DownloadOption.py | 14 +- lbrynet/core/StreamDescriptor.py | 20 +- lbrynet/core/client/ConnectionManager.py | 84 +++--- lbrynet/interfaces.py | 33 ++- lbrynet/lbryfile/client/LBRYFileDownloader.py | 26 +- lbrynet/lbryfile/client/LBRYFileOptions.py | 55 ++++ lbrynet/lbryfilemanager/LBRYFileDownloader.py | 19 +- lbrynet/lbryfilemanager/LBRYFileManager.py | 2 - lbrynet/lbrylive/LiveStreamCreator.py | 20 +- .../lbrylive/client/LiveStreamDownloader.py | 41 ++- lbrynet/lbrylive/client/LiveStreamOptions.py | 73 ++++++ lbrynet/lbrynet_console/ControlHandlers.py | 140 ++++++---- lbrynet/lbrynet_console/LBRYConsole.py | 2 + lbrynet/lbrynet_downloader_gui/downloader.py | 246 +++++++++++++++--- .../lbrynet_downloader_gui/hide_options.gif | Bin 0 -> 174 bytes .../lbrynet_downloader_gui/show_options.gif | Bin 0 -> 155 bytes setup.py | 2 + tests/functional_tests.py | 37 ++- 18 files changed, 583 insertions(+), 231 deletions(-) create mode 100644 lbrynet/lbryfile/client/LBRYFileOptions.py create mode 100644 lbrynet/lbrylive/client/LiveStreamOptions.py create mode 100644 lbrynet/lbrynet_downloader_gui/hide_options.gif create mode 100644 lbrynet/lbrynet_downloader_gui/show_options.gif diff --git a/lbrynet/core/DownloadOption.py b/lbrynet/core/DownloadOption.py index 0972cbe19..0e990902e 100644 --- a/lbrynet/core/DownloadOption.py +++ b/lbrynet/core/DownloadOption.py @@ -1,6 +1,16 @@ +class DownloadChoice(object): + def __init__(self, value, short_description, long_description, bool_options_description=None): + self.value = value + self.short_description = short_description + self.long_description = long_description + self.bool_options_description = bool_options_description + + class DownloadOption(object): - def __init__(self, option_types, long_description, short_description, default): + def __init__(self, option_types, long_description, short_description, default_value, + default_value_description): self.option_types = option_types self.long_description = long_description self.short_description = short_description - self.default = default \ No newline at end of file + self.default_value = default_value + self.default_value_description = default_value_description \ No newline at end of file diff --git a/lbrynet/core/StreamDescriptor.py b/lbrynet/core/StreamDescriptor.py index 1a33dbb64..04ed216ff 100644 --- a/lbrynet/core/StreamDescriptor.py +++ b/lbrynet/core/StreamDescriptor.py @@ -110,9 +110,10 @@ class StreamDescriptorIdentifier(object): """ def __init__(self): self._sd_info_validators = {} # {stream_type: IStreamDescriptorValidator} + self._stream_options = {} # {stream_type: IStreamOptions} self._stream_downloader_factories = defaultdict(list) # {stream_type: [IStreamDownloaderFactory]} - def add_stream_info_validator(self, stream_type, sd_info_validator): + def add_stream_type(self, stream_type, sd_info_validator, stream_options): """ This is how the StreamDescriptorIdentifier learns about new types of stream descriptors. @@ -126,9 +127,13 @@ class StreamDescriptorIdentifier(object): will then be called. If the validation step fails, an exception will be thrown, preventing the stream descriptor from being further processed. + @param stream_options: A class implementing the IStreamOptions interface. This class's constructor will be + passed the sd_info_validator object containing the raw metadata from the stream descriptor file. + @return: None """ self._sd_info_validators[stream_type] = sd_info_validator + self._stream_options[stream_type] = stream_options def add_stream_downloader_factory(self, stream_type, factory): """ @@ -167,14 +172,23 @@ class StreamDescriptorIdentifier(object): assert stream_type in self._sd_info_validators, "Unrecognized stream type: " + str(stream_type) return self._sd_info_validators[stream_type] + def _get_options(self, stream_type): + assert stream_type in self._sd_info_validators, "Unrecognized stream type: " + str(stream_type) + return self._stream_options[stream_type] + def _return_info_and_factories(self, sd_info): assert 'stream_type' in sd_info, 'Invalid stream descriptor. No stream_type parameter.' stream_type = sd_info['stream_type'] - factories = self._get_factories(stream_type) validator = self._get_validator(stream_type)(sd_info) + factories = [f for f in self._get_factories(stream_type) if f.can_download(validator)] + d = validator.validate() - d.addCallback(lambda _: (validator, factories)) + def get_options(): + options = self._get_options(stream_type) + return validator, options, factories + + d.addCallback(lambda _: get_options()) return d diff --git a/lbrynet/core/client/ConnectionManager.py b/lbrynet/core/client/ConnectionManager.py index 6f8ce06b9..11602fcb4 100644 --- a/lbrynet/core/client/ConnectionManager.py +++ b/lbrynet/core/client/ConnectionManager.py @@ -7,49 +7,55 @@ from lbrynet.core.client.ClientProtocol import ClientProtocolFactory from lbrynet.core.Error import InsufficientFundsError +class PeerConnectionHandler(object): + def __init__(self, request_creators, factory): + self.request_creators = request_creators + self.factory = factory + self.connection = None + + class ConnectionManager(object): implements(interfaces.IConnectionManager) def __init__(self, downloader, rate_limiter, primary_request_creators, secondary_request_creators): self.downloader = downloader self.rate_limiter = rate_limiter - self.primary_request_creators = primary_request_creators - self.secondary_request_creators = secondary_request_creators - self.peer_connections = {} # {Peer: {'connection': connection, - # 'request_creators': [IRequestCreator if using this connection]}} - self.connections_closing = {} # {Peer: deferred (fired when the connection is closed)} - self.next_manage_call = None + self._primary_request_creators = primary_request_creators + self._secondary_request_creators = secondary_request_creators + self._peer_connections = {} # {Peer: PeerConnectionHandler} + self._connections_closing = {} # {Peer: deferred (fired when the connection is closed)} + self._next_manage_call = None def start(self): from twisted.internet import reactor - if self.next_manage_call is not None and self.next_manage_call.active() is True: - self.next_manage_call.cancel() - self.next_manage_call = reactor.callLater(0, self._manage) + if self._next_manage_call is not None and self._next_manage_call.active() is True: + self._next_manage_call.cancel() + self._next_manage_call = reactor.callLater(0, self._manage) return defer.succeed(True) def stop(self): - if self.next_manage_call is not None and self.next_manage_call.active() is True: - self.next_manage_call.cancel() - self.next_manage_call = None + if self._next_manage_call is not None and self._next_manage_call.active() is True: + self._next_manage_call.cancel() + self._next_manage_call = None closing_deferreds = [] - for peer in self.peer_connections.keys(): + for peer in self._peer_connections.keys(): def close_connection(p): logging.info("Abruptly closing a connection to %s due to downloading being paused", str(p)) - if self.peer_connections[p]['factory'].p is not None: - d = self.peer_connections[p]['factory'].p.cancel_requests() + if self._peer_connections[p].factory.p is not None: + d = self._peer_connections[p].factory.p.cancel_requests() else: d = defer.succeed(True) def disconnect_peer(): - self.peer_connections[p]['connection'].disconnect() - if p in self.peer_connections: - del self.peer_connections[p] d = defer.Deferred() - self.connections_closing[p] = d + self._connections_closing[p] = d + self._peer_connections[p].connection.disconnect() + if p in self._peer_connections: + del self._peer_connections[p] return d d.addBoth(lambda _: disconnect_peer()) @@ -62,7 +68,7 @@ class ConnectionManager(object): logging.debug("Trying to get the next request for peer %s", str(peer)) - if not peer in self.peer_connections: + if not peer in self._peer_connections: logging.debug("The peer has already been told to shut down.") return defer.succeed(False) @@ -75,11 +81,11 @@ class ConnectionManager(object): def check_if_request_sent(request_sent, request_creator): if request_sent is False: - if request_creator in self.peer_connections[peer]['request_creators']: - self.peer_connections[peer]['request_creators'].remove(request_creator) + if request_creator in self._peer_connections[peer].request_creators: + self._peer_connections[peer].request_creators.remove(request_creator) else: - if not request_creator in self.peer_connections[peer]['request_creators']: - self.peer_connections[peer]['request_creators'].append(request_creator) + if not request_creator in self._peer_connections[peer].request_creators: + self._peer_connections[peer].request_creators.append(request_creator) return request_sent def check_requests(requests): @@ -89,7 +95,7 @@ class ConnectionManager(object): def get_secondary_requests_if_necessary(have_request): if have_request is True: ds = [] - for s_r_c in self.secondary_request_creators: + for s_r_c in self._secondary_request_creators: d = s_r_c.send_next_request(peer, protocol) ds.append(d) dl = defer.DeferredList(ds) @@ -100,7 +106,7 @@ class ConnectionManager(object): ds = [] - for p_r_c in self.primary_request_creators: + for p_r_c in self._primary_request_creators: d = p_r_c.send_next_request(peer, protocol) d.addErrback(handle_error) d.addCallback(check_if_request_sent, p_r_c) @@ -112,11 +118,11 @@ class ConnectionManager(object): return dl def protocol_disconnected(self, peer, protocol): - if peer in self.peer_connections: - del self.peer_connections[peer] - if peer in self.connections_closing: - d = self.connections_closing[peer] - del self.connections_closing[peer] + if peer in self._peer_connections: + del self._peer_connections[peer] + if peer in self._connections_closing: + d = self._connections_closing[peer] + del self._connections_closing[peer] d.callback(True) def _rank_request_creator_connections(self): @@ -125,9 +131,9 @@ class ConnectionManager(object): connections open that it likes """ def count_peers(request_creator): - return len([p for p in self.peer_connections.itervalues() if request_creator in p['request_creators']]) + return len([p for p in self._peer_connections.itervalues() if request_creator in p.request_creators]) - return sorted(self.primary_request_creators, key=count_peers) + return sorted(self._primary_request_creators, key=count_peers) def _connect_to_peer(self, peer): @@ -136,10 +142,10 @@ class ConnectionManager(object): if peer is not None: logging.debug("Trying to connect to %s", str(peer)) factory = ClientProtocolFactory(peer, self.rate_limiter, self) + self._peer_connections[peer] = PeerConnectionHandler(self._primary_request_creators[:], + factory) connection = reactor.connectTCP(peer.host, peer.port, factory) - self.peer_connections[peer] = {'connection': connection, - 'request_creators': self.primary_request_creators[:], - 'factory': factory} + self._peer_connections[peer].connection = connection def _manage(self): @@ -162,16 +168,16 @@ class ConnectionManager(object): if peers is None: return None for peer in peers: - if not peer in self.peer_connections: + if not peer in self._peer_connections: logging.debug("Got a good peer. Returning peer %s", str(peer)) return peer logging.debug("Couldn't find a good peer to connect to") return None - if len(self.peer_connections) < MAX_CONNECTIONS_PER_STREAM: + if len(self._peer_connections) < MAX_CONNECTIONS_PER_STREAM: ordered_request_creators = self._rank_request_creator_connections() d = get_new_peers(ordered_request_creators) d.addCallback(pick_best_peer) d.addCallback(self._connect_to_peer) - self.next_manage_call = reactor.callLater(1, self._manage) \ No newline at end of file + self._next_manage_call = reactor.callLater(1, self._manage) \ No newline at end of file diff --git a/lbrynet/interfaces.py b/lbrynet/interfaces.py index 6eae6af0e..de5de679a 100644 --- a/lbrynet/interfaces.py +++ b/lbrynet/interfaces.py @@ -445,10 +445,7 @@ class IQueryHandlerFactory(Interface): """ -class IStreamDownloaderFactory(Interface): - """ - Construct IStreamDownloaders and provide options that will be passed to those IStreamDownloaders. - """ +class IStreamDownloaderOptions(Interface): def get_downloader_options(self, sd_validator, payment_rate_manager): """ Return the list of options that can be used to modify IStreamDownloader behavior @@ -459,8 +456,28 @@ class IStreamDownloaderFactory(Interface): @param payment_rate_manager: The payment rate manager currently in effect for the downloader @type payment_rate_manager: PaymentRateManager - @return: [(option_description, default)] - @rtype: [(string, string)] + @return: [DownloadOption] + @rtype: [DownloadOption] + """ + + +class IStreamDownloaderFactory(Interface): + """ + Construct IStreamDownloaders and provide options that will be passed to those IStreamDownloaders. + """ + + def can_download(self, sd_validator, payment_rate_manager): + """ + Decide whether the downloaders created by this factory can download the stream described by sd_validator + + @param sd_validator: object containing stream metadata + @type sd_validator: object which implements IStreamDescriptorValidator interface + + @param payment_rate_manager: The payment rate manager currently in effect for the downloader + @type payment_rate_manager: PaymentRateManager + + @return: True if the downloaders can download the stream, False otherwise + @rtype: bool """ def make_downloader(self, sd_validator, options, payment_rate_manager): @@ -470,10 +487,10 @@ class IStreamDownloaderFactory(Interface): @param sd_validator: object containing stream metadata which will be given to the IStreamDownloader @type sd_validator: object which implements IStreamDescriptorValidator interface - @param options: a list of strings that will be used by the IStreamDownloaderFactory to + @param options: a list of values that will be used by the IStreamDownloaderFactory to construct the IStreamDownloader. the options are in the same order as they were given by get_downloader_options. - @type options: [string] + @type options: [Object] @param payment_rate_manager: the PaymentRateManager which the IStreamDownloader should use. @type payment_rate_manager: PaymentRateManager diff --git a/lbrynet/lbryfile/client/LBRYFileDownloader.py b/lbrynet/lbryfile/client/LBRYFileDownloader.py index 9613ca76f..b3604b036 100644 --- a/lbrynet/lbryfile/client/LBRYFileDownloader.py +++ b/lbrynet/lbryfile/client/LBRYFileDownloader.py @@ -3,7 +3,6 @@ import binascii from zope.interface import implements -from lbrynet.core.DownloadOption import DownloadOption from lbrynet.lbryfile.StreamDescriptor import save_sd_info from lbrynet.cryptstream.client.CryptStreamDownloader import CryptStreamDownloader from lbrynet.core.client.StreamProgressManager import FullStreamProgressManager @@ -11,6 +10,7 @@ from lbrynet.interfaces import IStreamDownloaderFactory from lbrynet.lbryfile.client.LBRYFileMetadataHandler import LBRYFileMetadataHandler import os from twisted.internet import defer, threads, reactor +from distutils.spawn import find_executable class LBRYFileDownloader(CryptStreamDownloader): @@ -94,26 +94,11 @@ class LBRYFileDownloaderFactory(object): self.stream_info_manager = stream_info_manager self.wallet = wallet - def get_downloader_options(self, sd_validator, payment_rate_manager): - options = [ - DownloadOption( - [float, None], - "rate which will be paid for data (None means use application default)", - "data payment rate", - None - ), - DownloadOption( - [bool], - "allow reuploading data downloaded for this file", - "allow upload", - True - ), - ] - return options + def can_download(self, sd_validator): + return True def make_downloader(self, sd_validator, options, payment_rate_manager, **kwargs): - if options[0] is not None: - payment_rate_manager.float(options[0]) + payment_rate_manager.min_blob_data_payment_rate = options[0] upload_allowed = options[1] def create_downloader(stream_hash): @@ -276,6 +261,9 @@ class LBRYFileOpener(LBRYFileDownloader): class LBRYFileOpenerFactory(LBRYFileDownloaderFactory): + def can_download(self, sd_validator): + return bool(find_executable('vlc')) + def _make_downloader(self, stream_hash, payment_rate_manager, stream_info, upload_allowed): return LBRYFileOpener(stream_hash, self.peer_finder, self.rate_limiter, self.blob_manager, self.stream_info_manager, payment_rate_manager, self.wallet, upload_allowed) diff --git a/lbrynet/lbryfile/client/LBRYFileOptions.py b/lbrynet/lbryfile/client/LBRYFileOptions.py new file mode 100644 index 000000000..daa4f899c --- /dev/null +++ b/lbrynet/lbryfile/client/LBRYFileOptions.py @@ -0,0 +1,55 @@ +from lbrynet.lbryfile.StreamDescriptor import LBRYFileStreamType, LBRYFileStreamDescriptorValidator +from lbrynet.core.DownloadOption import DownloadOption, DownloadChoice + + +def add_lbry_file_to_sd_identifier(sd_identifier): + sd_identifier.add_stream_type(LBRYFileStreamType, LBRYFileStreamDescriptorValidator, LBRYFileOptions()) + + +class LBRYFileOptions(object): + def __init__(self): + pass + + def get_downloader_options(self, sd_validator, payment_rate_manager): + prm = payment_rate_manager + + def get_default_data_rate_description(): + if prm.min_blob_data_payment_rate is None: + return "Application default (%s LBC/MB)" % str(prm.base.min_blob_data_payment_rate) + else: + return "%f LBC/MB" % prm.min_blob_data_payment_rate + + rate_choices = [] + rate_choices.append(DownloadChoice(prm.min_blob_data_payment_rate, + "No change - %s" % get_default_data_rate_description(), + "No change - %s" % get_default_data_rate_description())) + if prm.min_blob_data_payment_rate is not None: + rate_choices.append(DownloadChoice(None, + "Application default (%s LBC/MB)" % str(prm.base.min_blob_data_payment_rate), + "Application default (%s LBC/MB)" % str(prm.base.min_blob_data_payment_rate))) + rate_choices.append(DownloadChoice(float, + "Enter rate in LBC/MB", + "Enter rate in LBC/MB")) + + options = [ + DownloadOption( + rate_choices, + "Rate which will be paid for data", + "data payment rate", + prm.min_blob_data_payment_rate, + get_default_data_rate_description() + ), + DownloadOption( + [ + DownloadChoice(bool, + None, + None, + bool_options_description=("Allow", "Disallow")), + ], + "Allow reuploading data downloaded for this file", + "allow upload", + True, + "Allow" + ), + ] + return options \ No newline at end of file diff --git a/lbrynet/lbryfilemanager/LBRYFileDownloader.py b/lbrynet/lbryfilemanager/LBRYFileDownloader.py index edb6b3f7b..fa8a093ac 100644 --- a/lbrynet/lbryfilemanager/LBRYFileDownloader.py +++ b/lbrynet/lbryfilemanager/LBRYFileDownloader.py @@ -2,7 +2,6 @@ Download LBRY Files from LBRYnet and save them to disk. """ -from lbrynet.core.DownloadOption import DownloadOption from zope.interface import implements from lbrynet.core.client.StreamProgressManager import FullStreamProgressManager from lbrynet.lbryfile.client.LBRYFileDownloader import LBRYFileSaver, LBRYFileDownloader @@ -117,22 +116,8 @@ class ManagedLBRYFileDownloaderFactory(object): def __init__(self, lbry_file_manager): self.lbry_file_manager = lbry_file_manager - def get_downloader_options(self, sd_validator, payment_rate_manager): - options = [ - DownloadOption( - [float, None], - "rate which will be paid for data (None means use application default)", - "data payment rate", - None - ), - DownloadOption( - [bool], - "allow reuploading data downloaded for this file", - "allow upload", - True - ), - ] - return options + def can_download(self, sd_validator): + return True def make_downloader(self, sd_validator, options, payment_rate_manager): data_rate = options[0] diff --git a/lbrynet/lbryfilemanager/LBRYFileManager.py b/lbrynet/lbryfilemanager/LBRYFileManager.py index 365081b93..0400269a4 100644 --- a/lbrynet/lbryfilemanager/LBRYFileManager.py +++ b/lbrynet/lbryfilemanager/LBRYFileManager.py @@ -7,7 +7,6 @@ import json import leveldb -from lbrynet.lbryfile.StreamDescriptor import LBRYFileStreamDescriptorValidator import os from lbrynet.lbryfilemanager.LBRYFileDownloader import ManagedLBRYFileDownloader from lbrynet.lbryfilemanager.LBRYFileDownloader import ManagedLBRYFileDownloaderFactory @@ -98,7 +97,6 @@ class LBRYFileManager(object): def _add_to_sd_identifier(self): downloader_factory = ManagedLBRYFileDownloaderFactory(self) - self.sd_identifier.add_stream_info_validator(LBRYFileStreamType, LBRYFileStreamDescriptorValidator) self.sd_identifier.add_stream_downloader_factory(LBRYFileStreamType, downloader_factory) def _start_lbry_files(self): diff --git a/lbrynet/lbrylive/LiveStreamCreator.py b/lbrynet/lbrylive/LiveStreamCreator.py index 6b78d27ac..7641a3d30 100644 --- a/lbrynet/lbrylive/LiveStreamCreator.py +++ b/lbrynet/lbrylive/LiveStreamCreator.py @@ -1,15 +1,12 @@ from lbrynet.core.StreamDescriptor import BlobStreamDescriptorWriter -from lbrynet.lbrylive.StreamDescriptor import get_sd_info, LiveStreamType, LBRYLiveStreamDescriptorValidator +from lbrynet.lbrylive.StreamDescriptor import get_sd_info from lbrynet.cryptstream.CryptStreamCreator import CryptStreamCreator from lbrynet.lbrylive.LiveBlob import LiveStreamBlobMaker -from lbrynet.lbrylive.PaymentRateManager import BaseLiveStreamPaymentRateManager from lbrynet.core.cryptoutils import get_lbry_hash_obj, get_pub_key, sign_with_pass_phrase from Crypto import Random import binascii import logging from lbrynet.conf import CRYPTSD_FILE_EXTENSION -from lbrynet.conf import MIN_BLOB_INFO_PAYMENT_RATE -from lbrynet.lbrylive.client.LiveStreamDownloader import FullLiveStreamDownloaderFactory from twisted.internet import interfaces, defer from twisted.protocols.basic import FileSender from zope.interface import implements @@ -173,17 +170,4 @@ class StdinStreamProducer(object): self.consumer.write(data) def childConnectionLost(self, fd, reason): - self.stopProducing() - - -def add_live_stream_to_sd_identifier(session, stream_info_manager, sd_identifier): - downloader_factory = FullLiveStreamDownloaderFactory(session.peer_finder, - session.rate_limiter, - session.blob_manager, - stream_info_manager, - session.wallet, - BaseLiveStreamPaymentRateManager( - MIN_BLOB_INFO_PAYMENT_RATE - )) - sd_identifier.add_stream_info_validator(LiveStreamType, LBRYLiveStreamDescriptorValidator) - sd_identifier.add_stream_downloader_factory(LiveStreamType, downloader_factory) \ No newline at end of file + self.stopProducing() \ No newline at end of file diff --git a/lbrynet/lbrylive/client/LiveStreamDownloader.py b/lbrynet/lbrylive/client/LiveStreamDownloader.py index da7efae0f..871468191 100644 --- a/lbrynet/lbrylive/client/LiveStreamDownloader.py +++ b/lbrynet/lbrylive/client/LiveStreamDownloader.py @@ -1,5 +1,4 @@ import binascii -from lbrynet.core.DownloadOption import DownloadOption from lbrynet.cryptstream.client.CryptStreamDownloader import CryptStreamDownloader from zope.interface import implements from lbrynet.lbrylive.client.LiveStreamMetadataHandler import LiveStreamMetadataHandler @@ -9,6 +8,9 @@ from lbrynet.lbrylive.StreamDescriptor import save_sd_info from lbrynet.lbrylive.PaymentRateManager import LiveStreamPaymentRateManager from twisted.internet import defer, threads # , process from lbrynet.interfaces import IStreamDownloaderFactory +from lbrynet.lbrylive.PaymentRateManager import BaseLiveStreamPaymentRateManager +from lbrynet.conf import MIN_BLOB_INFO_PAYMENT_RATE +from lbrynet.lbrylive.StreamDescriptor import LiveStreamType class LiveStreamDownloader(CryptStreamDownloader): @@ -138,28 +140,8 @@ class FullLiveStreamDownloaderFactory(object): self.wallet = wallet self.default_payment_rate_manager = default_payment_rate_manager - def get_downloader_options(self, sd_validator, payment_rate_manager): - options = [ - DownloadOption( - [float, None], - "rate which will be paid for data (None means use application default)", - "data payment rate", - None - ), - DownloadOption( - [float, None], - "rate which will be paid for metadata (None means use application default)", - "metadata payment rate", - None - ), - DownloadOption( - [bool], - "allow reuploading data downloaded for this file", - "allow upload", - True - ), - ] - return options + def can_download(self, sd_validator): + return True def make_downloader(self, sd_validator, options, payment_rate_manager): # TODO: check options for payment rate manager parameters @@ -177,4 +159,15 @@ class FullLiveStreamDownloaderFactory(object): return d d.addCallback(create_downloader) - return d \ No newline at end of file + return d + + +def add_full_live_stream_downloader_to_sd_identifier(session, stream_info_manager, sd_identifier, + base_live_stream_payment_rate_manager): + downloader_factory = FullLiveStreamDownloaderFactory(session.peer_finder, + session.rate_limiter, + session.blob_manager, + stream_info_manager, + session.wallet, + base_live_stream_payment_rate_manager) + sd_identifier.add_stream_downloader_factory(LiveStreamType, downloader_factory) \ No newline at end of file diff --git a/lbrynet/lbrylive/client/LiveStreamOptions.py b/lbrynet/lbrylive/client/LiveStreamOptions.py new file mode 100644 index 000000000..04f78c62b --- /dev/null +++ b/lbrynet/lbrylive/client/LiveStreamOptions.py @@ -0,0 +1,73 @@ +from lbrynet.lbrylive.StreamDescriptor import LiveStreamType, LBRYLiveStreamDescriptorValidator +from lbrynet.core.DownloadOption import DownloadOption, DownloadChoice + + +def add_live_stream_to_sd_identifier(sd_identifier, base_live_stream_payment_rate_manager): + sd_identifier.add_stream_type(LiveStreamType, LBRYLiveStreamDescriptorValidator, + LiveStreamOptions(base_live_stream_payment_rate_manager)) + + +class LiveStreamOptions(object): + def __init__(self, base_live_stream_payment_rate_manager): + self.base_live_stream_prm = base_live_stream_payment_rate_manager + + def get_downloader_options(self, sd_validator, payment_rate_manager): + prm = payment_rate_manager + + def get_default_data_rate_description(): + if prm.min_blob_data_payment_rate is None: + return "Application default (%s LBC/MB)" % str(prm.base.min_blob_data_payment_rate) + else: + return "%f LBC/MB" % prm.min_blob_data_payment_rate + + options = [ + DownloadOption( + [ + DownloadChoice(None, + "No change", + "No change"), + DownloadChoice(None, + "Application default (%s LBC/MB)" % str(prm.base.min_blob_data_payment_rate), + "Default (%s LBC/MB)" % str(prm.base.min_blob_data_payment_rate)), + DownloadChoice(float, + "Rate in LBC/MB", + "Rate in LBC/MB") + ], + "rate which will be paid for data", + "data payment rate", + prm.min_blob_data_payment_rate, + get_default_data_rate_description() + ), + DownloadOption( + [ + DownloadChoice(None, + "No change", + "No change"), + DownloadChoice(None, + "Application default (%s LBC/MB)" % str(self.base_live_stream_prm.min_live_blob_info_payment_rate), + "Default (%s LBC/MB)" % str(self.base_live_stream_prm.min_live_blob_info_payment_rate)), + DownloadChoice(float, + "Rate in LBC/MB", + "Rate in LBC/MB") + ], + "rate which will be paid for metadata", + "metadata payment rate", + None, + "Application default (%s LBC/MB)" % str(self.base_live_stream_prm.min_live_blob_info_payment_rate) + ), + DownloadOption( + [ + DownloadChoice(True, + "Allow reuploading data downloaded for this file", + "Allow reuploading"), + DownloadChoice(False, + "Disallow reuploading data downloaded for this file", + "Disallow reuploading") + ], + "allow reuploading data downloaded for this file", + "allow upload", + True, + "Allow" + ), + ] + return options \ No newline at end of file diff --git a/lbrynet/lbrynet_console/ControlHandlers.py b/lbrynet/lbrynet_console/ControlHandlers.py index 92ffa5d29..79561dc9a 100644 --- a/lbrynet/lbrynet_console/ControlHandlers.py +++ b/lbrynet/lbrynet_console/ControlHandlers.py @@ -13,6 +13,10 @@ class InvalidChoiceError(Exception): pass +class InvalidValueError(Exception): + pass + + class ControlHandlerFactory(object): implements(IControlHandlerFactory) @@ -235,9 +239,11 @@ class AddStream(ControlHandler): self.factories = None self.factory = None self.info_validator = None + self.options = None self.options_left = [] self.options_chosen = [] self.current_option = None + self.current_choice = None self.downloader = None self.got_options_response = False self.loading_failed = False @@ -274,18 +280,31 @@ class AddStream(ControlHandler): return False, defer.succeed(self._show_factory_choices()) if self.got_options_response is False: self.got_options_response = True - if line == 'y' or line == 'Y': - if self.options_left: - return False, defer.succeed(self._get_next_option_prompt()) - self.options_chosen = [option.default for option in self.options_left] - self.options_left = [] - return False, defer.succeed(self.line_prompt3) + if line == 'y' or line == 'Y' and self.options_left: + return False, defer.succeed(self._get_next_option_prompt()) + else: + self.options_chosen = [option.default_value for option in self.options_left] + self.options_left = [] + return False, defer.succeed(self.line_prompt3) if self.current_option is not None: - try: - choice = self._get_choice_from_input(line) - except InvalidChoiceError: - return False, defer.succeed(self._get_next_option_prompt(invalid_response=True)) - self.options_chosen.append(choice) + if self.current_choice is None: + try: + self.current_choice = self._get_choice_from_input(line) + except InvalidChoiceError: + return False, defer.succeed(self._get_next_option_prompt(invalid_choice=True)) + choice = self.current_option.option_types[self.current_choice] + if choice.value == float or choice.value == bool: + return False, defer.succeed(self._get_choice_value_prompt()) + else: + value = choice.value + else: + try: + value = self._get_value_for_choice(line) + except InvalidValueError: + return False, defer.succeed(self._get_choice_value_prompt(invalid_value=True)) + self.options_chosen.append(value) + self.current_choice = None + self.current_option = None self.options_left = self.options_left[1:] if self.options_left: return False, defer.succeed(self._get_next_option_prompt()) @@ -299,23 +318,13 @@ class AddStream(ControlHandler): return True, d def _get_choice_from_input(self, line): - if line == "": - return self.current_option.default - for option_type in self.current_option.option_types: - if option_type == float: - try: - return float(line) - except ValueError: - pass - if option_type is None: - if line.lower() == "none": - return None - if option_type == bool: - if line.lower() == "true" or line.lower() == "t": - return True - if line.lower() == "false" or line.lower() == "f": - return False - raise InvalidChoiceError(line) + try: + choice_num = int(line) + except ValueError: + raise InvalidChoiceError() + if 0 <= choice_num < len(self.current_option.option_types): + return choice_num + raise InvalidChoiceError() def _load_info_and_factories(self, sd_file): return defer.fail(NotImplementedError()) @@ -333,7 +342,7 @@ class AddStream(ControlHandler): def _choose_factory(self, info_and_factories): self.loading_info_and_factories_deferred = None - self.info_validator, self.factories = info_and_factories + self.info_validator, self.options, self.factories = info_and_factories if len(self.factories) == 1: self.factory = self.factories[0] return self._show_info_and_options() @@ -346,38 +355,71 @@ class AddStream(ControlHandler): return str(prompt) def _show_info_and_options(self): - self.options_left = self.factory.get_downloader_options(self.info_validator, + self.options_left = self.options.get_downloader_options(self.info_validator, self.payment_rate_manager) prompt = "Stream info:\n" for info_line in self.info_validator.info_to_show(): prompt += info_line[0] + ": " + info_line[1] + "\n" prompt += "\nOptions:\n" for option in self.options_left: - prompt += option.long_description + ": " + str(option.default) + "\n" + prompt += option.long_description + ": " + str(option.default_value_description) + "\n" prompt += "\nModify options? (y/n)" return str(prompt) - def _get_option_type_description(self, option_type): - if option_type == float: - return "floating point number (e.g. 1.0)" - if option_type == bool: - return "True or False" - if option_type is None: - return "None" + def _get_list_of_option_types(self): + options_string = "" + for i, option_type in enumerate(self.current_option.option_types): + options_string += "[%s] %s\n" % (str(i), option_type.long_description) + options_string += "Enter choice:" + return options_string - def _get_next_option_prompt(self, invalid_response=False): - assert len(self.options_left), "Something went wrong. There were no options left" - choice = self.options_left[0] + def _get_choice_value_prompt(self, invalid_value=False): + choice = self.current_option.option_types[self.current_choice] choice_string = "" - if invalid_response is True: + if invalid_value is True: + "Invalid value entered. Try again.\n" + if choice.short_description is not None: + choice_string += choice.short_description + "\n" + if choice.value == float: + choice_string += "Enter floating point number (e.g. 1.0):" + elif choice.value == bool: + true_string = "Yes" + false_string = "No" + if choice.bool_options_description is not None: + true_string, false_string = choice.bool_options_description + choice_string += "[0] %s\n[1] %s\nEnter choice:" % (true_string, false_string) + else: + NotImplementedError() + return choice_string + + def _get_value_for_choice(self, input): + choice = self.current_option.option_types[self.current_choice] + if choice.value == float: + try: + return float(input) + except ValueError: + raise InvalidValueError() + elif choice.value == bool: + if input == "0": + return True + elif input == "1": + return False + raise InvalidValueError() + raise NotImplementedError() + + def _get_next_option_prompt(self, invalid_choice=False): + assert len(self.options_left), "Something went wrong. There were no options left" + self.current_option = self.options_left[0] + choice_string = "" + if invalid_choice is True: choice_string += "Invalid response entered. Try again.\n" - choice_string += choice.long_description + "\n" - choice_string += "Valid inputs:\n" - for option_type in choice.option_types: - choice_string += "\t" + self._get_option_type_description(option_type) + "\n" - choice_string += "Leave blank for default (" + str(choice.default) + ")\n" - choice_string += "Enter choice:" - self.current_option = choice + + choice_string += self.current_option.long_description + "\n" + if len(self.current_option.option_types) > 1: + choice_string += self._get_list_of_option_types() + elif len(self.current_option.option_types) == 1: + self.current_choice = 0 + choice_string += self._get_choice_value_prompt() return choice_string def _start_download(self): diff --git a/lbrynet/lbrynet_console/LBRYConsole.py b/lbrynet/lbrynet_console/LBRYConsole.py index 4b44aa358..0b3dbd150 100644 --- a/lbrynet/lbrynet_console/LBRYConsole.py +++ b/lbrynet/lbrynet_console/LBRYConsole.py @@ -15,6 +15,7 @@ from lbrynet.core.server.BlobAvailabilityHandler import BlobAvailabilityHandlerF from lbrynet.core.server.BlobRequestHandler import BlobRequestHandlerFactory from lbrynet.core.server.ServerProtocol import ServerProtocolFactory from lbrynet.core.PTCWallet import PTCWallet +from lbrynet.lbryfile.client.LBRYFileOptions import add_lbry_file_to_sd_identifier from lbrynet.lbryfile.client.LBRYFileDownloader import LBRYFileOpenerFactory from lbrynet.lbryfile.StreamDescriptor import LBRYFileStreamType from lbrynet.lbryfile.LBRYFileMetadataManager import DBLBRYFileMetadataManager, TempLBRYFileMetadataManager @@ -77,6 +78,7 @@ class LBRYConsole(): d = threads.deferToThread(self._create_directory) d.addCallback(lambda _: self._get_settings()) d.addCallback(lambda _: self._get_session()) + d.addCallback(lambda _: add_lbry_file_to_sd_identifier(self.sd_identifier)) d.addCallback(lambda _: self._setup_lbry_file_manager()) d.addCallback(lambda _: self._setup_lbry_file_opener()) d.addCallback(lambda _: self._setup_control_handlers()) diff --git a/lbrynet/lbrynet_downloader_gui/downloader.py b/lbrynet/lbrynet_downloader_gui/downloader.py index f13bf7593..29a48de04 100644 --- a/lbrynet/lbrynet_downloader_gui/downloader.py +++ b/lbrynet/lbrynet_downloader_gui/downloader.py @@ -17,7 +17,8 @@ from lbrynet.core.StreamDescriptor import StreamDescriptorIdentifier from lbrynet.core.PaymentRateManager import PaymentRateManager from lbrynet.lbryfile.LBRYFileMetadataManager import TempLBRYFileMetadataManager from lbrynet.core import StreamDescriptor -from lbrynet.lbryfile.StreamDescriptor import LBRYFileStreamType, LBRYFileStreamDescriptorValidator +from lbrynet.lbryfile.client.LBRYFileOptions import add_lbry_file_to_sd_identifier +from lbrynet.lbryfile.StreamDescriptor import LBRYFileStreamType import requests @@ -101,7 +102,7 @@ class LBRYDownloader(object): return defer.succeed(True) def _setup_stream_identifier(self): - self.sd_identifier.add_stream_info_validator(LBRYFileStreamType, LBRYFileStreamDescriptorValidator) + 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) @@ -184,7 +185,7 @@ class LBRYDownloader(object): return stream_name, stream_size def choose_download_factory(info_and_factories): - info_validator, factories = info_and_factories + info_validator, options, factories = info_and_factories stream_name, stream_size = get_info_from_validator(info_validator) if isinstance(stream_size, (int, long)): price = payment_rate_manager.get_effective_min_blob_data_payment_rate() @@ -192,26 +193,30 @@ class LBRYDownloader(object): else: estimated_cost = "unknown" - stream_frame.show_stream_metadata(stream_name, stream_size, estimated_cost) + stream_frame.show_stream_metadata(stream_name, stream_size) + + available_options = options.get_downloader_options(info_validator, payment_rate_manager) + + stream_frame.show_download_options(available_options) get_downloader_d = defer.Deferred() - def create_downloader(f): + 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() - download_options = [o.default for o in f.get_downloader_options(info_validator, payment_rate_manager)] - d = f.make_downloader(info_validator, download_options, + d = f.make_downloader(info_validator, chosen_options, payment_rate_manager) d.addCallback(fire_get_downloader_d) for factory in factories: def choose_factory(f=factory): - create_downloader(f) + chosen_options = stream_frame.get_chosen_options() + create_downloader(f, chosen_options) stream_frame.add_download_factory(factory, choose_factory) @@ -301,9 +306,9 @@ class StreamFrame(object): self.uri_label.grid(row=0, column=0, sticky=tk.W) if os.name == "nt": - close_cursor = "" + self.button_cursor = "" else: - close_cursor = "hand1" + self.button_cursor = "hand1" close_file_name = "close2.gif" try: @@ -316,7 +321,7 @@ class StreamFrame(object): file=close_file ) self.close_button = ttk.Button( - self.stream_frame_header, command=self.cancel, style="Stop.TButton", cursor=close_cursor + 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) @@ -334,26 +339,50 @@ class StreamFrame(object): self.stream_frame_body.grid_columnconfigure(0, weight=1) - self.info_frame = ttk.Frame(self.stream_frame_body, style="D.TFrame") - self.info_frame.grid(sticky=tk.W + tk.E, row=1) - self.info_frame.grid_columnconfigure(0, weight=1) - - self.metadata_frame = ttk.Frame(self.info_frame, style="E.TFrame") - self.metadata_frame.grid(sticky=tk.W + tk.E) + 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.outer_button_frame = ttk.Frame(self.stream_frame_body, style="D.TFrame") - self.outer_button_frame.grid(sticky=tk.W + tk.E, row=2) + self.options_frame = ttk.Frame(self.stream_frame_body, style="D.TFrame") - self.button_frame = ttk.Frame(self.outer_button_frame, style="E.TFrame") - self.button_frame.pack(side=tk.TOP) + 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" + try: + show_options_picture_file = os.path.join(os.path.dirname(__file__), + show_options_picture_file_name) + except NameError: + 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) + + self.show_options_picture = tk.PhotoImage( + file=show_options_picture_file + ) + + hide_options_picture_file_name = "hide_options.gif" + try: + hide_options_picture_file = os.path.join(os.path.dirname(__file__), + hide_options_picture_file_name) + except NameError: + 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) + + 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.bytes_downloaded_label = None self.bytes_outputted_label = None - + self.button_frame = None self.download_buttons = [] + self.option_frames = [] self.name_font = None self.description_label = None self.file_name_frame = None @@ -416,7 +445,7 @@ class StreamFrame(object): return "%.1f %s" % (round((stream_size * 1.0 / factor), 1), units) return stream_size - def show_stream_metadata(self, stream_name, stream_size, estimated_cost): + def show_stream_metadata(self, stream_name, stream_size): if self.status_label is not None: self.status_label.destroy() @@ -436,19 +465,6 @@ class StreamFrame(object): ) file_name_label.grid(row=0, column=3) - self.outer_button_frame = ttk.Frame(self.stream_frame_body, style="D.TFrame") - self.outer_button_frame.grid(sticky=tk.W + tk.E, row=2) - - self.cost_frame = ttk.Frame(self.outer_button_frame, style="F.TFrame") - self.cost_frame.grid(row=0, column=0, sticky=tk.W+tk.N, pady=(0, 12)) - - self.cost_label = ttk.Label( - self.cost_frame, - text=locale.format_string("%.2f LBC", (round(estimated_cost, 2),), grouping=True), - foreground="red" - ) - self.cost_label.grid(row=0, column=1, padx=(1, 0)) - self.button_frame = ttk.Frame(self.outer_button_frame, style="E.TFrame") self.button_frame.grid(row=0, column=1) @@ -457,13 +473,9 @@ class StreamFrame(object): self.outer_button_frame.grid_columnconfigure(2, weight=1, uniform="buttons") def add_download_factory(self, factory, download_func): - if os.name == "nt": - button_cursor = "" - else: - button_cursor = "hand1" download_button = ttk.Button( self.button_frame, text=factory.get_description(), command=download_func, - style='LBRY.TButton', cursor=button_cursor + 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)) @@ -477,11 +489,160 @@ class StreamFrame(object): 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, bytes_left_to_output, points_paid, points_remaining): if self.bytes_outputted_label is None: self.remove_download_buttons() self.button_frame.destroy() + for option, frame in self.option_frames: + frame.destroy() + self.options_frame.destroy() + self.show_options_button.destroy() + + self.cost_frame = ttk.Frame(self.outer_button_frame, style="F.TFrame") + self.cost_frame.grid(row=0, column=0, sticky=tk.W+tk.N, pady=(0, 12)) + + 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="") self.bytes_outputted_label = ttk.Label( @@ -667,6 +828,7 @@ class App(object): 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") @@ -674,6 +836,8 @@ class App(object): #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")) diff --git a/lbrynet/lbrynet_downloader_gui/hide_options.gif b/lbrynet/lbrynet_downloader_gui/hide_options.gif new file mode 100644 index 0000000000000000000000000000000000000000..9c95b5a9d3692981617338438f7cec1564c41569 GIT binary patch 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 literal 0 HcmV?d00001 diff --git a/lbrynet/lbrynet_downloader_gui/show_options.gif b/lbrynet/lbrynet_downloader_gui/show_options.gif new file mode 100644 index 0000000000000000000000000000000000000000..c5ed0b6fd3e31d8c7aaa861a62e575434df31a2c GIT binary patch literal 155 zcmZ?wbhEHb^k(2^XkcUjg8%>jEB<6*