Skip to content
This repository has been archived by the owner on Aug 19, 2024. It is now read-only.

Replace the Qt GUI with a React web GUI #82

Merged
merged 13 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ configobj
ipv8-rust-tunnels
libtorrent==1.2.19
lz4
pillow
pony
pystray
pywin32;sys_platform=="win32"

PyQt5
numpy
Expand Down
63 changes: 47 additions & 16 deletions src/run_tribler.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@
import logging.config
import os
import sys
import threading
import typing
import webbrowser
from pathlib import Path

import pystray
from PIL import Image
from tribler.core.session import Session
from tribler.tribler_config import TriblerConfigManager

logger = logging.getLogger(__name__)


Expand All @@ -18,7 +25,6 @@ class Arguments(typing.TypedDict):
"""

torrent: str
core: bool
log_level: str


Expand All @@ -28,7 +34,6 @@ def parse_args() -> Arguments:
"""
parser = argparse.ArgumentParser(prog='Tribler [Experimental]', description='Run Tribler BitTorrent client')
parser.add_argument('torrent', help='torrent file to download', default='', nargs='?')
parser.add_argument('--core', action="store_true", help="run core process")
parser.add_argument('--log-level', default="INFO", action="store_true", help="set the log level",
dest="log_level")
return vars(parser.parse_args())
Expand All @@ -44,30 +49,56 @@ def get_root_state_directory(requested_path: os.PathLike | None) -> Path:
return root_state_dir


def main() -> None:
async def main() -> None:
"""
The main script entry point for either the GUI or the core process.
The main script entry point.
"""
asyncio.set_event_loop(asyncio.SelectorEventLoop())

parsed_args = parse_args()
logging.basicConfig(level=parsed_args["log_level"], stream=sys.stdout)
logger.info("Run Tribler: %s", parsed_args)

root_state_dir = get_root_state_directory(os.environ.get('TSTATEDIR', 'state_directory'))
logger.info("Root state dir: %s", root_state_dir)

api_port, api_key = int(os.environ.get('CORE_API_PORT', '-1')), os.environ.get('CORE_API_KEY')
api_port, api_key = int(os.environ.get('CORE_API_PORT', '0')), os.environ.get('CORE_API_KEY')

config = TriblerConfigManager(root_state_dir / "configuration.json")
config.set("state_dir", str(root_state_dir))

if config.get("api/refresh_port_on_start"):
config.set("api/http_port", 0)
config.set("api/https_port", 0)

if api_key is None and config.get("api/key") is None:
api_key = os.urandom(16).hex()

# Check whether we need to start the core or the user interface
if parsed_args["core"]:
from tribler.core.start_core import run_core
run_core(api_port, api_key, root_state_dir)
else:
# GUI
from tribler.gui.start_gui import run_gui
run_gui(api_port, api_key, root_state_dir)
if api_key is not None and api_key != config.get("api/key"):
config.set("api/key", api_key)
config.write()

if api_port is not None and api_port != config.get("api/http_port"):
config.set("api/http_port", api_port)
config.write()

logger.info("Start tribler core. API port: %d. API key: %s.", api_port, config.get("api/key"))

session = Session(config)
await session.start()

image_path = Path(__file__).absolute() / "../tribler/ui/public/tribler.png"
image = Image.open(image_path.resolve())
url = f"http://localhost:{session.rest_manager.get_api_port()}/ui/#/downloads/all?key={config.get('api/key')}"
menu = (pystray.MenuItem('Open', lambda: webbrowser.open_new_tab(url)),
pystray.MenuItem('Quit', lambda: session.shutdown_event.set()))
icon = pystray.Icon("Tribler", icon=image, title="Tribler", menu=menu)
threading.Thread(target=icon.run).start()

await session.shutdown_event.wait()
await session.shutdown()
icon.stop()
logger.info("Tribler shutdown completed")


if __name__ == "__main__":
main()
asyncio.set_event_loop(asyncio.SelectorEventLoop())
asyncio.run(main())
qstokkink marked this conversation as resolved.
Show resolved Hide resolved
18 changes: 9 additions & 9 deletions src/tribler/core/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ def finalize(self, ipv8: IPv8, session: Session, community: Community) -> None:
"""
When we are done launching, register our REST API.
"""
session.rest_manager.get_endpoint("/search").content_discovery_community = community
session.rest_manager.get_endpoint("/api/search").content_discovery_community = community

def get_endpoints(self) -> list[RESTEndpoint]:
"""
Expand Down Expand Up @@ -161,10 +161,10 @@ def finalize(self, ipv8: IPv8, session: Session, community: Community) -> None:
"""
When we are done launching, register our REST API.
"""
session.rest_manager.get_endpoint("/downloads").mds = session.mds
session.rest_manager.get_endpoint("/statistics").mds = session.mds
session.rest_manager.get_endpoint("/api/downloads").mds = session.mds
session.rest_manager.get_endpoint("/api/statistics").mds = session.mds

db_endpoint = session.rest_manager.get_endpoint("/metadata")
db_endpoint = session.rest_manager.get_endpoint("/api/metadata")
db_endpoint.download_manager = session.download_manager
db_endpoint.mds = session.mds
db_endpoint.tribler_db = session.db
Expand Down Expand Up @@ -193,7 +193,7 @@ def finalize(self, ipv8: IPv8, session: Session, community: Community) -> None:
"""
When we are done launching, register our REST API.
"""
endpoint = session.rest_manager.get_endpoint("/knowledge")
endpoint = session.rest_manager.get_endpoint("/api/knowledge")
endpoint.db = session.db
endpoint.community = community

Expand Down Expand Up @@ -263,7 +263,7 @@ def finalize(self, ipv8: IPv8, session: Session, community: Community) -> None:
When we are done launching, register our REST API.
"""
community.register_task("Start torrent checker", session.torrent_checker.initialize)
session.rest_manager.get_endpoint("/metadata").torrent_checker = session.torrent_checker
session.rest_manager.get_endpoint("/api/metadata").torrent_checker = session.torrent_checker


@set_in_session("dht_discovery_community")
Expand All @@ -278,7 +278,7 @@ def finalize(self, ipv8: IPv8, session: Session, community: Community) -> None:
"""
When we are done launching, register our REST API.
"""
session.rest_manager.get_endpoint("/ipv8").endpoints["/dht"].dht = community
session.rest_manager.get_endpoint("/api/ipv8").endpoints["/dht"].dht = community


@set_in_session("tunnel_community")
Expand Down Expand Up @@ -314,8 +314,8 @@ def finalize(self, ipv8: IPv8, session: Session, community: Community) -> None:
"""
When we are done launching, register our REST API.
"""
session.rest_manager.get_endpoint("/downloads").tunnel_community = community
session.rest_manager.get_endpoint("/ipv8").endpoints["/tunnel"].tunnels = community
session.rest_manager.get_endpoint("/api/downloads").tunnel_community = community
session.rest_manager.get_endpoint("/api/ipv8").endpoints["/tunnel"].tunnels = community


@after("ContentDiscoveryComponent", "TorrentCheckerComponent")
Expand Down
10 changes: 4 additions & 6 deletions src/tribler/core/content_discovery/community.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,10 @@ def notify_gui(request: SelectRequest, processing_results: list[ProcessingResult
]
if self.composition.notifier:
self.composition.notifier.notify(Notification.remote_query_results,
data={
"query": kwargs.get("txt_filter"),
"results": results,
"uuid": str(request_uuid),
"peer": hexlify(request.peer.mid).decode()
})
query=kwargs.get("txt_filter"),
results=results,
uuid=str(request_uuid),
peer=hexlify(request.peer.mid).decode())

peers_to_query = self.get_random_peers(self.composition.max_query_peers)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class SearchEndpoint(RESTEndpoint):
This endpoint is responsible for searching in channels and torrents present in the local Tribler database.
"""

path = "/search"
path = "/api/search"

def __init__(self, middlewares: tuple = (), client_max_size: int = MAX_REQUEST_SIZE) -> None:
"""
Expand Down
9 changes: 4 additions & 5 deletions src/tribler/core/database/restapi/database_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class DatabaseEndpoint(RESTEndpoint):
/<public_key>
"""

path = "/metadata"
path = "/api/metadata"

def __init__(self, middlewares: tuple = (), client_max_size: int = MAX_REQUEST_SIZE) -> None:
"""
Expand Down Expand Up @@ -296,10 +296,9 @@ def search_db() -> tuple[list[dict], int, int]:
else:
total = max_rowid = None
if self.download_manager is not None:
self.download_manager.notifier.notify(Notification.local_query_results, data={
"query": request.query.get("fts_text"),
"results": list(search_results)
})
self.download_manager.notifier.notify(Notification.local_query_results,
query=request.query.get("fts_text"),
results=list(search_results))
return search_results, total, max_rowid

try:
Expand Down
2 changes: 1 addition & 1 deletion src/tribler/core/knowledge/restapi/knowledge_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class KnowledgeEndpoint(RESTEndpoint):
Top-level endpoint for knowledge management.
"""

path = "/knowledge"
path = "/api/knowledge"

def __init__(self, middlewares: tuple = (), client_max_size: int = MAX_REQUEST_SIZE) -> None:
"""
Expand Down
10 changes: 9 additions & 1 deletion src/tribler/core/libtorrent/download_manager/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -573,9 +573,17 @@ def update_lt_status(self, lt_status: lt.torrent_status) -> None:
"""
Update libtorrent stats and check if the download should be stopped.
"""
self.lt_status = lt_status
old_status = self.get_state().get_status()

self.lt_status = lt_status
state = self.get_state()

# Notify the GUI if the status has changed
if self.notifier and not self.hidden and state.get_status() != old_status:
self.notifier.notify(Notification.torrent_status_changed,
infohash=hexlify(self.tdef.get_infohash()).decode(),
status=state.get_status().name)

if state.get_status() == DownloadStatus.SEEDING:
mode = self.download_manager.config.get("libtorrent/download_defaults/seeding_mode")
seeding_ratio = self.download_manager.config.get("libtorrent/download_defaults/seeding_ratio")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class CreateTorrentEndpoint(RESTEndpoint):
See: http://www.bittorrent.org/beps/bep_0012.html
"""

path = "/createtorrent"
path = "/api/createtorrent"

def __init__(self, download_manager: DownloadManager, client_max_size: int = MAX_REQUEST_SIZE) -> None:
"""
Expand Down
25 changes: 19 additions & 6 deletions src/tribler/core/libtorrent/restapi/downloads_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from tribler.core.libtorrent.download_manager.download_manager import DownloadManager
from tribler.core.libtorrent.download_manager.download_state import DOWNLOAD, UPLOAD, DownloadStatus
from tribler.core.libtorrent.download_manager.stream import STREAM_PAUSE_TIME, Stream, StreamChunk
from tribler.core.libtorrent.torrentdef import TorrentDef
from tribler.core.restapi.rest_endpoint import (
HTTP_BAD_REQUEST,
HTTP_INTERNAL_SERVER_ERROR,
Expand Down Expand Up @@ -57,7 +58,7 @@ class DownloadsEndpoint(RESTEndpoint):
starting, pausing and stopping downloads.
"""

path = "/downloads"
path = "/api/downloads"

def __init__(self, download_manager: DownloadManager, metadata_store: MetadataStore | None = None,
tunnel_community: TriblerTunnelCommunity | None = None) -> None:
Expand Down Expand Up @@ -400,17 +401,29 @@ async def add_download(self, request: Request) -> RESTResponse:
"""
Start a download from a provided URI.
"""
params = await request.json()
uri = params.get("uri")
if not uri:
return RESTResponse({"error": "uri parameter missing"}, status=HTTP_BAD_REQUEST)
tdef = uri = None
if request.content_type == 'applications/x-bittorrent':
params = dict(request.query.items())
if 'anon_hops' in params:
params['anon_hops'] = int(params['anon_hops'])
if 'safe_seeding' in params:
params['safe_seeding'] = params['safe_seeding'] != 'false'
tdef = TorrentDef.load_from_memory(await request.read())
else:
params = await request.json()
uri = params.get("uri")
if not uri:
return RESTResponse({"error": "uri parameter missing"}, status=HTTP_BAD_REQUEST)

download_config, error = self.create_dconfig_from_params(params)
if error:
return RESTResponse({"error": error}, status=HTTP_BAD_REQUEST)

try:
download = await self.download_manager.start_download_from_uri(uri, config=download_config)
if tdef:
download = await self.download_manager.start_download(tdef=tdef, config=download_config)
elif uri:
download = await self.download_manager.start_download_from_uri(uri, config=download_config)
except Exception as e:
return RESTResponse({"error": str(e)}, status=HTTP_INTERNAL_SERVER_ERROR)

Expand Down
2 changes: 1 addition & 1 deletion src/tribler/core/libtorrent/restapi/libtorrent_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class LibTorrentEndpoint(RESTEndpoint):
Endpoint for getting information about libtorrent sessions and settings.
"""

path = "/libtorrent"
path = "/api/libtorrent"

def __init__(self, download_manager: DownloadManager) -> None:
"""
Expand Down
38 changes: 34 additions & 4 deletions src/tribler/core/libtorrent/restapi/torrentinfo_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,16 @@ class TorrentInfoEndpoint(RESTEndpoint):
This endpoint is responsible for handing all requests regarding torrent info in Tribler.
"""

path = "/torrentinfo"
path = "/api/torrentinfo"

def __init__(self, download_manager: DownloadManager) -> None:
"""
Create a new torrent info endpoint.
"""
super().__init__()
self.download_manager = download_manager
self.app.add_routes([web.get("", self.get_torrent_info)])
self.app.add_routes([web.get("", self.get_torrent_info),
web.put("", self.get_torrent_info_from_file)])

@docs(
tags=["Libtorrent"],
Expand Down Expand Up @@ -171,7 +172,7 @@ async def get_torrent_info(self, request: Request) -> RESTResponse: # noqa: C90
status=HTTP_INTERNAL_SERVER_ERROR
)

metainfo = await self.download_manager.get_metainfo(infohash, timeout=10.0, hops=i_hops,
metainfo = await self.download_manager.get_metainfo(infohash, timeout=60, hops=i_hops,
url=response.decode())
else:
metainfo = lt.bdecode(response)
Expand All @@ -190,7 +191,7 @@ async def get_torrent_info(self, request: Request) -> RESTResponse: # noqa: C90
{"error": f'Error while getting an infohash from magnet: {e.__class__.__name__}: {e}'},
status=HTTP_BAD_REQUEST
)
metainfo = await self.download_manager.get_metainfo(infohash, timeout=10.0, hops=i_hops, url=uri)
metainfo = await self.download_manager.get_metainfo(infohash, timeout=60, hops=i_hops, url=uri)
else:
return RESTResponse({"error": "invalid uri"}, status=HTTP_BAD_REQUEST)

Expand Down Expand Up @@ -219,3 +220,32 @@ async def get_torrent_info(self, request: Request) -> RESTResponse: # noqa: C90

return RESTResponse({"metainfo": hexlify(json_dump.encode()).decode(),
"download_exists": download and not download_is_metainfo_request})

@docs(
tags=["Libtorrent"],
summary="Return metainfo from a torrent found at a provided .torrent file.",
responses={
200: {
"description": "Return a hex-encoded json-encoded string with torrent metainfo",
"schema": schema(GetMetainfoResponse={"metainfo": String})
}
}
)
async def get_torrent_info_from_file(self, request: web.Request) -> RESTResponse:
"""
Return metainfo from a torrent found at a provided .torrent file.
"""
tdef = TorrentDef.load_from_memory(await request.read())
infohash = tdef.get_infohash()

# Check if the torrent is already in the downloads
download = self.download_manager.downloads.get(infohash)
metainfo_lookup = self.download_manager.metainfo_requests.get(infohash)
metainfo_download = metainfo_lookup.download if metainfo_lookup else None
requesting_metainfo = download == metainfo_download

metainfo_unicode = recursive_unicode(deepcopy(tdef.get_metainfo()), ignore_errors=True)
metainfo_json = json.dumps(metainfo_unicode, ensure_ascii=False)
return RESTResponse({"infohash": hexlify(infohash).decode(),
"metainfo": hexlify(metainfo_json.encode('utf-8')).decode(),
"download_exists": download and not requesting_metainfo})
Loading