diff --git a/.github/workflows/scripttest.yml b/.github/workflows/scripttest.yml index 667d23a7134..ff86ef79fc5 100644 --- a/.github/workflows/scripttest.yml +++ b/.github/workflows/scripttest.yml @@ -98,20 +98,3 @@ jobs: command: python ./scripts/experiments/tunnel_community/speed_test_exit.py --fragile duration: ${{inputs.duration}} - #seedbox - - - name: generate_test_data.py - run: | - python ./scripts/seedbox/generate_test_data.py --destination=./test_data --count=10 - - - name: disseminator.py - uses: ./.github/actions/timeout - with: - command: python ./scripts/seedbox/disseminator.py --source=./test_data --fragile --testnet - duration: ${{inputs.duration}} - - - name: seeder.py - uses: ./.github/actions/timeout - with: - command: python ./scripts/seedbox/seeder.py --source=./test_data --testnet - duration: ${{inputs.duration}} diff --git a/scripts/seedbox/README.md b/scripts/seedbox/README.md deleted file mode 100644 index c85ed69dbb2..00000000000 --- a/scripts/seedbox/README.md +++ /dev/null @@ -1,98 +0,0 @@ -# Seedbox - -This folder contains scripts for effortlessly setting up a seedbox. - -The seedbox consists of two parts: - -1. Torrent seeding (by using a LibTorrent protocol) -1. Channel disseminating (by using the Tribler network) - -## Prerequisites - -1. Clone the tribler repo: - ```shell - git clone https://github.com/Tribler/tribler.git - ``` -1. Install Tribler requirements: - ```bash - python3 -m pip install -r requirements.txt - ``` -1. Add Tribler `src` folder to `PYTHONPATH` (below the bash example) - ```shell - export PYTHONPATH=${PYTHONPATH}:./src - ``` - -## Torrent seeding - -To start torrents' seeding run the following script: - -```bash -python3 seeder.py -``` - -Consider the following folder structure: - -``` -source folder -├ sub_directory -| ├ file1 -| └file2 -├ sub_directory2 -| ├ file3 -| └ file4 -├ thumbnail.png -└ description.md -``` - -In this particular example, `seeder.py` will create two torrents: -`sub_directory.torrent` and `sub_directory2.torrent`. - -`seeder.py` will start to seed them through BitTorrent protocol after creating. - -## Data disseminating - -To start disseminating data through Tribler's network run the following script: - -```bash -python3 disseminator.py -``` - -This script will create a channel and will disseminate it to Tribler. - -Consider the following folder structure: - -``` -source folder -├ sub_directory.torrent -├ sub_directory2.torrent -├ thumbnail.png -└ description.md -``` - -Above you can see two "special" files: - -* thumbnail.png -* description.md - -The channel will be created with description based on these files. -As the channel name, the source folder's name will be used. - -### Error reporting - -In case you want errors to be reported, you can use [Sentry](https://develop.sentry.dev/) - -To enable error reporting, specify the following environment variable: - -```bash -export SENTRY_URL= -``` - -URL can be taken directly from a corresponding Sentry project. - -### Generate test data - -The following script generates `1GB` dataset divided into `1024` folders: - -```shell -python3 generate_test_data.py -d /tmp/test_data -``` \ No newline at end of file diff --git a/scripts/seedbox/disseminator.py b/scripts/seedbox/disseminator.py deleted file mode 100644 index 3b2dbf5fbfb..00000000000 --- a/scripts/seedbox/disseminator.py +++ /dev/null @@ -1,218 +0,0 @@ -""" -This script scans the input directory and create a Tribler channel based on -torrents found. - -For available parameters see "parse_args" function below. - -Folder structure: - -my channel -├ sub_directory -| ├ file1.torrent -| └ file2.torrent -├ file3.torrent -├ thumbnail.png -└ description.md -""" - -import argparse -import logging -import os -from json import dumps -from pathlib import Path -from types import SimpleNamespace - -import libtorrent -import sentry_sdk -from pony.orm import db_session - -from tribler.core.components import libtorrent -from tribler.core.components.gigachannel.gigachannel_component import GigaChannelComponent -from tribler.core.components.ipv8.ipv8_component import Ipv8Component -from tribler.core.components.key.key_component import KeyComponent -from tribler.core.components.knowledge.knowledge_component import KnowledgeComponent -from tribler.core.components.libtorrent.libtorrent_component import LibtorrentComponent -from tribler.core.components.libtorrent.torrentdef import TorrentDef -from tribler.core.components.metadata_store.db.orm_bindings.channel_node import NEW -from tribler.core.components.metadata_store.metadata_store_component import MetadataStoreComponent -from tribler.core.components.socks_servers.socks_servers_component import SocksServersComponent -from tribler.core.utilities.tiny_tribler_service import TinyTriblerService - -_description_file_name = 'description.md' -_thumbnail_file_name = 'thumbnail.png' - -_logger = logging.getLogger('Disseminator') - -sentry_sdk.init( - os.environ.get('SENTRY_URL'), - traces_sample_rate=1.0 -) - - -def parse_args(): - parser = argparse.ArgumentParser(description='Disseminate data by using the Tribler network') - - parser.add_argument('-s', '--source', type=str, help='path to data folder', default='.') - parser.add_argument('-d', '--tribler_dir', type=str, help='path to data folder', default='./.Tribler') - parser.add_argument('-v', '--verbosity', help='increase output verbosity', action='store_true') - parser.add_argument('-f', '--fragile', help='Fail at the first error', action='store_true') - parser.add_argument('-t', '--testnet', help='Testnet run', action='store_true') - - return parser.parse_args() - - -def setup_logger(verbosity): - logging_level = logging.DEBUG if verbosity else logging.INFO - logging.basicConfig(level=logging_level) - - -class ChannelHelper: - def __init__(self, community): - self.community = community - self.directories = SimpleNamespace(tree={}, directory=None) - - @db_session - def create_root_channel(self, name, description=''): - _logger.info(f'Creating channel: {name}') - channels = self.community.mds.ChannelMetadata - - if len(channels.get_channels_by_title(name)) >= 1: - _logger.warning(f'Channel with name {name} already exists') - return False - - self.directories.directory = channels.create_channel(name, description) - self.flush() - - return True - - @db_session - def add_torrent(self, file, relative_path): - _logger.info(f'Add torrent: {file}') - - directory = self.get_directory(relative_path) - decoded_torrent = libtorrent.bdecode(file.read_bytes()) - directory.add_torrent_to_channel(TorrentDef(metainfo=decoded_torrent), None) - - @db_session - def add_thumbnail(self, thumbnail): - if not thumbnail: - return - - _logger.info(f'Add thumbnail: {thumbnail}') - - root_channel = self.directories.directory - self.community.mds.ChannelThumbnail(public_key=root_channel.public_key, - origin_id=root_channel.id_, - status=NEW, - binary_data=thumbnail, - data_type='image/png') - - @db_session - def add_description(self, description): - if not description: - return - - _logger.info(f'Add description: {description}') - - root_channel = self.directories.directory - self.community.mds.ChannelDescription(public_key=root_channel.public_key, - origin_id=root_channel.id_, - json_text=dumps({"description_text": description}), - status=NEW) - - @db_session - def get_directory(self, path): - current = self.directories - - for part in path.parts[:-1]: - next_directory = current.tree.get(part, None) - if next_directory is not None: - current = next_directory - continue - - next_directory = SimpleNamespace( - tree={}, - directory=self.community.mds.CollectionNode(title=part, origin_id=current.directory.id_, status=NEW) - ) - - current.tree[part] = next_directory - current = next_directory - self.flush() - - _logger.info(f'Directory created: {part}') - - return current.directory - - @db_session - def commit(self): - _logger.info('Commit changes') - - for t in self.community.mds.CollectionNode.commit_all_channels(): - self.manager.updated_my_channel(TorrentDef.load_from_dict(t)) - - @db_session - def flush(self): - _logger.debug('Flush') - - self.community.mds.db.flush() - - -class Service(TinyTriblerService): - def __init__(self, source_dir, testnet: bool, *args, **kwargs): - super().__init__(*args, **kwargs, - components=[ - KnowledgeComponent(), MetadataStoreComponent(), KeyComponent(), Ipv8Component(), - SocksServersComponent(), LibtorrentComponent(), GigaChannelComponent() - ]) - self.config.general.testnet = testnet - self.source_dir = Path(source_dir) - - def get_torrents_from_source(self): - return [(file, file.relative_to(self.source_dir)) for file in self.source_dir.rglob('*.torrent')] - - def get_thumbnail(self): - f = self.source_dir / _thumbnail_file_name - return f.read_bytes() if f.exists() else None - - def get_description(self): - f = self.source_dir / _description_file_name - return f.read_text() if f.exists() else None - - async def create_channel(self, community): - channel_helper = ChannelHelper(community) - channel_name = self.source_dir.name - - if not channel_helper.create_root_channel(channel_name): - return - - torrents = self.get_torrents_from_source() - - for file, relative_path in torrents: - channel_helper.add_torrent(file, relative_path) - - channel_helper.add_thumbnail(self.get_thumbnail()) - channel_helper.add_description(self.get_description()) - - channel_helper.commit() - - _logger.info(f'{len(torrents)} torrents where added') - - async def on_tribler_started(self): - await super().on_tribler_started() - gigachannel_component = self.session.get_instance(GigaChannelComponent) - await self.create_channel(gigachannel_component.community) - - -if __name__ == "__main__": - _arguments = parse_args() - print(f"Arguments: {_arguments}") - - setup_logger(_arguments.verbosity) - - service = Service( - source_dir=Path(_arguments.source), - state_dir=Path(_arguments.tribler_dir), - testnet=_arguments.testnet - ) - - service.run(fragile=_arguments.fragile) diff --git a/scripts/seedbox/generate_test_data.py b/scripts/seedbox/generate_test_data.py deleted file mode 100644 index 7ce9275ffbe..00000000000 --- a/scripts/seedbox/generate_test_data.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -This scripts generates data for testing purposes. -It generates given amount of folders which contains given amount of files. - -For available parameters see "parse_args" function below. -""" -import argparse -import os -from pathlib import Path - -_file_count_per_folder = 8 -_file_size = 128 * 1024 - - -def parse_args(): - parser = argparse.ArgumentParser(description='Generate test data') - - parser.add_argument('-d', '--destination', type=str, help='path to data folder', default='.') - parser.add_argument('-c', '--count', type=int, help='folders count', default=1024) - - return parser.parse_args() - - -def generate(arguments): - print(arguments) - destination = Path(arguments.destination) - destination.mkdir(exist_ok=True) - - for folder_index in range(arguments.count): - folder = Path(destination / f'{folder_index}') - folder.mkdir(exist_ok=True) - - for file_index in range(_file_count_per_folder): - f = Path(folder) / f'{file_index}.txt' - content = os.urandom(_file_size) - f.write_bytes(content) - - print(folder) - - -if __name__ == "__main__": - args = parse_args() - generate(args) diff --git a/scripts/seedbox/seeder.py b/scripts/seedbox/seeder.py deleted file mode 100644 index 0c1ca1a8ce8..00000000000 --- a/scripts/seedbox/seeder.py +++ /dev/null @@ -1,211 +0,0 @@ -""" -This script generates torrents in input folder and seed them. -For available parameters see "parse_args" function below. - -Folder structure: - -# my channel -# ├ sub_directory -# | ├ file1 -# | └ file2 -# ├ sub_directory2 -# | ├ file3 -# | └ file4 -# ├ file5 -# └ file6 - -The script generates torrents for each folder contains files. -There are a possibility to add ignored files (see "_ignore_glob" below). -""" -import argparse -import logging -import os -import time -from collections import defaultdict -from pathlib import Path - -import libtorrent -import sentry_sdk - -# fmt: off -# flake8: noqa - -UNLIMITED = -1 - -_creator = 'TU Delft' - -_dht_routers = [ - ('router.utorrent.com', 6881), - ("router.utorrent.com", 6881), - ("router.bittorrent.com", 6881), - ("dht.transmissionbt.com", 6881), - ("dht.aelitis.com", 6881), - ("router.bitcomet.com", 6881), -] -_port_range = (6881, 7000) -_log_statistics_interval_in_sec = 10 -_add_torrent_delay_in_sec = 1 -_ignore_glob = [ - '*DS_Store', - '*.torrent', - 'thumbnail.png', - 'description.md', -] - -_logger = logging.getLogger('Seeder') - -sentry_sdk.init( - os.environ.get('SENTRY_URL'), - traces_sample_rate=1.0 -) - - -def parse_args(): - parser = argparse.ArgumentParser(description='Seed data by using the LibTorrent protocol') - - parser.add_argument('-s', '--source', type=str, help='path to data folder', default='.') - parser.add_argument('-v', '--verbosity', help='increase output verbosity', action='store_true') - parser.add_argument('-t', '--testnet', help='Testnet run', action='store_true') - - return parser.parse_args() - - -def setup_logger(verbosity): - logging_level = logging.DEBUG if verbosity else logging.INFO - logging.basicConfig(level=logging_level) - - -def get_folders_with_files(source): - """ Return all folders that contains files - - Args: - source: a source folder - - Returns: - Dictionary where - * key: is a folder - * value: is a file list - """ - result = {} - - for file in Path(source).rglob('*'): - ignore = any(file.match(a) for a in _ignore_glob) - if file.is_file() and not ignore: - result.setdefault(file.parent, set()).add(file) - - return result - - -def create_torrents(folders, source): - _logger.info(f'Creating {len(folders)} torrent files...') - - for folder in folders: - if folder.match(source): - continue - - torrent_file = folder.parent / f'{folder.name}.torrent' - - if not torrent_file.exists(): - original, encoded = create_torrent_from_folder(folder, folders[folder]) - torrent_file.write_bytes(encoded) - _logger.info(f'Created: {torrent_file}') - - yield original, folder - else: - _logger.info(f'Skipped (file already exists): {torrent_file}') - - encoded = torrent_file.read_bytes() - decoded = libtorrent.bdecode(encoded) - - yield decoded, folder - - -def create_torrent_from_folder(folder, files): - file_storage = libtorrent.file_storage() - file_storage.set_name(folder.name) - - for file in files: - relative = file.relative_to(folder.parent) - size = file.stat().st_size - - file_storage.add_file(str(relative), size) - - flags = libtorrent.create_torrent_flags_t.optimize - torrent = libtorrent.create_torrent(file_storage, flags=flags) - - torrent.set_creator(_creator) - libtorrent.set_piece_hashes(torrent, str(folder.parent)) - - torrent_data = torrent.generate() - return torrent_data, libtorrent.bencode(torrent_data) - - -def log_all_alerts(session): - for a in session.pop_alerts(): - if a.category() & libtorrent.alert.category_t.error_notification: - _logger.error(a) - else: - _logger.info(a) - - -def log_statistics(session, handlers, interval): - while True: - time.sleep(interval) - log_all_alerts(session) - - states = defaultdict(int) - errors = defaultdict(int) - - for h in handlers: - status = h.status() - states[status.state] += 1 - if status.errc.value() != 0: - errors[status.errc.message()] += 1 - - _logger.info(f'Torrents states: {states}') - if errors: - _logger.info(f'Torrents errors: {errors}') - - -def seed(torrents): - _logger.info(f'Create torrent session in port range: {_port_range}') - session = libtorrent.session() - session.listen_on(*_port_range) - for router in _dht_routers: - session.add_dht_router(*router) - - session.start_dht() - - session.apply_settings({ - 'active_seeds': UNLIMITED, - 'active_limit': UNLIMITED - }) - - handlers = [] - for torrent, folder in torrents: - torrent_info = libtorrent.torrent_info(torrent) - params = { - 'save_path': str(folder.parent), - 'ti': torrent_info, - 'name': folder.name, - } - - _logger.info(f'Add torrent: {params}') - result = session.add_torrent(params) - handlers.append(result) - - time.sleep(_add_torrent_delay_in_sec) - log_all_alerts(session) - - log_statistics(session, handlers, _log_statistics_interval_in_sec) - - -if __name__ == "__main__": - _arguments = parse_args() - print(f"Arguments: {_arguments}") - - setup_logger(_arguments.verbosity) - _folders = get_folders_with_files(_arguments.source) - _torrents = list(create_torrents(_folders, _arguments.source)) - if not _arguments.testnet: - seed(_torrents) diff --git a/src/tribler/core/components/metadata_store/db/orm_bindings/torrent_metadata.py b/src/tribler/core/components/metadata_store/db/orm_bindings/torrent_metadata.py index ae2cc037414..a65a0153193 100644 --- a/src/tribler/core/components/metadata_store/db/orm_bindings/torrent_metadata.py +++ b/src/tribler/core/components/metadata_store/db/orm_bindings/torrent_metadata.py @@ -75,21 +75,6 @@ def tdef_to_metadata_dict(tdef, category_filter: Category = None): } -def generate_dict_from_pony_args(cls, skip_list=None, **kwargs): - """ - Note: this is a way to manually define Pony entity default attributes in case we - have to generate the signature before creating an object - """ - d = {} - skip_list = skip_list or [] - for attr in cls._attrs_: # pylint: disable=W0212 - val = kwargs.get(attr.name, DEFAULT) - if attr.name in skip_list: - continue - d[attr.name] = attr.validate(val, entity=cls) - return d - - def entries_to_chunk(metadata_list, chunk_size, start_index=0, include_health=False): """ Put serialized data of one or more metadata entries into a single binary chunk. The data is added @@ -145,7 +130,7 @@ def entries_to_chunk(metadata_list, chunk_size, start_index=0, include_health=Fa return result, index + 1 -def define_binding(db, notifier: Optional[Notifier], tag_processor_version: int): +def define_binding(db, notifier: Optional[Notifier], tag_processor_version: int): # noqa: MC0001 class TorrentMetadata(db.Entity): """ This ORM binding class is intended to store Torrent objects, i.e. infohashes along with some related metadata. @@ -211,16 +196,7 @@ def __init__(self, *args, **kwargs): # We have to give the entry an unique sig to honor the DB constraints. We use the entry's id_ # as the sig to keep it unique and short. The uniqueness is guaranteed by DB as it already # imposes uniqueness constraints on the id_+public_key combination. - if "id_" in kwargs: - kwargs["signature"] = None - else: - # Trying to create an FFA entry without specifying the id_ should be considered an error, - # because assigning id_ automatically by clock breaks anonymity. - # FFA entries should be "timeless" and anonymous. - raise InvalidChannelNodeException( - "Attempted to create %s free-for-all (unsigned) object without specifying id_ : " - % str(self.__class__.__name__) - ) + kwargs["signature"] = None super().__init__(*args, **kwargs) diff --git a/src/tribler/core/components/metadata_store/db/serialization.py b/src/tribler/core/components/metadata_store/db/serialization.py index 44026c859ab..301e0474719 100644 --- a/src/tribler/core/components/metadata_store/db/serialization.py +++ b/src/tribler/core/components/metadata_store/db/serialization.py @@ -81,7 +81,9 @@ def read_payload_with_offset(data, offset=0): class SignedPayload(VariablePayload): names = ['metadata_type', 'reserved_flags', 'public_key'] format_list = ['H', 'H', '64s'] - signature = NULL_SIG + signature: bytes = NULL_SIG + + public_key: bytes def serialized(self): return default_serializer.pack_serializable(self) diff --git a/src/tribler/core/components/metadata_store/db/tests/test_serialization.py b/src/tribler/core/components/metadata_store/db/tests/test_serialization.py new file mode 100644 index 00000000000..9f15b40cc74 --- /dev/null +++ b/src/tribler/core/components/metadata_store/db/tests/test_serialization.py @@ -0,0 +1,26 @@ +from datetime import datetime + +from tribler.core.components.metadata_store.db.serialization import TorrentMetadataPayload, int2time + + +def test_fix_torrent_metadata_payload(): + """ + Check that TorrentMetadataPayload can handle both timestamps and datetime "torrent_date"s. + """ + payload_1 = TorrentMetadataPayload(0, 0, bytes(range(64)), 0, 0, 0, bytes(range(20)), 0, 0, + "title", "tags", "tracker_info") + payload_2 = TorrentMetadataPayload(0, 0, bytes(range(64)), 0, 0, 0, bytes(range(20)), 0, datetime(1970, 1, 1), + "title", "tags", "tracker_info") + + assert payload_1.serialized() == payload_2.serialized() + + +def test_torrent_metadata_payload_magnet(): + """ + Check that TorrentMetadataPayload produces an appropriate magnet link. + """ + payload = TorrentMetadataPayload(0, 0, bytes(range(64)), 0, 0, 0, bytes(range(20)), 0, 0, + "title", "tags", "tracker_info") + expected = "magnet:?xt=urn:btih:000102030405060708090a0b0c0d0e0f10111213&dn=b'title'&tr=b'tracker_info'" + + assert expected == payload.get_magnet() diff --git a/src/tribler/core/components/metadata_store/db/tests/test_store.py b/src/tribler/core/components/metadata_store/db/tests/test_store.py index a608dbee3fe..f07b1cfb0a2 100644 --- a/src/tribler/core/components/metadata_store/db/tests/test_store.py +++ b/src/tribler/core/components/metadata_store/db/tests/test_store.py @@ -121,21 +121,68 @@ def test_process_forbidden_payload(metadata_store): @db_session def test_process_payload(metadata_store): sender_key = default_eccrypto.generate_key("curve25519") - for md_class in ( - metadata_store.TorrentMetadata, - ): - node, node_payload = get_payloads(md_class, sender_key) - node_dict = node.to_dict() - node.delete() + node, node_payload = get_payloads(metadata_store.TorrentMetadata, sender_key) + node_payload.add_signature(sender_key) + node_dict = node.to_dict() + node.delete() + + # Check if node metadata object is properly created on payload processing + result, = metadata_store.process_payload(node_payload) + assert result.obj_state == ObjState.NEW_OBJECT + assert node_dict['metadata_type'] == result.md_obj.to_dict()['metadata_type'] + + # Check that we flag this as duplicate in case we already know about the local node + result, = metadata_store.process_payload(node_payload) + assert result.obj_state == ObjState.DUPLICATE_OBJECT + + +@db_session +def test_process_payload_invalid_sig(metadata_store): + sender_key = default_eccrypto.generate_key("curve25519") + node, node_payload = get_payloads(metadata_store.TorrentMetadata, sender_key) + node_payload.add_signature(sender_key) + node_payload.signature = bytes(127 ^ byte for byte in node_payload.signature) + node.delete() + + assert [] == metadata_store.process_payload(node_payload) + + +@db_session +def test_process_payload_invalid_metadata_type(metadata_store): + sender_key = default_eccrypto.generate_key("curve25519") + node, node_payload = get_payloads(metadata_store.TorrentMetadata, sender_key) + node_payload.metadata_type = -1 + node.delete() + + assert [] == metadata_store.process_payload(node_payload) - # Check if node metadata object is properly created on payload processing - result = metadata_store.process_payload(node_payload)[0] - assert result.obj_state == ObjState.NEW_OBJECT - assert node_dict['metadata_type'] == result.md_obj.to_dict()['metadata_type'] +@db_session +def test_process_payload_skip_personal(metadata_store): + sender_key = default_eccrypto.generate_key("curve25519") + metadata_store.my_public_key_bin = sender_key.pub().key_to_bin()[10:] + node, node_payload = get_payloads(metadata_store.TorrentMetadata, sender_key) + node_payload.add_signature(sender_key) + node.delete() - # Check that nothing happens in case in case we already know about the local node - assert metadata_store.process_payload(node_payload) == [] + assert [] == metadata_store.process_payload(node_payload) + + +@db_session +def test_process_payload_unsigned(metadata_store): + sender_key = default_eccrypto.generate_key("curve25519") + node, node_payload = get_payloads(metadata_store.TorrentMetadata, sender_key) + node_dict = node.to_dict() + infohash = node_dict['infohash'] + node.delete() + + # Check if node metadata object is properly created on payload processing + result, = metadata_store.process_payload(node_payload) + assert result.obj_state == ObjState.NEW_OBJECT + assert node_dict['metadata_type'] == result.md_obj.to_dict()['metadata_type'] + + # Check that nothing happens in case we don't know about the local node + assert metadata_store.process_payload(node_payload) == [] @db_session @@ -179,3 +226,15 @@ def f1(a, b, *, c, d): with pytest.raises(ThreadedTestException, match='^test exception$'): await run_threaded(metadata_store.db, f1, 1, 2, c=5, d=6) + + +def test_get_entries_query_sort_by_size(metadata_store): + with db_session: + metadata_store.TorrentMetadata.add_ffa_from_dict({"infohash": b"\xab" * 20, "title": "abc", "size": 20}) + metadata_store.TorrentMetadata.add_ffa_from_dict({"infohash": b"\xcd" * 20, "title": "def", "size": 1}) + metadata_store.TorrentMetadata.add_ffa_from_dict({"infohash": b"\xef" * 20, "title": "ghi", "size": 10}) + + ordered1, ordered2, ordered3 = metadata_store.get_entries_query(sort_by="size", sort_desc=True)[:] + assert ordered1.size == 20 + assert ordered2.size == 10 + assert ordered3.size == 1 diff --git a/src/tribler/core/components/metadata_store/db/tests/test_torrent_metadata.py b/src/tribler/core/components/metadata_store/db/tests/test_torrent_metadata.py index 77f0f6c1caf..5878823157c 100644 --- a/src/tribler/core/components/metadata_store/db/tests/test_torrent_metadata.py +++ b/src/tribler/core/components/metadata_store/db/tests/test_torrent_metadata.py @@ -8,7 +8,8 @@ from pony.orm import db_session from tribler.core.components.libtorrent.torrentdef import TorrentDef -from tribler.core.components.metadata_store.db.orm_bindings.torrent_metadata import tdef_to_metadata_dict, TODELETE +from tribler.core.components.metadata_store.db.orm_bindings.torrent_metadata import tdef_to_metadata_dict, TODELETE, \ + entries_to_chunk from tribler.core.components.metadata_store.db.serialization import CHANNEL_TORRENT, REGULAR_TORRENT from tribler.core.tests.tools.common import TORRENT_UBUNTU_FILE from tribler.core.utilities.utilities import random_infohash @@ -31,6 +32,17 @@ def test_serialization(metadata_store): assert torrent_metadata.serialized() +def test_entries_to_chunk(): + """ + Test that calling entries_to_chunk with a start index >= the length of the metadata list raises an Exception. + """ + with pytest.raises(Exception): + entries_to_chunk([], 10, 0) + + with pytest.raises(Exception): + entries_to_chunk([], 10, 1) + + async def test_create_ffa_from_dict(metadata_store): """ Test creating a free-for-all torrent entry @@ -259,9 +271,7 @@ def test_get_entries(metadata_store): for _ in range(5): tlist.extend( [ - metadata_store.TorrentMetadata( - title='torrent%d' % torrent_ind, infohash=random_infohash(), size=123 - ) + metadata_store.TorrentMetadata(title=f'torrent{torrent_ind}', infohash=random_infohash(), size=123) for torrent_ind in range(5) ] ) diff --git a/src/tribler/core/components/metadata_store/remote_query_community/remote_query_community.py b/src/tribler/core/components/metadata_store/remote_query_community/remote_query_community.py index 5e4b7c8eaf4..441b32d87df 100644 --- a/src/tribler/core/components/metadata_store/remote_query_community/remote_query_community.py +++ b/src/tribler/core/components/metadata_store/remote_query_community/remote_query_community.py @@ -1,4 +1,5 @@ import json +import logging import struct import time from asyncio import Future @@ -156,7 +157,6 @@ def __init__(self, my_peer, endpoint, network, self.remote_queries_in_progress = 0 self.next_remote_query_num = count().__next__ # generator of sequential numbers, for logging & debug purposes - import logging self.logger.setLevel(logging.DEBUG) async def on_receive(self, result: TransferResult): diff --git a/src/tribler/core/components/metadata_store/remote_query_community/tests/test_remote_query_community.py b/src/tribler/core/components/metadata_store/remote_query_community/tests/test_remote_query_community.py index c7e63ed4f20..a174eccaf15 100644 --- a/src/tribler/core/components/metadata_store/remote_query_community/tests/test_remote_query_community.py +++ b/src/tribler/core/components/metadata_store/remote_query_community/tests/test_remote_query_community.py @@ -14,13 +14,13 @@ from pony.orm.dbapiprovider import OperationalError from tribler.core.components.ipv8.adapters_tests import TriblerTestBase -from tribler.core.components.metadata_store.db.orm_bindings.torrent_metadata import NEW +from tribler.core.components.metadata_store.db.orm_bindings.torrent_metadata import NEW, LZ4_EMPTY_ARCHIVE from tribler.core.components.metadata_store.db.serialization import CHANNEL_THUMBNAIL, REGULAR_TORRENT, \ NULL_KEY from tribler.core.components.metadata_store.db.store import MetadataStore from tribler.core.components.metadata_store.remote_query_community.remote_query_community import ( RemoteQueryCommunity, - sanitize_query, + sanitize_query, SelectResponsePayload, ) from tribler.core.components.metadata_store.remote_query_community.settings import RemoteQueryCommunitySettings from tribler.core.utilities.path_util import Path @@ -133,6 +133,17 @@ async def test_remote_select(self): await self.deliver_messages(timeout=0.5) callback.assert_called() + async def test_remote_select_deprecated(self): + """ + Test deprecated search keys receiving an empty archive response. + """ + with self.assertReceivedBy(0, [SelectResponsePayload]) as responses: + self.overlay(0).send_remote_select(self.peer(1), subscribed=1) + await self.deliver_messages() + response, = responses + + assert response.raw_blob == LZ4_EMPTY_ARCHIVE + async def test_push_entry_update(self): """ Test if sending back information on updated version of a metadata entry works diff --git a/src/tribler/core/components/metadata_store/restapi/search_endpoint.py b/src/tribler/core/components/metadata_store/restapi/search_endpoint.py index f26b81d85f9..bd04fde58f8 100644 --- a/src/tribler/core/components/metadata_store/restapi/search_endpoint.py +++ b/src/tribler/core/components/metadata_store/restapi/search_endpoint.py @@ -64,9 +64,10 @@ def build_snippets(self, search_results: typing.List[typing.Dict]) -> typing.Lis if "infohash" not in search_result: continue with db_session: - content_items: typing.List[str] = self.tribler_db.get_objects(subject_type=ResourceType.TORRENT, - subject=search_result["infohash"], - predicate=ResourceType.CONTENT_ITEM) + content_items: typing.List[str] = self.tribler_db.knowledge.get_objects( + subject_type=ResourceType.TORRENT, + subject=search_result["infohash"], + predicate=ResourceType.CONTENT_ITEM) if content_items: for content_id in content_items: content_to_torrents[content_id].append(search_result) diff --git a/src/tribler/core/components/metadata_store/restapi/tests/test_metadata_endpoint.py b/src/tribler/core/components/metadata_store/restapi/tests/test_metadata_endpoint.py index 6eee0f1af0c..673ea5a2aa4 100644 --- a/src/tribler/core/components/metadata_store/restapi/tests/test_metadata_endpoint.py +++ b/src/tribler/core/components/metadata_store/restapi/tests/test_metadata_endpoint.py @@ -1,4 +1,4 @@ -from unittest.mock import MagicMock +from unittest.mock import MagicMock, Mock, AsyncMock import pytest @@ -53,3 +53,31 @@ async def test_check_torrent_query(rest_api, udp_tracker, metadata_store): """ infohash = b'a' * 20 await do_request(rest_api, f"metadata/torrents/{infohash}/health?timeout=wrong_value&refresh=1", expected_code=400) + + +async def test_get_popular_torrents(rest_api, endpoint, metadata_store): + """ + Test that the endpoint responds with its known entries. + """ + fake_entry = { + "name": "Torrent Name", + "category": "", + "infohash": "ab" * 20, + "size": 1, + "num_seeders": 1234, + "num_leechers": 123, + "last_tracker_check": 17000000, + "created": 15000000, + "tag_processor_version": 1, + "type": 101, + "id": 0, + "origin_id": 0, + "public_key": "ab" * 64, + "status": 2, + } + metadata_store.get_entries = Mock(return_value=[Mock(to_simple_dict=lambda: fake_entry)]) + endpoint.tag_rules_processor = Mock(process_queue=AsyncMock()) + response = await do_request(rest_api, f"metadata/torrents/popular") + + endpoint.tag_rules_processor.process_queue.assert_called_once() + assert response == {'results': [fake_entry], 'first': 1, 'last': 50} diff --git a/src/tribler/core/components/metadata_store/restapi/tests/test_search_endpoint.py b/src/tribler/core/components/metadata_store/restapi/tests/test_search_endpoint.py index c635ee77d10..d149d6c90f7 100644 --- a/src/tribler/core/components/metadata_store/restapi/tests/test_search_endpoint.py +++ b/src/tribler/core/components/metadata_store/restapi/tests/test_search_endpoint.py @@ -8,7 +8,6 @@ from pony.orm import db_session from tribler.core.components.database.db.layers.knowledge_data_access_layer import KnowledgeDataAccessLayer -from tribler.core.components.database.db.tribler_database import TriblerDatabase from tribler.core.components.metadata_store.db.serialization import REGULAR_TORRENT, SNIPPET from tribler.core.components.metadata_store.restapi.search_endpoint import SearchEndpoint from tribler.core.components.restapi.rest.base_api_test import do_request @@ -75,7 +74,7 @@ def mocked_get_subjects_intersection(*_, objects: Set[str], **__): return None return {hexlify(os.urandom(20))} - with patch.object(TriblerDatabase, 'get_subjects_intersection', wraps=mocked_get_subjects_intersection): + with patch.object(KnowledgeDataAccessLayer, 'get_subjects_intersection', wraps=mocked_get_subjects_intersection): parsed = await do_request(rest_api, 'search/local?txt_filter=needle&tags=real_tag', expected_code=200) assert len(parsed["results"]) == 0 @@ -232,12 +231,7 @@ def test_build_snippets_no_infohash(endpoint: SearchEndpoint): assert result == search_results -@pytest.fixture -def mock_gigachannel_community(): - return Mock() - - -async def test_create_remote_search_request(rest_api, mock_gigachannel_community): +async def test_create_remote_search_request(rest_api, mock_popularity_community): """ Test that remote search call is sent on a REST API search request """ @@ -268,3 +262,16 @@ def mock_send(**kwargs): rest_api, f'search/remote?channel_pk={channel_pk}&metadata_type=torrent', request_type="PUT", expected_code=200 ) assert hexlify(sent['channel_pk']) == channel_pk + + +async def test_create_remote_search_request_illegal(rest_api, mock_popularity_community): + """ + Test that remote search call is sent on a REST API search request + """ + response = await do_request( + rest_api, + f'search/remote?origin_id=a', + request_type="PUT", + expected_code=400 + ) + assert "error" in response diff --git a/src/tribler/core/components/popularity/community/popularity_community.py b/src/tribler/core/components/popularity/community/popularity_community.py index b0c1fcf1657..0a6453faf77 100644 --- a/src/tribler/core/components/popularity/community/popularity_community.py +++ b/src/tribler/core/components/popularity/community/popularity_community.py @@ -67,23 +67,6 @@ def __init__(self, *args, torrent_checker=None, notifier=None, **kwargs): self.discovery_booster = DiscoveryBooster() self.discovery_booster.apply(self) - def guess_address(self, interface): - # Address caching allows 100x speedup of EdgeWalk.take_step() in DiscoveryBooster, from 3.0 to 0.03 seconds. - # The overridden method can be removed after IPv8 adds internal caching of addresses. - now = time.time() - cache_lifetime = now - self.address_cache_created_at - if cache_lifetime > max_address_cache_lifetime: - self.address_cache.clear() - self.address_cache_created_at = now - - result = self.address_cache.get(interface) - if result is not None: - return result - - result = super().guess_address(interface) - self.address_cache[interface] = result - return result - def introduction_request_callback(self, peer, dist, payload): super().introduction_request_callback(peer, dist, payload) # Send request to peer to send popular torrents @@ -177,9 +160,7 @@ def get_random_torrents(self) -> List[HealthInfo]: def get_random_peers(self, sample_size=None): # Randomly sample sample_size peers from the complete list of our peers all_peers = self.get_peers() - if sample_size is not None and sample_size < len(all_peers): - return random.sample(all_peers, sample_size) - return all_peers + return random.sample(all_peers, min(sample_size or len(all_peers), len(all_peers))) def send_search_request(self, **kwargs): # Send a remote query request to multiple random peers to search for some terms diff --git a/src/tribler/core/components/popularity/community/tests/test_popularity_community.py b/src/tribler/core/components/popularity/community/tests/test_popularity_community.py index 41c51836edc..f449782eccc 100644 --- a/src/tribler/core/components/popularity/community/tests/test_popularity_community.py +++ b/src/tribler/core/components/popularity/community/tests/test_popularity_community.py @@ -1,227 +1 @@ -import time -from random import randint -from typing import List -from unittest.mock import Mock - -from ipv8.keyvault.crypto import default_eccrypto -from pony.orm import db_session - -from tribler.core.components.ipv8.adapters_tests import TriblerMockIPv8, TriblerTestBase -from tribler.core.components.metadata_store.db.store import MetadataStore -from tribler.core.components.metadata_store.remote_query_community.settings import RemoteQueryCommunitySettings -from tribler.core.components.popularity.community.popularity_community import PopularityCommunity -from tribler.core.components.torrent_checker.torrent_checker.torrentchecker_session import HealthInfo -from tribler.core.tests.tools.base_test import MockObject -from tribler.core.utilities.path_util import Path -from tribler.core.utilities.utilities import random_infohash - - -def _generate_single_checked_torrent(status: str = None) -> HealthInfo: - """ - Assumptions - DEAD -> peers: 0 - POPULAR -> Peers: [101, 1000] - DEFAULT -> peers: [1, 100] # alive - """ - - def get_peers_for(health_status): - if health_status == 'DEAD': - return 0 - if health_status == 'POPULAR': - return randint(101, 1000) - return randint(1, 100) - - return HealthInfo(random_infohash(), seeders=get_peers_for(status), leechers=get_peers_for(status)) - - -def _generate_checked_torrents(count: int, status: str = None) -> List[HealthInfo]: - return [_generate_single_checked_torrent(status) for _ in range(count)] - - -class TestPopularityCommunity(TriblerTestBase): - NUM_NODES = 2 - - def setUp(self): - super().setUp() - self.count = 0 - self.metadata_store_set = set() - self.initialize(PopularityCommunity, self.NUM_NODES) - - async def tearDown(self): - for metadata_store in self.metadata_store_set: - metadata_store.shutdown() - await super().tearDown() - - def create_node(self, *args, **kwargs): - mds = MetadataStore(Path(self.temporary_directory()) / f"{self.count}", - Path(self.temporary_directory()), - default_eccrypto.generate_key("curve25519")) - self.metadata_store_set.add(mds) - torrent_checker = MockObject() - torrent_checker.torrents_checked = {} - - self.count += 1 - - rqc_settings = RemoteQueryCommunitySettings() - return TriblerMockIPv8("curve25519", PopularityCommunity, metadata_store=mds, - torrent_checker=torrent_checker, - rqc_settings=rqc_settings - ) - - @db_session - def fill_database(self, metadata_store, last_check_now=False): - for torrent_ind in range(5): - last_check = int(time.time()) if last_check_now else 0 - metadata_store.TorrentState( - infohash=str(torrent_ind).encode() * 20, seeders=torrent_ind + 1, last_check=last_check) - - async def init_first_node_and_gossip(self, checked_torrent_info: HealthInfo, deliver_timeout: float = 0.1): - self.nodes[0].overlay.torrent_checker.torrents_checked[checked_torrent_info.infohash] = checked_torrent_info - await self.introduce_nodes() - - self.nodes[0].overlay.gossip_random_torrents_health() - - await self.deliver_messages(timeout=deliver_timeout) - - def torrent_metadata(self, i): - return self.overlay(i).mds.TorrentMetadata - - async def test_torrents_health_gossip(self): - """ - Test whether torrent health information is correctly gossiped around - """ - checked_torrent_info = HealthInfo(b'a' * 20, seeders=200, leechers=0) - node0_db = self.nodes[0].overlay.mds.TorrentState - node1_db2 = self.nodes[1].overlay.mds.TorrentState - - with db_session: - assert node0_db.select().count() == 0 - assert node1_db2.select().count() == 0 - - await self.init_first_node_and_gossip(checked_torrent_info) - - # Check whether node 1 has new torrent health information - with db_session: - torrent = node1_db2.select().first() - assert torrent.infohash == checked_torrent_info.infohash - assert torrent.seeders == checked_torrent_info.seeders - assert torrent.leechers == checked_torrent_info.leechers - assert torrent.last_check == checked_torrent_info.last_check - - def test_get_alive_torrents(self): - dead_torrents = _generate_checked_torrents(100, 'DEAD') - popular_torrents = _generate_checked_torrents(100, 'POPULAR') - alive_torrents = _generate_checked_torrents(100) - - all_checked_torrents = dead_torrents + alive_torrents + popular_torrents - self.nodes[0].overlay.torrent_checker.torrents_checked.update( - {health.infohash: health for health in all_checked_torrents}) - - actual_alive_torrents = self.nodes[0].overlay.get_alive_checked_torrents() - assert len(actual_alive_torrents) == len(alive_torrents + popular_torrents) - - async def test_torrents_health_gossip_multiple(self): - """ - Test whether torrent health information is correctly gossiped around - """ - dead_torrents = _generate_checked_torrents(100, 'DEAD') - popular_torrents = _generate_checked_torrents(100, 'POPULAR') - alive_torrents = _generate_checked_torrents(100) - - all_checked_torrents = dead_torrents + alive_torrents + popular_torrents - - node0_db = self.nodes[0].overlay.mds.TorrentState - node1_db = self.nodes[1].overlay.mds.TorrentState - - # Given, initially there are no torrents in the database - with db_session: - node0_count = node0_db.select().count() - node1_count = node1_db.select().count() - assert node0_count == 0 - assert node1_count == 0 - - # Setup, node 0 checks some torrents, both dead and alive (including popular ones). - self.nodes[0].overlay.torrent_checker.torrents_checked.update( - {health.infohash: health for health in all_checked_torrents}) - - # Nodes are introduced - await self.introduce_nodes() - - # Since on introduction request callback, node asks for popular torrents, we expect that - # popular torrents are shared by node 0 to node 1. - with db_session: - node0_count = node0_db.select().count() - node1_count = node1_db.select().count() - - assert node0_count == 0 # Nothing received from Node 1 because it hasn't checked anything to share. - assert node1_count == PopularityCommunity.GOSSIP_POPULAR_TORRENT_COUNT - - node1_db_last_count = node1_count - - # Now, assuming Node 0 gossips random torrents to Node 1 multiple times to simulate periodic nature - for _ in range(10): - self.nodes[0].overlay.gossip_random_torrents_health() - await self.deliver_messages(timeout=0.1) - - # After gossip, Node 1 should have received some random torrents from Node 0. - # Note that random torrents can also include popular torrents sent during introduction - # and random torrents sent in earlier gossip since no state is maintained. - with db_session: - node0_count = node0_db.select().count() - node1_count = node1_db.select().count() - - assert node0_count == 0 # Still nothing received from Node 1 because it hasn't checked torrents - assert node1_count >= node1_db_last_count - - node1_db_last_count = node1_count - - async def test_torrents_health_update(self): - """ - Test updating the local torrent health information from network - """ - self.fill_database(self.nodes[1].overlay.mds) - - checked_torrent_info = HealthInfo(b'0' * 20, seeders=200, leechers=0) - await self.init_first_node_and_gossip(checked_torrent_info, deliver_timeout=0.5) - - # Check whether node 1 has new torrent health information - with db_session: - state = self.nodes[1].overlay.mds.TorrentState.get(infohash=b'0' * 20) - self.assertIsNot(state.last_check, 0) - - async def test_unknown_torrent_query_back(self): - """ - Test querying sender for metadata upon receiving an unknown torrent - """ - - infohash = b'1' * 20 - with db_session: - self.nodes[0].overlay.mds.TorrentMetadata(infohash=infohash) - await self.init_first_node_and_gossip( - HealthInfo(infohash, seeders=200, leechers=0)) - with db_session: - assert self.nodes[1].overlay.mds.TorrentMetadata.get() - - async def test_skip_torrent_query_back_for_known_torrent(self): - # Test that we _don't_ send the query if we already know about the infohash - infohash = b'1' * 20 - with db_session: - self.nodes[0].overlay.mds.TorrentMetadata(infohash=infohash) - self.nodes[1].overlay.mds.TorrentMetadata(infohash=infohash) - self.nodes[1].overlay.send_remote_select = Mock() - await self.init_first_node_and_gossip( - HealthInfo(infohash, seeders=200, leechers=0)) - self.nodes[1].overlay.send_remote_select.assert_not_called() - - async def test_popularity_search(self): - """ - Test searching several nodes for metadata entries based on title text - """ - with db_session: - # Add test metadata to node ID2 - self.torrent_metadata(1)(title="ubuntu torrent", infohash=random_infohash()) - self.torrent_metadata(1)(title="debian torrent", infohash=random_infohash()) - - self.overlay(0).send_search_request(**{"txt_filter": "ubuntu*"}) - - await self.deliver_messages() \ No newline at end of file +import time from random import randint from typing import List from unittest.mock import Mock from ipv8.keyvault.crypto import default_eccrypto from pony.orm import db_session from tribler.core import notifications from tribler.core.components.ipv8.adapters_tests import TriblerMockIPv8, TriblerTestBase from tribler.core.components.metadata_store.db.store import MetadataStore from tribler.core.components.metadata_store.remote_query_community.settings import RemoteQueryCommunitySettings from tribler.core.components.popularity.community.popularity_community import PopularityCommunity from tribler.core.components.torrent_checker.torrent_checker.torrentchecker_session import HealthInfo from tribler.core.tests.tools.base_test import MockObject from tribler.core.utilities.path_util import Path from tribler.core.utilities.utilities import random_infohash def _generate_single_checked_torrent(status: str = None) -> HealthInfo: """ Assumptions DEAD -> peers: 0 POPULAR -> Peers: [101, 1000] DEFAULT -> peers: [1, 100] # alive """ def get_peers_for(health_status): if health_status == 'DEAD': return 0 if health_status == 'POPULAR': return randint(101, 1000) return randint(1, 100) return HealthInfo(random_infohash(), seeders=get_peers_for(status), leechers=get_peers_for(status)) def _generate_checked_torrents(count: int, status: str = None) -> List[HealthInfo]: return [_generate_single_checked_torrent(status) for _ in range(count)] class TestPopularityCommunity(TriblerTestBase): NUM_NODES = 2 def setUp(self): super().setUp() self.count = 0 self.metadata_store_set = set() self.initialize(PopularityCommunity, self.NUM_NODES) async def tearDown(self): for metadata_store in self.metadata_store_set: metadata_store.shutdown() await super().tearDown() def create_node(self, *args, **kwargs): mds = MetadataStore(Path(self.temporary_directory()) / f"{self.count}", Path(self.temporary_directory()), default_eccrypto.generate_key("curve25519")) self.metadata_store_set.add(mds) torrent_checker = MockObject() torrent_checker.torrents_checked = {} self.count += 1 rqc_settings = RemoteQueryCommunitySettings() return TriblerMockIPv8("curve25519", PopularityCommunity, metadata_store=mds, torrent_checker=torrent_checker, rqc_settings=rqc_settings ) @db_session def fill_database(self, metadata_store, last_check_now=False): for torrent_ind in range(5): last_check = int(time.time()) if last_check_now else 0 metadata_store.TorrentState( infohash=str(torrent_ind).encode() * 20, seeders=torrent_ind + 1, last_check=last_check) async def init_first_node_and_gossip(self, checked_torrent_info: HealthInfo, deliver_timeout: float = 0.1): self.nodes[0].overlay.torrent_checker.torrents_checked[checked_torrent_info.infohash] = checked_torrent_info await self.introduce_nodes() self.nodes[0].overlay.gossip_random_torrents_health() await self.deliver_messages(timeout=deliver_timeout) def torrent_metadata(self, i): return self.overlay(i).mds.TorrentMetadata async def test_torrents_health_gossip(self): """ Test whether torrent health information is correctly gossiped around """ checked_torrent_info = HealthInfo(b'a' * 20, seeders=200, leechers=0) node0_db = self.nodes[0].overlay.mds.TorrentState node1_db2 = self.nodes[1].overlay.mds.TorrentState with db_session: assert node0_db.select().count() == 0 assert node1_db2.select().count() == 0 await self.init_first_node_and_gossip(checked_torrent_info) # Check whether node 1 has new torrent health information with db_session: torrent = node1_db2.select().first() assert torrent.infohash == checked_torrent_info.infohash assert torrent.seeders == checked_torrent_info.seeders assert torrent.leechers == checked_torrent_info.leechers assert torrent.last_check == checked_torrent_info.last_check def test_get_alive_torrents(self): dead_torrents = _generate_checked_torrents(100, 'DEAD') popular_torrents = _generate_checked_torrents(100, 'POPULAR') alive_torrents = _generate_checked_torrents(100) all_checked_torrents = dead_torrents + alive_torrents + popular_torrents self.nodes[0].overlay.torrent_checker.torrents_checked.update( {health.infohash: health for health in all_checked_torrents}) actual_alive_torrents = self.nodes[0].overlay.get_alive_checked_torrents() assert len(actual_alive_torrents) == len(alive_torrents + popular_torrents) async def test_torrents_health_gossip_multiple(self): """ Test whether torrent health information is correctly gossiped around """ dead_torrents = _generate_checked_torrents(100, 'DEAD') popular_torrents = _generate_checked_torrents(100, 'POPULAR') alive_torrents = _generate_checked_torrents(100) all_checked_torrents = dead_torrents + alive_torrents + popular_torrents node0_db = self.nodes[0].overlay.mds.TorrentState node1_db = self.nodes[1].overlay.mds.TorrentState # Given, initially there are no torrents in the database with db_session: node0_count = node0_db.select().count() node1_count = node1_db.select().count() assert node0_count == 0 assert node1_count == 0 # Setup, node 0 checks some torrents, both dead and alive (including popular ones). self.nodes[0].overlay.torrent_checker.torrents_checked.update( {health.infohash: health for health in all_checked_torrents}) # Nodes are introduced await self.introduce_nodes() # Since on introduction request callback, node asks for popular torrents, we expect that # popular torrents are shared by node 0 to node 1. with db_session: node0_count = node0_db.select().count() node1_count = node1_db.select().count() assert node0_count == 0 # Nothing received from Node 1 because it hasn't checked anything to share. assert node1_count == PopularityCommunity.GOSSIP_POPULAR_TORRENT_COUNT node1_db_last_count = node1_count # Now, assuming Node 0 gossips random torrents to Node 1 multiple times to simulate periodic nature for _ in range(10): self.nodes[0].overlay.gossip_random_torrents_health() await self.deliver_messages(timeout=0.1) # After gossip, Node 1 should have received some random torrents from Node 0. # Note that random torrents can also include popular torrents sent during introduction # and random torrents sent in earlier gossip since no state is maintained. with db_session: node0_count = node0_db.select().count() node1_count = node1_db.select().count() assert node0_count == 0 # Still nothing received from Node 1 because it hasn't checked torrents assert node1_count >= node1_db_last_count node1_db_last_count = node1_count async def test_torrents_health_update(self): """ Test updating the local torrent health information from network """ self.fill_database(self.nodes[1].overlay.mds) checked_torrent_info = HealthInfo(b'0' * 20, seeders=200, leechers=0) await self.init_first_node_and_gossip(checked_torrent_info, deliver_timeout=0.5) # Check whether node 1 has new torrent health information with db_session: state = self.nodes[1].overlay.mds.TorrentState.get(infohash=b'0' * 20) self.assertIsNot(state.last_check, 0) async def test_unknown_torrent_query_back(self): """ Test querying sender for metadata upon receiving an unknown torrent """ infohash = b'1' * 20 with db_session: self.nodes[0].overlay.mds.TorrentMetadata(infohash=infohash) await self.init_first_node_and_gossip( HealthInfo(infohash, seeders=200, leechers=0)) with db_session: assert self.nodes[1].overlay.mds.TorrentMetadata.get() async def test_skip_torrent_query_back_for_known_torrent(self): # Test that we _don't_ send the query if we already know about the infohash infohash = b'1' * 20 with db_session: self.nodes[0].overlay.mds.TorrentMetadata(infohash=infohash) self.nodes[1].overlay.mds.TorrentMetadata(infohash=infohash) self.nodes[1].overlay.send_remote_select = Mock() await self.init_first_node_and_gossip( HealthInfo(infohash, seeders=200, leechers=0)) self.nodes[1].overlay.send_remote_select.assert_not_called() async def test_popularity_search(self): """ Test searching several nodes for metadata entries based on title text """ with db_session: # Add test metadata to node ID2 self.torrent_metadata(1)(title="ubuntu torrent", infohash=random_infohash()) self.torrent_metadata(1)(title="debian torrent", infohash=random_infohash()) notifier = Mock() self.overlay(0).notifier = {notifications.remote_query_results: notifier} self.overlay(0).send_search_request(**{"txt_filter": "ubuntu*"}) await self.deliver_messages() notifier.assert_called() \ No newline at end of file diff --git a/src/tribler/core/components/restapi/tests/test_restapi_component.py b/src/tribler/core/components/restapi/tests/test_restapi_component.py index fe9f8fc8f8a..501a600bbc4 100644 --- a/src/tribler/core/components/restapi/tests/test_restapi_component.py +++ b/src/tribler/core/components/restapi/tests/test_restapi_component.py @@ -5,7 +5,6 @@ from tribler.core.components.bandwidth_accounting.bandwidth_accounting_component import BandwidthAccountingComponent from tribler.core.components.database.database_component import DatabaseComponent from tribler.core.components.exceptions import NoneComponent -from tribler.core.components.gigachannel.gigachannel_component import GigaChannelComponent from tribler.core.components.ipv8.ipv8_component import Ipv8Component from tribler.core.components.key.key_component import KeyComponent from tribler.core.components.knowledge.knowledge_component import KnowledgeComponent @@ -22,7 +21,7 @@ # pylint: disable=protected-access, not-callable, redefined-outer-name async def test_rest_component(tribler_config): components = [KeyComponent(), RESTComponent(), Ipv8Component(), LibtorrentComponent(), ResourceMonitorComponent(), - BandwidthAccountingComponent(), GigaChannelComponent(), KnowledgeComponent(), SocksServersComponent(), + BandwidthAccountingComponent(), KnowledgeComponent(), SocksServersComponent(), MetadataStoreComponent(), DatabaseComponent()] async with Session(tribler_config, components) as session: # Test REST component starts normally diff --git a/src/tribler/core/start_core.py b/src/tribler/core/start_core.py index 4294ecc5b45..49c18f053f3 100644 --- a/src/tribler/core/start_core.py +++ b/src/tribler/core/start_core.py @@ -71,10 +71,6 @@ def components_gen(config: TriblerConfig): if config.resource_monitor.enabled: yield ResourceMonitorComponent() - # The components below are skipped if config.gui_test_mode == True - if config.gui_test_mode: - return - if config.libtorrent.enabled: yield SocksServersComponent() @@ -82,6 +78,11 @@ def components_gen(config: TriblerConfig): yield TorrentCheckerComponent() if config.ipv8.enabled and config.torrent_checking.enabled and config.popularity_community.enabled: yield PopularityComponent() + + # The components below are skipped if config.gui_test_mode == True + if config.gui_test_mode: + return + if config.ipv8.enabled and config.tunnel_community.enabled: yield TunnelsComponent() if config.ipv8.enabled: diff --git a/src/tribler/core/tests/test_start_core.py b/src/tribler/core/tests/test_start_core.py index 7b5f20842fc..43f0e1b8330 100644 --- a/src/tribler/core/tests/test_start_core.py +++ b/src/tribler/core/tests/test_start_core.py @@ -14,3 +14,15 @@ def test_start_tribler_core_no_exceptions(mocked_core_session): # test that base logic of tribler core runs without exceptions run_tribler_core_session(1, 'key', Path('.'), False) mocked_core_session.assert_called_once() + + +@patch('tribler.core.logger.logger.load_logger_config', new=MagicMock()) +@patch('tribler.core.start_core.set_process_priority', new=MagicMock()) +@patch('tribler.core.start_core.check_and_enable_code_tracing', new=MagicMock()) +@patch('asyncio.get_event_loop', new=MagicMock()) +@patch('tribler.core.start_core.TriblerConfig.load', new=MagicMock()) +@patch('tribler.core.start_core.core_session') +def test_start_tribler_core_gui_test_mode(mocked_core_session): + # test that base logic of tribler core runs without exceptions + run_tribler_core_session(1, 'key', Path('.'), True) + mocked_core_session.assert_called_once() diff --git a/src/tribler/core/upgrade/tags_to_knowledge/previous_dbs/tags_db.py b/src/tribler/core/upgrade/tags_to_knowledge/previous_dbs/tags_db.py index 3df9229202e..69dabbd050d 100644 --- a/src/tribler/core/upgrade/tags_to_knowledge/previous_dbs/tags_db.py +++ b/src/tribler/core/upgrade/tags_to_knowledge/previous_dbs/tags_db.py @@ -1,7 +1,9 @@ import datetime +from contextlib import suppress from typing import Optional from pony import orm +from pony.orm import db_session, OperationalError from tribler.core.utilities.pony_utils import TrackedDatabase, get_or_create @@ -11,6 +13,10 @@ def __init__(self, filename: Optional[str] = None, *, create_tables: bool = True self.instance = TrackedDatabase() self.define_binding(self.instance) self.instance.bind('sqlite', filename or ':memory:', create_db=True) + if create_tables: + with db_session, suppress(OperationalError): + cursor = self.instance.execute("ALTER TABLE TorrentTagOp ADD auto_generated INTEGER") + cursor.close() generate_mapping_kwargs['create_tables'] = create_tables self.instance.generate_mapping(**generate_mapping_kwargs) diff --git a/src/tribler/core/upgrade/tests/test_upgrader.py b/src/tribler/core/upgrade/tests/test_upgrader.py index 4bb5d65f445..b20f7602ca0 100644 --- a/src/tribler/core/upgrade/tests/test_upgrader.py +++ b/src/tribler/core/upgrade/tests/test_upgrader.py @@ -10,6 +10,7 @@ from pony.orm import db_session, select from tribler.core.components.bandwidth_accounting.db.database import BandwidthDatabase +from tribler.core.components.metadata_store.db.orm_bindings.torrent_metadata import CHANNEL_DIR_NAME_LENGTH from tribler.core.components.metadata_store.db.store import CURRENT_DB_VERSION, MetadataStore from tribler.core.tests.tools.common import TESTS_DATA_DIR from tribler.core.upgrade.db8_to_db10 import calc_progress @@ -68,11 +69,7 @@ def test_upgrade_pony_db_complete(upgrader, channels_dir, state_dir, trustchain_ mds = MetadataStore(mds_path, channels_dir, trustchain_keypair) db = mds.db - existing_indexes = [ - 'idx_channelnode__metadata_type__partial', - 'idx_channelnode__metadata_subscribed__partial', - 'idx_torrentstate__last_check__partial', - ] + existing_indexes = [] removed_indexes = [ 'idx_channelnode__public_key', @@ -88,8 +85,6 @@ def test_upgrade_pony_db_complete(upgrader, channels_dir, state_dir, trustchain_ ] with db_session: - assert mds.TorrentMetadata.select().count() == 23 - assert mds.ChannelMetadata.select().count() == 2 assert mds.get_value("db_version") == str(CURRENT_DB_VERSION) for index_name in existing_indexes: assert list(db.execute(f'PRAGMA index_info("{index_name}")')) @@ -129,7 +124,6 @@ def test_upgrade_pony_8to10(upgrader, channels_dir, mds_path, trustchain_keypair mds = MetadataStore(mds_path, channels_dir, trustchain_keypair, check_tables=False, db_version=10) with db_session: assert mds.get_value("db_version") == '10' - assert mds.ChannelNode.select().count() == 23 mds.shutdown() @@ -256,11 +250,7 @@ def test_upgrade_pony12to13(upgrader, channels_dir, mds_path, trustchain_keypair mds = MetadataStore(mds_path, channels_dir, trustchain_keypair, check_tables=False, db_version=12) db = mds.db - existing_indexes = [ - 'idx_channelnode__metadata_type__partial', - 'idx_channelnode__metadata_subscribed__partial', - 'idx_torrentstate__last_check__partial', - ] + existing_indexes = ['idx_torrentstate__last_check__partial'] removed_indexes = [ 'idx_channelnode__public_key', @@ -276,8 +266,7 @@ def test_upgrade_pony12to13(upgrader, channels_dir, mds_path, trustchain_keypair ] with db_session: - assert mds.TorrentMetadata.select().count() == 23 - assert mds.ChannelMetadata.select().count() == 2 + assert mds.TorrentMetadata.select().count() == 21 assert mds.get_value("db_version") == '13' for index_name in existing_indexes: assert list(db.execute(f'PRAGMA index_info("{index_name}")')), index_name diff --git a/src/tribler/gui/debug_window.py b/src/tribler/gui/debug_window.py index 1356c2ff7b6..774f8ec2c19 100644 --- a/src/tribler/gui/debug_window.py +++ b/src/tribler/gui/debug_window.py @@ -115,10 +115,6 @@ def __init__(self, settings, gui_settings, tribler_version): # Libtorrent tab self.init_libtorrent_tab() - # Channels tab - connect(self.window().channels_tab_widget.currentChanged, self.channels_tab_changed) - self.window().channels_tab_widget.setCurrentIndex(0) - # Position to center frame_geometry = self.frameGeometry() screen = QDesktopWidget().screenNumber(QDesktopWidget().cursor().pos()) @@ -199,8 +195,6 @@ def tab_changed(self, index): self.load_libtorrent_data() elif index == 9: self.load_logs_tab() - elif index == 10: - self.channels_tab_changed(self.window().channels_tab_widget.currentIndex()) def ipv8_tab_changed(self, index): if index == 0: @@ -943,31 +937,3 @@ def save_to_file(self, filename, data): torrent_file.write(json.dumps(data)) except OSError as exc: ConfirmationDialog.show_error(self.window(), "Error exporting file", str(exc)) - - def on_channels_peers(self, data): - widget = self.window().channels_peers_tree_widget - widget.clear() - if not data: - return - - for c in data["channels_list"]: - channel_item = QTreeWidgetItem() - channel_item.setText(0, str(c["channel_name"])) - channel_item.setText(1, str(c["channel_pk"])) - channel_item.setText(2, str(c["channel_id"])) - channel_item.setData(3, Qt.DisplayRole, len(c["peers"])) # Peers count - for p in c["peers"]: - peer_item = QTreeWidgetItem() - peer_item.setText(1, str(p[0])) # Peer mid - peer_item.setData(4, Qt.DisplayRole, p[1]) # Peer age - channel_item.addChild(peer_item) - widget.addTopLevelItem(channel_item) - - def load_channels_peers_tab(self): - request_manager.get("remote_query/channels_peers", self.on_channels_peers) - - def channels_tab_changed(self, index): - if index == 0: - self.run_with_timer(self.load_channels_peers_tab) - elif index == 1: - pass diff --git a/src/tribler/gui/defs.py b/src/tribler/gui/defs.py index 40cc4a8bd63..38042f75157 100644 --- a/src/tribler/gui/defs.py +++ b/src/tribler/gui/defs.py @@ -16,12 +16,7 @@ PAGE_SETTINGS = 1 PAGE_DOWNLOADS = 2 PAGE_LOADING = 3 -PAGE_DISCOVERING = 4 -PAGE_DISCOVERED = 5 -PAGE_TRUST = 6 -PAGE_TRUST_GRAPH_PAGE = 7 -PAGE_CHANNEL_CONTENTS = 8 -PAGE_POPULAR = 9 +PAGE_POPULAR = 4 PAGE_EDIT_CHANNEL_TORRENTS = 2 diff --git a/src/tribler/gui/dialogs/addtopersonalchanneldialog.py b/src/tribler/gui/dialogs/addtopersonalchanneldialog.py deleted file mode 100644 index 737a78ed277..00000000000 --- a/src/tribler/gui/dialogs/addtopersonalchanneldialog.py +++ /dev/null @@ -1,143 +0,0 @@ -import json - -from PyQt5 import QtWidgets, uic -from PyQt5.QtCore import pyqtSignal - -from tribler.core.components.metadata_store.db.serialization import CHANNEL_TORRENT, COLLECTION_NODE -from tribler.gui.dialogs.dialogcontainer import DialogContainer -from tribler.gui.dialogs.new_channel_dialog import NewChannelDialog -from tribler.gui.network.request_manager import request_manager -from tribler.gui.utilities import connect, get_ui_file_path - - -class ChannelQTreeWidgetItem(QtWidgets.QTreeWidgetItem): - def __init__(self, *args, **kwargs): - self.id_ = kwargs.pop("id_") if "id_" in kwargs else 0 - QtWidgets.QTreeWidgetItem.__init__(self, *args, **kwargs) - - -class AddToChannelDialog(DialogContainer): - create_torrent_notification = pyqtSignal(dict) - - def __init__(self, parent): - DialogContainer.__init__(self, parent) - uic.loadUi(get_ui_file_path('addtochanneldialog.ui'), self.dialog_widget) - connect(self.dialog_widget.btn_cancel.clicked, self.close_dialog) - connect(self.dialog_widget.btn_confirm.clicked, self.on_confirm_clicked) - connect(self.dialog_widget.btn_new_channel.clicked, self.on_create_new_channel_clicked) - connect(self.dialog_widget.btn_new_folder.clicked, self.on_create_new_folder_clicked) - - self.confirm_clicked_callback = None - - self.root_requests_list = [] - - self.channels_tree = {} - self.id2wt_mapping = {0: self.dialog_widget.channels_tree_wt} - connect(self.dialog_widget.channels_tree_wt.itemExpanded, self.on_item_expanded) - - self.dialog_widget.channels_tree_wt.setHeaderLabels(['Name']) - self.on_main_window_resize() - - def on_new_channel_response(self, response): - if not response or not response.get("results", None): - return - self.window().channels_menu_list.reload_if_necessary(response["results"]) - self.load_channel(response["results"][0]["origin_id"]) - - def on_create_new_channel_clicked(self, checked): - def create_channel_callback(channel_name=None): - request_manager.post("channels/mychannel/0/channels", self.on_new_channel_response, - data=json.dumps({"name": channel_name}) if channel_name else None) - - NewChannelDialog(self, create_channel_callback) - - def on_create_new_folder_clicked(self, checked): - selected = self.dialog_widget.channels_tree_wt.selectedItems() - if not selected: - return - - channel_id = selected[0].id_ - postfix = "channels" if not channel_id else "collections" - endpoint = f"channels/mychannel/{channel_id}/{postfix}" - - def create_channel_callback(channel_name=None): - request_manager.post(endpoint, self.on_new_channel_response, - data=json.dumps({"name": channel_name}) if channel_name else None) - - NewChannelDialog(self, create_channel_callback) - - def clear_channels_tree(self): - # ACHTUNG! All running requests must always be cancelled first to prevent race condition! - for rq in self.root_requests_list: - rq.cancel() - self.dialog_widget.channels_tree_wt.clear() - self.id2wt_mapping = {0: self.dialog_widget.channels_tree_wt} - self.load_channel(0) - - def show_dialog(self, on_confirm, confirm_button_text="CONFIRM_BUTTON"): - self.dialog_widget.btn_confirm.setText(confirm_button_text) - self.show() - self.confirm_clicked_callback = on_confirm - - def on_item_expanded(self, item): - # Load the grand-children - for channel_id in self.channels_tree.get(item.id_, None): - # "None" means that the node was previously loaded and has no children - # Empty set means it is still not known if it has children or not - # Non-empty set means it was already loaded before - subchannels_set = self.channels_tree.get(channel_id, set()) - if subchannels_set is None or subchannels_set: - continue - self.load_channel(channel_id) - - def load_channel(self, channel_id): - request = request_manager.get( - f"channels/mychannel/{channel_id}", - on_success=lambda result: self.on_channel_contents(result, channel_id), - url_params={ - "metadata_type": [CHANNEL_TORRENT, COLLECTION_NODE], - "first": 1, - "last": 1000, - "exclude_deleted": True, - } - ) - if request: - self.root_requests_list.append(request) - - def get_selected_channel_id(self): - selected = self.dialog_widget.channels_tree_wt.selectedItems() - return None if not selected else selected[0].id_ - - def on_confirm_clicked(self, checked): - channel_id = self.get_selected_channel_id() - if channel_id is None: - return - if self.confirm_clicked_callback: - self.confirm_clicked_callback(channel_id) - self.close_dialog() - - def on_channel_contents(self, response, channel_id): - if not response: - return - - # No results means this node is a leaf - self.channels_tree[channel_id] = set() if response.get("results") else None - - for subchannel in response.get("results", []): - subchannel_id = subchannel["id"] - if subchannel_id in self.id2wt_mapping: - continue - wt = ChannelQTreeWidgetItem(self.id2wt_mapping[channel_id], [subchannel["name"]], id_=subchannel_id) - self.id2wt_mapping[subchannel_id] = wt - # Add the received node to the tree - self.channels_tree[channel_id].add(subchannel_id) - # For top-level channels, we want to immediately load their children so "expand" arrows are shown - if channel_id == 0: - self.load_channel(subchannel_id) - - def close_dialog(self, checked=False): - # Instead of deleting the dialog, hide it. We do this for two reasons: - # a. we do not want to lose the channels tree structure loaded from the core. - # b. we want the tree state (open subtrees, selection) to stay the same, as the user is - # likely to put stuff into the same channel they did before. - self.hide() diff --git a/src/tribler/gui/dialogs/createtorrentdialog.py b/src/tribler/gui/dialogs/createtorrentdialog.py index 329a7b73acd..dc27711d35d 100644 --- a/src/tribler/gui/dialogs/createtorrentdialog.py +++ b/src/tribler/gui/dialogs/createtorrentdialog.py @@ -45,7 +45,6 @@ def sanitize_filename(filename: str) -> str: class CreateTorrentDialog(DialogContainer): create_torrent_notification = pyqtSignal(dict) - add_to_channel_selected = pyqtSignal(str) def __init__(self, parent): DialogContainer.__init__(self, parent) @@ -61,7 +60,6 @@ def __init__(self, parent): connect(self.dialog_widget.create_torrent_files_list.customContextMenuRequested, self.on_right_click_file_item) self.dialog_widget.create_torrent_files_list.clear() connect(self.dialog_widget.save_directory_chooser.clicked, self.on_select_save_directory) - self.dialog_widget.edit_channel_create_torrent_progress_label.setText("") self.dialog_widget.file_export_dir.setText(os.path.expanduser("~")) self.dialog_widget.adjustSize() @@ -158,8 +156,6 @@ def on_torrent_created(self, result): if 'torrent' in result: self.create_torrent_notification.emit({"msg": tr("Torrent successfully created")}) self.close_dialog() - if self.dialog_widget.add_to_channel_checkbox.isChecked(): - self.add_to_channel_selected.emit(result['torrent']) def on_select_save_directory(self, checked): chosen_dir = QFileDialog.getExistingDirectory( diff --git a/src/tribler/gui/dialogs/new_channel_dialog.py b/src/tribler/gui/dialogs/new_channel_dialog.py deleted file mode 100644 index 8df7b94a8a6..00000000000 --- a/src/tribler/gui/dialogs/new_channel_dialog.py +++ /dev/null @@ -1,32 +0,0 @@ -from tribler.gui.defs import BUTTON_TYPE_CONFIRM, BUTTON_TYPE_NORMAL -from tribler.gui.dialogs.confirmationdialog import ConfirmationDialog -from tribler.gui.utilities import connect, tr - - -class NewChannelDialog(ConfirmationDialog): - def __init__(self, parent, create_channel_callback): - super().__init__( - parent, - tr("Create new channel"), - tr("Enter the name of the channel/folder to create:"), - [(tr("NEW"), BUTTON_TYPE_NORMAL), (tr("CANCEL"), BUTTON_TYPE_CONFIRM)], - show_input=True, - ) - - # Submitting channel model is necessary because the model will trigger - # some signals to update its on-screen data on adding a new subchannel - # Also, the type of the created entity (channel vs collection) is decided - # by the model. That is a rough hack, but works. - self.create_channel_callback = create_channel_callback - self.dialog_widget.dialog_input.setPlaceholderText(tr("Channel name")) - self.dialog_widget.dialog_input.setFocus() - connect(self.button_clicked, self.on_channel_name_dialog_done) - self.show() - - def on_channel_name_dialog_done(self, action): - if action == 0: - text = self.dialog_widget.dialog_input.text() - if text: - self.create_channel_callback(channel_name=text) - - self.close_dialog() diff --git a/src/tribler/gui/dialogs/startdownloaddialog.py b/src/tribler/gui/dialogs/startdownloaddialog.py index 339f660be64..4d3c43b75dc 100644 --- a/src/tribler/gui/dialogs/startdownloaddialog.py +++ b/src/tribler/gui/dialogs/startdownloaddialog.py @@ -102,9 +102,6 @@ def __init__(self, parent, download_uri): self.dialog_widget.safe_seed_checkbox.setChecked( self.window().tribler_settings['download_defaults']['safeseeding_enabled'] ) - self.dialog_widget.add_to_channel_checkbox.setChecked( - self.window().tribler_settings['download_defaults']['add_download_to_channel'] - ) self.dialog_widget.safe_seed_checkbox.setEnabled(self.dialog_widget.anon_download_checkbox.isChecked()) diff --git a/src/tribler/gui/event_request_manager.py b/src/tribler/gui/event_request_manager.py index 8b26321de6a..581017d32c1 100644 --- a/src/tribler/gui/event_request_manager.py +++ b/src/tribler/gui/event_request_manager.py @@ -28,7 +28,6 @@ class EventRequestManager(QNetworkAccessManager): received_remote_query_results = pyqtSignal(object) core_connected = pyqtSignal(object) new_version_available = pyqtSignal(str) - discovered_channel = pyqtSignal(object) torrent_finished = pyqtSignal(object) low_storage_signal = pyqtSignal(object) tribler_shutdown_signal = pyqtSignal(str) @@ -56,9 +55,7 @@ def __init__(self, api_port: Optional[int], api_key, error_handler): self.notifier = notifier = Notifier() notifier.add_observer(notifications.events_start, self.on_events_start) notifier.add_observer(notifications.tribler_exception, self.on_tribler_exception) - notifier.add_observer(notifications.channel_entity_updated, self.on_channel_entity_updated) notifier.add_observer(notifications.tribler_new_version, self.on_tribler_new_version) - notifier.add_observer(notifications.channel_discovered, self.on_channel_discovered) notifier.add_observer(notifications.torrent_finished, self.on_torrent_finished) notifier.add_observer(notifications.low_space, self.on_low_space) notifier.add_observer(notifications.remote_query_results, self.on_remote_query_results) @@ -87,15 +84,9 @@ def on_events_start(self, public_key: str, version: str): def on_tribler_exception(self, error: dict): self.error_handler.core_error(ReportedError(**error)) - def on_channel_entity_updated(self, channel_update_dict: dict): - self.node_info_updated.emit(channel_update_dict) - def on_tribler_new_version(self, version: str): self.new_version_available.emit(version) - def on_channel_discovered(self, data: dict): - self.discovered_channel.emit(data) - def on_torrent_finished(self, infohash: str, name: str, hidden: bool): self.torrent_finished.emit(dict(infohash=infohash, name=name, hidden=hidden)) diff --git a/src/tribler/gui/qt_resources/addtochanneldialog.ui b/src/tribler/gui/qt_resources/addtochanneldialog.ui deleted file mode 100644 index c03b18c683c..00000000000 --- a/src/tribler/gui/qt_resources/addtochanneldialog.ui +++ /dev/null @@ -1,323 +0,0 @@ - - - Form - - - - 0 - 0 - 1030 - 500 - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - ArrowCursor - - - Form - - - false - - - QWidget { -background-color: #333333; -border-radius: 2px; -} -QLabel { - color: #ffffff; -} -QToolButton { -border: 1px solid #B5B5B5; -border-radius: 12px; -color: white; -padding-left: 4px; -padding-right: 4px; -} -QToolButton::hover { -border: 1px solid white; -color: white; -} - - - - 0 - - - QLayout::SetMinimumSize - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - padding: 12px 12px 8px 12px; -font-size: 16px; -font-weight: bold; -color: white; - - - Add torrent(s) to personal channel - - - - - - - PointingHandCursor - - - border-radius: 4px; -padding: 4px; -margin-right:8px; -border: None; -background-color:#555; - - - CANCEL (X) - - - - - - - - - - - 0 - 0 - - - - - - - - 1 - - - - - - - - - - - border-radius: 12px; -padding-left: 4px; -padding-right: 4px; - - - - 8 - - - 8 - - - 8 - - - 8 - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 20 - 20 - - - - - - - - - 0 - 24 - - - - - 16777212 - 24 - - - - PointingHandCursor - - - border-radius: 12px; -padding-left: 4px; -padding-right: 4px; - - - New channel - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 10 - 20 - - - - - - - - - 0 - 24 - - - - - 16777212 - 24 - - - - PointingHandCursor - - - border-radius: 12px; -padding-left: 4px; -padding-right: 4px; - - - New folder - - - - - - - Qt::Horizontal - - - - 570 - 20 - - - - - - - - - 0 - 24 - - - - - 16777212 - 24 - - - - PointingHandCursor - - - border-radius: 12px; -padding-left: 4px; -padding-right: 4px; - - - CONFIRM_BUTTON - - - - - - - - - - - - - diff --git a/src/tribler/gui/qt_resources/channel_description.ui b/src/tribler/gui/qt_resources/channel_description.ui deleted file mode 100644 index 2e60ec319c1..00000000000 --- a/src/tribler/gui/qt_resources/channel_description.ui +++ /dev/null @@ -1,387 +0,0 @@ - - - channel_description_widget - - - - 0 - 0 - 823 - 463 - - - - - - - - 0 - - - QLayout::SetMinimumSize - - - 0 - - - 0 - - - - - - 0 - - - 0 - - - 0 - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 10 - 20 - - - - - - - - - 0 - 0 - - - - - 0 - 36 - - - - - 16777215 - 36 - - - - PointingHandCursor - - - EDIT - - - true - - - false - - - - - - - - 0 - 0 - - - - - 0 - 36 - - - - - 16777215 - 36 - - - - PointingHandCursor - - - - - - PREVIEW - - - true - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - - 0 - 0 - - - - - 200 - 200 - - - - - - - ../images/chan_thumb.png - - - false - - - Qt::AlignCenter - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 5 - 20 - - - - - - - - 0 - - - - - 0 - - - - QTextEdit{border-radius: 4px; background-color: rgba(255,255,255,0); border: 0px;} - - - # Welcome -* You can use Markdown syntax here -* https://guides.github.com/features/mastering-markdown/ - - - - - - QPlainTextEdit{border-radius: 4px; background-color: #303030} - - - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 10 - 20 - - - - - - - - Save - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 5 - 20 - - - - - - - - Cancel - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - - - - - - - 0 - - - 0 - - - 0 - - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - You can create a description for your channel - - - Qt::AlignCenter - - - - - - - - - - 0 - 0 - - - - Create - - - - - - - - - - - - - - - - ClickableLabel - QLabel -
tribler.gui.widgets.clickablewidgets.h
-
- - UnderlineTabButton - QToolButton -
tribler.gui.widgets.underlinetabbutton.h
-
- - TabButtonPanel - QWidget -
tribler.gui.widgets.tabbuttonpanel.h
- 1 -
-
- - -
diff --git a/src/tribler/gui/qt_resources/createtorrentdialog.ui b/src/tribler/gui/qt_resources/createtorrentdialog.ui index c32ca46ec10..721eea0287e 100644 --- a/src/tribler/gui/qt_resources/createtorrentdialog.ui +++ b/src/tribler/gui/qt_resources/createtorrentdialog.ui @@ -398,19 +398,6 @@ padding:4px; - - - - color: #bbb; - - - Add this torrent to your channel - - - true - - - diff --git a/src/tribler/gui/qt_resources/debugwindow.ui b/src/tribler/gui/qt_resources/debugwindow.ui index e693e3086f2..9c0866ac45a 100644 --- a/src/tribler/gui/qt_resources/debugwindow.ui +++ b/src/tribler/gui/qt_resources/debugwindow.ui @@ -1721,95 +1721,6 @@ - - - Channels - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 1 - - - - Channels peers - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - false - - - true - - - 200 - - - - Channel name - - - - - Public key - - - - - Id - - - - - Peer count - - - - - Peer age (seconds) - - - - - - - - - - diff --git a/src/tribler/gui/qt_resources/mainwindow.ui b/src/tribler/gui/qt_resources/mainwindow.ui index 554f30e358f..1e1a96d55bd 100644 --- a/src/tribler/gui/qt_resources/mainwindow.ui +++ b/src/tribler/gui/qt_resources/mainwindow.ui @@ -1171,13 +1171,6 @@ color: white;margin-top:10px; - - - Add torrent to My channel - - - - font-weight: bold; @@ -1188,7 +1181,7 @@ color: white;margin-top:10px; - + @@ -1295,7 +1288,7 @@ color: white;margin-top:10px; - + font-weight: bold; @@ -1306,32 +1299,21 @@ color: white;margin-top:10px - + Minimize to system tray? - + Use monochrome icon? - - - - font-weight: bold; -color: white;margin-top:10px; - - - Personal channel settings - - - - + Commit changes automatically (requires Tribler restart) @@ -1369,7 +1351,7 @@ color: white;margin-top:10px; - + @@ -1386,7 +1368,7 @@ color: white;margin-top:10px; - + Hide tags from content items @@ -3023,31 +3005,6 @@ padding-left: 2px; - - - - - 0 - 36 - - - - - 16777215 - 36 - - - - PointingHandCursor - - - CHANNELS - - - true - - - @@ -3962,1131 +3919,122 @@ font-weight:bold; - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - color: white; font-size: 18px; - - - Discovering your first content... - - - Qt::AlignCenter - - - - - - - - 0 - 0 - - - - - 0 - 200 - - - - - 16777215 - 200 - - - - border: none; - - - - - - - - 0 - 0 - - - - color: white; font-size: 14px; - - - This process might take around a minute. - - - Qt::AlignCenter - - - - + + + + + + + + + + 0 + 0 + + + + + 0 + 50 + + + + + 16777215 + 50 + + + + QWidget { +background-color: #202020; +border-bottom: 1px solid #242424; +} + +CircleButton { +border: 2px solid white; +} + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + + + + 16 + 18 + + + + + 16 + 18 + + + + PointingHandCursor + + + border: none; + + + + + - - + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 10 + 20 + + + + + + - QLabel { color: #eee; } - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 50 - - - - - 16777215 - 50 - - - - color: #eee; - background-color: transparent; - font-size: 20px; - font-weight: bold; - margin: 10px; -margin-right: 4px; -margin-top: 9px; - - - Trust statistics - - - - - - - - 20 - 20 - - - - - 20 - 20 - - - - - 14 - - - - PointingHandCursor - - - border: 1px solid white; -color: white; -border-radius: 10px; - - - - i - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 5 - 20 - - - - - - - - - 20 - 20 - - - - PointingHandCursor - - - Show trust graph - - - QToolButton{ -border: 1px solid white; -color: white; -border-radius: 10px; -} - - - - - - - - ../images/network.png../images/network.png - - - - - - - Qt::Horizontal - - - QSizePolicy::Expanding - - - - 10 - 20 - - - - - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 6 - - - - - - - - padding-left: 10px; padding-right: 10px; - - - You can build trust by contributing bandwidth to the Tribler network. This is done by letting Tribler run idle. - - - true - - - - - - - - 0 - 0 - - - - - - - - 20 - - - 20 - - - 20 - - - 20 - - - 20 - - - - - QWidget { -background-color: #282828; border: 1px solid #555; -} -QLabel { -border: none; -} - - - - 0 - - - - - font-size: 20px; font-weight: bold; - - - - MBytes - - - - - - - Qt::Vertical - - - QSizePolicy::Minimum - - - - 20 - 3 - - - - - - - - font-size: 15px; - - - Given to community - - - - - - - Qt::Vertical - - - QSizePolicy::Minimum - - - - 20 - 20 - - - - - - - - font-size: 18px; - - - - - - - - - - - font-size: 15px; - - - People you helped - - - - - - - Qt::Vertical - - - QSizePolicy::Expanding - - - - 20 - 40 - - - - - - - - - - - QWidget { -background-color: #282828; border: 1px solid #555; -} -QLabel { -border: none; -} - - - - 0 - - - - - font-size: 20px; font-weight: bold; - - - - MBytes - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 3 - - - - - - - - font-size: 15px; - - - Taken from community - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 20 - - - - - - - - font-size: 18px; - - - - - - - - - - - font-size: 15px; - - - People who helped you - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - - - - 0 - 0 - - - - - - - - 20 - - - 20 - - - 0 - - - 20 - - - 20 - - - - - - - - - 0 - - - 2 - - - 2 - - - 2 - - - 2 - - - - - - 0 - 250 - - - - - 0 - 250 - - - - border: none; - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - - - - - - QLabel { color: #eee; } - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 50 - - - - - 16777215 - 50 - - - - color: #eee; -background-color: transparent; -font-size: 20px; -font-weight: bold; -margin: 10px 4px 2px 10px; - - - Trust Graph - - - - - - - Qt::Horizontal - - - QSizePolicy::Expanding - - - - 10 - 20 - - - - - - - - - 64 - 28 - - - - - 64 - 28 - - - - PointingHandCursor - - - margin-right:16px - - - REFRESH - - - - ../images/refresh.png../images/refresh.png - - - - 18 - 18 - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 10 - 20 - - - - - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 20 - 6 - - - - - - - - - true - - - - padding-left: 10px; padding-right: 10px;margin-bottom:8px; - - - The graph below is based on your historical interactions with other users in the network. - - - Qt::RichText - - - true - - - - - - - - 0 - 0 - - - - - 0 - 32 - - - - - 16777215 - 32 - - - - QProgressBar { - background-color: #FFF; - border: 0px; - padding: 0px; - height: 10px; - margin: 0 16px; - } - QProgressBar::chunk { - background: #5c58ee; - width:5px - } - - - 0 - - - 24 - - - Qt::AlignCenter - - - QProgressBar::TopToBottom - - - Progress %p% - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 32 - - - - - 16777215 - 32 - - - - - 10 - 50 - false - - - - margin:8px 16px 0 16px - - - - - - Qt::RichText - - - 0 - - - - - - - - - - - 1 - 1 - - - - - 250 - 250 - - - - - 1 - 1 - - - - - 0 - 250 - - - - false - - - border: None; margin:0 - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - - - 0 - 0 - - - - - 10 - - - - margin:4px 16px 4px 16px - - - Peer : - - - Qt::RichText - - - false - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - 10 - - - - margin:4px 16px 16px 16px - - - Balance Given: XX Taken: YY - - - Qt::RichText - - - true - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - - - - - - - - - - 0 - 0 - - - - - 0 - 50 - - - - - 16777215 - 50 - - - - QWidget { -background-color: #202020; -border-bottom: 1px solid #242424; -} - -CircleButton { -border: 2px solid white; -} - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 10 - 20 - - - - - - - - - 16 - 18 - - - - - 16 - 18 - - - - PointingHandCursor - - - border: none; - - - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 10 - 20 - - - - - - - - color: #e67300; -font-size: 28px; -font-weight: bold; -font-family: "Arial"; + color: #e67300; +font-size: 28px; +font-weight: bold; +font-family: "Arial"; Tribler @@ -5214,77 +4162,6 @@ border: 1px solid #FF924F; - - - - - 0 - 0 - - - - - 92 - 34 - - - - - 92 - 34 - - - - PointingHandCursor - - - QWidget { background-color: #4c4c4c; border-radius: 4px; } - - - - 0 - - - 0 - - - 2 - - - 0 - - - 2 - - - - - color: #ddd; font-weight: bold; font-size: 14px; border: none; - - - - - - - Qt::AlignCenter - - - - - - - color: #ddd; font-size: 12px; border: none; - - - Token balance - - - Qt::AlignCenter - - - - - - @@ -5431,33 +4308,16 @@ background: none;
tribler.gui.widgets.downloadprogressbar.h
1 - - TrustPage - QWidget -
tribler.gui.widgets.trustpage.h
- 1 -
ChannelContentsWidget QWidget
tribler.gui.widgets.channelcontentswidget.h
- - TrustGraphPage - QWidget -
tribler.gui.widgets.trustgraphpage.h
- 1 -
ClickableLineEdit QLineEdit
tribler.gui.widgets.clickable_line_edit.h
- - ChannelsMenuListWidget - QListWidget -
tribler.gui.widgets.channelsmenulistwidget.h
-
SearchResultsWidget QWidget @@ -5583,13 +4443,9 @@ background: none; on_top_search_button_click() on_add_torrent_button_click() on_top_menu_button_click() - on_channel_item_click(QListWidgetItem*) - clicked_menu_button_my_channel() clicked_menu_button_downloads() clicked_menu_button_subscriptions() - clicked_edit_channel_commit_button() on_search_text_change() - on_edit_channel_clicked() clicked_menu_button_discovered() clicked_menu_button_trust() on_settings_button_click() diff --git a/src/tribler/gui/qt_resources/startdownloaddialog.ui b/src/tribler/gui/qt_resources/startdownloaddialog.ui index ca6cf04f20e..ca56477462d 100644 --- a/src/tribler/gui/qt_resources/startdownloaddialog.ui +++ b/src/tribler/gui/qt_resources/startdownloaddialog.ui @@ -417,25 +417,6 @@ background: yellow;
- - - - - 0 - 24 - - - - - 16777215 - 24 - - - - Add to my channel - - -
diff --git a/src/tribler/gui/qt_resources/torrents_list.ui b/src/tribler/gui/qt_resources/torrents_list.ui index ecd8beddee0..dd6464bb2ff 100644 --- a/src/tribler/gui/qt_resources/torrents_list.ui +++ b/src/tribler/gui/qt_resources/torrents_list.ui @@ -435,101 +435,9 @@ QToolButton{border:none;margin-left:16px;}
- - - - - 0 - 0 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 20 - 0 - - - - - - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 10 - 20 - - - - - - - - ||||| - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 10 - 20 - - - - - - -
- - - - - 0 - 0 - - - - @@ -627,96 +535,6 @@ QToolButton{border:none;margin-left:16px;} - - - - - 0 - 0 - - - - - 50 - 0 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 0 - 30 - - - - - 16777215 - 30 - - - - font-size: 14px; color: #cc6600; -margin-right: 8px; - - - - Your channel has uncommitted changes. - - - Qt::AlignCenter - - - - - - - - 0 - 28 - - - - - 16777215 - 28 - - - - border-radius: 12px; -padding-left: 4px; -padding-right: 4px; - - - APPLY CHANGES - - - - - -
@@ -840,12 +658,6 @@ QTableView::item::hover {
tribler.gui.widgets.qtbug.h
1 - - SubscriptionsWidget - QWidget -
tribler.gui.widgets.subscriptionswidget.h
- 1 -
TriblerContentTableView QTableView @@ -856,11 +668,6 @@ QTableView::item::hover { QWidget
tribler.gui.widgets.togglebutton.h
- - ChannelDescriptionWidget - QWidget -
tribler.gui.widgets.channeldescriptionwidget.h
-
InstantTooltipButton QToolButton diff --git a/src/tribler/gui/tests/gui_test_data.py b/src/tribler/gui/tests/gui_test_data.py deleted file mode 100644 index ad8d6f20b79..00000000000 --- a/src/tribler/gui/tests/gui_test_data.py +++ /dev/null @@ -1,104 +0,0 @@ -# Test data for token balance history - -negative_token_balance_history = [ - {'timestamp': 1639989682705, 'balance': -4242445126}, - {'timestamp': 1639989682715, 'balance': -4242942932}, - {'timestamp': 1639989682725, 'balance': -4243010528}, - {'timestamp': 1639989836725, 'balance': -4243010528}, - {'timestamp': 1639989836756, 'balance': -4243010528}, - {'timestamp': 1639989837713, 'balance': -4242920753}, - {'timestamp': 1639989838040, 'balance': -4242920753}, - {'timestamp': 1639989838050, 'balance': -4242920753}, - {'timestamp': 1639989865600, 'balance': -4243183609}, - {'timestamp': 1639989865600, 'balance': -4243364423}, - {'timestamp': 1639989865615, 'balance': -4243542847}, - {'timestamp': 1639989865615, 'balance': -4243706616}, - {'timestamp': 1639989865631, 'balance': -4243826841}, - {'timestamp': 1639989865647, 'balance': -4243891015}, - {'timestamp': 1639989904528, 'balance': -4243891015}, - {'timestamp': 1639989921434, 'balance': -4243891015}, - {'timestamp': 1639989921450, 'balance': -4243891015}, - {'timestamp': 1639989921997, 'balance': -4243891015}, - {'timestamp': 1639989921997, 'balance': -4243891015}, - {'timestamp': 1639989931318, 'balance': -4244475952}, - {'timestamp': 1639989931334, 'balance': -4244704214}, - {'timestamp': 1639989931334, 'balance': -4244732128}, - {'timestamp': 1639989931350, 'balance': -4244758963}, - {'timestamp': 1639989931365, 'balance': -4244781725}, - {'timestamp': 1639989931365, 'balance': -4244806208}, - {'timestamp': 1639990014979, 'balance': -4245061842}, - {'timestamp': 1639990014995, 'balance': -4245120697}, - {'timestamp': 1639990014995, 'balance': -4245168539}, - {'timestamp': 1639990015010, 'balance': -4245216358}, - {'timestamp': 1639990015010, 'balance': -4245221001}, - {'timestamp': 1639990015026, 'balance': -4245225659}, - {'timestamp': 1639990193728, 'balance': -4245241816}, - {'timestamp': 1639990250821, 'balance': -4245243295}, - {'timestamp': 1639990448300, 'balance': -4245243295}, - {'timestamp': 1639990454609, 'balance': -4245243295}, - {'timestamp': 1639991174571, 'balance': -4245852308}, - {'timestamp': 1639991174587, 'balance': -4245986649}, - {'timestamp': 1639991174587, 'balance': -4246441060}, - {'timestamp': 1639991174602, 'balance': -4248106527}, - {'timestamp': 1639991174602, 'balance': -4249141793}, - {'timestamp': 1639991174618, 'balance': -4249402255}, - {'timestamp': 1639991279504, 'balance': -4249454759}, - {'timestamp': 1639991279519, 'balance': -4249569078}, - {'timestamp': 1639991279519, 'balance': -4249804508}, - {'timestamp': 1639991279535, 'balance': -4249919744}, - {'timestamp': 1639991279582, 'balance': -4249927169}, - {'timestamp': 1639991279598, 'balance': -4249939590}, - {'timestamp': 1639991355956, 'balance': -4250162322}, - {'timestamp': 1639991355971, 'balance': -4250284312}, - {'timestamp': 1639991355971, 'balance': -4250512181}, - {'timestamp': 1639991355987, 'balance': -4250567205}, - {'timestamp': 1639991355987, 'balance': -4250726076}, - {'timestamp': 1639991356003, 'balance': -4251008765}, - {'timestamp': 1639991356003, 'balance': -4251491838}, - {'timestamp': 1639991484789, 'balance': -4251490530}, - {'timestamp': 1639991484789, 'balance': -4251491838}, - {'timestamp': 1639991491322, 'balance': -4251493162}, - {'timestamp': 1639991491338, 'balance': -4251497771}, - {'timestamp': 1639991491338, 'balance': -4251498931}, - {'timestamp': 1639991491353, 'balance': -4251501656}, - {'timestamp': 1639991491369, 'balance': -4251502083}, - {'timestamp': 1639991491385, 'balance': -4251502510}, - {'timestamp': 1639991901542, 'balance': -4251500986}, - {'timestamp': 1639991948147, 'balance': -4253313336}, - {'timestamp': 1639991948162, 'balance': -4253442797}, - {'timestamp': 1639991948162, 'balance': -4253643279}, - {'timestamp': 1639991948162, 'balance': -4257892944}, - {'timestamp': 1639991948178, 'balance': -4260661821}, - {'timestamp': 1639991948188, 'balance': -4261099251}, - {'timestamp': 1639992013196, 'balance': -4261261311}, - {'timestamp': 1639992013206, 'balance': -4261438136}, - {'timestamp': 1639992013216, 'balance': -4261805891}, - {'timestamp': 1639992013216, 'balance': -4262175431}, - {'timestamp': 1639992013236, 'balance': -4262246474}, - {'timestamp': 1639992013236, 'balance': -4262542764}, - {'timestamp': 1639992111113, 'balance': -4262590904}, - {'timestamp': 1639992111128, 'balance': -4262657779}, - {'timestamp': 1639992111144, 'balance': -4262720033}, - {'timestamp': 1639992111144, 'balance': -4262831837}, - {'timestamp': 1639992111160, 'balance': -4262832264}, - {'timestamp': 1639992111160, 'balance': -4262832691}, - {'timestamp': 1639992534358, 'balance': -4262824219}, - {'timestamp': 1639992597448, 'balance': -4262825830}, - {'timestamp': 1639992740444, 'balance': -4262820880}, - {'timestamp': 1639992740447, 'balance': -4262822860}, - {'timestamp': 1639992740728, 'balance': -4262822860}, - {'timestamp': 1639992863178, 'balance': -4457312394}, - {'timestamp': 1639992863194, 'balance': -4526475834}, - {'timestamp': 1639992863213, 'balance': -4640392288}, - {'timestamp': 1639992863222, 'balance': -4806582573}, - {'timestamp': 1639992863232, 'balance': -4929355104}, - {'timestamp': 1639992863490, 'balance': -4929355104}, - {'timestamp': 1639992863513, 'balance': -4815438650}, - {'timestamp': 1639992863517, 'balance': -4815438650}, - {'timestamp': 1639992863604, 'balance': -4929355104}, - {'timestamp': 1639992866229, 'balance': -4929355104}, - {'timestamp': 1639992868239, 'balance': -5008495201}, - {'timestamp': 1639992868395, 'balance': -5008495201}, - {'timestamp': 1639992883269, 'balance': -5008495201}, - {'timestamp': 1639992883456, 'balance': -5008495201}, -] diff --git a/src/tribler/gui/tests/test_gui.py b/src/tribler/gui/tests/test_gui.py index 683e2acf26c..33f2ba07e09 100644 --- a/src/tribler/gui/tests/test_gui.py +++ b/src/tribler/gui/tests/test_gui.py @@ -20,13 +20,10 @@ from tribler.core.utilities.unicode import hexlify from tribler.gui.app_manager import AppManager from tribler.gui.dialogs.feedbackdialog import FeedbackDialog -from tribler.gui.dialogs.new_channel_dialog import NewChannelDialog -from tribler.gui.tests.gui_test_data import negative_token_balance_history from tribler.gui.tribler_app import TriblerApplication from tribler.gui.tribler_window import TriblerWindow from tribler.gui.utilities import connect from tribler.gui.widgets.loading_list_item import LoadingListItem -from tribler.gui.widgets.tablecontentmodel import Column from tribler.gui.widgets.tagbutton import TagButton from tribler.gui.widgets.torrentfiletreewidget import CHECKBOX_COL, PreformattedTorrentFileTreeWidget @@ -265,107 +262,50 @@ def get_index_of_row_column(table_view, row, column): return table_view.indexAt(QPoint(x, y)) -def tst_channels_widget(window, widget, widget_name, sort_column=1, test_filter=True, test_subscribe=True): - wait_for_list_populated(widget.content_table) - screenshot(window, name=f"{widget_name}-page") - - # Sort - widget.content_table.sortByColumn(sort_column, 1) - wait_for_list_populated(widget.content_table) - screenshot(window, name=f"{widget_name}-sorted") - total = widget.content_table.model().channel_info.get("total") - if total is not None: - max_items = min(total, 50) - assert widget.content_table.verticalHeader().count() <= max_items - - # Filter - if test_filter: - old_num_items = widget.content_table.verticalHeader().count() - widget.channel_torrents_filter_input.setText("nonrandom") - widget.controller.on_filter_input_return_pressed() - wait_for_list_populated(widget.content_table) - screenshot(window, name=f"{widget_name}-filtered") - assert widget.content_table.verticalHeader().count() <= old_num_items - widget.channel_torrents_filter_input.setText("") - widget.controller.on_filter_input_return_pressed() - wait_for_list_populated(widget.content_table) - - if test_subscribe: - widget.content_table.sortByColumn(0, 0) - wait_for_list_populated(widget.content_table) - screenshot(window, name=f"{widget_name}-sorted-on-subscribe") - # Subscribe - index = get_index_of_row_column(widget.content_table, 0, widget.model.column_position[Column.VOTES]) - widget.content_table.on_subscribe_control_clicked(index) - QTest.qWait(200) - - # Unsubscribe - widget.content_table.on_subscribe_control_clicked(index) - QTest.qWait(200) - screenshot(window, name=f"{widget_name}-unsubscribed") - window.dialog.button_clicked.emit(0) - - # Test channel view - index = get_index_of_row_column(widget.content_table, 0, widget.model.column_position[Column.NAME]) - widget.content_table.on_table_item_clicked(index) - wait_for_list_populated(widget.content_table) - screenshot(window, name=f"{widget_name}-channel_loaded") - - # Click the first torrent - index = get_index_of_row_column(widget.content_table, 0, widget.model.column_position[Column.NAME]) - widget.content_table.on_table_item_clicked(index) - QTest.qWait(WAIT_INTERVAL_MSEC) - screenshot(window, name=f"{widget_name}-torrent_details") - - -@pytest.mark.guitest -def test_discovered_page(window): - QTest.mouseClick(window.left_menu_button_discovered, Qt.LeftButton) - tst_channels_widget(window, window.discovered_page, "discovered_page", sort_column=2) +def fake_core_response_popular(window): + widget = window.popular_page + wait_for_list_populated(widget.content_table, num_items=0) + widget.model.on_query_results({ + 'results': [ + { + 'name': 'Some Torrent', + 'category': 'other', + 'infohash': 'af' * 20, + 'size': 1234, + 'num_seeders': 1, + 'num_leechers': 1000000, + 'last_tracker_check': 1500000000, + 'created': 1000000000, + 'tag_processor_version': 5, + 'type': 300, + 'id': 1, + 'origin_id': 0, + 'public_key': '', + 'status': 2, + 'statements': [{ + 'subject_type': 102, + 'object': '2023', + 'predicate': 101, + 'subject': 'ec34c231dde3e92d8d26a17c223152c8541295aa' + }, { + 'subject_type': 102, + 'object': 'Ubuntu', + 'predicate': 101, + 'subject': '382b14edddae478f2148d1ec7c6cc6311d261caf' + }] + }] + }) @pytest.mark.guitest def test_popular_page(window): QTest.mouseClick(window.left_menu_button_popular, Qt.LeftButton) widget = window.popular_page + fake_core_response_popular(window) wait_for_list_populated(widget.content_table) screenshot(window, name="popular_page") -def wait_for_thumbnail(chan_widget): - """ Wait for the thumbnail to be populated. - - Args: - chan_widget: The channel widget to check - """ - # Wait for the thumbnail to be populated in intervals of `DEFAULT_WAIT_INTERVAL_MSEC` - for _ in range(0, 1000 * DEFAULT_TIMEOUT_SEC, WAIT_INTERVAL_MSEC): - QTest.qWait(WAIT_INTERVAL_MSEC) - if chan_widget.channel_description_container.channel_thumbnail_bytes is not None: - return - - # thumbnail was not populated in time, fail the test - raise TimeoutException(f"The thumbnail was not shown within {DEFAULT_TIMEOUT_SEC} seconds") - - -@pytest.mark.guitest -def test_edit_channel_torrents(window): - wait_for_list_populated(window.channels_menu_list) - - idx = window.channels_menu_list.model().index(0, 0) - item_pos = window.channels_menu_list.visualRect(idx).center() - QTest.mouseClick(window.channels_menu_list.viewport(), Qt.LeftButton, pos=item_pos) - wait_for_list_populated(window.channel_contents_page.content_table) - screenshot(window, name="edit_channel_committed") - - idx = window.channels_menu_list.model().index(1, 0) - item_pos = window.channels_menu_list.visualRect(idx).center() - QTest.mouseClick(window.channels_menu_list.viewport(), Qt.LeftButton, pos=item_pos) - wait_for_list_populated(window.channel_contents_page.content_table) - wait_for_thumbnail(window.channel_contents_page) - screenshot(window, name="edit_channel_thumbnail_description") - - @pytest.mark.guitest def test_settings(window): QTest.mouseClick(window.settings_button, Qt.LeftButton) @@ -399,8 +339,6 @@ def test_downloads(window): screenshot(window, name="downloads_active") QTest.mouseClick(window.downloads_inactive_button, Qt.LeftButton) screenshot(window, name="downloads_inactive") - QTest.mouseClick(window.downloads_channels_button, Qt.LeftButton) - screenshot(window, name="downloads_channels") @pytest.mark.guitest @@ -451,14 +389,6 @@ def test_search(window): QTest.keyClick(window.top_search_bar, Qt.Key_Enter) QTest.qWait(WAIT_INTERVAL_MSEC) screenshot(window, name="search_loading_page") - tst_channels_widget( - window, - window.search_results_page.results_page_content, - "search_results", - sort_column=2, - test_filter=False, - test_subscribe=False, - ) @pytest.mark.guitest @@ -631,39 +561,13 @@ def test_debug_pane(window): window.debug_window.close() -@pytest.mark.guitest -@pytest.mark.skip(reason="This element not in UI anymore") -def test_trust_page(window): - QTest.mouseClick(window.token_balance_widget, Qt.LeftButton) - wait_for_variable(window, "trust_page.history") - screenshot(window, name="trust_page_values") - - -@pytest.mark.guitest -@pytest.mark.skip(reason="This element not in UI anymore") -def test_big_negative_token_balance(window): - QTest.mouseClick(window.token_balance_widget, Qt.LeftButton) - wait_for_variable(window, "trust_page.history") - window.trust_page.history = negative_token_balance_history - window.trust_page.plot_absolute_values() - screenshot(window, name="big_negative_token_balance") - - -@pytest.mark.guitest -def test_close_dialog_with_esc_button(window): - QTest.mouseClick(window.left_menu_button_new_channel, Qt.LeftButton) - screenshot(window, name="create_new_channel_dialog") - assert window.findChildren(NewChannelDialog) - QTest.keyPress(window, Qt.Key_Escape) - assert not window.findChildren(NewChannelDialog) - - @pytest.mark.guitest def test_tags_dialog(window): """ Test the behaviour of the dialog where a user can edit tags. """ QTest.mouseClick(window.left_menu_button_popular, Qt.LeftButton) + fake_core_response_popular(window) widget = window.popular_page wait_for_list_populated(widget.content_table) @@ -757,6 +661,7 @@ def test_tags_dialog(window): assert not tags_input.hasFocus() # Click on a suggestion + widget.content_table.add_tags_dialog.on_received_tag_suggestions({"suggestions": ["Tribler"]}) tag_suggestion_buttons = widget.content_table.add_tags_dialog.dialog_widget.suggestions.findChildren(TagButton) assert tag_suggestion_buttons QTest.mouseClick(tag_suggestion_buttons[0], Qt.LeftButton) @@ -783,6 +688,7 @@ def test_no_tags(window): Test removing all tags from a content item. """ QTest.mouseClick(window.left_menu_button_popular, Qt.LeftButton) + fake_core_response_popular(window) widget = window.popular_page wait_for_list_populated(widget.content_table) diff --git a/src/tribler/gui/tribler_window.py b/src/tribler/gui/tribler_window.py index 372e2dc7117..a1dc3d6ea44 100644 --- a/src/tribler/gui/tribler_window.py +++ b/src/tribler/gui/tribler_window.py @@ -3,7 +3,6 @@ import signal import sys import time -from base64 import b64encode from pathlib import Path from typing import Optional @@ -47,9 +46,6 @@ from tribler.core.utilities.network_utils import default_network_utils from tribler.core.utilities.process_manager import ProcessManager from tribler.core.utilities.rest_utils import ( - FILE_SCHEME, - MAGNET_SCHEME, - scheme_from_url, url_is_valid_file, url_to_path, ) @@ -64,22 +60,15 @@ BUTTON_TYPE_NORMAL, CATEGORY_SELECTOR_FOR_POPULAR_ITEMS, DARWIN, - PAGE_CHANNEL_CONTENTS, - PAGE_DISCOVERED, - PAGE_DISCOVERING, PAGE_DOWNLOADS, PAGE_LOADING, PAGE_POPULAR, PAGE_SEARCH_RESULTS, PAGE_SETTINGS, - PAGE_TRUST, - PAGE_TRUST_GRAPH_PAGE, SHUTDOWN_WAITING_PERIOD, ) -from tribler.gui.dialogs.addtopersonalchanneldialog import AddToChannelDialog from tribler.gui.dialogs.confirmationdialog import ConfirmationDialog from tribler.gui.dialogs.createtorrentdialog import CreateTorrentDialog -from tribler.gui.dialogs.new_channel_dialog import NewChannelDialog from tribler.gui.dialogs.startdownloaddialog import StartDownloadDialog from tribler.gui.error_handler import ErrorHandler from tribler.gui.event_request_manager import EventRequestManager @@ -104,10 +93,8 @@ show_message_box, tr, ) -from tribler.gui.widgets.channelsmenulistwidget import ChannelsMenuListWidget from tribler.gui.widgets.instanttooltipstyle import InstantTooltipStyle from tribler.gui.widgets.tablecontentmodel import ( - DiscoveredChannelsModel, PopularTorrentsModel, ) from tribler.gui.widgets.triblertablecontrollers import ( @@ -213,7 +200,6 @@ def __init__( self.start_download_dialog_active = False self.selected_torrent_files = [] self.start_time = time.time() - self.token_refresh_timer = None self.shutdown_timer = None self.add_torrent_url_dialog_active = False @@ -234,8 +220,6 @@ def __init__( RequestManager.window = self self.tribler_status_bar.hide() - self.token_balance_widget.mouseReleaseEvent = self.on_token_balance_click - self.magnet_handler = MagnetHandler(self.window) QDesktopServices.setUrlHandler("magnet", self.magnet_handler, "on_open_magnet_link") @@ -263,7 +247,6 @@ def __init__( self.menu_buttons = [ self.left_menu_button_downloads, - # self.left_menu_button_discovered, self.left_menu_button_popular, ] @@ -274,9 +257,6 @@ def __init__( self.settings_page.initialize_settings_page(version_history=self.version_history) self.downloads_page.initialize_downloads_page() self.loading_page.initialize_loading_page() - self.discovering_page.initialize_discovering_page() - - self.discovered_page.initialize_content_page(hide_xxx=self.hide_xxx) self.popular_page.initialize_content_page( hide_xxx=self.hide_xxx, @@ -284,9 +264,6 @@ def __init__( categories=CATEGORY_SELECTOR_FOR_POPULAR_ITEMS, ) - self.trust_page.initialize_trust_page() - self.trust_graph_page.initialize_trust_graph() - self.stackedWidget.setCurrentIndex(PAGE_LOADING) # Create the system tray icon @@ -313,7 +290,6 @@ def __init__( self.debug_panel_button.setHidden(True) self.top_menu_button.setHidden(True) self.left_menu.setHidden(True) - self.token_balance_widget.setHidden(True) self.settings_button.setHidden(True) self.add_torrent_button.setHidden(True) self.top_search_bar.setHidden(True) @@ -364,24 +340,10 @@ def sigint_handler(*_): self.show() - self.add_to_channel_dialog = AddToChannelDialog(self.window()) - self.add_torrent_menu = self.create_add_torrent_menu() self.add_torrent_button.setMenu(self.add_torrent_menu) - # self.channels_menu_list = self.findChild(ChannelsMenuListWidget, "channels_menu_list") - # - # connect(self.channels_menu_list.itemClicked, self.open_channel_contents_page) - - # The channels content page is only used to show subscribed channels, so we always show xxx - # contents in it. - # connect( - # self.core_manager.events_manager.node_info_updated, - # lambda data: self.channels_menu_list.reload_if_necessary([data]), - # ) - # connect(self.left_menu_button_new_channel.clicked, self.create_new_channel) connect(self.debug_panel_button.clicked, self.clicked_debug_panel_button) - connect(self.trust_graph_button.clicked, self.clicked_trust_graph_page_button) # Apply a custom style to our checkboxes, with custom images. stylesheet = self.styleSheet() @@ -433,27 +395,6 @@ def restore_position(): restore_size() restore_position() - # def create_new_channel(self, checked): - # # TODO: DRY this with tablecontentmodel, possibly using QActions - # - # def update_channels_state(_): - # self.channels_menu_list.load_channels() - # self.add_to_channel_dialog.clear_channels_tree() - # - # def create_channel_callback(channel_name): - # request_manager.post("channels/mychannel/0/channels", update_channels_state, - # data={"name": channel_name} if channel_name else None) - # - # NewChannelDialog(self, create_channel_callback) - - def open_channel_contents_page(self, channel_list_item): - if not channel_list_item.flags() & Qt.ItemIsEnabled: - return - - self.channel_contents_page.initialize_root_model_from_channel_info(channel_list_item.channel_info) - self.stackedWidget.setCurrentIndex(PAGE_CHANNEL_CONTENTS) - self.deselect_all_menu_buttons() - def update_tray_icon(self, use_monochrome_icon): if not QSystemTrayIcon.isSystemTrayAvailable() or not self.tray_icon: return @@ -508,7 +449,6 @@ def on_torrent_finished(self, torrent_info): def show_loading_screen(self): self.top_menu_button.setHidden(True) self.left_menu.setHidden(True) - self.token_balance_widget.setHidden(True) self.debug_panel_button.setHidden(True) self.settings_button.setHidden(True) self.add_torrent_button.setHidden(True) @@ -558,7 +498,6 @@ def on_receive_settings(self, settings): def start_ui(self): self.top_menu_button.setHidden(False) self.left_menu.setHidden(False) - # self.token_balance_widget.setHidden(False) # restore it after the token balance calculation is fixed self.settings_button.setHidden(False) self.add_torrent_button.setHidden(False) self.top_search_bar.setHidden(False) @@ -568,37 +507,12 @@ def start_ui(self): self.setAcceptDrops(True) self.setWindowTitle(f"Tribler {self.tribler_version}") - autocommit_enabled = ( - get_gui_setting(self.gui_settings, "autocommit_enabled", True, is_bool=True) if self.gui_settings else True - ) - self.channel_contents_page.initialize_content_page(autocommit_enabled=autocommit_enabled, hide_xxx=False) - - # self.discovered_page.initialize_root_model( - # DiscoveredChannelsModel( - # channel_info={"name": tr("Discovered channels")}, endpoint_url="channels", hide_xxx=self.hide_xxx - # ) - # ) - # connect(self.core_manager.events_manager.discovered_channel, self.discovered_page.model.on_new_entry_received) - self.popular_page.initialize_root_model( PopularTorrentsModel(channel_info={"name": tr("Popular torrents")}, hide_xxx=self.hide_xxx) ) self.popular_page.explanation_tooltip_button.setHidden(False) - # self.add_to_channel_dialog.load_channel(0) - - if not self.gui_settings.value("first_discover", False) and not self.core_manager.use_existing_core: - connect(self.core_manager.events_manager.discovered_channel, self.stop_discovering) - self.window().gui_settings.setValue("first_discover", True) - self.discovering_page.is_discovering = True - self.stackedWidget.setCurrentIndex(PAGE_DISCOVERING) - else: - self.clicked_menu_button_downloads() - # else: - # self.clicked_menu_button_discovered() - # self.left_menu_button_discovered.setChecked(True) - - # self.channels_menu_list.load_channels() + self.clicked_menu_button_downloads() # Toggle debug if developer mode is enabled self.window().debug_panel_button.setHidden(not get_gui_setting(self.gui_settings, "debug", False, is_bool=True)) @@ -609,15 +523,6 @@ def start_ui(self): def hide_xxx(self): return get_gui_setting(self.gui_settings, "family_filter", True, is_bool=True) - def stop_discovering(self, response): - if not self.discovering_page.is_discovering: - return - disconnect(self.core_manager.events_manager.discovered_channel, self.stop_discovering) - self.discovering_page.is_discovering = False - if self.stackedWidget.currentIndex() == PAGE_DISCOVERING: - self.clicked_menu_button_discovered() - # self.left_menu_button_discovered.setChecked(True) - def on_events_started(self, json_dict): self.setWindowTitle(f"Tribler {json_dict['version']}") @@ -663,7 +568,6 @@ def perform_start_download_request( safe_seeding, destination, selected_files, - add_to_channel=False, callback=None, ): # Check if destination directory is writable @@ -692,40 +596,6 @@ def perform_start_download_request( self.update_recent_download_locations(destination) - if add_to_channel: - self.show_add_torrent_to_channel_dialog_from_uri(uri) - - def show_add_torrent_to_channel_dialog_from_uri(self, uri): - def on_add_button_pressed(channel_id): - post_data = {} - scheme = scheme_from_url(uri) - if scheme == FILE_SCHEME: - file_path = url_to_path(uri) - content = Path(file_path).read_bytes() - post_data['torrent'] = b64encode(content).decode('ascii') - elif scheme == MAGNET_SCHEME: - post_data['uri'] = uri - - if post_data: - request_manager.put(f"channels/mychannel/{channel_id}/torrents", - on_success=lambda _: self.tray_show_message(tr("Channel update"), - tr("Torrent(s) added to your channel")), - data=post_data) - - self.window().add_to_channel_dialog.show_dialog(on_add_button_pressed, confirm_button_text="Add torrent") - - def show_add_torrent_to_channel_dialog_from_torrent_data(self, torrent_data): - def on_add_button_pressed(channel_id): - post_data = {'torrent': torrent_data} - - if post_data: - request_manager.put(f"channels/mychannel/{channel_id}/torrents", - on_success=lambda _: self.tray_show_message(tr("Channel update"), - tr("Torrent(s) added to your channel")), - data=post_data) - - self.window().add_to_channel_dialog.show_dialog(on_add_button_pressed, confirm_button_text="Add torrent") - def on_new_version_available(self, version): self.upgrade_manager.on_new_version_available(tribler_window=self, new_version=version) @@ -750,49 +620,6 @@ def on_settings_button_click(self): self.stackedWidget.setCurrentIndex(PAGE_SETTINGS) self.settings_page.load_settings() - def enable_token_balance_refresh(self): - # Set token balance refresh timer and load the token balance - self.token_refresh_timer = QTimer() - connect(self.token_refresh_timer.timeout, self.load_token_balance) - self.token_refresh_timer.start(2000) - - self.load_token_balance() - - def on_token_balance_click(self, _): - self.raise_window() - self.deselect_all_menu_buttons() - self.stackedWidget.setCurrentIndex(PAGE_TRUST) - self.load_token_balance() - self.trust_page.load_history() - - def load_token_balance(self): - request_manager.get("bandwidth/statistics", self.received_bandwidth_statistics, capture_errors=False) - - def received_bandwidth_statistics(self, statistics): - if not statistics or "statistics" not in statistics: - return - - self.trust_page.received_bandwidth_statistics(statistics) - - statistics = statistics["statistics"] - balance = statistics["total_given"] - statistics["total_taken"] - self.set_token_balance(balance) - - # If trust page is currently visible, then load the graph as well - if self.stackedWidget.currentIndex() == PAGE_TRUST: - self.trust_page.load_history() - - def set_token_balance(self, balance): - if abs(balance) > 1024 ** 4: # Balance is over a TB - balance /= 1024.0 ** 4 - self.token_balance_label.setText(f"{balance:.1f} TB") - elif abs(balance) > 1024 ** 3: # Balance is over a GB - balance /= 1024.0 ** 3 - self.token_balance_label.setText(f"{balance:.1f} GB") - else: - balance /= 1024.0 ** 2 - self.token_balance_label.setText("%d MB" % balance) - def on_system_tray_icon_activated(self, reason): if reason != QSystemTrayIcon.DoubleClick: return @@ -839,7 +666,6 @@ def on_create_torrent(self, checked): self.create_dialog = CreateTorrentDialog(self) connect(self.create_dialog.create_torrent_notification, self.on_create_torrent_updates) - connect(self.create_dialog.add_to_channel_selected, self.show_add_torrent_to_channel_dialog_from_torrent_data) self.create_dialog.show() def on_create_torrent_updates(self, update_dict): @@ -887,8 +713,7 @@ def on_start_download_action(self, action): self.dialog.dialog_widget.anon_download_checkbox.isChecked(), self.dialog.dialog_widget.safe_seed_checkbox.isChecked(), self.dialog.dialog_widget.destination_input.currentText(), - self.dialog.dialog_widget.files_list_view.get_selected_files_indexes(), - add_to_channel=self.dialog.dialog_widget.add_to_channel_checkbox.isChecked(), + self.dialog.dialog_widget.files_list_view.get_selected_files_indexes() ) else: ConfirmationDialog.show_error( @@ -928,25 +753,6 @@ def on_add_torrent_browse_dir(self, checked): def on_confirm_add_directory_dialog(self, action): if action == 0: - if self.dialog.checkbox.isChecked(): - # TODO: add recursive directory scanning - def on_add_button_pressed(channel_id): - if not Path(self.chosen_dir).is_dir(): - show_message_box(f'"{self.chosen_dir}" is not a directory') - return - - request_manager.put( - endpoint=f"channels/mychannel/{channel_id}/torrents", - on_success=lambda _: self.tray_show_message( - tr("Channels update"), tr("%s added to your channel") % self.chosen_dir - ), - data={"torrents_dir": self.chosen_dir} - ) - - self.window().add_to_channel_dialog.show_dialog( - on_add_button_pressed, confirm_button_text=tr("Add torrent(s)") - ) - for torrent_file in self.selected_torrent_files: self.perform_start_download_request( torrent_file.as_uri(), @@ -1043,15 +849,6 @@ def on_top_search_bar_return_pressed(self): self.deselect_all_menu_buttons() self.stackedWidget.setCurrentIndex(PAGE_SEARCH_RESULTS) - # def clicked_menu_button_discovered(self): - # self.deselect_all_menu_buttons() - # self.left_menu_button_discovered.setChecked(True) - # if self.stackedWidget.currentIndex() == PAGE_DISCOVERED: - # self.discovered_page.go_back_to_level(0) - # self.discovered_page.reset_view() - # self.stackedWidget.setCurrentIndex(PAGE_DISCOVERED) - # self.discovered_page.content_table.setFocus() - def clicked_menu_button_popular(self): self.deselect_all_menu_buttons() self.left_menu_button_popular.setChecked(True) @@ -1061,10 +858,6 @@ def clicked_menu_button_popular(self): self.stackedWidget.setCurrentIndex(PAGE_POPULAR) self.popular_page.content_table.setFocus() - def clicked_trust_graph_page_button(self, _): - self.deselect_all_menu_buttons() - self.stackedWidget.setCurrentIndex(PAGE_TRUST_GRAPH_PAGE) - def clicked_menu_button_downloads(self): self.deselect_all_menu_buttons(self.left_menu_button_downloads) self.raise_window() @@ -1114,10 +907,6 @@ def show_force_shutdown(): self.downloads_page.stop_refreshing_downloads() request_manager.clear() - # Stop the token balance timer - if self.token_refresh_timer: - self.token_refresh_timer.stop() - def closeEvent(self, close_event): self.close_tribler() close_event.ignore() @@ -1178,58 +967,6 @@ def clicked_skip_conversion(self): def node_info_updated(self, node_info): self.core_manager.events_manager.node_info_updated.emit(node_info) - def on_channel_subscribe(self, channel_info): - patch_data = [{ - "public_key": channel_info['public_key'], - "id": channel_info['id'], - "subscribed": True - }] - request_manager.patch("metadata", lambda data: self.node_info_updated(data[0]), data=patch_data) - - def on_channel_unsubscribe(self, channel_info): - def _on_unsubscribe_action(action): - if action == 0: - patch_data = [{"public_key": channel_info['public_key'], "id": channel_info['id'], "subscribed": False}] - request_manager.patch("metadata", lambda data: self.node_info_updated(data[0]), data=patch_data) - if self.dialog: - self.dialog.close_dialog() - self.dialog = None - - self.dialog = ConfirmationDialog( - self, - tr("Unsubscribe from channel"), - tr("Are you sure you want to unsubscribe from channel
") - + '\"' - + f"{channel_info['name']}" - + '\"' - + tr("
and remove its contents?"), - [(tr("UNSUBSCRIBE"), BUTTON_TYPE_NORMAL), (tr("CANCEL"), BUTTON_TYPE_CONFIRM)], - ) - connect(self.dialog.button_clicked, _on_unsubscribe_action) - self.dialog.show() - - def on_channel_delete(self, channel_info): - def _on_delete_action(action): - if action == 0: - delete_data = [{"public_key": channel_info['public_key'], "id": channel_info['id']}] - request_manager.delete("metadata", lambda data: self.node_info_updated(data[0]), data=delete_data) - if self.dialog: - self.dialog.close_dialog() - self.dialog = None - - self.dialog = ConfirmationDialog( - self, - tr("Delete channel"), - tr("Are you sure you want to delete your personal channel
") - + '\"' - + f"{channel_info['name']}" - + '\"' - + tr("
and all its contents?"), - [(tr("DELETE"), BUTTON_TYPE_NORMAL), (tr("CANCEL"), BUTTON_TYPE_CONFIRM)], - ) - connect(self.dialog.button_clicked, _on_delete_action) - self.dialog.show() - def on_skip_conversion_dialog(self, action): if action == 0: self.upgrade_manager.stop_upgrade() diff --git a/src/tribler/gui/widgets/channelcontentswidget.py b/src/tribler/gui/widgets/channelcontentswidget.py index f8ab43aa8b0..4831d3f4b2d 100644 --- a/src/tribler/gui/widgets/channelcontentswidget.py +++ b/src/tribler/gui/widgets/channelcontentswidget.py @@ -6,6 +6,7 @@ from PyQt5.QtWidgets import QAction, QFileDialog from psutil import LINUX +from tribler.core.components.metadata_store.db.orm_bindings.torrent_metadata import NEW from tribler.core.components.metadata_store.db.serialization import CHANNEL_TORRENT, COLLECTION_NODE from tribler.core.utilities.simpledefs import CHANNEL_STATE from tribler.gui.defs import ( @@ -15,22 +16,13 @@ ContentCategories, ) from tribler.gui.dialogs.confirmationdialog import ConfirmationDialog -from tribler.gui.dialogs.new_channel_dialog import NewChannelDialog from tribler.gui.network.request_manager import request_manager from tribler.gui.sentry_mixin import AddBreadcrumbOnShowMixin from tribler.gui.tribler_action_menu import TriblerActionMenu from tribler.gui.utilities import connect, disconnect, get_image_path, get_ui_file_path, tr -from tribler.gui.widgets.tablecontentmodel import ( - ChannelContentModel, - ChannelPreviewModel, - DiscoveredChannelsModel, - PersonalChannelsModel, - SearchResultsModel, - SimplifiedPersonalChannelsModel, -) +from tribler.gui.widgets.tablecontentmodel import ChannelContentModel, SearchResultsModel from tribler.gui.widgets.triblertablecontrollers import ContentTableViewController -CHANNEL_COMMIT_DELAY = 30000 # milliseconds widget_form, widget_class = uic.loadUiType(get_ui_file_path('torrents_list.ui')) @@ -62,8 +54,6 @@ def __init__(self, parent=None): self.chosen_dir = None self.dialog = None self.controller = None - self.commit_timer = None - self.autocommit_enabled = None self.channel_options_menu = None self.channels_stack = [] @@ -91,20 +81,14 @@ def __exit__(self, *args): obj.blockSignals(False) self.freeze_controls = freeze_controls_class - self.channel_description_container.setHidden(True) self.explanation_tooltip_button.setHidden(True) def hide_all_labels(self): self.edit_channel_contents_top_bar.setHidden(True) - self.subscription_widget.setHidden(True) self.channel_num_torrents_label.setHidden(True) self.channel_state_label.setHidden(True) - @property - def personal_channel_model(self): - return SimplifiedPersonalChannelsModel if self.autocommit_enabled else PersonalChannelsModel - @property def model(self): return self.channels_stack[-1] if self.channels_stack else None @@ -113,27 +97,8 @@ def model(self): def root_model(self): return self.channels_stack[0] if self.channels_stack else None - def on_channel_committed(self, response): - if not response or not response.get("success", False): - return - - if not self.autocommit_enabled: - self.commit_control_bar.setHidden(True) - - if not self.model: - return - - info = self.model.channel_info - if info.get("state") == "Personal" and info.get("dirty"): - self.model.reset() - self.update_labels() - - # def commit_channels(self, checked=False): # pylint: disable=W0613 - # request_manager.post("channels/mychannel/0/commit", on_success=self.on_channel_committed) - def initialize_content_page( self, - autocommit_enabled=False, hide_xxx=None, controller_class=ContentTableViewController, categories=CATEGORY_SELECTOR_FOR_SEARCH_ITEMS, @@ -150,7 +115,6 @@ def initialize_content_page( self.channel_back_button.setHidden(True) connect(self.channel_back_button.clicked, self.go_back) connect(self.channel_name_label.linkActivated, self.on_breadcrumb_clicked) - self.commit_control_bar.setHidden(True) if LINUX: # On Linux, the default font sometimes does not contain the emoji characters. @@ -158,30 +122,7 @@ def initialize_content_page( self.controller = controller_class(self.content_table, filter_input=self.channel_torrents_filter_input) - # Hide channel description on scroll - connect(self.controller.table_view.verticalScrollBar().valueChanged, self._on_table_scroll) - - self.autocommit_enabled = autocommit_enabled - # if self.autocommit_enabled: - # self._enable_autocommit_timer() - - # New channel button - connect(self.new_channel_button.clicked, self.create_new_channel) - connect(self.content_table.channel_clicked, self.on_channel_clicked) - # connect(self.edit_channel_commit_button.clicked, self.commit_channels) - - self.subscription_widget.initialize(self) - - self.channel_options_menu = self.create_channel_options_menu() - self.channel_options_button.setMenu(self.channel_options_menu) - connect(self.channel_description_container.became_hidden, self.run_brain_dead_refresh) - connect(self.channel_description_container.description_changed, self._description_changed) - def _description_changed(self): - # Initialize commit timer on channel description change - # if self.autocommit_enabled: - # self.commit_timer.stop() - # self.commit_timer.start(CHANNEL_COMMIT_DELAY) self.model.channel_info["dirty"] = True self.update_labels() @@ -189,22 +130,6 @@ def run_brain_dead_refresh(self): if self.model: self.controller.brain_dead_refresh() - def _on_table_scroll(self, event): # pylint: disable=unused-argument - # Hide the description widget when the channel is scrolled down - if not self.model.data_items: - return - - scrollbar = self.controller.table_view.verticalScrollBar() - container = self.channel_description_container - - is_time_to_hide = scrollbar.minimum() < scrollbar.value() - 10 and scrollbar.maximum() > 100 - is_time_to_show = scrollbar.minimum() == scrollbar.value() - - if is_time_to_hide and not container.isHidden(): - container.setHidden(True) - elif is_time_to_show and container.isHidden() and container.initialized: - container.setHidden(False) - def on_category_selector_changed(self, ind): category = self.categories[ind] if ind else None content_category = ContentCategories.get(category) @@ -239,10 +164,6 @@ def on_model_info_changed(self, changed_entries): if structure_changed: self.window().add_to_channel_dialog.clear_channels_tree() - # if self.autocommit_enabled and dirty: - # self.commit_timer.stop() - # self.commit_timer.start(CHANNEL_COMMIT_DELAY) - self.model.channel_info["dirty"] = dirty self.update_labels() @@ -281,13 +202,11 @@ def reset_view(self, text_filter=None, category_filter=None): def disconnect_current_model(self): disconnect(self.window().core_manager.events_manager.node_info_updated, self.model.update_node_info) - # disconnect(self.model.info_changed, self.on_model_info_changed) disconnect(self.model.query_complete, self.on_model_query_completed) self.controller.unset_model() # Disconnect the selectionChanged signal def connect_current_model(self): - # connect(self.model.info_changed, self.on_model_info_changed) connect(self.model.query_complete, self.on_model_query_completed) connect(self.window().core_manager.events_manager.node_info_updated, self.model.update_node_info) @@ -345,26 +264,6 @@ def go_back_to_level(self, level): self.connect_current_model() self.update_labels() - def on_channel_clicked(self, channel_dict): - self.initialize_with_channel(channel_dict) - - def create_new_channel(self, checked): # pylint: disable=W0613 - NewChannelDialog(self, self.model.create_new_channel) - - def initialize_with_channel(self, channel_info): - # Hide the edit controls by default, to prevent the user clicking the buttons prematurely - self.hide_all_labels() - # Turn off sorting by default to speed up SQL queries - if channel_info.get("state") == CHANNEL_STATE.PREVIEW.value: - self.push_channels_stack(ChannelPreviewModel(channel_info=channel_info)) - else: - self.push_channels_stack(self.default_channel_model(channel_info=channel_info)) - self.controller.set_model(self.model) - self.update_navigation_breadcrumbs() - self.controller.table_view.deselect_all_rows() - self.controller.table_view.resizeEvent(None) - - self.content_table.setFocus() def update_navigation_breadcrumbs(self): # Assemble the channels navigation breadcrumb by utilising RichText links feature @@ -413,41 +312,20 @@ def update_labels(self): personal = self.model.channel_info.get("state", None) == CHANNEL_STATE.PERSONAL.value root = self.current_level == 0 legacy = self.model.channel_info.get("state", None) == CHANNEL_STATE.LEGACY.value - discovered = isinstance(self.model, DiscoveredChannelsModel) - personal_model = isinstance(self.model, PersonalChannelsModel) is_a_channel = self.model.channel_info.get("type", None) == CHANNEL_TORRENT - description_flag = self.model.channel_info.get("description_flag") - thumbnail_flag = self.model.channel_info.get("thumbnail_flag") - dirty = self.model.channel_info.get("dirty") self.update_navigation_breadcrumbs() - info = self.model.channel_info container = self.channel_description_container - if is_a_channel and (description_flag or thumbnail_flag or personal_model): - container.initialize_with_channel(info["public_key"], info["id"], edit=personal and personal_model) - else: - container.initialized = False - container.setHidden(True) + container.initialized = False + container.setHidden(True) - self.category_selector.setHidden(root and (discovered or personal_model)) - # initialize the channel page - - self.edit_channel_contents_top_bar.setHidden(not personal) - self.new_channel_button.setText(tr("NEW CHANNEL") if not is_a_channel and not folder else tr("NEW FOLDER")) - self.channel_options_button.setHidden(not personal_model or not personal or (root and not is_a_channel)) - self.new_channel_button.setHidden(not personal_model or not personal) - - self.channel_state_label.setText(self.model.channel_info.get("state", "This text should not ever be shown")) + self.category_selector.setHidden(root) self.subscription_widget.setHidden(not is_a_channel or personal or folder or legacy) if not self.subscription_widget.isHidden(): self.subscription_widget.update_subscribe_button(self.model.channel_info) - self.channel_state_label.setHidden((root and not is_a_channel) or personal) - - self.commit_control_bar.setHidden(self.autocommit_enabled or not dirty or not personal) - if "total" in self.model.channel_info: self.channel_num_torrents_label.setHidden(False) if "torrents" in self.model.channel_info: diff --git a/src/tribler/gui/widgets/channeldescriptionwidget.py b/src/tribler/gui/widgets/channeldescriptionwidget.py deleted file mode 100644 index 3f84d8dd998..00000000000 --- a/src/tribler/gui/widgets/channeldescriptionwidget.py +++ /dev/null @@ -1,280 +0,0 @@ -from pathlib import Path - -from PyQt5 import QtCore, uic -from PyQt5.QtCore import QDir, pyqtSignal, pyqtSlot -from PyQt5.QtGui import QIcon, QImage, QPixmap -from PyQt5.QtWidgets import QFileDialog, QPushButton - -from tribler.gui.dialogs.confirmationdialog import ConfirmationDialog -from tribler.gui.network.request_manager import request_manager -from tribler.gui.sentry_mixin import AddBreadcrumbOnShowMixin -from tribler.gui.utilities import connect, get_image_path, get_ui_file_path, tr - -widget_form, widget_class = uic.loadUiType(get_ui_file_path('channel_description.ui')) - -EDIT_BUTTON = "edit_mode_button" -PREVIEW_BUTTON = "preview_mode_button" -EDIT_BUTTON_NUM = 0 -PREVIEW_BUTTON_NUM = 1 - -DEFAULT_THUMBNAIL_PIXMAP = QPixmap(get_image_path('chan_thumb.png')) -CREATE_THUMBNAIL_TEXT = tr("Click this to add \n channel thumbnail \n (max. 1MB JPG/PNG)") - -PREVIEW_PAGE = 0 -EDIT_PAGE = 1 - - -class FloatingButtonWidget(QPushButton): - # Solution inspired by https://gist.github.com/namuan/floating_button_widget.py - - def __init__(self, parent): - super().__init__(QIcon(QPixmap(get_image_path('edit.png'))), "", parent) - - self.setGeometry(20, 20, 20, 20) - - self.setFlat(True) - self.paddingRight = 5 - self.paddingTop = 5 - - def update_position(self): - if hasattr(self.parent(), 'viewport'): - parent_rect = self.parent().viewport().rect() - else: - parent_rect = self.parent().rect() - - if not parent_rect: - return - - x = parent_rect.width() - self.width() - self.paddingRight - y = self.paddingTop - self.setGeometry(x, y, self.width(), self.height()) - - def resizeEvent(self, event): - super().resizeEvent(event) - self.update_position() - - -class ChannelDescriptionWidget(AddBreadcrumbOnShowMixin, widget_form, widget_class): - became_hidden = pyqtSignal() - description_changed = pyqtSignal() - - def __init__(self, parent=None): - widget_class.__init__(self, parent=parent) - try: - self.setupUi(self) - except SystemError: - pass - self.edit_mode_tab.initialize() - - # Set the preview tab and button as default - self.edit_mode_tab.buttons[PREVIEW_BUTTON_NUM].setEnabled(True) - self.edit_mode_tab.buttons[PREVIEW_BUTTON_NUM].setChecked(True) - - # Note that button signals are connected - # automatically by connectSlotsByName when loading the .ui file - connect(self.edit_mode_tab.clicked_tab_button, self.tab_button_clicked) - - self.description_text = None - self.channel_thumbnail_bytes = None - self.channel_thumbnail_qimage = None - - self.channel_pk = None - self.channel_id = None - - self.edit_enabled = False - - self.bottom_buttons_container.setHidden(True) - - self.initialized = False - - self.dialog = None - - self.floating_edit_button = FloatingButtonWidget(parent=self.description_text_preview) - self.floating_edit_button.setHidden(True) - connect(self.floating_edit_button.pressed, self.on_start_editing) - - def resizeEvent(self, event): - super().resizeEvent(event) - self.floating_edit_button.update_position() - - def hideEvent(self, event): - # This one is unfortunately necessary to ensure thant brain_dead_refresh will - # run every time this thing is hidden - self.became_hidden.emit() - super().hideEvent(event) - - def showEvent(self, *args): - # This one is unfortunately necessary to ensure thant brain_dead_refresh will - # run every time this thing is shown - self.became_hidden.emit() - super().showEvent(*args) - - def tab_button_clicked(self, button_name): - if button_name == EDIT_BUTTON: - self.switch_to_edit() - elif button_name == PREVIEW_BUTTON: - self.description_text = self.description_text_edit.toPlainText() - self.switch_to_preview() - - def on_start_editing(self): - self.edit_buttons_panel_widget.setHidden(False) - self.floating_edit_button.setHidden(True) - self.switch_to_edit(update_buttons=True) - self.bottom_buttons_container.setHidden(False) - if self.channel_thumbnail_bytes is None: - self.channel_thumbnail.setText(CREATE_THUMBNAIL_TEXT) - - @pyqtSlot() - def on_create_description_button_clicked(self, *args): - self.description_text = "" - self.channel_thumbnail_bytes = None - self.show_description_page() - self.on_start_editing() - - @pyqtSlot() - def on_save_button_clicked(self): - self.bottom_buttons_container.setHidden(True) - self.description_text = self.description_text_edit.toPlainText() - - self.switch_to_preview(update_buttons=True) - - descr_changed = False - thumb_changed = False - - if self.description_text is not None: - descr_changed = True - request_manager.put(f'channels/{self.channel_pk}/{self.channel_id}/description', - on_success=self._on_description_received, - data={"description_text": self.description_text}) - - if self.channel_thumbnail_bytes is not None: - thumb_changed = True - - def _on_thumbnail_updated(_): - pass - - request_manager.put(f'channels/{self.channel_pk}/{self.channel_id}/thumbnail', - on_success=_on_thumbnail_updated, - data=self.channel_thumbnail_bytes, - raw_response=True) - - if descr_changed or thumb_changed: - self.description_changed.emit() - - def on_channel_thumbnail_clicked(self): - if not (self.edit_enabled and self.edit_mode_tab.get_selected_index() == EDIT_BUTTON_NUM): - return - filename = QFileDialog.getOpenFileName( - self, - tr("Please select a thumbnail file"), - QDir.homePath(), - filter=(tr("PNG/XPM/JPG images %s") % '(*.png *.xpm *.jpg)'), - )[0] - - if not filename: - return - - content_type = f"image/{str(Path(filename).suffix)[1:]}" - - with open(filename, "rb") as f: - data = f.read() - - if len(data) > 1024 ** 2: - self.dialog = ConfirmationDialog.show_error( - self, - tr(tr("Image too large error")), - tr(tr("Image file you're trying to upload is too large.")), - ) - return - - self.channel_thumbnail_bytes = data - self.update_channel_thumbnail(data, content_type) - - @pyqtSlot() - def on_cancel_button_clicked(self): - self.initialize_with_channel(self.channel_pk, self.channel_id, edit=self.edit_enabled) - - def switch_to_preview(self, update_buttons=False): - self.description_stack_widget.setCurrentIndex(PREVIEW_PAGE) - if self.edit_enabled: - self.floating_edit_button.setHidden(False) - self.description_text_preview.setMarkdown(self.description_text) - self.description_text_preview.setReadOnly(True) - if self.channel_thumbnail_bytes is None: - self.channel_thumbnail.setPixmap(DEFAULT_THUMBNAIL_PIXMAP) - if update_buttons: - self.edit_mode_tab.deselect_all_buttons(except_select=self.edit_mode_tab.buttons[PREVIEW_BUTTON_NUM]) - - def switch_to_edit(self, update_buttons=False): - self.description_stack_widget.setCurrentIndex(EDIT_PAGE) - self.floating_edit_button.setHidden(True) - self.description_text_edit.setPlainText(self.description_text) - self.description_text_edit.setReadOnly(False) - if self.channel_thumbnail_bytes is None: - self.channel_thumbnail.setText(CREATE_THUMBNAIL_TEXT) - if update_buttons: - self.edit_mode_tab.deselect_all_buttons(except_select=self.edit_mode_tab.buttons[EDIT_BUTTON_NUM]) - - def show_create_page(self): - self.create_page.setHidden(False) - self.description_page.setHidden(True) - - def show_description_page(self): - self.create_page.setHidden(True) - self.description_page.setHidden(False) - - def _on_description_received(self, result): - self.description_text = result.get("description_text") if result else None - self.description_text_preview.setMarkdown(self.description_text or "") - - request_manager.get(f'channels/{self.channel_pk}/{self.channel_id}/thumbnail', - on_success=self._on_thumbnail_received, - raw_response=True) - - def set_widget_visible(self, show): - self.bottom_buttons_container.setHidden(True) - self.setHidden(not self.edit_enabled) - if not show: - # No data + edit enabled = invite to create a description - if self.edit_enabled: - self.show_create_page() - return - self.show_description_page() - self.setHidden(False) - self.initialized = True - self.switch_to_preview(update_buttons=True) - self.edit_buttons_panel_widget.setHidden(True) - if self.edit_enabled: - self.enable_edit() - else: - self.disable_edit() - - def update_channel_thumbnail(self, image_data: bytes, image_type: str): - w = self.channel_thumbnail.width() - h = self.channel_thumbnail.height() - qimage = QImage.fromData(image_data, image_type.split("/")[1]) - self.channel_thumbnail.setPixmap(QPixmap.fromImage(qimage).scaled(w, h, QtCore.Qt.KeepAspectRatio)) - - def _on_thumbnail_received(self, result_and_header): - result, header = result_and_header - if not (result and header): - self.channel_thumbnail_bytes = None - self.channel_thumbnail.setPixmap(DEFAULT_THUMBNAIL_PIXMAP) - self.set_widget_visible(self.description_text is not None) - return - self.channel_thumbnail_bytes = result - self.update_channel_thumbnail(result, header) - self.set_widget_visible(True) - - def initialize_with_channel(self, channel_pk, channel_id, edit=True): - self.initialized = False - self.edit_enabled = edit - self.floating_edit_button.setHidden(not self.edit_enabled) - self.channel_pk, self.channel_id = channel_pk, channel_id - request_manager.get(f'channels/{self.channel_pk}/{self.channel_id}/description', self._on_description_received) - - def enable_edit(self): - self.floating_edit_button.setHidden(False) - - def disable_edit(self): - self.edit_buttons_panel_widget.setHidden(True) diff --git a/src/tribler/gui/widgets/channelsmenulistwidget.py b/src/tribler/gui/widgets/channelsmenulistwidget.py deleted file mode 100644 index dead62c5279..00000000000 --- a/src/tribler/gui/widgets/channelsmenulistwidget.py +++ /dev/null @@ -1,142 +0,0 @@ -from PyQt5.QtCore import QSize, Qt -from PyQt5.QtGui import QBrush, QColor, QIcon, QPixmap -from PyQt5.QtWidgets import QAbstractItemView, QAbstractScrollArea, QAction, QListWidget, QListWidgetItem - -from tribler.core.components.metadata_store.db.serialization import CHANNEL_TORRENT -from tribler.core.utilities.simpledefs import CHANNEL_STATE -from tribler.gui.network.request_manager import request_manager -from tribler.gui.tribler_action_menu import TriblerActionMenu -from tribler.gui.utilities import connect, get_image_path, tr - - -def entry_to_tuple(entry): - return entry["public_key"], entry["id"], entry.get('subscribed', False), entry.get('state'), entry.get('progress') - - -class ChannelListItem(QListWidgetItem): - loading_brush = QBrush(Qt.darkGray) - - def __init__(self, parent=None, channel_info=None): - self.channel_info = channel_info - title = channel_info.get('name') - QListWidgetItem.__init__(self, title, parent=parent) - # This is necessary to increase vertical height of the items - self.setSizeHint(QSize(50, 25)) - if channel_info.get('state') not in (CHANNEL_STATE.COMPLETE.value, CHANNEL_STATE.PERSONAL.value): - self.setForeground(self.loading_brush) - - def setData(self, role, new_value): - # TODO: call higher-level signal to propagate the change to other widgets - if role == Qt.EditRole: - item = self.channel_info - if item['name'] != new_value: - request_manager.patch(f"metadata/{item['public_key']}/{item['id']}", data={"title": new_value}) - - return super().setData(role, new_value) - - -class ChannelsMenuListWidget(QListWidget): - def __init__(self, parent=None): - QListWidget.__init__(self, parent=parent) - self.base_url = "channels" - self.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContents) - - # Items set, used for checking changes - self.items_set = frozenset() - self.personal_channel_icon = QIcon(get_image_path("share.png")) - empty_transparent_image = QPixmap(15, 15) - empty_transparent_image.fill(QColor(0, 0, 0, 0)) - self.empty_image = QIcon(empty_transparent_image) - - self.foreign_channel_menu = self.create_foreign_menu() - self.personal_channel_menu = self.create_personal_menu() - self.setSelectionMode(QAbstractItemView.NoSelection) - - def sizeHint(self): - count = self.count() - height = self.sizeHintForRow(0) * count if count else 0 - # !!!ACHTUNG!!! - # !!! Qt Bug !!! - # Qt never shrinks QListWidget vertically to less than the size - # that is required to contain list three items. Even if there a no items. - # sizeHint is ignored completely, the real minimum size is always at least - # three items. Also, Qt ignores the overloaded self.maximumHeight method. - # So, the only way to shrink it is to call setMaximumHeight manually. - # Qt, I hate you! Why are you doing this to me!? - self.setMaximumHeight(height) - return QSize(self.width(), height) - - def contextMenuEvent(self, event): - item = self.itemAt(event.pos()) - if item is None: - return - - if item.channel_info["state"] == CHANNEL_STATE.PERSONAL.value: - self.personal_channel_menu.exec_(self.mapToGlobal(event.pos())) - else: - self.foreign_channel_menu.exec_(self.mapToGlobal(event.pos())) - - def create_foreign_menu(self): - menu = TriblerActionMenu(self) - unsubscribe_action = QAction(tr("Unsubscribe"), self) - connect(unsubscribe_action.triggered, self._on_unsubscribe_action) - menu.addAction(unsubscribe_action) - return menu - - def create_personal_menu(self): - menu = TriblerActionMenu(self) - delete_action = QAction(tr("Delete channel"), self) - connect(delete_action.triggered, self._on_delete_action) - menu.addAction(delete_action) - - rename_action = QAction(tr("Rename channel"), self) - connect(rename_action.triggered, self._trigger_name_editor) - menu.addAction(rename_action) - return menu - - def _trigger_name_editor(self, checked): - self.editItem(self.currentItem()) - - def _on_unsubscribe_action(self, checked): - self.window().on_channel_unsubscribe(self.currentItem().channel_info) - - def _on_delete_action(self, checked): - self.window().on_channel_delete(self.currentItem().channel_info) - - def on_query_results(self, response): - channels = response.get('results') - if channels is None: - return - self.clear() - for channel_info in sorted(channels, key=lambda x: x.get('state') != 'Personal'): - item = ChannelListItem(channel_info=channel_info) - self.addItem(item) - # ACHTUNG! Qt bug prevents moving this thing into ChannelListItem ! - if channel_info.get('state') == CHANNEL_STATE.PERSONAL.value: - item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable) - item.setIcon(self.personal_channel_icon) - else: - # We assign a transparent icon to foreign channels to align - # their text with the personal ones - item.setIcon(self.empty_image) - tooltip_text = channel_info['name'] + "\n" + channel_info['state'] - if channel_info.get('progress'): - tooltip_text += f" {int(float(channel_info['progress']) * 100)}%" - item.setToolTip(tooltip_text) - - self.items_set = frozenset(entry_to_tuple(channel_info) for channel_info in channels) - - def load_channels(self): - request_manager.get(self.base_url, self.on_query_results, url_params={"subscribed": True, "last": 1000}) - - def reload_if_necessary(self, changed_entries): - # Compare the state changes in the changed entries list to our current list - # and update the list if necessary - changeset = frozenset( - entry_to_tuple(entry) - for entry in changed_entries - if entry.get("state") == "Deleted" or entry.get("type") == CHANNEL_TORRENT - ) - need_update = not self.items_set.issuperset(changeset) - if need_update: - self.load_channels() diff --git a/src/tribler/gui/widgets/createtorrentpage.py b/src/tribler/gui/widgets/createtorrentpage.py index 4f377beb021..03168e332c0 100644 --- a/src/tribler/gui/widgets/createtorrentpage.py +++ b/src/tribler/gui/widgets/createtorrentpage.py @@ -4,7 +4,7 @@ from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QAction, QFileDialog, QWidget -from tribler.gui.defs import BUTTON_TYPE_NORMAL, PAGE_EDIT_CHANNEL_TORRENTS +from tribler.gui.defs import BUTTON_TYPE_NORMAL from tribler.gui.dialogs.confirmationdialog import ConfirmationDialog from tribler.gui.network.request_manager import request_manager from tribler.gui.sentry_mixin import AddBreadcrumbOnShowMixin @@ -19,7 +19,6 @@ class CreateTorrentPage(AddBreadcrumbOnShowMixin, QWidget): def __init__(self): QWidget.__init__(self) - self.channel_identifier = None self.dialog = None self.selected_item_index = -1 self.initialized = False @@ -44,9 +43,6 @@ def initialize(self): self.initialized = True - def on_create_torrent_manage_back_clicked(self, checked): - self.window().edit_channel_details_stacked_widget.setCurrentIndex(PAGE_EDIT_CHANNEL_TORRENTS) - def on_choose_files_clicked(self, checked): filenames, _ = QFileDialog.getOpenFileNames(self.window(), "Please select the files", QDir.homePath()) @@ -104,19 +100,6 @@ def on_torrent_created(self, result): if not result: return self.window().edit_channel_create_torrent_button.setEnabled(True) - if 'torrent' in result: - self.add_torrent_to_channel(result['torrent']) - - def add_torrent_to_channel(self, torrent): - request_manager.put("mychannel/torrents", self.on_torrent_to_channel_added, data={"torrent": torrent}) - - def on_torrent_to_channel_added(self, result): - if not result: - return - self.window().edit_channel_create_torrent_progress_label.hide() - if 'added' in result: - self.window().edit_channel_details_stacked_widget.setCurrentIndex(PAGE_EDIT_CHANNEL_TORRENTS) - self.window().personal_channel_page.load_my_torrents() def on_remove_entry(self): self.window().create_torrent_files_list.takeItem(self.selected_item_index) diff --git a/src/tribler/gui/widgets/discoveringpage.py b/src/tribler/gui/widgets/discoveringpage.py deleted file mode 100644 index dc92a607d09..00000000000 --- a/src/tribler/gui/widgets/discoveringpage.py +++ /dev/null @@ -1,33 +0,0 @@ -from PyQt5.QtWidgets import QWidget - -from tribler.gui.sentry_mixin import AddBreadcrumbOnShowMixin -from tribler.gui.utilities import connect -from tribler.gui.widgets.loadingpage import LOADING_ANIMATION - - -class DiscoveringPage(AddBreadcrumbOnShowMixin, QWidget): - """ - The DiscoveringPage is shown when users are starting Tribler for the first time. It hides when there are at least - five discovered channels. - """ - - def __init__(self): - QWidget.__init__(self) - self.found_channels = 0 - self.is_discovering = False - - def initialize_discovering_page(self): - self.window().discovering_svg_view.setScene(LOADING_ANIMATION) - connect(self.window().core_manager.events_manager.discovered_channel, self.on_discovered_channel) - - def on_discovered_channel(self, _): - self.found_channels += 1 - - if self.found_channels >= 5 and self.is_discovering: - self.is_discovering = False - self.window().clicked_menu_button_discovered() - return - - self.window().discovering_top_label.setText( - "Discovering your first content...\n\nFound %d channels" % self.found_channels - ) diff --git a/src/tribler/gui/widgets/downloadspage.py b/src/tribler/gui/widgets/downloadspage.py index 9706a36f2de..1baa861cd33 100644 --- a/src/tribler/gui/widgets/downloadspage.py +++ b/src/tribler/gui/widgets/downloadspage.py @@ -264,14 +264,9 @@ def update_download_visibility(self): continue filter_match = self.window().downloads_filter_input.text().lower() in item.download_info["name"].lower() - is_channel = item.download_info["channel_download"] - if self.filter == DOWNLOADS_FILTER_CHANNELS: - hide = not (is_channel and filter_match) - item.setHidden(hide) - else: - filtered = DOWNLOADS_FILTER_DEFINITION[self.filter] - hide = item.get_status() not in filtered or not filter_match or is_channel - item.setHidden(hide) + filtered = DOWNLOADS_FILTER_DEFINITION[self.filter] + hide = item.get_status() not in filtered or not filter_match + item.setHidden(hide) def on_downloads_tab_button_clicked(self, button_name): self.filter = button_name2filter[button_name] @@ -516,22 +511,6 @@ def on_export_download_request_done(self, filename, data): tr("Torrent file exported"), tr("Torrent file exported to %s") % str(dest_path) ) - def on_add_to_channel(self, checked): - def on_add_button_pressed(channel_id): - for item in self.selected_items: - infohash = item.infohash - name = item.download_info["name"] - request_manager.put( - f"channels/mychannel/{channel_id}/torrents", - on_success=lambda _: self.window().tray_show_message( - tr("Channel update"), - tr("Torrent(s) added to your channel") - ), - data={"uri": compose_magnetlink(infohash, name=name)} - ) - - self.window().add_to_channel_dialog.show_dialog(on_add_button_pressed, confirm_button_text=tr("Add torrent(s)")) - def on_right_click_item(self, pos): item_clicked = self.window().downloads_list.itemAt(pos) if not item_clicked or not self.selected_items: @@ -545,7 +524,6 @@ def on_right_click_item(self, pos): start_action = QAction(tr("Start"), self) stop_action = QAction(tr("Stop"), self) remove_download_action = QAction(tr("Remove download"), self) - add_to_channel_action = QAction(tr("Add to my channel"), self) force_recheck_action = QAction(tr("Force recheck"), self) export_download_action = QAction(tr("Export .torrent file"), self) explore_files_action = QAction(tr("Explore files"), self) @@ -560,7 +538,6 @@ def on_right_click_item(self, pos): start_action.setEnabled(DownloadsPage.start_download_enabled(self.selected_items)) connect(stop_action.triggered, self.on_stop_download_clicked) stop_action.setEnabled(DownloadsPage.stop_download_enabled(self.selected_items)) - connect(add_to_channel_action.triggered, self.on_add_to_channel) connect(remove_download_action.triggered, self.on_remove_download_clicked) connect(force_recheck_action.triggered, self.on_force_recheck_download) force_recheck_action.setEnabled(DownloadsPage.force_recheck_download_enabled(self.selected_items)) @@ -576,8 +553,6 @@ def on_right_click_item(self, pos): menu.addAction(start_action) menu.addAction(stop_action) - menu.addSeparator() - menu.addAction(add_to_channel_action) menu.addSeparator() menu.addAction(remove_download_action) menu.addSeparator() diff --git a/src/tribler/gui/widgets/lazytableview.py b/src/tribler/gui/widgets/lazytableview.py index 78dbd5367ba..fa966149a5e 100644 --- a/src/tribler/gui/widgets/lazytableview.py +++ b/src/tribler/gui/widgets/lazytableview.py @@ -5,9 +5,7 @@ from PyQt5.QtGui import QGuiApplication, QMouseEvent, QMovie from PyQt5.QtWidgets import QAbstractItemView, QApplication, QHeaderView, QLabel, QTableView -from tribler.core.components.metadata_store.db.serialization import CHANNEL_TORRENT, COLLECTION_NODE, REGULAR_TORRENT, \ - SNIPPET -from tribler.gui.defs import COMMIT_STATUS_COMMITTED +from tribler.core.components.metadata_store.db.serialization import SNIPPET from tribler.gui.dialogs.editmetadatadialog import EditMetadataDialog from tribler.gui.network.request_manager import request_manager from tribler.gui.utilities import connect, data_item2uri, get_image_path, index2uri @@ -48,7 +46,6 @@ class TriblerContentTableView(QTableView): When the user reached the end of the table, it will ask the model for more items, and load them dynamically. """ - channel_clicked = pyqtSignal(dict) torrent_clicked = pyqtSignal(dict) torrent_doubleclicked = pyqtSignal(dict) edited_metadata = pyqtSignal(dict) @@ -182,19 +179,6 @@ def redraw(self, index, redraw_whole_row): for control in self.delegate.controls: control.rect = QRect() - def on_subscribe_control_clicked(self, index): - item = index.model().data_items[index.row()] - # skip LEGACY entries, regular torrents and personal channel - if 'subscribed' not in item or item['status'] == LEGACY_ENTRY or item['state'] == 'Personal': - return - - status = int(item['subscribed']) - - if status: - self.window().on_channel_unsubscribe(item) - else: - self.window().on_channel_subscribe(item) - def on_edit_tags_clicked(self, index: QModelIndex) -> None: self.add_tags_dialog = EditMetadataDialog(self.window(), index) self.add_tags_dialog.show() @@ -217,29 +201,11 @@ def on_table_item_clicked(self, item, doubleclick=False): return data_item = self.model().data_items[item.row()] - # Safely determine if the thing is a channel. A little bit hackish - if data_item.get('type') in [CHANNEL_TORRENT, COLLECTION_NODE]: - self.channel_clicked.emit(data_item) - elif data_item.get('type') == REGULAR_TORRENT: - if not doubleclick: - self.torrent_clicked.emit(data_item) - else: - self.torrent_doubleclicked.emit(data_item) - - def on_torrent_status_updated(self, json_result, index): - if not json_result: - return - - if 'success' in json_result and json_result['success']: - index.model().data_items[index.row()]['status'] = json_result['new_status'] - # Note: this should instead use signal and do not address the widget globally - # and properly handle entry removal - self.window().personal_channel_page.channel_dirty = ( - self.table_view.window().edit_channel_page.channel_dirty - or json_result['new_status'] != COMMIT_STATUS_COMMITTED - ) - self.window().personal_channel_page.update_channel_commit_views(deleted_index=index) + if not doubleclick: + self.torrent_clicked.emit(data_item) + else: + self.torrent_doubleclicked.emit(data_item) def on_delete_button_clicked(self, _index): self.model().delete_rows(self.selectionModel().selectedRows()) diff --git a/src/tribler/gui/widgets/searchresultswidget.py b/src/tribler/gui/widgets/searchresultswidget.py index e3397530fab..e7112ff4b44 100644 --- a/src/tribler/gui/widgets/searchresultswidget.py +++ b/src/tribler/gui/widgets/searchresultswidget.py @@ -65,7 +65,6 @@ def __init__(self, parent=None): def initialize(self, hide_xxx=False): self.hide_xxx = hide_xxx self.results_page_content.initialize_content_page(hide_xxx=hide_xxx) - self.results_page_content.channel_torrents_filter_input.setHidden(True) @property def has_results(self): diff --git a/src/tribler/gui/widgets/settingspage.py b/src/tribler/gui/widgets/settingspage.py index b736da1fc0d..7473ad21cb1 100644 --- a/src/tribler/gui/widgets/settingspage.py +++ b/src/tribler/gui/widgets/settingspage.py @@ -59,7 +59,6 @@ def initialize_settings_page(self, version_history): connect(self.window().download_location_chooser_button.clicked, self.on_choose_download_dir_clicked) connect(self.window().watch_folder_chooser_button.clicked, self.on_choose_watch_dir_clicked) - connect(self.window().channel_autocommit_checkbox.stateChanged, self.on_channel_autocommit_checkbox_changed) connect(self.window().family_filter_checkbox.stateChanged, self.on_family_filter_checkbox_changed) connect(self.window().developer_mode_enabled_checkbox.stateChanged, self.on_developer_mode_checkbox_changed) connect(self.window().use_monochrome_icon_checkbox.stateChanged, self.on_use_monochrome_icon_checkbox_changed) @@ -83,9 +82,6 @@ def showEvent(self, *args): super().showEvent(*args) self.window().settings_tab.process_button_click(self.window().settings_general_button) - def on_channel_autocommit_checkbox_changed(self, _): - self.window().gui_settings.setValue("autocommit_enabled", self.window().channel_autocommit_checkbox.isChecked()) - def on_family_filter_checkbox_changed(self, _): self.window().gui_settings.setValue("family_filter", self.window().family_filter_checkbox.isChecked()) @@ -175,17 +171,9 @@ def initialize_with_settings(self, settings): self.window().download_settings_anon_seeding_checkbox.setChecked( settings['download_defaults']['safeseeding_enabled'] ) - self.window().download_settings_add_to_channel_checkbox.setChecked( - settings['download_defaults']['add_download_to_channel'] - ) self.window().watchfolder_enabled_checkbox.setChecked(settings['watch_folder']['enabled']) self.window().watchfolder_location_input.setText(settings['watch_folder']['directory']) - # Channel settings - self.window().channel_autocommit_checkbox.setChecked( - get_gui_setting(gui_settings, "autocommit_enabled", True, is_bool=True) - ) - # Tags settings self.window().disable_tags_checkbox.setChecked( get_gui_setting(gui_settings, "disable_tags", False, is_bool=True) @@ -498,9 +486,6 @@ def save_settings(self, checked): settings_data['download_defaults'][ 'safeseeding_enabled' ] = self.window().download_settings_anon_seeding_checkbox.isChecked() - settings_data['download_defaults'][ - 'add_download_to_channel' - ] = self.window().download_settings_add_to_channel_checkbox.isChecked() settings_data['resource_monitor']['enabled'] = self.window().checkbox_enable_resource_log.isChecked() settings_data['resource_monitor']['cpu_priority'] = int(self.window().slider_cpu_level.value()) @@ -533,7 +518,6 @@ def on_settings_saved(self, data): # Now save the GUI settings self.window().gui_settings.setValue("family_filter", self.window().family_filter_checkbox.isChecked()) self.window().gui_settings.setValue("disable_tags", self.window().disable_tags_checkbox.isChecked()) - self.window().gui_settings.setValue("autocommit_enabled", self.window().channel_autocommit_checkbox.isChecked()) self.window().gui_settings.setValue( "ask_download_settings", self.window().always_ask_location_checkbox.isChecked() ) diff --git a/src/tribler/gui/widgets/subscriptionswidget.py b/src/tribler/gui/widgets/subscriptionswidget.py deleted file mode 100644 index a86725f2063..00000000000 --- a/src/tribler/gui/widgets/subscriptionswidget.py +++ /dev/null @@ -1,81 +0,0 @@ -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QFont -from PyQt5.QtWidgets import QLabel, QWidget - -from tribler.gui.sentry_mixin import AddBreadcrumbOnShowMixin -from tribler.gui.utilities import connect, format_votes_rich_text, get_votes_rating_description, tr -from tribler.gui.widgets.tablecontentdelegate import DARWIN, WINDOWS - - -class SubscriptionsWidget(AddBreadcrumbOnShowMixin, QWidget): - """ - This widget shows a favorite button and the number of subscriptions that a specific channel has. - """ - - def __init__(self, parent): - QWidget.__init__(self, parent) - self.subscribe_button = None - self.initialized = False - self.contents_widget = None - self.channel_rating_label = None - - def initialize(self, contents_widget): - if not self.initialized: - # We supply a link to the parent channelcontentswidget to use its property that - # returns the current model in use (top of the stack) - self.contents_widget = contents_widget - self.subscribe_button = self.findChild(QWidget, "subscribe_button") - self.channel_rating_label = self.findChild(QLabel, "channel_rating_label") - self.channel_rating_label.setTextFormat(Qt.RichText) - - connect(self.subscribe_button.clicked, self.on_subscribe_button_click) - self.subscribe_button.setToolTip(tr("Click to subscribe/unsubscribe")) - connect(self.subscribe_button.toggled, self._adjust_tooltip) - self.initialized = True - - def _adjust_tooltip(self, toggled): - tooltip = (tr("Subscribed.") if toggled else tr("Not subscribed.")) + tr("\n(Click to unsubscribe)") - self.subscribe_button.setToolTip(tooltip) - - def update_subscribe_button_if_channel_matches(self, changed_channels_list): - # TODO: the stuff requires MAJOR refactoring with properly implemented QT MVC model... - if not (self.contents_widget.model and self.contents_widget.model.channel_info.get("public_key")): - return - for channel_info in changed_channels_list: - if ( - self.contents_widget.model.channel_info["public_key"] == channel_info["public_key"] - and self.contents_widget.model.channel_info["id"] == channel_info["id"] - ): - self.update_subscribe_button(remote_response=channel_info) - return - - def update_subscribe_button(self, remote_response=None): - # A safeguard against race condition that happens when the user changed - # the channel view before the response came in - if self.isHidden(): - return - - if remote_response and "subscribed" in remote_response: - self.contents_widget.model.channel_info["subscribed"] = remote_response["subscribed"] - - self.subscribe_button.setChecked(bool(remote_response["subscribed"])) - self._adjust_tooltip(bool(remote_response["subscribed"])) - - # Update rating display - votes = remote_response['votes'] - self.channel_rating_label.setText(format_votes_rich_text(votes)) - if DARWIN or WINDOWS: - font = QFont() - font.setLetterSpacing(QFont.PercentageSpacing, 60.0) - self.channel_rating_label.setFont(font) - - self.channel_rating_label.setToolTip(get_votes_rating_description(votes)) - - def on_subscribe_button_click(self, checked): - self.subscribe_button.setCheckedInstant(bool(self.contents_widget.model.channel_info["subscribed"])) - channel_info = self.contents_widget.model.channel_info - if channel_info["subscribed"]: - # Show the global unsubscribe confirmation dialog - self.window().on_channel_unsubscribe(channel_info) - else: - self.window().on_channel_subscribe(channel_info) diff --git a/src/tribler/gui/widgets/tablecontentdelegate.py b/src/tribler/gui/widgets/tablecontentdelegate.py index db7713d6447..005171674aa 100644 --- a/src/tribler/gui/widgets/tablecontentdelegate.py +++ b/src/tribler/gui/widgets/tablecontentdelegate.py @@ -301,74 +301,6 @@ def createEditor(self, parent, option, index): return super().createEditor(parent, option, index) -class ChannelStateMixin: - wait_png = QIcon(get_image_path("wait.png")) - share_icon = QIcon(get_image_path("share.png")) - downloading_icon = QIcon(get_image_path("downloads.png")) - - @staticmethod - def get_indicator_rect(rect): - r = rect - indicator_border = 1 - indicator_side = (r.height() if r.width() > r.height() else r.width()) - indicator_border * 2 - y = int(r.top() + (r.height() - indicator_side) // 2) - x = r.left() + indicator_border - w = indicator_side - h = indicator_side - indicator_rect = QRect(x, y, w, h) - return indicator_rect - - def draw_channel_state(self, painter, option, index, data_item): - # Draw empty cell as the background - - self.paint_empty_background(painter, option) - text_rect = option.rect - - if data_item['status'] == CHANNEL_STATE.LEGACY.value: - painter.drawText(text_rect, Qt.AlignCenter, "Legacy") - return True - - if 'type' in data_item and data_item['type'] != CHANNEL_TORRENT: - return True - if data_item['state'] == CHANNEL_STATE.COMPLETE.value: - painter.drawText(text_rect, Qt.AlignCenter, "✔") - return True - if data_item['state'] == CHANNEL_STATE.PERSONAL.value: - self.share_icon.paint(painter, self.get_indicator_rect(option.rect)) - return True - if data_item['state'] == CHANNEL_STATE.DOWNLOADING.value: - painter.drawText(text_rect, Qt.AlignCenter, "⏳") - return True - if data_item['state'] == CHANNEL_STATE.METAINFO_LOOKUP.value: - painter.drawText(text_rect, Qt.AlignCenter, "❓") - return True - if data_item['state'] == CHANNEL_STATE.UPDATING.value: - progress = data_item.get('progress') - if progress is not None: - draw_progress_bar(painter, option.rect, float(progress)) - return True - return True - - -class SubscribedControlMixin: - def draw_subscribed_control(self, painter, option, index, data_item): - # Draw empty cell as the background - self.paint_empty_background(painter, option) - - if 'type' in data_item and data_item['type'] != CHANNEL_TORRENT: - return True - if data_item['status'] == LEGACY_ENTRY: - return True - if data_item['state'] == 'Personal': - return True - - self.subscribe_control.paint( - painter, option.rect, index, toggled=data_item.get('subscribed'), hover=index == self.hover_index - ) - - return True - - class TagsMixin: edit_tags_icon = QIcon(get_image_path("edit_white.png")) edit_tags_icon_hover = QIcon(get_image_path("edit_orange.png")) @@ -529,13 +461,6 @@ def draw_rating_control(self, painter, option, index, data_item): # Draw empty cell as the background self.paint_empty_background(painter, option) - if 'type' in data_item and data_item['type'] != CHANNEL_TORRENT: - return True - if data_item['status'] == LEGACY_ENTRY: - return True - - self.rating_control.paint(painter, option.rect, index, votes=data_item['votes']) - return True @@ -544,12 +469,7 @@ def draw_category_label(self, painter, option, index, data_item): # Draw empty cell as the background self.paint_empty_background(painter, option) - if 'type' in data_item and data_item['type'] == CHANNEL_TORRENT: - if data_item['state'] == 'Personal': - category_txt = "\U0001F3E0" # 'home' emoji - else: - category_txt = "🌐" - elif 'type' in data_item and data_item['type'] == COLLECTION_NODE: + if 'type' in data_item and data_item['type'] == COLLECTION_NODE: category_txt = "\U0001F4C1" # 'folder' emoji else: # Precautions to safely draw wrong category descriptions @@ -616,15 +536,11 @@ class TriblerContentDelegate( RatingControlMixin, DownloadControlsMixin, HealthLabelMixin, - ChannelStateMixin, - SubscribedControlMixin, TagsMixin, ): def __init__(self, table_view, parent=None): # TODO: refactor this not to rely on inheritance order, but instead use interface method pattern TriblerButtonsDelegate.__init__(self, parent) - self.subscribe_control = SubscribeToggleControl(Column.SUBSCRIBED) - self.rating_control = RatingControl(Column.VOTES) self.download_button = DownloadIconButton() self.ondemand_container = [self.download_button] @@ -632,21 +548,16 @@ def __init__(self, table_view, parent=None): self.commit_control = CommitStatusControl(Column.STATUS) self.health_status_widget = HealthStatusControl(Column.HEALTH) self.controls = [ - self.subscribe_control, self.download_button, - self.commit_control, - self.rating_control, self.health_status_widget, ] self.column_drawing_actions = [ - (Column.SUBSCRIBED, self.draw_subscribed_control), (Column.NAME, self.draw_title_and_tags), (Column.VOTES, self.draw_rating_control), (Column.ACTIONS, self.draw_action_column), (Column.CATEGORY, self.draw_category_label), (Column.HEALTH, self.draw_health_column), (Column.STATUS, self.draw_commit_status_column), - (Column.STATE, self.draw_channel_state), ] self.table_view = table_view @@ -704,95 +615,6 @@ def paint(self, painter, option, _, draw_border=True): painter.restore() -class SubscribeToggleControl(QObject, CheckClickedMixin): - clicked = pyqtSignal(QModelIndex) - - def __init__(self, column_name, parent=None): - QObject.__init__(self, parent=parent) - self.column_name = column_name - self.last_index = QModelIndex() - - self._track_radius = 10 - self._thumb_radius = 8 - self._line_thickness = self._track_radius - self._thumb_radius - self._margin = max(0, self._thumb_radius - self._track_radius) - self._base_offset = max(self._thumb_radius, self._track_radius) - - self._width = 4 * self._track_radius + 2 * self._margin - self._height = 2 * self._track_radius + 2 * self._margin - - self._end_offset = {True: lambda: self._width - self._base_offset, False: lambda: self._base_offset} - - self._offset = self._base_offset - - self._thumb_color = {True: TRIBLER_PALETTE.highlightedText(), False: TRIBLER_PALETTE.light()} - self._track_color = {True: TRIBLER_PALETTE.highlight(), False: TRIBLER_PALETTE.dark()} - self._text_color = {True: TRIBLER_PALETTE.highlight().color(), False: TRIBLER_PALETTE.dark().color()} - self._thumb_text = {True: '✔', False: '✕'} - self._track_opacity = 0.8 - - def paint(self, painter, rect, index, toggled=False, hover=False): - data_item = index.model().data_items[index.row()] - complete = data_item.get('state') == CHANNEL_STATE.COMPLETE.value - - painter.save() - - x = int(rect.x() + (rect.width() - self._width) // 2) - y = int(rect.y() + (rect.height() - self._height) // 2) - - offset = self._end_offset[toggled]() - p = painter - - p.setRenderHint(QPainter.Antialiasing, True) - track_opacity = 1.0 if hover else self._track_opacity - thumb_opacity = 1.0 - text_opacity = 1.0 - track_brush = self._track_color[toggled] - thumb_brush = self._thumb_color[toggled] - text_color = self._text_color[toggled] - - p.setBrush(track_brush) - p.setPen(QPen(track_brush.color(), 2)) - if not complete and toggled: - p.setBrush(Qt.NoBrush) - p.setOpacity(track_opacity) - p.drawRoundedRect( - x, - y, - self._width - 2 * self._margin, - self._height - 2 * self._margin, - self._track_radius, - self._track_radius, - ) - p.setPen(Qt.NoPen) - - p.setBrush(thumb_brush) - p.setOpacity(thumb_opacity) - p.drawEllipse( - x + offset - self._thumb_radius, - y + self._base_offset - self._thumb_radius, - 2 * self._thumb_radius, - 2 * self._thumb_radius, - ) - p.setPen(text_color) - p.setOpacity(text_opacity) - font = p.font() - font.setPixelSize(int(1.5 * self._thumb_radius)) - p.setFont(font) - p.drawText( - QRectF( - x + offset - self._thumb_radius, - y + self._base_offset - self._thumb_radius, - 2 * self._thumb_radius, - 2 * self._thumb_radius, - ), - Qt.AlignCenter, - self._thumb_text[toggled], - ) - - painter.restore() - - class CommitStatusControl(QObject, CheckClickedMixin): # Column-level controls are stateless collections of methods for visualizing cell data and # triggering corresponding events. @@ -921,33 +743,3 @@ def __init__(self, column_name, parent=None): QObject.__init__(self, parent=parent) self.column_name = column_name self.last_index = QModelIndex() - - -class RatingControl(QObject, CheckClickedMixin): - """ - Controls for visualizing the votes and subscription information for channels. - """ - - rating_colors = { - "BACKGROUND": QColor("#444444"), - "FOREGROUND": QColor("#BBBBBB"), - # "SUBSCRIBED_HOVER": QColor("#FF5722"), - } - - clicked = pyqtSignal(QModelIndex) - - def __init__(self, column_name, parent=None): - QObject.__init__(self, parent=parent) - self.column_name = column_name - self.last_index = QModelIndex() - self.font = None - # For some reason, on MacOS default inter-character spacing for some symbols - # is too wide. We have to adjust it manually. - if DARWIN or WINDOWS: - self.font = QFont() - self.font.setLetterSpacing(QFont.PercentageSpacing, 60.0) - - def paint(self, painter, rect, _index, votes=0): - lpad = " " # we pad it to move it closer to the center - draw_text(painter, rect, lpad + format_votes(1.0), color=self.rating_colors["BACKGROUND"], font=self.font) - draw_text(painter, rect, lpad + format_votes(votes), color=self.rating_colors["FOREGROUND"], font=self.font) diff --git a/src/tribler/gui/widgets/tablecontentmodel.py b/src/tribler/gui/widgets/tablecontentmodel.py index 3f1da08ca48..cd057d7f7a8 100644 --- a/src/tribler/gui/widgets/tablecontentmodel.py +++ b/src/tribler/gui/widgets/tablecontentmodel.py @@ -10,12 +10,11 @@ from PyQt5.QtCore import QAbstractTableModel, QModelIndex, QRectF, QSize, QTimerEvent, Qt, pyqtSignal -from tribler.core.components.metadata_store.db.serialization import CHANNEL_TORRENT, COLLECTION_NODE, REGULAR_TORRENT, \ - SNIPPET +from tribler.core.components.metadata_store.db.serialization import COLLECTION_NODE, REGULAR_TORRENT, SNIPPET from tribler.core.utilities.search_utils import item_rank -from tribler.core.utilities.simpledefs import CHANNELS_VIEW_UUID, CHANNEL_STATE +from tribler.core.utilities.simpledefs import CHANNEL_STATE from tribler.core.utilities.utilities import to_fts_query -from tribler.gui.defs import BITTORRENT_BIRTHDAY, COMMIT_STATUS_TODELETE, HEALTH_CHECKING +from tribler.gui.defs import BITTORRENT_BIRTHDAY, HEALTH_CHECKING from tribler.gui.network.request_manager import request_manager from tribler.gui.utilities import connect, format_size, format_votes, get_votes_rating_description, pretty_date, tr @@ -437,11 +436,9 @@ def __init__( self.edit_tags_rects: Dict[QModelIndex, QRectF] = {} self.download_popular_content_rects: Dict[QModelIndex, List[QRectF]] = {} - # Current channel attributes. This is intentionally NOT copied, so local changes - # can propagate to the origin, e.g. parent channel. - self.channel_info = channel_info or {"name": "My channels", "status": 123} + self.channel_info = channel_info - self.endpoint_url_override = endpoint_url + self.endpoint_url = endpoint_url # Load the initial batch of entries self.perform_initial_query() @@ -450,13 +447,6 @@ def __init__( def edit_enabled(self): return False - @property - def endpoint_url(self): - return self.endpoint_url_override or "channels/%s/%i" % ( - self.channel_info["public_key"], - self.channel_info["id"], - ) - def headerData(self, num, orientation, role=None): if orientation == Qt.Horizontal and role == Qt.DisplayRole: header_text = self.columns[num].header @@ -501,7 +491,7 @@ def item_txt(self, index, role, is_editing: bool = False): column_type == Column.SIZE and "torrents" not in self.columns and "torrents" in item - and item["type"] in (CHANNEL_TORRENT, COLLECTION_NODE, SNIPPET) + and item["type"] in (COLLECTION_NODE, SNIPPET) ): if item["type"] == SNIPPET: return "" @@ -645,12 +635,6 @@ def on_new_entry_received(self, response): self.on_query_results(response, remote=True) -class ChannelPreviewModel(ChannelContentModel): - def perform_query(self, **kwargs): - kwargs["remote"] = True - super().perform_query(**kwargs) - - class SearchResultsModel(ChannelContentModel): def __init__(self, original_query, **kwargs): self.original_query = original_query @@ -713,89 +697,3 @@ class PopularTorrentsModel(ChannelContentModel): def __init__(self, *args, **kwargs): kwargs["endpoint_url"] = 'metadata/torrents/popular' super().__init__(*args, **kwargs) - - -class DiscoveredChannelsModel(ChannelContentModel): - columns_shown = (Column.SUBSCRIBED, Column.NAME, Column.STATE, Column.TORRENTS, Column.VOTES, Column.CREATED) - - @property - def default_sort_column(self): - return self.columns_shown.index(Column.VOTES) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # Subscribe to new channels updates notified over the Events endpoint - self.remote_queries.add(CHANNELS_VIEW_UUID) - - -class PersonalChannelsModel(ChannelContentModel): - columns_shown = ( - Column.ACTIONS, - Column.CATEGORY, - Column.NAME, - Column.SIZE, - Column.HEALTH, - Column.CREATED, - Column.STATUS, - ) - - def __init__(self, *args, **kwargs): - kwargs["hide_xxx"] = kwargs.get("hide_xxx", False) - super().__init__(*args, **kwargs) - self.columns[self.column_position[Column.CATEGORY]].qt_flags |= Qt.ItemIsEditable - self.columns[self.column_position[Column.NAME]].qt_flags |= Qt.ItemIsEditable - - def delete_rows(self, rows): - patch_data = [] - delete_data = [] - for entry in [row.model().data_items[row.row()] for row in rows]: - if entry["status"] == NEW: - delete_data.append({"public_key": entry['public_key'], "id": entry['id']}) - else: - patch_data.append( - {"public_key": entry['public_key'], "id": entry['id'], "status": COMMIT_STATUS_TODELETE} - ) - - # We don't wait for the Core to report back and emit - # the info_changed signal speculativley to prevent the race condition between - # Python object deletion and PyQT one. Otherwise, if the users e.g. clicks the back - # button, by the moment the request callback triggers some actions on the model, - # QT could have already deleted the underlying model object, which will result in - # "wrapped C/C++ object has been deleted" error (see e.g. https://github.com/Tribler/tribler/issues/6083) - - if patch_data: - self.remove_items(patch_data) - request_manager.patch("metadata", data=patch_data) - - if delete_data: - self.remove_items(delete_data) - request_manager.delete("metadata", data=delete_data) - - def create_new_channel(self, channel_name=None): - public_key = self.channel_info.get("public_key", '') - channel_id = self.channel_info.get("id", 0) - - endpoint = self.endpoint_url_override or f"channels/{public_key}/{channel_id}" - postfix = "channels" if not channel_id else "collections" - request_manager.post(f'{endpoint}/{postfix}', self.on_create_query_results, - data=json.dumps({"name": channel_name}) if channel_name else None) - - def on_create_query_results(self, response, **kwargs): - # This is a hack to put the newly created object at the top of the table - kwargs["on_top"] = 1 - self.on_query_results(response, **kwargs) - if not response or self.qt_object_destroyed: - return False - self.info_changed.emit(response['results']) - - @property - def edit_enabled(self): - return self.channel_info.get("state", None) == "Personal" - - -class SimplifiedPersonalChannelsModel(PersonalChannelsModel): - columns_shown = (Column.ACTIONS, Column.CATEGORY, Column.NAME, Column.SIZE, Column.HEALTH, Column.CREATED) - - def __init__(self, *args, **kwargs): - kwargs["exclude_deleted"] = kwargs.get("exclude_deleted", True) - super().__init__(*args, **kwargs) diff --git a/src/tribler/gui/widgets/triblertablecontrollers.py b/src/tribler/gui/widgets/triblertablecontrollers.py index 7b17d0b2799..e130036835e 100644 --- a/src/tribler/gui/widgets/triblertablecontrollers.py +++ b/src/tribler/gui/widgets/triblertablecontrollers.py @@ -10,8 +10,7 @@ from PyQt5.QtNetwork import QNetworkRequest from PyQt5.QtWidgets import QAction -from tribler.core.components.metadata_store.db.serialization import CHANNEL_TORRENT, COLLECTION_NODE, REGULAR_TORRENT -from tribler.core.utilities.simpledefs import CHANNEL_STATE +from tribler.core.components.metadata_store.db.serialization import REGULAR_TORRENT from tribler.gui.defs import HEALTH_CHECKING, HEALTH_UNCHECKED from tribler.gui.network.request_manager import request_manager from tribler.gui.tribler_action_menu import TriblerActionMenu @@ -33,7 +32,6 @@ def __init__(self, table_view, *args, filter_input=None, **kwargs): self.table_view = table_view connect(self.table_view.verticalScrollBar().valueChanged, self._on_list_scroll) - connect(self.table_view.delegate.subscribe_control.clicked, self.table_view.on_subscribe_control_clicked) connect(self.table_view.delegate.download_button.clicked, self.table_view.start_download_from_index) connect(self.table_view.torrent_doubleclicked, self.table_view.start_download_from_dataitem) @@ -223,54 +221,6 @@ def _show_context_menu(self, pos): item_index.model().data_items[item_index.row()], lambda x: self.check_torrent_health(x, forced=True), ) - if num_selected == 1 and item_index.model().column_position.get(Column.SUBSCRIBED) is not None: - data_item = item_index.model().data_items[item_index.row()] - if data_item["type"] == CHANNEL_TORRENT and data_item["state"] != CHANNEL_STATE.PERSONAL.value: - self.add_menu_item( - menu, - tr("Unsubscribe channel") if data_item["subscribed"] else tr("Subscribe channel"), - item_index.model().index(item_index.row(), item_index.model().column_position[Column.SUBSCRIBED]), - self.table_view.delegate.subscribe_control.clicked.emit, - ) - - # Add menu separator for channel stuff - menu.addSeparator() - - entries = [self.model.data_items[index.row()] for index in self.table_view.selectionModel().selectedRows()] - - def on_add_to_channel(_): - def on_confirm_clicked(channel_id): - request_manager.post(f"channels/mychannel/{channel_id}/copy", - on_success=lambda _: self.table_view.window().tray_show_message( - tr("Channel update"), tr("Torrent(s) added to your channel") - ), - data=json.dumps(entries)) - - self.table_view.window().add_to_channel_dialog.show_dialog( - on_confirm_clicked, confirm_button_text=tr("Copy") - ) - - def on_move(_): - def on_confirm_clicked(channel_id): - changes_list = [ - {'public_key': entry['public_key'], 'id': entry['id'], 'origin_id': channel_id} for entry in entries - ] - self.model.remove_items(entries) - request_manager.patch("metadata", data=changes_list) - - self.table_view.window().add_to_channel_dialog.show_dialog( - on_confirm_clicked, confirm_button_text=tr("Move") - ) - - if not self.model.edit_enabled: - if self.selection_can_be_added_to_channel(): - self.add_menu_item(menu, tr(" Copy into personal channel"), item_index, on_add_to_channel) - else: - self.add_menu_item(menu, tr(" Move "), item_index, on_move) - self.add_menu_item(menu, tr(" Rename "), item_index, self._trigger_name_editor) - self.add_menu_item(menu, tr(" Change category "), item_index, self._trigger_category_editor) - menu.addSeparator() - self.add_menu_item(menu, tr(" Remove from channel"), item_index, self.table_view.on_delete_button_clicked) menu.exec_(QCursor.pos()) @@ -279,13 +229,6 @@ def add_menu_item(self, menu, name, item_index, callback): connect(action.triggered, lambda _: callback(item_index)) menu.addAction(action) - def selection_can_be_added_to_channel(self): - for row in self.table_view.selectionModel().selectedRows(): - data_item = row.model().data_items[row.row()] - if dict_item_is_any_of(data_item, 'type', [REGULAR_TORRENT, CHANNEL_TORRENT, COLLECTION_NODE]): - return True - return False - class PopularContentTableViewController( TableSelectionMixin, ContextMenuMixin, TableLoadingAnimationMixin, TriblerTableViewController