Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

All-debrid support #1452

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 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
File renamed without changes.
4 changes: 2 additions & 2 deletions python_rpc/http_downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,5 @@ def get_download_status(self):
'status': download.status,
'bytesDownloaded': download.completed_length,
}

return response
return response
151 changes: 151 additions & 0 deletions python_rpc/http_multi_link_downloader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import aria2p
from aria2p.client import ClientException as DownloadNotFound

class HttpMultiLinkDownloader:
def __init__(self):
self.downloads = []
self.completed_downloads = []
self.total_size = None
self.aria2 = aria2p.API(
aria2p.Client(
host="http://localhost",
port=6800,
secret=""
)
)

def start_download(self, urls: list[str], save_path: str, header: str = None, out: str = None, total_size: int = None):
"""Add multiple URLs to download queue with same options"""
options = {"dir": save_path}
if header:
options["header"] = header
if out:
options["out"] = out

# Clear any existing downloads first
self.cancel_download()
self.completed_downloads = []
self.total_size = total_size

for url in urls:
try:
added_downloads = self.aria2.add(url, options=options)
self.downloads.extend(added_downloads)
except Exception as e:
print(f"Error adding download for URL {url}: {str(e)}")

def pause_download(self):
"""Pause all active downloads"""
if self.downloads:
try:
self.aria2.pause(self.downloads)
except Exception as e:
print(f"Error pausing downloads: {str(e)}")

def cancel_download(self):
"""Cancel and remove all downloads"""
if self.downloads:
try:
# First try to stop the downloads
self.aria2.remove(self.downloads)
except Exception as e:
print(f"Error removing downloads: {str(e)}")
finally:
# Clear the downloads list regardless of success/failure
self.downloads = []
self.completed_downloads = []

def get_download_status(self):
"""Get status for all tracked downloads, auto-remove completed/failed ones"""
if not self.downloads and not self.completed_downloads:
return []

total_completed = 0
current_download_speed = 0
active_downloads = []
to_remove = []

# First calculate sizes from completed downloads
for completed in self.completed_downloads:
total_completed += completed['size']

# Then check active downloads
for download in self.downloads:
try:
current_download = self.aria2.get_download(download.gid)

# Skip downloads that are not properly initialized
if not current_download or not current_download.files:
to_remove.append(download)
continue

# Add to completed size and speed calculations
total_completed += current_download.completed_length
current_download_speed += current_download.download_speed

# If download is complete, move it to completed_downloads
if current_download.status == 'complete':
self.completed_downloads.append({
'name': current_download.name,
'size': current_download.total_length
})
to_remove.append(download)
else:
active_downloads.append({
'name': current_download.name,
'size': current_download.total_length,
'completed': current_download.completed_length,
'speed': current_download.download_speed
})

except DownloadNotFound:
to_remove.append(download)
continue
except Exception as e:
print(f"Error getting download status: {str(e)}")
continue

# Clean up completed/removed downloads from active list
for download in to_remove:
try:
if download in self.downloads:
self.downloads.remove(download)
except ValueError:
pass

# Return aggregate status
if self.total_size or active_downloads or self.completed_downloads:
# Use the first active download's name as the folder name, or completed if none active
folder_name = None
if active_downloads:
folder_name = active_downloads[0]['name']
elif self.completed_downloads:
folder_name = self.completed_downloads[0]['name']

if folder_name and '/' in folder_name:
folder_name = folder_name.split('/')[0]

# Use provided total size if available, otherwise sum from downloads
total_size = self.total_size
if not total_size:
total_size = sum(d['size'] for d in active_downloads) + sum(d['size'] for d in self.completed_downloads)

# Calculate completion status based on total downloaded vs total size
is_complete = len(active_downloads) == 0 and total_completed >= (total_size * 0.99) # Allow 1% margin for size differences

# If all downloads are complete, clear the completed_downloads list to prevent status updates
if is_complete:
self.completed_downloads = []

return [{
'folderName': folder_name,
'fileSize': total_size,
'progress': total_completed / total_size if total_size > 0 else 0,
'downloadSpeed': current_download_speed,
'numPeers': 0,
'numSeeds': 0,
'status': 'complete' if is_complete else 'active',
'bytesDownloaded': total_completed,
}]

return []
58 changes: 50 additions & 8 deletions python_rpc/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import sys, json, urllib.parse, psutil
from torrent_downloader import TorrentDownloader
from http_downloader import HttpDownloader
from http_multi_link_downloader import HttpMultiLinkDownloader
from profile_image_processor import ProfileImageProcessor
import libtorrent as lt

Expand All @@ -24,7 +25,15 @@
initial_download = json.loads(urllib.parse.unquote(start_download_payload))
downloading_game_id = initial_download['game_id']

if initial_download['url'].startswith('magnet'):
if isinstance(initial_download['url'], list):
# Handle multiple URLs using HttpMultiLinkDownloader
http_multi_downloader = HttpMultiLinkDownloader()
downloads[initial_download['game_id']] = http_multi_downloader
try:
http_multi_downloader.start_download(initial_download['url'], initial_download['save_path'], initial_download.get('header'), initial_download.get("out"))
except Exception as e:
print("Error starting multi-link download", e)
elif initial_download['url'].startswith('magnet'):
torrent_downloader = TorrentDownloader(torrent_session)
downloads[initial_download['game_id']] = torrent_downloader
try:
Expand Down Expand Up @@ -62,12 +71,23 @@ def status():
return auth_error

downloader = downloads.get(downloading_game_id)
if downloader:
status = downloads.get(downloading_game_id).get_download_status()
return jsonify(status), 200
else:
if not downloader:
return jsonify(None)

status = downloader.get_download_status()
if not status:
return jsonify(None)

if isinstance(status, list):
if not status: # Empty list
return jsonify(None)

# For multi-link downloader, use the aggregated status
# The status will already be aggregated by the HttpMultiLinkDownloader
return jsonify(status[0]), 200

return jsonify(status), 200

@app.route("/seed-status", methods=["GET"])
def seed_status():
auth_error = validate_rpc_password()
Expand All @@ -81,10 +101,24 @@ def seed_status():
continue

response = downloader.get_download_status()
if response is None:
if not response:
continue

if response.get('status') == 5:
if isinstance(response, list):
# For multi-link downloader, check if all files are complete
if response and all(item['status'] == 'complete' for item in response):
seed_status.append({
'gameId': game_id,
'status': 'complete',
'folderName': response[0]['folderName'],
'fileSize': sum(item['fileSize'] for item in response),
'bytesDownloaded': sum(item['bytesDownloaded'] for item in response),
'downloadSpeed': 0,
'numPeers': 0,
'numSeeds': 0,
'progress': 1.0
})
elif response.get('status') == 5: # Original torrent seeding check
seed_status.append({
'gameId': game_id,
**response,
Expand Down Expand Up @@ -138,7 +172,15 @@ def action():

existing_downloader = downloads.get(game_id)

if url.startswith('magnet'):
if isinstance(url, list):
# Handle multiple URLs using HttpMultiLinkDownloader
if existing_downloader and isinstance(existing_downloader, HttpMultiLinkDownloader):
existing_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))
else:
http_multi_downloader = HttpMultiLinkDownloader()
downloads[game_id] = http_multi_downloader
http_multi_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))
elif url.startswith('magnet'):
if existing_downloader and isinstance(existing_downloader, TorrentDownloader):
existing_downloader.start_download(url, data['save_path'])
else:
Expand Down
16 changes: 14 additions & 2 deletions src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@
"seeding": "Seeding",
"stop_seeding": "Stop seeding",
"resume_seeding": "Resume seeding",
"options": "Manage"
"options": "Manage",
"alldebrid_size_not_supported": "Download info for AllDebrid is not supported yet"
},
"settings": {
"downloads_path": "Downloads path",
Expand Down Expand Up @@ -306,7 +307,18 @@
"enable_torbox": "Enable Torbox",
"torbox_description": "TorBox is your premium seedbox service rivaling even the best servers on the market.",
"torbox_account_linked": "TorBox account linked",
"real_debrid_account_linked": "Real-Debrid account linked"
"real_debrid_account_linked": "Real-Debrid account linked",
"enable_all_debrid": "Enable All-Debrid",
"all_debrid_description": "All-Debrid is an unrestricted downloader that allows you to quickly download files from various sources.",
"all_debrid_free_account_error": "The account \"{{username}}\" is a free account. Please subscribe to All-Debrid",
"all_debrid_account_linked": "All-Debrid account linked successfully",
"alldebrid_missing_key": "Please provide an API key",
"alldebrid_invalid_key": "Invalid API key",
"alldebrid_blocked": "Your API key is geo-blocked or IP-blocked",
"alldebrid_banned": "This account has been banned",
"alldebrid_unknown_error": "An unknown error occurred",
"alldebrid_invalid_response": "Invalid response from All-Debrid",
"alldebrid_network_error": "Network error. Please check your connection"
},
"notifications": {
"download_complete": "Download complete",
Expand Down
6 changes: 5 additions & 1 deletion src/locales/ro/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,11 @@
"real_debrid_free_account_error": "Contul \"{{username}}\" este un cont gratuit. Te rugăm să te abonezi la Real-Debrid",
"debrid_linked_message": "Contul \"{{username}}\" a fost legat",
"save_changes": "Salvează modificările",
"changes_saved": "Modificările au fost salvate cu succes"
"changes_saved": "Modificările au fost salvate cu succes",
"enable_all_debrid": "Activează All-Debrid",
"all_debrid_description": "All-Debrid este un descărcător fără restricții care îți permite să descarci fișiere din diverse surse.",
"all_debrid_free_account_error": "Contul \"{{username}}\" este un cont gratuit. Te rugăm să te abonezi la All-Debrid",
"all_debrid_account_linked": "Contul All-Debrid a fost conectat cu succes"
},
"notifications": {
"download_complete": "Descărcare completă",
Expand Down
1 change: 1 addition & 0 deletions src/main/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import "./user-preferences/auto-launch";
import "./autoupdater/check-for-updates";
import "./autoupdater/restart-and-install-update";
import "./user-preferences/authenticate-real-debrid";
import "./user-preferences/authenticate-all-debrid";
import "./user-preferences/authenticate-torbox";
import "./download-sources/put-download-source";
import "./auth/sign-out";
Expand Down
18 changes: 18 additions & 0 deletions src/main/events/user-preferences/authenticate-all-debrid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { AllDebridClient } from "@main/services/download/all-debrid";
import { registerEvent } from "../register-event";

const authenticateAllDebrid = async (
_event: Electron.IpcMainInvokeEvent,
apiKey: string
) => {
AllDebridClient.authorize(apiKey);
const result = await AllDebridClient.getUser();

if ('error_code' in result) {
return { error_code: result.error_code };
}

return result.user;
};

registerEvent("authenticateAllDebrid", authenticateAllDebrid);
6 changes: 6 additions & 0 deletions src/main/events/user-preferences/get-user-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ const getUserPreferences = async () =>
);
}

if (userPreferences?.allDebridApiKey) {
userPreferences.allDebridApiKey = Crypto.decrypt(
userPreferences.allDebridApiKey
);
}

if (userPreferences?.torBoxApiToken) {
userPreferences.torBoxApiToken = Crypto.decrypt(
userPreferences.torBoxApiToken
Expand Down
6 changes: 6 additions & 0 deletions src/main/events/user-preferences/update-user-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ const updateUserPreferences = async (
);
}

if (preferences.allDebridApiKey) {
preferences.allDebridApiKey = Crypto.encrypt(
preferences.allDebridApiKey
);
}

if (preferences.torBoxApiToken) {
preferences.torBoxApiToken = Crypto.encrypt(preferences.torBoxApiToken);
}
Expand Down
Loading
Loading