diff --git a/src/tribler/core/components/knowledge/db/knowledge_db.py b/src/tribler/core/components/knowledge/db/knowledge_db.py
index 65fd233e4f7..9067d55828d 100644
--- a/src/tribler/core/components/knowledge/db/knowledge_db.py
+++ b/src/tribler/core/components/knowledge/db/knowledge_db.py
@@ -2,7 +2,7 @@
import logging
from dataclasses import dataclass
from enum import IntEnum
-from typing import Callable, Iterable, List, Optional, Set
+from typing import Callable, Iterable, List, Optional, Set, Dict
from pony import orm
from pony.orm.core import Entity
diff --git a/src/tribler/core/components/metadata_store/remote_query_community/tests/test_remote_search_by_tags.py b/src/tribler/core/components/metadata_store/remote_query_community/tests/test_remote_search_by_tags.py
index 0bf477b3c08..b12e258eacd 100644
--- a/src/tribler/core/components/metadata_store/remote_query_community/tests/test_remote_search_by_tags.py
+++ b/src/tribler/core/components/metadata_store/remote_query_community/tests/test_remote_search_by_tags.py
@@ -58,7 +58,7 @@ def rqc(self) -> RemoteQueryCommunity:
@patch.object(RemoteQueryCommunity, 'knowledge_db', new=PropertyMock(return_value=None), create=True)
async def test_search_for_tags_no_db(self):
- # test that in case of missed `tags_db`, function `search_for_tags` returns None
+ # test that in case of missed `knowledge_db`, function `search_for_tags` returns None
assert self.rqc.search_for_tags(tags=['tag']) is None
@patch.object(KnowledgeDatabase, 'get_subjects_intersection')
diff --git a/src/tribler/core/components/metadata_store/restapi/channels_endpoint.py b/src/tribler/core/components/metadata_store/restapi/channels_endpoint.py
index 3f9fbd2c094..48463c1af42 100644
--- a/src/tribler/core/components/metadata_store/restapi/channels_endpoint.py
+++ b/src/tribler/core/components/metadata_store/restapi/channels_endpoint.py
@@ -196,7 +196,7 @@ async def get_channel_contents(self, request):
contents_list.append(entry.to_simple_dict())
total = self.mds.get_total_count(**sanitized) if include_total else None
self.add_download_progress_to_metadata_list(contents_list)
- self.add_tags_to_metadata_list(contents_list, hide_xxx=sanitized["hide_xxx"])
+ self.add_statements_to_metadata_list(contents_list, hide_xxx=sanitized["hide_xxx"])
response_dict = {
"results": contents_list,
"first": sanitized['first'],
@@ -504,7 +504,7 @@ async def get_popular_torrents_channel(self, request):
contents_list.append(entry.to_simple_dict())
self.add_download_progress_to_metadata_list(contents_list)
- self.add_tags_to_metadata_list(contents_list, hide_xxx=sanitized["hide_xxx"])
+ self.add_statements_to_metadata_list(contents_list, hide_xxx=sanitized["hide_xxx"])
response_dict = {
"results": contents_list,
"first": sanitized['first'],
diff --git a/src/tribler/core/components/metadata_store/restapi/metadata_endpoint_base.py b/src/tribler/core/components/metadata_store/restapi/metadata_endpoint_base.py
index ff1b04c2a16..9d4c14bd031 100644
--- a/src/tribler/core/components/metadata_store/restapi/metadata_endpoint_base.py
+++ b/src/tribler/core/components/metadata_store/restapi/metadata_endpoint_base.py
@@ -1,3 +1,4 @@
+from dataclasses import asdict
from typing import Optional
from pony.orm import db_session
@@ -38,11 +39,11 @@
class MetadataEndpointBase(RESTEndpoint):
- def __init__(self, metadata_store: MetadataStore, *args, tags_db: KnowledgeDatabase = None,
+ def __init__(self, metadata_store: MetadataStore, *args, knowledge_db: KnowledgeDatabase = None,
tag_rules_processor: KnowledgeRulesProcessor = None, **kwargs):
super().__init__(*args, **kwargs)
self.mds = metadata_store
- self.tags_db: Optional[KnowledgeDatabase] = tags_db
+ self.knowledge_db: Optional[KnowledgeDatabase] = knowledge_db
self.tag_rules_processor: Optional[KnowledgeRulesProcessor] = tag_rules_processor
@classmethod
@@ -84,13 +85,15 @@ def extract_tags(self, entry):
self._logger.info(f'Generated {generated} tags for {hexlify(entry.infohash)}')
@db_session
- def add_tags_to_metadata_list(self, contents_list, hide_xxx=False):
- if self.tags_db is None:
- self._logger.error(f'Cannot add tags to metadata list: tags_db is not set in {self.__class__.__name__}')
+ def add_statements_to_metadata_list(self, contents_list, hide_xxx=False):
+ if self.knowledge_db is None:
+ self._logger.error(f'Cannot add statements to metadata list: '
+ f'knowledge_db is not set in {self.__class__.__name__}')
return
for torrent in contents_list:
if torrent['type'] == REGULAR_TORRENT:
- tags = self.tags_db.get_objects(torrent["infohash"], predicate=ResourceType.TAG)
+ statements = [asdict(stmt) for stmt in self.knowledge_db.get_statements(torrent["infohash"])]
if hide_xxx:
- tags = [tag.lower() for tag in tags if not default_xxx_filter.isXXX(tag, isFilename=False)]
- torrent["tags"] = tags
+ statements = [stmt for stmt in statements if not default_xxx_filter.isXXX(stmt["object"],
+ isFilename=False)]
+ torrent["statements"] = statements
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 8edba7670a9..e0710d53db0 100644
--- a/src/tribler/core/components/metadata_store/restapi/search_endpoint.py
+++ b/src/tribler/core/components/metadata_store/restapi/search_endpoint.py
@@ -45,8 +45,8 @@ def build_snippets(self, search_results: List[Dict]) -> List[Dict]:
content_to_torrents: Dict[str, list] = defaultdict(list)
for search_result in search_results:
with db_session:
- content_items: List[str] = self.tags_db.get_objects(search_result["infohash"],
- predicate=ResourceType.TITLE)
+ content_items: List[str] = self.knowledge_db.get_objects(search_result["infohash"],
+ predicate=ResourceType.TITLE)
if content_items:
for content_id in content_items:
content_to_torrents[content_id].append(search_result)
@@ -131,8 +131,8 @@ def search_db():
try:
with db_session:
if tags:
- infohash_set = self.tags_db.get_subjects_intersection(set(tags), predicate=ResourceType.TAG,
- case_sensitive=False)
+ infohash_set = self.knowledge_db.get_subjects_intersection(set(tags), predicate=ResourceType.TAG,
+ case_sensitive=False)
if infohash_set:
sanitized['infohash_set'] = {bytes.fromhex(s) for s in infohash_set}
@@ -141,7 +141,7 @@ def search_db():
self._logger.exception("Error while performing DB search: %s: %s", type(e).__name__, e)
return RESTResponse(status=HTTP_BAD_REQUEST)
- self.add_tags_to_metadata_list(search_results, hide_xxx=sanitized["hide_xxx"])
+ self.add_statements_to_metadata_list(search_results, hide_xxx=sanitized["hide_xxx"])
if sanitized["first"] == 1: # Only show a snippet on top
search_results = self.build_snippets(search_results)
diff --git a/src/tribler/core/components/metadata_store/restapi/tests/test_channels_endpoint.py b/src/tribler/core/components/metadata_store/restapi/tests/test_channels_endpoint.py
index f4b2afa1d6d..8e44849e1b1 100644
--- a/src/tribler/core/components/metadata_store/restapi/tests/test_channels_endpoint.py
+++ b/src/tribler/core/components/metadata_store/restapi/tests/test_channels_endpoint.py
@@ -13,6 +13,7 @@
import pytest
from tribler.core.components.gigachannel.community.gigachannel_community import NoChannelSourcesException
+from tribler.core.components.knowledge.db.knowledge_db import ResourceType
from tribler.core.components.libtorrent.torrentdef import TorrentDef
from tribler.core.components.metadata_store.category_filter.family_filter import default_xxx_filter
from tribler.core.components.metadata_store.db.orm_bindings.channel_node import NEW
@@ -49,7 +50,7 @@ def return_exc(*args, **kwargs):
mock_gigachannel_community.remote_select_channel_contents = return_exc
ep_args = [mock_dlmgr, mock_gigachannel_manager, mock_gigachannel_community, metadata_store]
- ep_kwargs = {'tags_db': knowledge_db}
+ ep_kwargs = {'knowledge_db': knowledge_db}
collections_endpoint = ChannelsEndpoint(*ep_args, **ep_kwargs)
channels_endpoint = ChannelsEndpoint(*ep_args, **ep_kwargs)
@@ -713,7 +714,7 @@ async def test_get_my_channel_tags(metadata_store, mock_dlmgr_get_download, my_c
assert len(json_dict['results']) == 9
for item in json_dict['results']:
- assert len(item["tags"]) >= 2
+ assert len(item["statements"]) >= 2
async def test_get_my_channel_tags_xxx(metadata_store, knowledge_db, mock_dlmgr_get_download, my_channel,
@@ -739,4 +740,6 @@ async def test_get_my_channel_tags_xxx(metadata_store, knowledge_db, mock_dlmgr_
)
assert len(json_dict['results']) == 1
- assert len(json_dict['results'][0]["tags"]) == 1
+ print(json_dict)
+ tag_statements = [s for s in json_dict["results"][0]["statements"] if s["predicate"] == ResourceType.TAG]
+ assert len(tag_statements) == 1
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 28d1cd56a6a..85394f4c33d 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
@@ -252,7 +252,7 @@ def test_extract_tags():
# see: https://github.com/Tribler/tribler/issues/6986
mds_endpoint = MetadataEndpointBase(
MagicMock(),
- tags_db=MagicMock(),
+ knowledge_db=MagicMock(),
tag_rules_processor=MagicMock(
version=1
)
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 08375079a9c..f0cc69f5e3a 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
@@ -34,7 +34,7 @@ def needle_in_haystack_mds(metadata_store):
@pytest.fixture
def rest_api(loop, needle_in_haystack_mds, aiohttp_client, knowledge_db):
- channels_endpoint = SearchEndpoint(needle_in_haystack_mds, tags_db=knowledge_db)
+ channels_endpoint = SearchEndpoint(needle_in_haystack_mds, knowledge_db=knowledge_db)
app = Application()
app.add_subapp('/search', channels_endpoint.app)
return loop.run_until_complete(aiohttp_client(app))
diff --git a/src/tribler/core/components/metadata_store/utils.py b/src/tribler/core/components/metadata_store/utils.py
index 31191d3f315..74483b34993 100644
--- a/src/tribler/core/components/metadata_store/utils.py
+++ b/src/tribler/core/components/metadata_store/utils.py
@@ -57,13 +57,13 @@ def tag_torrent(infohash, tags_db, tags=None, suggested_tags=None):
if tag not in suggested_tags:
suggested_tags.append(tag)
- def _add_operation(_tag, _op, _key):
- operation = StatementOperation(subject_type=ResourceType.TORRENT, subject=infohash, predicate=ResourceType.TAG,
- object=_tag, operation=_op, clock=0, creator_public_key=_key.pub().key_to_bin())
+ def _add_operation(_obj, _op, _key, _predicate=ResourceType.TAG):
+ operation = StatementOperation(subject_type=ResourceType.TORRENT, subject=infohash, predicate=_predicate,
+ object=_obj, operation=_op, clock=0, creator_public_key=_key.pub().key_to_bin())
operation.clock = tags_db.get_clock(operation) + 1
tags_db.add_operation(operation, b"")
- # Give each torrent some tags
+ # Give the torrent some tags
for tag in tags:
for key in [random_key_1, random_key_2]: # Each tag should be proposed by two unique users
_add_operation(tag, Operation.ADD, key)
@@ -73,6 +73,17 @@ def _add_operation(_tag, _op, _key):
_add_operation(tag, Operation.ADD, random_key_3)
_add_operation(tag, Operation.REMOVE, random_key_2)
+ # Give the torrent some simple attributes
+ random_title = generate_title(2)
+ random_year = f"{random.randint(1990, 2040)}"
+ random_description = generate_title(5)
+ random_lang = random.choice(["english", "russian", "dutch", "klingon", "valyerian"])
+ for key in [random_key_1, random_key_2]: # Each statement should be proposed by two unique users
+ _add_operation(random_title, Operation.ADD, key, _predicate=ResourceType.TITLE)
+ _add_operation(random_year, Operation.ADD, key, _predicate=ResourceType.DATE)
+ _add_operation(random_description, Operation.ADD, key, _predicate=ResourceType.DESCRIPTION)
+ _add_operation(random_lang, Operation.ADD, key, _predicate=ResourceType.LANGUAGE)
+
@db_session
def generate_torrent(metadata_store, tags_db, parent):
diff --git a/src/tribler/core/components/restapi/restapi_component.py b/src/tribler/core/components/restapi/restapi_component.py
index e4e6300c9e6..fab9b55ac44 100644
--- a/src/tribler/core/components/restapi/restapi_component.py
+++ b/src/tribler/core/components/restapi/restapi_component.py
@@ -110,18 +110,18 @@ async def run(self):
self.maybe_add('/libtorrent', LibTorrentEndpoint, libtorrent_component.download_manager)
self.maybe_add('/torrentinfo', TorrentInfoEndpoint, libtorrent_component.download_manager)
self.maybe_add('/metadata', MetadataEndpoint, torrent_checker, metadata_store_component.mds,
- tags_db=knowledge_component.knowledge_db,
+ knowledge_db=knowledge_component.knowledge_db,
tag_rules_processor=knowledge_component.rules_processor)
self.maybe_add('/channels', ChannelsEndpoint, libtorrent_component.download_manager, gigachannel_manager,
gigachannel_component.community, metadata_store_component.mds,
- tags_db=knowledge_component.knowledge_db,
+ knowledge_db=knowledge_component.knowledge_db,
tag_rules_processor=knowledge_component.rules_processor)
self.maybe_add('/collections', ChannelsEndpoint, libtorrent_component.download_manager, gigachannel_manager,
gigachannel_component.community, metadata_store_component.mds,
- tags_db=knowledge_component.knowledge_db,
+ knowledge_db=knowledge_component.knowledge_db,
tag_rules_processor=knowledge_component.rules_processor)
self.maybe_add('/search', SearchEndpoint, metadata_store_component.mds,
- tags_db=knowledge_component.knowledge_db)
+ knowledge_db=knowledge_component.knowledge_db)
self.maybe_add('/remote_query', RemoteQueryEndpoint, gigachannel_component.community,
metadata_store_component.mds)
self.maybe_add('/knowledge', KnowledgeEndpoint, db=knowledge_component.knowledge_db,
diff --git a/src/tribler/gui/dialogs/addtagsdialog.py b/src/tribler/gui/dialogs/addtagsdialog.py
deleted file mode 100644
index 3c6f2c2694b..00000000000
--- a/src/tribler/gui/dialogs/addtagsdialog.py
+++ /dev/null
@@ -1,92 +0,0 @@
-from typing import Dict, Optional, List
-
-from PyQt5 import uic
-from PyQt5.QtCore import QModelIndex, QPoint, pyqtSignal
-from PyQt5.QtWidgets import QSizePolicy, QWidget
-
-from tribler.core.components.knowledge.db.knowledge_db import ResourceType
-from tribler.core.components.knowledge.knowledge_constants import MAX_RESOURCE_LENGTH, MIN_RESOURCE_LENGTH
-
-from tribler.gui.defs import TAG_HORIZONTAL_MARGIN
-from tribler.gui.dialogs.dialogcontainer import DialogContainer
-from tribler.gui.tribler_request_manager import TriblerNetworkRequest
-from tribler.gui.utilities import connect, get_ui_file_path, tr
-from tribler.gui.widgets.tagbutton import TagButton
-
-
-class AddTagsDialog(DialogContainer):
- """
- This dialog enables a user to add new tags to/remove existing tags from content.
- """
-
- save_button_clicked = pyqtSignal(QModelIndex, list)
- suggestions_loaded = pyqtSignal()
-
- def __init__(self, parent: QWidget, infohash: str) -> None:
- DialogContainer.__init__(self, parent, left_right_margin=400)
- self.index: Optional[QModelIndex] = None
- self.infohash = infohash
-
- uic.loadUi(get_ui_file_path('add_tags_dialog.ui'), self.dialog_widget)
-
- self.dialog_widget.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
- connect(self.dialog_widget.close_button.clicked, self.close_dialog)
- connect(self.dialog_widget.save_button.clicked, self.on_save_tags_button_clicked)
- connect(self.dialog_widget.edit_tags_input.enter_pressed, lambda: self.on_save_tags_button_clicked(None))
- connect(self.dialog_widget.edit_tags_input.escape_pressed, self.close_dialog)
-
- self.dialog_widget.edit_tags_input.setFocus()
- self.dialog_widget.error_text_label.hide()
- self.dialog_widget.suggestions_container.hide()
-
- # Fetch suggestions
- TriblerNetworkRequest(f"knowledge/{infohash}/tag_suggestions", self.on_received_tag_suggestions)
-
- self.update_window()
-
- def on_save_tags_button_clicked(self, _) -> None:
- statements: List[Dict] = []
-
- # Sanity check the entered tags
- entered_tags = self.dialog_widget.edit_tags_input.get_entered_tags()
- for tag in entered_tags:
- if len(tag) < MIN_RESOURCE_LENGTH or len(tag) > MAX_RESOURCE_LENGTH:
- self.dialog_widget.error_text_label.setText(
- tr(
- "Each tag should be at least %d characters and can be at most %d characters."
- % (MIN_RESOURCE_LENGTH, MAX_RESOURCE_LENGTH)
- )
- )
- self.dialog_widget.error_text_label.setHidden(False)
- return
-
- statements.append({
- "predicate": ResourceType.TAG,
- "object": tag,
- })
-
- self.save_button_clicked.emit(self.index, statements)
-
- def on_received_tag_suggestions(self, data: Dict) -> None:
- self.suggestions_loaded.emit()
- if data["suggestions"]:
- self.dialog_widget.suggestions_container.show()
-
- cur_x = 0
-
- for suggestion in data["suggestions"]:
- tag_button = TagButton(self.dialog_widget.suggestions, suggestion)
- connect(tag_button.clicked, lambda _, btn=tag_button: self.clicked_suggestion(btn))
- tag_button.move(QPoint(cur_x, tag_button.y()))
- cur_x += tag_button.width() + TAG_HORIZONTAL_MARGIN
- tag_button.show()
-
- self.update_window()
-
- def clicked_suggestion(self, tag_button: TagButton) -> None:
- self.dialog_widget.edit_tags_input.add_tag(tag_button.text())
- tag_button.setParent(None)
-
- def update_window(self) -> None:
- self.dialog_widget.adjustSize()
- self.on_main_window_resize()
diff --git a/src/tribler/gui/dialogs/editmetadatadialog.py b/src/tribler/gui/dialogs/editmetadatadialog.py
new file mode 100644
index 00000000000..1b375b38943
--- /dev/null
+++ b/src/tribler/gui/dialogs/editmetadatadialog.py
@@ -0,0 +1,161 @@
+import json
+from typing import Dict, List
+
+from PyQt5 import uic
+from PyQt5.QtCore import QModelIndex, QPoint, pyqtSignal, Qt
+from PyQt5.QtWidgets import QComboBox, QSizePolicy, QWidget
+
+from tribler.core.components.knowledge.db.knowledge_db import ResourceType
+from tribler.core.components.knowledge.knowledge_constants import MAX_RESOURCE_LENGTH, MIN_RESOURCE_LENGTH
+
+from tribler.gui.defs import TAG_HORIZONTAL_MARGIN
+from tribler.gui.dialogs.dialogcontainer import DialogContainer
+from tribler.gui.tribler_request_manager import TriblerNetworkRequest
+from tribler.gui.utilities import connect, get_ui_file_path, tr, get_objects_with_predicate
+from tribler.gui.widgets.tagbutton import TagButton
+
+
+METADATA_TABLE_PREDICATES = [ResourceType.TITLE, ResourceType.DESCRIPTION, ResourceType.DATE, ResourceType.LANGUAGE]
+
+
+class EditMetadataDialog(DialogContainer):
+ """
+ This dialog enables a user to edit metadata associated with particular content.
+ """
+
+ save_button_clicked = pyqtSignal(QModelIndex, list)
+ suggestions_loaded = pyqtSignal()
+
+ def __init__(self, parent: QWidget, index: QModelIndex) -> None:
+ DialogContainer.__init__(self, parent, left_right_margin=400)
+ self.index: QModelIndex = index
+ self.data_item = self.index.model().data_items[self.index.row()]
+ self.infohash = self.data_item["infohash"]
+
+ uic.loadUi(get_ui_file_path('edit_metadata_dialog.ui'), self.dialog_widget)
+
+ self.dialog_widget.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
+ connect(self.dialog_widget.close_button.clicked, self.close_dialog)
+ connect(self.dialog_widget.save_button.clicked, self.on_save_metadata_button_clicked)
+ connect(self.dialog_widget.edit_tags_input.enter_pressed, lambda: self.on_save_metadata_button_clicked(None))
+ connect(self.dialog_widget.edit_tags_input.escape_pressed, self.close_dialog)
+
+ self.dialog_widget.edit_tags_input.setFocus()
+ self.dialog_widget.error_text_label.hide()
+ self.dialog_widget.suggestions_container.hide()
+
+ connect(self.dialog_widget.edit_metadata_table.doubleClicked, self.on_edit_metadata_table_item_clicked)
+
+ # Load the languages
+ with open(get_ui_file_path("languages.json"), "r") as languages_file:
+ self.languages = json.loads(languages_file.read())
+
+ # Fill in the metadata table and make the items in the 2nd column editable
+ for ind in range(self.dialog_widget.edit_metadata_table.topLevelItemCount()):
+ item = self.dialog_widget.edit_metadata_table.topLevelItem(ind)
+ objects = get_objects_with_predicate(self.data_item, METADATA_TABLE_PREDICATES[ind])
+ if METADATA_TABLE_PREDICATES[ind] == ResourceType.LANGUAGE:
+ # We use a drop-down menu to select the language of a torrent
+ combobox = QComboBox(self)
+ combobox.addItems(self.languages.values())
+ self.dialog_widget.edit_metadata_table.setItemWidget(item, 1, combobox)
+ if objects and objects[0] in self.languages.keys():
+ combobox.setCurrentIndex(list(self.languages.keys()).index(objects[0]))
+ else:
+ # Otherwise, we show an editing field
+ if objects:
+ item.setText(1, objects[0])
+ item.setFlags(item.flags() | Qt.ItemIsEditable)
+
+ if get_objects_with_predicate(self.data_item, ResourceType.TAG):
+ self.dialog_widget.edit_tags_input.set_tags(get_objects_with_predicate(self.data_item, ResourceType.TAG))
+ self.dialog_widget.content_name_label.setText(self.data_item["name"])
+
+ # Fetch suggestions
+ TriblerNetworkRequest(f"knowledge/{self.infohash}/tag_suggestions", self.on_received_tag_suggestions)
+
+ self.update_window()
+
+ def on_edit_metadata_table_item_clicked(self, index):
+ if index.column() == 1:
+ item = self.dialog_widget.edit_metadata_table.topLevelItem(index.row())
+ self.dialog_widget.edit_metadata_table.editItem(item, index.column())
+
+ def show_error_text(self, text: str) -> None:
+ self.dialog_widget.error_text_label.setText(tr(text))
+ self.dialog_widget.error_text_label.setHidden(False)
+
+ def on_save_metadata_button_clicked(self, _) -> None:
+ statements: List[Dict] = []
+
+ # Sanity check the entered tags
+ entered_tags = self.dialog_widget.edit_tags_input.get_entered_tags()
+ for tag in entered_tags:
+ if len(tag) < MIN_RESOURCE_LENGTH or len(tag) > MAX_RESOURCE_LENGTH:
+ error_text = f"Each tag should be at least {MIN_RESOURCE_LENGTH} characters and can be at most " \
+ f"{MAX_RESOURCE_LENGTH} characters."
+ self.show_error_text(error_text)
+ return
+
+ statements.append({
+ "predicate": ResourceType.TAG,
+ "object": tag,
+ })
+
+ # Sanity check the entries in the metadata table and convert them to statements
+ for ind in range(self.dialog_widget.edit_metadata_table.topLevelItemCount()):
+ item = self.dialog_widget.edit_metadata_table.topLevelItem(ind)
+ entered_text: str = item.text(1)
+
+ if METADATA_TABLE_PREDICATES[ind] == ResourceType.LANGUAGE:
+ combobox = self.dialog_widget.edit_metadata_table.itemWidget(item, 1)
+ if combobox.currentIndex() != 0: # Ignore the 'unknown' option in the dropdown menu at index zero
+ statements.append({
+ "predicate": METADATA_TABLE_PREDICATES[ind],
+ "object": list(self.languages.keys())[combobox.currentIndex()],
+ })
+ continue
+
+ if entered_text and (len(entered_text) < MIN_RESOURCE_LENGTH or len(entered_text) > MAX_RESOURCE_LENGTH):
+ error_text = f"Each metadata item should be at least {MIN_RESOURCE_LENGTH} characters and can be at " \
+ f"most {MAX_RESOURCE_LENGTH} characters."
+ self.show_error_text(error_text)
+ return
+
+ # Check if the 'year' field is a number
+ if METADATA_TABLE_PREDICATES[ind] == ResourceType.DATE and entered_text and not entered_text.isdigit():
+ error_text = "The year field should contain a valid year."
+ self.show_error_text(error_text)
+ return
+
+ if entered_text:
+ statements.append({
+ "predicate": METADATA_TABLE_PREDICATES[ind],
+ "object": entered_text,
+ })
+
+ self.save_button_clicked.emit(self.index, statements)
+
+ def on_received_tag_suggestions(self, data: Dict) -> None:
+ self.suggestions_loaded.emit()
+ if data["suggestions"]:
+ self.dialog_widget.suggestions_container.show()
+
+ cur_x = 0
+
+ for suggestion in data["suggestions"]:
+ tag_button = TagButton(self.dialog_widget.suggestions, suggestion)
+ connect(tag_button.clicked, lambda _, btn=tag_button: self.clicked_suggestion(btn))
+ tag_button.move(QPoint(cur_x, tag_button.y()))
+ cur_x += tag_button.width() + TAG_HORIZONTAL_MARGIN
+ tag_button.show()
+
+ self.update_window()
+
+ def clicked_suggestion(self, tag_button: TagButton) -> None:
+ self.dialog_widget.edit_tags_input.add_tag(tag_button.text())
+ tag_button.setParent(None)
+
+ def update_window(self) -> None:
+ self.dialog_widget.adjustSize()
+ self.on_main_window_resize()
diff --git a/src/tribler/gui/qt_resources/add_tags_dialog.ui b/src/tribler/gui/qt_resources/edit_metadata_dialog.ui
similarity index 78%
rename from src/tribler/gui/qt_resources/add_tags_dialog.ui
rename to src/tribler/gui/qt_resources/edit_metadata_dialog.ui
index b253f2a8fcf..ab593852982 100644
--- a/src/tribler/gui/qt_resources/add_tags_dialog.ui
+++ b/src/tribler/gui/qt_resources/edit_metadata_dialog.ui
@@ -6,8 +6,8 @@
0
0
- 538
- 360
+ 552
+ 570
@@ -59,6 +59,24 @@ border: none;
color: #C0C0C0;
padding: 4px;
margin-top: 4px;
+}
+
+QTreeWidget::item {
+color: white;
+height: 24px;
+}
+
+QTreeWidget {
+background-color: #444;
+}
+
+QTreeWidget QHeaderView::section {
+background-color: #666;
+font-size: 12px;
+border: 1px solid #333;
+padding: 5px;
+padding-left: 10px;
+margin: 0px;
}
@@ -134,7 +152,7 @@ margin-top: 4px;
font-size: 16px; font-weight: bold;
- Suggest Tags
+ Edit metadata
Qt::AlignCenter
@@ -157,6 +175,32 @@ margin-top: 4px;
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 30
+
+
+
+
+ 50
+ true
+ false
+
+
+
+ Content name
+
+
+
-
@@ -168,20 +212,20 @@ margin-top: 4px;
0
- 60
+ 40
16777215
- 60
+ 40
color: white;
- <html><head/><body><p align="center">To organize the content in Tribler, this item would benefit from tags.<br/>You can help by suggesting several tags below that accurately describe this item (e.g., <span style=" font-style:italic;">video</span>).</p></body></html>
+ <html><head/><body><p align="center">To help organizing the content in Tribler, you can edit this items' data.</p></body></html>
true
@@ -189,9 +233,9 @@ margin-top: 4px;
-
-
+
-
+
0
0
@@ -199,19 +243,57 @@ margin-top: 4px;
0
- 30
+ 135
-
-
- 50
- true
- false
-
+
+
+ 16777215
+ 135
+
-
- Content name
+
+
+
+ QAbstractItemView::NoEditTriggers
+
+
+ 5
+
+
+ 200
+
+
+
+ Property
+
+
+
+
+ Value
+
+
+
-
+
+ Title
+
+
+ -
+
+ Description
+
+
+ -
+
+ Year
+
+
+ -
+
+ Language
+
+
-
@@ -230,6 +312,50 @@ margin-top: 4px;
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 50
+
+
+
+
+ 16777215
+ 50
+
+
+
+ <html><head/><body><p align="center">You can also help by suggesting several tags below that accurately describe this item (e.g., <span style=" font-style:italic;">video</span>).</p></body></html>
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+ QSizePolicy::Maximum
+
+
+
+ 20
+ 10
+
+
+
+
-
diff --git a/src/tribler/gui/qt_resources/languages.json b/src/tribler/gui/qt_resources/languages.json
new file mode 100644
index 00000000000..d44c0417807
--- /dev/null
+++ b/src/tribler/gui/qt_resources/languages.json
@@ -0,0 +1,220 @@
+{
+ "unknown": "Unknown",
+ "aa": "Afar",
+ "ab": "Abkhazian",
+ "af": "Afrikaans",
+ "am": "Amharic",
+ "ar": "Arabic",
+ "ar-ae": "Arabic (U.A.E.)",
+ "ar-bh": "Arabic (Bahrain)",
+ "ar-dz": "Arabic (Algeria)",
+ "ar-eg": "Arabic (Egypt)",
+ "ar-iq": "Arabic (Iraq)",
+ "ar-jo": "Arabic (Jordan)",
+ "ar-kw": "Arabic (Kuwait)",
+ "ar-lb": "Arabic (Lebanon)",
+ "ar-ly": "Arabic (libya)",
+ "ar-ma": "Arabic (Morocco)",
+ "ar-om": "Arabic (Oman)",
+ "ar-qa": "Arabic (Qatar)",
+ "ar-sa": "Arabic (Saudi Arabia)",
+ "ar-sy": "Arabic (Syria)",
+ "ar-tn": "Arabic (Tunisia)",
+ "ar-ye": "Arabic (Yemen)",
+ "as": "Assamese",
+ "ay": "Aymara",
+ "az": "Azeri",
+ "ba": "Bashkir",
+ "be": "Belarusian",
+ "bg": "Bulgarian",
+ "bh": "Bihari",
+ "bi": "Bislama",
+ "bn": "Bengali",
+ "bo": "Tibetan",
+ "br": "Breton",
+ "ca": "Catalan",
+ "co": "Corsican",
+ "cs": "Czech",
+ "cy": "Welsh",
+ "da": "Danish",
+ "de": "German",
+ "de-at": "German (Austria)",
+ "de-ch": "German (Switzerland)",
+ "de-li": "German (Liechtenstein)",
+ "de-lu": "German (Luxembourg)",
+ "div": "Divehi",
+ "dz": "Bhutani",
+ "el": "Greek",
+ "en": "English",
+ "en-au": "English (Australia)",
+ "en-bz": "English (Belize)",
+ "en-ca": "English (Canada)",
+ "en-gb": "English (United Kingdom)",
+ "en-ie": "English (Ireland)",
+ "en-jm": "English (Jamaica)",
+ "en-nz": "English (New Zealand)",
+ "en-ph": "English (Philippines)",
+ "en-tt": "English (Trinidad)",
+ "en-us": "English (United States)",
+ "en-za": "English (South Africa)",
+ "en-zw": "English (Zimbabwe)",
+ "eo": "Esperanto",
+ "es": "Spanish",
+ "es-ar": "Spanish (Argentina)",
+ "es-bo": "Spanish (Bolivia)",
+ "es-cl": "Spanish (Chile)",
+ "es-co": "Spanish (Colombia)",
+ "es-cr": "Spanish (Costa Rica)",
+ "es-do": "Spanish (Dominican Republic)",
+ "es-ec": "Spanish (Ecuador)",
+ "es-es": "Spanish (EspaƱa)",
+ "es-gt": "Spanish (Guatemala)",
+ "es-hn": "Spanish (Honduras)",
+ "es-mx": "Spanish (Mexico)",
+ "es-ni": "Spanish (Nicaragua)",
+ "es-pa": "Spanish (Panama)",
+ "es-pe": "Spanish (Peru)",
+ "es-pr": "Spanish (Puerto Rico)",
+ "es-py": "Spanish (Paraguay)",
+ "es-sv": "Spanish (El Salvador)",
+ "es-us": "Spanish (United States)",
+ "es-uy": "Spanish (Uruguay)",
+ "es-ve": "Spanish (Venezuela)",
+ "et": "Estonian",
+ "eu": "Basque",
+ "fa": "Farsi",
+ "fi": "Finnish",
+ "fj": "Fiji",
+ "fo": "Faeroese",
+ "fr": "French",
+ "fr-be": "French (Belgium)",
+ "fr-ca": "French (Canada)",
+ "fr-ch": "French (Switzerland)",
+ "fr-lu": "French (Luxembourg)",
+ "fr-mc": "French (Monaco)",
+ "fy": "Frisian",
+ "ga": "Irish",
+ "gd": "Gaelic",
+ "gl": "Galician",
+ "gn": "Guarani",
+ "gu": "Gujarati",
+ "ha": "Hausa",
+ "he": "Hebrew",
+ "hi": "Hindi",
+ "hr": "Croatian",
+ "hu": "Hungarian",
+ "hy": "Armenian",
+ "ia": "Interlingua",
+ "id": "Indonesian",
+ "ie": "Interlingue",
+ "ik": "Inupiak",
+ "in": "Indonesian",
+ "is": "Icelandic",
+ "it": "Italian",
+ "it-ch": "Italian (Switzerland)",
+ "iw": "Hebrew",
+ "ja": "Japanese",
+ "ji": "Yiddish",
+ "jw": "Javanese",
+ "ka": "Georgian",
+ "kk": "Kazakh",
+ "kl": "Greenlandic",
+ "km": "Cambodian",
+ "kn": "Kannada",
+ "ko": "Korean",
+ "kok": "Konkani",
+ "ks": "Kashmiri",
+ "ku": "Kurdish",
+ "ky": "Kirghiz",
+ "kz": "Kyrgyz",
+ "la": "Latin",
+ "ln": "Lingala",
+ "lo": "Laothian",
+ "ls": "Slovenian",
+ "lt": "Lithuanian",
+ "lv": "Latvian",
+ "mg": "Malagasy",
+ "mi": "Maori",
+ "mk": "FYRO Macedonian",
+ "ml": "Malayalam",
+ "mn": "Mongolian",
+ "mo": "Moldavian",
+ "mr": "Marathi",
+ "ms": "Malay",
+ "mt": "Maltese",
+ "my": "Burmese",
+ "na": "Nauru",
+ "nb-no": "Norwegian (Bokmal)",
+ "ne": "Nepali (India)",
+ "nl": "Dutch",
+ "nl-be": "Dutch (Belgium)",
+ "nn-no": "Norwegian",
+ "no": "Norwegian (Bokmal)",
+ "oc": "Occitan",
+ "om": "(Afan)/Oromoor/Oriya",
+ "or": "Oriya",
+ "pa": "Punjabi",
+ "pl": "Polish",
+ "ps": "Pashto/Pushto",
+ "pt": "Portuguese",
+ "pt-br": "Portuguese (Brazil)",
+ "qu": "Quechua",
+ "rm": "Rhaeto-Romanic",
+ "rn": "Kirundi",
+ "ro": "Romanian",
+ "ro-md": "Romanian (Moldova)",
+ "ru": "Russian",
+ "ru-md": "Russian (Moldova)",
+ "rw": "Kinyarwanda",
+ "sa": "Sanskrit",
+ "sb": "Sorbian",
+ "sd": "Sindhi",
+ "sg": "Sangro",
+ "sh": "Serbo-Croatian",
+ "si": "Singhalese",
+ "sk": "Slovak",
+ "sl": "Slovenian",
+ "sm": "Samoan",
+ "sn": "Shona",
+ "so": "Somali",
+ "sq": "Albanian",
+ "sr": "Serbian",
+ "ss": "Siswati",
+ "st": "Sesotho",
+ "su": "Sundanese",
+ "sv": "Swedish",
+ "sv-fi": "Swedish (Finland)",
+ "sw": "Swahili",
+ "sx": "Sutu",
+ "syr": "Syriac",
+ "ta": "Tamil",
+ "te": "Telugu",
+ "tg": "Tajik",
+ "th": "Thai",
+ "ti": "Tigrinya",
+ "tk": "Turkmen",
+ "tl": "Tagalog",
+ "tn": "Tswana",
+ "to": "Tonga",
+ "tr": "Turkish",
+ "ts": "Tsonga",
+ "tt": "Tatar",
+ "tw": "Twi",
+ "uk": "Ukrainian",
+ "ur": "Urdu",
+ "us": "English",
+ "uz": "Uzbek",
+ "vi": "Vietnamese",
+ "vo": "Volapuk",
+ "wo": "Wolof",
+ "xh": "Xhosa",
+ "yi": "Yiddish",
+ "yo": "Yoruba",
+ "zh": "Chinese",
+ "zh-cn": "Chinese (China)",
+ "zh-hk": "Chinese (Hong Kong SAR)",
+ "zh-mo": "Chinese (Macau SAR)",
+ "zh-sg": "Chinese (Singapore)",
+ "zh-tw": "Chinese (Taiwan)",
+ "zu": "Zulu"
+}
diff --git a/src/tribler/gui/utilities.py b/src/tribler/gui/utilities.py
index 2ba24fbcce0..0cb5f34fed8 100644
--- a/src/tribler/gui/utilities.py
+++ b/src/tribler/gui/utilities.py
@@ -9,7 +9,7 @@
import types
from datetime import datetime, timedelta
from pathlib import Path
-from typing import Callable, Dict
+from typing import Callable, Dict, List
from urllib.parse import quote_plus
from uuid import uuid4
@@ -26,6 +26,7 @@
from PyQt5.QtWidgets import QApplication, QMessageBox
import tribler.gui
+from tribler.core.components.knowledge.db.knowledge_db import ResourceType
from tribler.gui.defs import HEALTH_DEAD, HEALTH_GOOD, HEALTH_MOOT, HEALTH_UNCHECKED
# fmt: off
@@ -517,3 +518,10 @@ def get_color(name):
blue = int(md5_str_hash[20:30], 16) % 128 + 100
return f'#{red:02x}{green:02x}{blue:02x}'
+
+
+def get_objects_with_predicate(data_item: Dict, predicate: ResourceType) -> List[str]:
+ """
+ Extract the objects that have a particular predicate from a particular data item.
+ """
+ return [stmt["object"] for stmt in data_item.get("statements", ()) if stmt["predicate"] == predicate]
diff --git a/src/tribler/gui/widgets/lazytableview.py b/src/tribler/gui/widgets/lazytableview.py
index 7616b43a256..ec5419456f5 100644
--- a/src/tribler/gui/widgets/lazytableview.py
+++ b/src/tribler/gui/widgets/lazytableview.py
@@ -5,13 +5,12 @@
from PyQt5.QtGui import QGuiApplication, QMouseEvent, QMovie
from PyQt5.QtWidgets import QAbstractItemView, QApplication, QHeaderView, QLabel, QTableView
-from tribler.core.components.knowledge.db.knowledge_db import ResourceType
from tribler.core.components.metadata_store.db.orm_bindings.channel_node import LEGACY_ENTRY
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.gui.dialogs.addtagsdialog import AddTagsDialog
+from tribler.gui.dialogs.editmetadatadialog import EditMetadataDialog
from tribler.gui.tribler_request_manager import TriblerNetworkRequest
from tribler.gui.utilities import connect, data_item2uri, get_image_path, index2uri
from tribler.gui.widgets.tablecontentdelegate import TriblerContentDelegate
@@ -196,12 +195,7 @@ def on_subscribe_control_clicked(self, index):
self.window().on_channel_subscribe(item)
def on_edit_tags_clicked(self, index: QModelIndex) -> None:
- data_item = index.model().data_items[index.row()]
- self.add_tags_dialog = AddTagsDialog(self.window(), data_item["infohash"])
- self.add_tags_dialog.index = index
- if data_item.get("tags", ()):
- self.add_tags_dialog.dialog_widget.edit_tags_input.set_tags(data_item.get("tags", ()))
- self.add_tags_dialog.dialog_widget.content_name_label.setText(data_item["name"])
+ self.add_tags_dialog = EditMetadataDialog(self.window(), index)
self.add_tags_dialog.show()
connect(self.add_tags_dialog.save_button_clicked, self.save_edited_metadata)
@@ -277,7 +271,7 @@ def on_metadata_edited(self, index, statements: List[Dict]):
self.add_tags_dialog = None
data_item = self.model().data_items[index.row()]
- data_item["tags"] = [stmt["object"] for stmt in statements if stmt["predicate"] == ResourceType.TAG]
+ data_item["statements"] = statements
self.redraw(index, True)
self.edited_metadata.emit(data_item)
diff --git a/src/tribler/gui/widgets/tablecontentdelegate.py b/src/tribler/gui/widgets/tablecontentdelegate.py
index cc88a5c45b1..c9fd42ce1e7 100644
--- a/src/tribler/gui/widgets/tablecontentdelegate.py
+++ b/src/tribler/gui/widgets/tablecontentdelegate.py
@@ -6,6 +6,7 @@
from PyQt5.QtWidgets import QApplication, QComboBox, QStyle, QStyleOptionViewItem, QStyledItemDelegate, QToolTip
from psutil import LINUX
+from tribler.core.components.knowledge.db.knowledge_db import ResourceType
from tribler.core.components.metadata_store.db.orm_bindings.channel_node import LEGACY_ENTRY
from tribler.core.components.metadata_store.db.serialization import CHANNEL_TORRENT, COLLECTION_NODE, REGULAR_TORRENT, \
SNIPPET
@@ -32,7 +33,8 @@
TAG_TOP_MARGIN,
WINDOWS,
)
-from tribler.gui.utilities import format_votes, get_color, get_gui_setting, get_health, get_image_path, tr
+from tribler.gui.utilities import format_votes, get_color, get_gui_setting, get_health, get_image_path, tr, \
+ get_objects_with_predicate
from tribler.gui.widgets.tablecontentmodel import Column
from tribler.gui.widgets.tableiconbuttons import DownloadIconButton
@@ -185,7 +187,7 @@ def sizeHint(self, _, index: QModelIndex) -> QSize:
cur_tag_x = 6
cur_tag_y = TAG_TOP_MARGIN
- for tag_text in data_item.get("tags", ())[:MAX_TAGS_TO_SHOW]:
+ for tag_text in get_objects_with_predicate(data_item, ResourceType.TAG)[:MAX_TAGS_TO_SHOW]:
text_width = self.font_metrics.horizontalAdvance(tag_text)
tag_box_width = text_width + 2 * TAG_TEXT_HORIZONTAL_PADDING
@@ -458,7 +460,7 @@ def draw_title_and_tags(
edit_tags_button_hovered = self.hovering_over_tag_edit_button and self.hover_index == index
# If there are no tags (yet), ask the user to add some tags
- if len(data_item.get("tags", ())) == 0:
+ if len(get_objects_with_predicate(data_item, ResourceType.TAG)) == 0:
no_tags_text = tr("Be the first to suggest tags!")
painter.setPen(QColor(TRIBLER_ORANGE) if edit_tags_button_hovered else QColor("#aaa"))
text_width = painter.fontMetrics().horizontalAdvance(no_tags_text)
@@ -467,7 +469,7 @@ def draw_title_and_tags(
painter.drawText(edit_tags_rect, no_tags_text)
return
- for tag_text in data_item.get("tags", ())[:MAX_TAGS_TO_SHOW]:
+ for tag_text in get_objects_with_predicate(data_item, ResourceType.TAG)[:MAX_TAGS_TO_SHOW]:
text_width = painter.fontMetrics().horizontalAdvance(tag_text)
tag_box_width = text_width + 2 * TAG_TEXT_HORIZONTAL_PADDING
diff --git a/src/tribler/gui/widgets/tagslineedit.py b/src/tribler/gui/widgets/tagslineedit.py
index e9a4e6bba06..41e7331a734 100644
--- a/src/tribler/gui/widgets/tagslineedit.py
+++ b/src/tribler/gui/widgets/tagslineedit.py
@@ -361,7 +361,7 @@ def paintEvent(self, _) -> None:
# Draw the tags after the cursor.
self.draw_tags(p, self.editing_index + 1, len(self.tags))
- else:
+ elif len(self.tags) > 1 or self.tags[0].text:
self.draw_tags(p, 0, len(self.tags))
p.end()