diff --git a/Tribler/Core/Libtorrent/LibtorrentMgr.py b/Tribler/Core/Libtorrent/LibtorrentMgr.py index adc7e9c657d..1008c45004c 100644 --- a/Tribler/Core/Libtorrent/LibtorrentMgr.py +++ b/Tribler/Core/Libtorrent/LibtorrentMgr.py @@ -445,7 +445,9 @@ def get_metainfo(self, infohash_or_magnet, callback, timeout=30, timeout_callbac infohash_bin = infohash_or_magnet if not magnet else parse_magnetlink(magnet)[1] infohash = hexlify(infohash_bin) - if infohash in self.torrents: + if infohash in self.torrents and hasattr(self.torrents[infohash], 'handle'): + metainfo = {"info": lt.bdecode(get_info_from_handle(self.torrents[infohash].handle).metadata())} + callback(metainfo) return with self.metainfo_lock: @@ -536,7 +538,7 @@ def got_metainfo(self, infohash, timeout=False): metainfo["nodes"] = [] if peers and notify: self.notifier.notify(NTFY_TORRENTS, NTFY_MAGNET_GOT_PEERS, infohash_bin, len(peers)) - metainfo["initial peers"] = peers + metainfo["leechers"] = leechers metainfo["seeders"] = seeders diff --git a/Tribler/Core/Modules/MetadataStore/OrmBindings/channel_metadata.py b/Tribler/Core/Modules/MetadataStore/OrmBindings/channel_metadata.py index 8790615d1c9..5b3740e0710 100644 --- a/Tribler/Core/Modules/MetadataStore/OrmBindings/channel_metadata.py +++ b/Tribler/Core/Modules/MetadataStore/OrmBindings/channel_metadata.py @@ -16,7 +16,7 @@ from Tribler.Core.Category.Category import default_category_filter from Tribler.Core.Modules.MetadataStore.OrmBindings.channel_node import COMMITTED, LEGACY_ENTRY, NEW, PUBLIC_KEY_LEN, \ TODELETE, UPDATED -from Tribler.Core.Modules.MetadataStore.serialization import CHANNEL_TORRENT, ChannelMetadataPayload +from Tribler.Core.Modules.MetadataStore.serialization import CHANNEL_TORRENT, ChannelMetadataPayload, REGULAR_TORRENT from Tribler.Core.TorrentDef import TorrentDef from Tribler.Core.Utilities.tracker_utils import get_uniformed_tracker_url from Tribler.Core.exceptions import DuplicateChannelIdError, DuplicateTorrentFileError @@ -253,6 +253,18 @@ def get_torrent(self, infohash): """ return db.TorrentMetadata.get(public_key=self.public_key, infohash=infohash) + @db_session + def torrent_exists(self, infohash): + """ + Return True if torrent with given infohash exists in the user channel + :param infohash: The infohash of the torrent + :return: True if torrent exists else False + """ + return db.TorrentMetadata.exists(lambda g: g.metadata_type == REGULAR_TORRENT + and g.status != LEGACY_ENTRY + and g.public_key == self.public_key + and g.infohash == database_blob(infohash)) + @db_session def add_torrent_to_channel(self, tdef, extra_info=None): """ @@ -297,6 +309,10 @@ def add_torrent_to_channel(self, tdef, extra_info=None): torrent_metadata = db.TorrentMetadata.from_dict(new_entry_dict) return torrent_metadata + @db_session + def copy_to_channel(self, infohash): + return db.TorrentMetadata.copy_to_channel(infohash) + @property def dirty(self): return self.contents.where(lambda g: g.status in [NEW, TODELETE, UPDATED]).exists() diff --git a/Tribler/Core/Modules/MetadataStore/OrmBindings/torrent_metadata.py b/Tribler/Core/Modules/MetadataStore/OrmBindings/torrent_metadata.py index 1c6b8f3e342..0fa6bf79a1e 100644 --- a/Tribler/Core/Modules/MetadataStore/OrmBindings/torrent_metadata.py +++ b/Tribler/Core/Modules/MetadataStore/OrmBindings/torrent_metadata.py @@ -1,15 +1,16 @@ from __future__ import absolute_import -from binascii import hexlify +from binascii import hexlify, unhexlify from datetime import datetime from pony import orm from pony.orm import db_session, desc, raw_sql, select from Tribler.Core.Category.FamilyFilter import default_xxx_filter -from Tribler.Core.Modules.MetadataStore.OrmBindings.channel_node import LEGACY_ENTRY, TODELETE, UPDATED +from Tribler.Core.Modules.MetadataStore.OrmBindings.channel_node import LEGACY_ENTRY, NEW, TODELETE, UPDATED from Tribler.Core.Modules.MetadataStore.serialization import REGULAR_TORRENT, TorrentMetadataPayload from Tribler.Core.Utilities.tracker_utils import get_uniformed_tracker_url +from Tribler.Core.Utilities.utilities import is_channel_public_key, is_hex_string, is_infohash from Tribler.pyipv8.ipv8.database import database_blob @@ -71,6 +72,15 @@ def search_keyword(cls, query, lim=100): fts_ids = raw_sql( 'SELECT rowid FROM FtsIndex WHERE FtsIndex MATCH $query ORDER BY bm25(FtsIndex) LIMIT $lim') + + # TODO: Check for complex query + normal_query = query.replace('"', '').replace("*", "") + if is_hex_string(normal_query) and len(normal_query) % 2 == 0: + query_blob = database_blob(unhexlify(normal_query)) + if is_channel_public_key(normal_query): + return cls.select(lambda g: g.public_key == query_blob or g.rowid in fts_ids) + if is_infohash(normal_query): + return cls.select(lambda g: g.infohash == query_blob or g.rowid in fts_ids) return cls.select(lambda g: g.rowid in fts_ids) @classmethod @@ -217,4 +227,30 @@ def update_properties(self, update_dict): self.timestamp = self._clock.tick() self.sign() + @classmethod + @db_session + def copy_to_channel(cls, infohash, public_key=None): + """ + Create a new signed copy of the given torrent metadata + :param metadata: Metadata to copy + :return: New TorrentMetadata signed with your key + """ + + existing = cls.get(public_key=public_key, infohash=infohash) if public_key \ + else cls.select(lambda g: g.infohash == database_blob(infohash)).first() + + if not existing: + return None + + new_entry_dict = { + "infohash": existing.infohash, + "title": existing.title, + "tags": existing.tags, + "size": existing.size, + "torrent_date": existing.torrent_date, + "tracker_info": existing.tracker_info, + "status": NEW + } + return db.TorrentMetadata.from_dict(new_entry_dict) + return TorrentMetadata diff --git a/Tribler/Core/Modules/restapi/mychannel_endpoint.py b/Tribler/Core/Modules/restapi/mychannel_endpoint.py index be885290469..010111b3b25 100644 --- a/Tribler/Core/Modules/restapi/mychannel_endpoint.py +++ b/Tribler/Core/Modules/restapi/mychannel_endpoint.py @@ -1,6 +1,8 @@ from __future__ import absolute_import import base64 +import codecs +import json import logging import os from binascii import hexlify, unhexlify @@ -18,7 +20,7 @@ import Tribler.Core.Utilities.json_util as json from Tribler.Core.Modules.restapi.metadata_endpoint import SpecificChannelTorrentsEndpoint from Tribler.Core.TorrentDef import TorrentDef -from Tribler.Core.Utilities.utilities import http_get +from Tribler.Core.Utilities.utilities import http_get, is_infohash, parse_magnetlink from Tribler.Core.exceptions import DuplicateTorrentFileError from Tribler.pyipv8.ipv8.database import database_blob @@ -265,6 +267,10 @@ def _on_timeout(_): deferred = http_get(uri) deferred.addCallback(_on_url_fetched) elif uri.startswith("magnet:"): + _, xt, _ = parse_magnetlink(uri) + if xt and is_infohash(codecs.encode(xt, 'hex')) \ + and (my_channel.torrent_exists(xt) or my_channel.copy_to_channel(xt)): + return json.dumps({"added": 1}) try: self.session.lm.ltmgr.get_metainfo(uri, callback=deferred.callback, timeout=30, timeout_callback=_on_timeout, notify=True) diff --git a/Tribler/Core/Utilities/utilities.py b/Tribler/Core/Utilities/utilities.py index 4bbcc1fa4bc..212a5816a01 100644 --- a/Tribler/Core/Utilities/utilities.py +++ b/Tribler/Core/Utilities/utilities.py @@ -225,3 +225,19 @@ def has_bep33_support(): Also see https://github.com/devos50/libtorrent/tree/bep33_support """ return 'dht_pkt_alert' in dir(libtorrent) + + +def is_infohash(infohash): + return infohash and len(infohash) == 40 and is_hex_string(infohash) + + +def is_channel_public_key(key): + return key and len(key) == 128 and is_hex_string(key) + + +def is_hex_string(text): + try: + int(text, 16) + return True + except ValueError: + return False diff --git a/Tribler/Test/Core/Libtorrent/test_libtorrent_mgr.py b/Tribler/Test/Core/Libtorrent/test_libtorrent_mgr.py index 4fa878263e2..cd60f0d4c44 100644 --- a/Tribler/Test/Core/Libtorrent/test_libtorrent_mgr.py +++ b/Tribler/Test/Core/Libtorrent/test_libtorrent_mgr.py @@ -100,7 +100,7 @@ def test_get_metainfo(self): def metainfo_cb(metainfo): self.assertEqual(metainfo, {'info': {'pieces': ['a']}, 'leechers': 0, - 'nodes': [], 'seeders': 0, 'initial peers': []}) + 'nodes': [], 'seeders': 0}) test_deferred.callback(None) infohash = "a" * 20 @@ -152,7 +152,7 @@ def test_got_metainfo(self): def metainfo_cb(metainfo): self.assertDictEqual(metainfo, {'info': {'pieces': ['a']}, 'leechers': 0, - 'nodes': [], 'seeders': 0, 'initial peers': []}) + 'nodes': [], 'seeders': 0}) test_deferred.callback(None) fake_handle = MockObject() diff --git a/Tribler/Test/Core/Modules/MetadataStore/test_channel_metadata.py b/Tribler/Test/Core/Modules/MetadataStore/test_channel_metadata.py index 31310be9737..da8e2569808 100644 --- a/Tribler/Test/Core/Modules/MetadataStore/test_channel_metadata.py +++ b/Tribler/Test/Core/Modules/MetadataStore/test_channel_metadata.py @@ -201,6 +201,40 @@ def test_add_torrent_to_channel(self): self.assertTrue(channel_metadata.contents_list) self.assertRaises(DuplicateTorrentFileError, channel_metadata.add_torrent_to_channel, tdef, None) + @db_session + def test_torrent_exists_in_channel(self): + """ + Test torrent already exists in the channel. + """ + channel_metadata = self.mds.ChannelMetadata.create_channel('test', 'test') + self.mds.TorrentMetadata.from_dict(dict(self.torrent_template, infohash="1")) + self.assertTrue(channel_metadata.torrent_exists("1")) + self.assertFalse(channel_metadata.torrent_exists("0")) + + @db_session + def test_copy_to_channel(self): + """ + Test copying a torrent from an another channel. + """ + self.mds.ChannelNode._my_key = default_eccrypto.generate_key('low') + channel1 = self.mds.ChannelMetadata(infohash=str(random.getrandbits(160))) + self.mds.TorrentMetadata.from_dict(dict(self.torrent_template, infohash="1")) + + self.mds.ChannelNode._my_key = default_eccrypto.generate_key('low') + channel2 = self.mds.ChannelMetadata(infohash=str(random.getrandbits(160))) + + # Trying copying existing torrent to channel + new_torrent = channel2.copy_to_channel("1") + self.assertIsNotNone(new_torrent) + self.assertEqual(1, len(channel1.contents_list)) + self.assertEqual(1, len(channel2.contents_list)) + + # Try copying non-existing torrent ot channel + new_torrent2 = channel2.copy_to_channel("2") + self.assertIsNone(new_torrent2) + self.assertEqual(1, len(channel1.contents_list)) + self.assertEqual(1, len(channel2.contents_list)) + @db_session def test_restore_torrent_in_channel(self): """ diff --git a/Tribler/Test/Core/Modules/MetadataStore/test_torrent_metadata.py b/Tribler/Test/Core/Modules/MetadataStore/test_torrent_metadata.py index 55095941503..f603b0c0554 100644 --- a/Tribler/Test/Core/Modules/MetadataStore/test_torrent_metadata.py +++ b/Tribler/Test/Core/Modules/MetadataStore/test_torrent_metadata.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import +import codecs import random from datetime import datetime @@ -135,6 +136,34 @@ def test_stemming_search(self): results = self.mds.TorrentMetadata.search_keyword("sheeps")[:] self.assertEqual(torrent.rowid, results[0].rowid) + @db_session + def test_infohash_search(self): + """ + Test searching for torrents with infohash works. + """ + infohash = b"e84213a794f3ccd890382a54a64ca68b7e925433" + torrent = self.mds.TorrentMetadata.from_dict(dict(rnd_torrent(), infohash=codecs.decode(infohash, 'hex'), + title="mountains sheep", tags="video")) + + # Search with the hex encoded infohash + query = '"%s"*' % infohash + results = self.mds.TorrentMetadata.search_keyword(query)[:] + self.assertEqual(torrent.rowid, results[0].rowid) + + @db_session + def test_channel_public_key_search(self): + """ + Test searching for channels with public key works. + """ + self.mds.ChannelNode._my_key = default_eccrypto.generate_key('curve25519') + channel = self.mds.ChannelMetadata(title='My channel', infohash=str(random.getrandbits(160))) + + # Search with the hex encoded channel public key + query = '"%s"*' % codecs.encode(channel.public_key, 'hex') + results = self.mds.TorrentMetadata.search_keyword(query)[:] + self.assertIsNotNone(results) + self.assertTrue(channel.rowid, results[0].rowid) + @db_session def test_get_autocomplete_terms(self): """ diff --git a/Tribler/Test/Core/Utilities/test_utilities.py b/Tribler/Test/Core/Utilities/test_utilities.py index 76f430a9059..a66f0f918f8 100644 --- a/Tribler/Test/Core/Utilities/test_utilities.py +++ b/Tribler/Test/Core/Utilities/test_utilities.py @@ -6,7 +6,8 @@ from twisted.web.util import Redirect from Tribler.Core.Utilities.network_utils import get_random_port -from Tribler.Core.Utilities.utilities import http_get, is_simple_match_query, is_valid_url, parse_magnetlink +from Tribler.Core.Utilities.utilities import http_get, is_channel_public_key, is_infohash, is_simple_match_query, \ + is_valid_url, parse_magnetlink from Tribler.Test.test_as_server import AbstractServer from Tribler.Test.tools import trial_timeout @@ -82,3 +83,24 @@ def test_simple_search_query(self): query2 = '"\xc1ubuntu"* OR "debian"*' self.assertFalse(is_simple_match_query(query2)) + + def test_is_infohash(self): + hex_40 = "DC4B96CF85A85CEEDB8ADC4B96CF85A85CEEDB8A" + self.assertTrue(is_infohash(hex_40)) + + hex_not_40 = "DC4B96CF85A85CEEDB8ADC4B96CF85" + self.assertFalse(is_infohash(hex_not_40)) + + not_hex = "APPLE6CF85A85CEEDB8ADC4B96CF85A85CEEDB8A" + self.assertFalse(is_infohash(not_hex)) + + def test_is_channel_public_key(self): + hex_128 = "224b20c30b90d0fc7b2cf844f3d651de4481e21c7cdbbff258fa737d117d2c4ac7536de5cc93f4e9d5" \ + "1012a1ae0c46e9a05505bd017f0ecb78d8eec4506e848a" + self.assertTrue(is_channel_public_key(hex_128)) + + hex_not_128 = "DC4B96CF85A85CEEDB8ADC4B96CF85" + self.assertFalse(is_channel_public_key(hex_not_128)) + + not_hex = "APPLE6CF85A85CEEDB8ADC4B96CF85A85CEEDB8A" + self.assertFalse(is_channel_public_key(not_hex)) diff --git a/TriblerGUI/qt_resources/mainwindow.ui b/TriblerGUI/qt_resources/mainwindow.ui index 3d819e6f773..9b0911e6788 100644 --- a/TriblerGUI/qt_resources/mainwindow.ui +++ b/TriblerGUI/qt_resources/mainwindow.ui @@ -3736,19 +3736,25 @@ font-weight: bold; - 8 + 12 20 - + + + + color: #eee; font-size: 15px; - Preview + Preview ⟳ + + + 8 @@ -4421,7 +4427,7 @@ border-top: 1px solid #555; 0 0 - 755 + 300 646 @@ -4505,7 +4511,7 @@ color: white; - Family filter enabled? + Family filter enabled? (requires Tribler restart) @@ -7897,15 +7903,15 @@ margin-right: 10px; PointingHandCursor + + Click to copy magnet link + border: none; - - Click to copy magnet link - ../images/magnet.png../images/magnet.png diff --git a/TriblerGUI/qt_resources/torrent_details_container.ui b/TriblerGUI/qt_resources/torrent_details_container.ui index b18b58c362c..9b44fc71af5 100644 --- a/TriblerGUI/qt_resources/torrent_details_container.ui +++ b/TriblerGUI/qt_resources/torrent_details_container.ui @@ -270,6 +270,9 @@ QTabBar::tab:selected { 00000000000000000000 + + Qt::TextSelectableByMouse + diff --git a/TriblerGUI/widgets/channelpage.py b/TriblerGUI/widgets/channelpage.py index be591112235..c75017237de 100644 --- a/TriblerGUI/widgets/channelpage.py +++ b/TriblerGUI/widgets/channelpage.py @@ -34,6 +34,13 @@ def initialize_channel_page(self, gui_settings): commit_control = self.window().channel_page_container.content_table.delegate.commit_control self.window().channel_page_container.content_table.delegate.controls.remove(commit_control) + # To reload the preview + self.window().channel_preview_label.clicked.connect(self.preview_clicked) + + def preview_clicked(self): + self.controller.fetch_preview() + self.initialize_with_channel(self.channel_info) + def initialize_with_channel(self, channel_info): self.channel_info = channel_info diff --git a/TriblerGUI/widgets/lazytableview.py b/TriblerGUI/widgets/lazytableview.py index 09cd8f286ab..ed071be93f5 100644 --- a/TriblerGUI/widgets/lazytableview.py +++ b/TriblerGUI/widgets/lazytableview.py @@ -94,6 +94,10 @@ def on_subscribe_control_clicked(self, index): data={"subscribe": int(not status)}, method='POST') index.model().data_items[index.row()][u'subscribed'] = int(not status) + # Update votes + votes = index.model().data_items[index.row()][u'votes'] + index.model().data_items[index.row()][u'votes'] = votes + 1 if not status else votes - 1 + class ItemClickedMixin(TriblerContentTableView): def on_table_item_clicked(self, item): @@ -148,8 +152,22 @@ def on_delete_button_clicked(self, index): data={"status" : COMMIT_STATUS_TODELETE}, method='PATCH') +class AddToChannelButtonMixin(CommitControlMixin): + + def on_add_to_channel_button_clicked(self, _): + for row in self.selectionModel().selectedRows(): + post_data = {"uri": index2uri(row)} + request_mgr = TriblerRequestManager() + request_mgr.perform_request("mychannel/torrents", self.on_torrent_added, + method='PUT', data=post_data) + + def on_torrent_added(self, _): + self.window().edit_channel_page.load_my_torrents() + self.window().tray_show_message("Channel update", "Torrent is added to your channel") + + class SearchResultsTableView(ItemClickedMixin, DownloadButtonMixin, PlayButtonMixin, SubscribeButtonMixin, - TriblerContentTableView): + AddToChannelButtonMixin, TriblerContentTableView): on_subscribed_channel = pyqtSignal(QModelIndex) on_unsubscribed_channel = pyqtSignal(QModelIndex) @@ -178,7 +196,7 @@ def resizeEvent(self, _): class TorrentsTableView(ItemClickedMixin, DeleteButtonMixin, DownloadButtonMixin, PlayButtonMixin, - TriblerContentTableView): + AddToChannelButtonMixin, TriblerContentTableView): """ This table displays various torrents. """ diff --git a/TriblerGUI/widgets/tablecontentdelegate.py b/TriblerGUI/widgets/tablecontentdelegate.py index 89ccbe95cf6..986dcb0c4d4 100644 --- a/TriblerGUI/widgets/tablecontentdelegate.py +++ b/TriblerGUI/widgets/tablecontentdelegate.py @@ -193,7 +193,7 @@ def paint_exact(self, painter, option, index): data_item = index.model().data_items[index.row()] if index == self.hover_index: - self.subscribe_control.paint_hover(painter, option.rect, index) + self.subscribe_control.paint_hover(painter, option.rect, index, toggled=data_item['subscribed']) else: self.subscribe_control.paint(painter, option.rect, index, toggled=data_item['subscribed']) @@ -330,8 +330,8 @@ def paint(self, painter, rect, _, toggled=False): icon.paint(painter, icon_rect) - def paint_hover(self, painter, rect, _): - icon = self.hover_icon + def paint_hover(self, painter, rect, _index, toggled=False): + icon = self.on_icon if toggled else self.hover_icon x = rect.left() + (rect.width() - self.w) / 2 y = rect.top() + (rect.height() - self.h) / 2 icon_rect = QRect(x, y, self.w, self.h) diff --git a/TriblerGUI/widgets/triblertablecontrollers.py b/TriblerGUI/widgets/triblertablecontrollers.py index 9d53b4d3506..7290d46acdf 100644 --- a/TriblerGUI/widgets/triblertablecontrollers.py +++ b/TriblerGUI/widgets/triblertablecontrollers.py @@ -6,8 +6,13 @@ import uuid +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QCursor +from PyQt5.QtWidgets import QAction + from six import text_type +from TriblerGUI.tribler_action_menu import TriblerActionMenu from TriblerGUI.tribler_request_manager import TriblerRequestManager @@ -74,7 +79,7 @@ def perform_query(self, **kwargs): sort_by, sort_asc = self._get_sort_parameters() kwargs.update({ "uuid": self.query_uuid, - "filter": to_fts_query(self.query_text), + "filter": to_fts_query(kwargs.pop('query_filter') if 'query_filter' in kwargs else self.query_text), "sort_by": sort_by, "sort_asc": sort_asc, "hide_xxx": self.model.hide_xxx}) @@ -110,7 +115,7 @@ def is_new_result(self, response): :param response: List of items :return: True for fresh response else False """ - if 'uuid' in response and response['uuid'] != self.query_uuid: + if self.query_uuid and 'uuid' in response and response['uuid'] != self.query_uuid: return False if 'first' in response and response['first'] < self.model.rowCount(): return False @@ -155,7 +160,45 @@ def _on_selection_changed(self, _): window.resize(window.geometry().width() - 1, window.geometry().height()) -class SearchResultsTableViewController(TableSelectionMixin, TriblerTableViewController): +class ContextMenuMixin(object): + + table_view = None + + def enable_context_menu(self, widget): + self.table_view = widget + self.table_view.setContextMenuPolicy(Qt.CustomContextMenu) + self.table_view.customContextMenuRequested.connect(self._show_context_menu) + + def _show_context_menu(self, pos): + if not self.table_view: + return + + item_index = self.table_view.indexAt(pos) + if not item_index or item_index.row() < 0: + return + + menu = TriblerActionMenu(self.table_view) + + # Single selection menu items + num_selected = len(self.table_view.selectionModel().selectedRows()) + if num_selected == 1: + self.add_menu_item(menu, ' Download ', item_index, self.table_view.on_download_button_clicked) + self.add_menu_item(menu, ' Play ', item_index, self.table_view.on_play_button_clicked) + + if not isinstance(self, MyTorrentsTableViewController): + self.add_menu_item(menu, ' Add to channel ', item_index, self.table_view.on_add_to_channel_button_clicked) + else: + self.add_menu_item(menu, ' Remove from channel ', item_index, self.table_view.on_delete_button_clicked) + + menu.exec_(QCursor.pos()) + + def add_menu_item(self, menu, name, item_index, callback): + action = QAction(name, self.table_view) + action.triggered.connect(lambda _: callback(item_index)) + menu.addAction(action) + + +class SearchResultsTableViewController(TableSelectionMixin, ContextMenuMixin, TriblerTableViewController): """ Controller for the table view that handles search results. """ @@ -165,6 +208,7 @@ def __init__(self, model, table_view, details_container, num_results_label=None) self.num_results_label = num_results_label self.details_container = details_container table_view.selectionModel().selectionChanged.connect(self._on_selection_changed) + self.enable_context_menu(self.table_view) def perform_query(self, **kwargs): """ @@ -199,7 +243,7 @@ def perform_query(self, **kwargs): super(ChannelsTableViewController, self).perform_query(**kwargs) -class TorrentsTableViewController(TableSelectionMixin, FilterInputMixin, TriblerTableViewController): +class TorrentsTableViewController(TableSelectionMixin, FilterInputMixin, ContextMenuMixin, TriblerTableViewController): """ This class manages a list with torrents. """ @@ -212,6 +256,7 @@ def __init__(self, model, table_view, details_container, num_results_label=None, table_view.selectionModel().selectionChanged.connect(self._on_selection_changed) if self.filter_input: self.filter_input.textChanged.connect(self._on_filter_input_change) + self.enable_context_menu(self.table_view) def perform_query(self, **kwargs): if "rest_endpoint_url" not in kwargs: @@ -219,6 +264,15 @@ def perform_query(self, **kwargs): "rest_endpoint_url": "metadata/channels/%s/torrents" % self.model.channel_pk}) super(TorrentsTableViewController, self).perform_query(**kwargs) + def fetch_preview(self): + params = {'query_filter': self.model.channel_pk, + 'metadata_type': 'torrent', + 'rest_endpoint_url': 'search', + 'first': 1, + 'last': 50 + } + super(TorrentsTableViewController, self).perform_query(**params) + class MyTorrentsTableViewController(TorrentsTableViewController): """