Skip to content

Commit aab6710

Browse files
feat: streams download response to client
1 parent f4f204d commit aab6710

File tree

3 files changed

+80
-7
lines changed

3 files changed

+80
-7
lines changed

functions/tinfoilFunctions.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import asyncio
2-
from functions.torboxFunctions import getDownloads
2+
from functions.torboxFunctions import getDownloads, getDownloadLink
33
import logging
44
from library.tinfoil import errorMessage
5-
from fastapi.responses import JSONResponse
5+
from fastapi import BackgroundTasks
6+
from fastapi.responses import JSONResponse, StreamingResponse
67
import fnmatch
78
import human_readable
9+
import httpx
810

911
ACCEPTABLE_SWITCH_FILES = [".nsp", ".nsz", ".xci", ".xcz"]
1012

@@ -23,7 +25,6 @@ async def generateIndex(base_url: str):
2325
success_message = "Welcome to your self-hosted TorBox Tinfoil Server! You are now able to directly download your files from TorBox to your switch.\n\n"
2426
files = []
2527

26-
2728
try:
2829
# runs all requests in parallel for faster responses
2930
torrents, usenet_downloads, web_downloads = await asyncio.gather(getDownloads("torrents"), getDownloads("usenet"), getDownloads("webdl"))
@@ -63,6 +64,36 @@ async def generateIndex(base_url: str):
6364
content=errorMessage(f"There was an error generating the index. Error: {str(e)}", error_code="UNKOWN_ERROR")
6465
)
6566

67+
async def serveFile(background_task: BackgroundTasks, download_type: str, download_id: int, file_id: int = 0):
68+
"""
69+
Retrieves the TorBox download link and starts proxying the download through the server. This is necessary as generating a bunch of links through the index generation process can take some time, and is wasteful.
70+
71+
Requires:
72+
- download_type: the download type of the file. Must be either 'torrents', 'usenet' or 'webdl'.
73+
- download_id: an integer which represents the id of the download in the TorBox database.
74+
- file_id: an integer which represents the id of the file which is inside of the download.
75+
76+
Returns:
77+
- Streaming Response: containing the download of the file to be served on the fly.
78+
"""
79+
80+
download_link = await getDownloadLink(download_type=download_type, download_id=download_id, file_id=file_id)
81+
82+
if not download_link:
83+
return JSONResponse(
84+
status_code=500,
85+
content=errorMessage("There was an error retrieving the download link from TorBox. Please try again.", error_code="DATABASE_ERROR")
86+
)
87+
88+
# now stream link and stream out
89+
client = httpx.AsyncClient()
90+
request = client.build_request(method="GET", url=download_link, timeout=90)
91+
response = await client.send(request, stream=True)
92+
93+
cleanup = background_task.add_task(response.aclose)
94+
95+
return StreamingResponse(content=response.aiter_bytes(), background=cleanup)
96+
6697

6798

6899

functions/torboxFunctions.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ async def getDownloads(type: str):
3636
# only get downloads that are completely ready for download
3737
if not download.get("download_present", False):
3838
continue
39-
type = "download"
4039
id = download.get("id", None)
4140
for file in download.get("files", []):
4241
if not file.get("s3_path", None):
@@ -57,4 +56,39 @@ async def getDownloads(type: str):
5756
traceback.print_exc()
5857
logging.error(f"There was an error getting {type} downloads from TorBox. Error: {str(e)}")
5958
return []
59+
60+
async def getDownloadLink(download_type: str, download_id: int, file_id: int = 0):
61+
if download_type not in ["torrents", "usenet", "webdl"]:
62+
logging.error("Please provide a type of either 'torrents', 'usenet' or 'webdl'.")
63+
return None
64+
65+
try:
66+
async with httpx.AsyncClient() as client:
67+
if download_type == "torrents":
68+
id_type = "torrent_id"
69+
elif download_type == "usenet":
70+
id_type = "usenet_id"
71+
elif download_type == "webdl":
72+
id_type = "web_id"
73+
params = {
74+
"token": TORBOX_API_KEY,
75+
id_type: download_id,
76+
file_id: file_id
77+
}
78+
response = await client.get(
79+
url=f"{TORBOX_API_URL}/{download_type}/requestdl",
80+
params=params,
81+
headers={
82+
"User-Agent": "TorBox SelfHosted Tinfoil Server/1.0.0"
83+
}
84+
)
85+
if response.status_code != httpx.codes.OK:
86+
logging.error(f"Unable to retrieve TorBox {download_type} download link. Response Code: {response.status_code}. Response: {response.json()}")
87+
return None
88+
89+
json = response.json()
90+
link = json.get("data", None)
91+
return link
92+
except Exception as e:
93+
logging.error(f"There was an error retrieving the download url from TorBox. Error: {str(e)}")
6094

main.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import uvicorn
2-
from fastapi import FastAPI, HTTPException, status, Depends, Header, Request
2+
from fastapi import FastAPI, HTTPException, status, Depends, Header, Request, BackgroundTasks
33
from fastapi.responses import JSONResponse
44
from typing_extensions import Annotated, Union
55
from library.tinfoil import errorMessage
66
from library.server import PORT
77
from functions.authFunctions import checkCorrectCredentials
88
from functions.serverFunctions import checkAllowed
99
import logging
10-
from functions.tinfoilFunctions import generateIndex
10+
from functions.tinfoilFunctions import generateIndex, serveFile
1111

1212
app = FastAPI()
1313
logging.basicConfig(level=logging.INFO)
@@ -36,7 +36,15 @@ async def get_user_files(
3636
status_code=401
3737
)
3838
return await generateIndex(base_url=request.base_url)
39-
39+
40+
@app.get("/{download_type}/{download_id}/{file_id}")
41+
async def get_file(
42+
background_task: BackgroundTasks, # background_task is used to clean up the httpx response afterwards to prevent leakage
43+
download_type: str,
44+
download_id: int,
45+
file_id: int = 0
46+
):
47+
return await serveFile(background_task=background_task, download_type=download_type, download_id=download_id, file_id=file_id)
4048

4149
if __name__ == "__main__":
4250
uvicorn.run(app, host="0.0.0.0", port=PORT)

0 commit comments

Comments
 (0)