This commit is contained in:
parent
b7f1c74e38
commit
dfd619a1d9
|
@ -0,0 +1,242 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Download App</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.0.2/dist/tailwind.min.css" rel="stylesheet">
|
||||||
|
<script>
|
||||||
|
var socket = null;
|
||||||
|
var init_count = 0;
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
if (init_count) {
|
||||||
|
alert("Already connected.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert("Connected to server.");
|
||||||
|
var username = document.getElementById('username').value;
|
||||||
|
setInterval(fetchDownloads, 1000);
|
||||||
|
fetchDownloads();
|
||||||
|
socket = new WebSocket("wss://" + window.location.host + "/ws/" + username);
|
||||||
|
|
||||||
|
socket.onmessage = function (event) {
|
||||||
|
var data = JSON.parse(event.data);
|
||||||
|
var card = document.getElementById('card-' + data.url);
|
||||||
|
var progressBar = document.getElementById('progress-' + data.url);
|
||||||
|
var percentageElement = document.getElementById('percentage-' + data.url);
|
||||||
|
progressBar.style.width = data.progress + '%';
|
||||||
|
percentageElement.innerText = data.progress + '%';
|
||||||
|
|
||||||
|
if (data.progress == '100') {
|
||||||
|
percentageElement.style.display = 'none';
|
||||||
|
progressBar.parentElement.style.display = 'none';
|
||||||
|
card.closeButton.style.display = 'block';
|
||||||
|
card.cancelButton.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
init_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchDownloads() {
|
||||||
|
var username = document.getElementById('username').value;
|
||||||
|
var password = document.getElementById('password').value;
|
||||||
|
var auth = "Basic " + btoa(username + ":" + password);
|
||||||
|
|
||||||
|
var response = await fetch('/downloads/', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': auth
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
var data = await response.json();
|
||||||
|
alert('Could not authenticate: ' + data.detail);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = await response.json();
|
||||||
|
updateCards(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCards(data) {
|
||||||
|
var cards = document.getElementsByClassName("downloadCard")
|
||||||
|
var cardsArray = Array.from(cards);
|
||||||
|
cardsArray.forEach(function (card) {
|
||||||
|
var url = card.id.replace('card-', '');
|
||||||
|
if (data.in_progress.includes(url)){
|
||||||
|
} else if (data.completed.includes(url)) {
|
||||||
|
console.log("Client: Completed: " + url);
|
||||||
|
createDownloadCard(url, true);
|
||||||
|
} else {
|
||||||
|
console.log("Client: Remove: " + url);
|
||||||
|
removeCard(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
data.in_progress.forEach(function (url) {
|
||||||
|
if ( document.getElementById('card-' + url) == null ) {
|
||||||
|
console.log("Server: New: " + url);
|
||||||
|
createDownloadCard(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
data.completed.forEach(function (url) {
|
||||||
|
if ( document.getElementById('card-' + url) == null ) {
|
||||||
|
console.log("Server: Completed: " + url);
|
||||||
|
createDownloadCard(url, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCard(url) {
|
||||||
|
var card = document.getElementById('card-' + url);
|
||||||
|
if ( card ) {
|
||||||
|
card.parentNode.removeChild(card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cancelDownload(url) {
|
||||||
|
var username = document.getElementById('username').value;
|
||||||
|
var password = document.getElementById('password').value;
|
||||||
|
var auth = "Basic " + btoa(username + ":" + password);
|
||||||
|
|
||||||
|
var response = await fetch('/cancel/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': auth
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ 'url': url })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
var data = await response.json();
|
||||||
|
alert('Could not cancel download: ' + data.detail);
|
||||||
|
} else {
|
||||||
|
var card = document.getElementById('card-' + url);
|
||||||
|
card.parentNode.removeChild(card);
|
||||||
|
}
|
||||||
|
console.log("Client: canceled: " + url);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startDownload() {
|
||||||
|
var url = document.getElementById('url').value;
|
||||||
|
var username = document.getElementById('username').value;
|
||||||
|
var password = document.getElementById('password').value;
|
||||||
|
var auth = "Basic " + btoa(username + ":" + password);
|
||||||
|
|
||||||
|
if (document.getElementById('card-' + url)) {
|
||||||
|
alert('A download for this URL already exists.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var filename = url.split("/").pop();
|
||||||
|
var response = await fetch('/file_exists/?filename=' + filename, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': auth
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var data = await response.json();
|
||||||
|
|
||||||
|
if (data.exists) {
|
||||||
|
alert('A file with this name already exists.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
createDownloadCard(url);
|
||||||
|
|
||||||
|
fetch('/download/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': auth
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ url: url })
|
||||||
|
});
|
||||||
|
console.log("Client: started: " + url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDownloadCard(url, completed = false) {
|
||||||
|
if (document.getElementById('card-' + url)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var card = document.createElement('div');
|
||||||
|
card.id = 'card-' + url;
|
||||||
|
card.className = 'downloadCard bg-white shadow rounded-lg p-5 mb-4 relative ml-auto mr-auto';
|
||||||
|
|
||||||
|
|
||||||
|
var closeButton = document.createElement('button');
|
||||||
|
closeButton.className = 'absolute top-1 right-1 p-2';
|
||||||
|
closeButton.style.display = 'none';
|
||||||
|
closeButton.innerHTML = '×';
|
||||||
|
closeButton.addEventListener('click', function () {
|
||||||
|
cancelDownload(url);
|
||||||
|
});
|
||||||
|
card.appendChild(closeButton);
|
||||||
|
card.closeButton = closeButton;
|
||||||
|
|
||||||
|
var cancelButton = document.createElement('button');
|
||||||
|
cancelButton.className = 'text-red-500 absolute top-1 right-1 p-2';
|
||||||
|
cancelButton.innerHTML = '×';
|
||||||
|
cancelButton.addEventListener('click', function () {
|
||||||
|
cancelDownload(url);
|
||||||
|
});
|
||||||
|
card.appendChild(cancelButton);
|
||||||
|
card.cancelButton = cancelButton;
|
||||||
|
|
||||||
|
var urlElement = document.createElement('a');
|
||||||
|
urlElement.className = 'text-black font-bold mb-2 overflow-auto';
|
||||||
|
urlElement.href = url;
|
||||||
|
urlElement.innerText = url;
|
||||||
|
card.appendChild(urlElement);
|
||||||
|
|
||||||
|
var flexContainer = document.createElement('div');
|
||||||
|
flexContainer.className = 'flex items-center';
|
||||||
|
|
||||||
|
var percentageElement = document.createElement('p');
|
||||||
|
percentageElement.id = 'percentage-' + url;
|
||||||
|
percentageElement.innerText = '0%';
|
||||||
|
percentageElement.className = 'mr-2';
|
||||||
|
flexContainer.appendChild(percentageElement);
|
||||||
|
|
||||||
|
var progressBarContainer = document.createElement('div');
|
||||||
|
progressBarContainer.className = 'h-2 w-full bg-gray-200 rounded-full';
|
||||||
|
var progressBar = document.createElement('div');
|
||||||
|
progressBar.id = 'progress-' + url;
|
||||||
|
progressBar.className = 'h-2 bg-blue-500 rounded-full';
|
||||||
|
progressBar.style.width = '0%';
|
||||||
|
progressBarContainer.appendChild(progressBar);
|
||||||
|
flexContainer.appendChild(progressBarContainer);
|
||||||
|
|
||||||
|
card.appendChild(flexContainer);
|
||||||
|
|
||||||
|
if (completed) {
|
||||||
|
percentageElement.style.display = 'none';
|
||||||
|
progressBar.parentElement.style.display = 'none';
|
||||||
|
closeButton.style.display = 'block';
|
||||||
|
cancelButton.style.display = 'none';
|
||||||
|
}
|
||||||
|
document.getElementById('downloads').appendChild(card);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="bg-gray-200 py-10">
|
||||||
|
<div class="container mx-auto max-w-5xl px-20">
|
||||||
|
<div class="mb-5 flex space-x-2">
|
||||||
|
<input id="username" class="flex-grow p-2 rounded shadow h-10 w-full sm:w-auto" type="text"
|
||||||
|
placeholder="Enter Username">
|
||||||
|
<input id="password" class="flex-grow p-2 rounded shadow h-10 w-full sm:w-auto" type="password"
|
||||||
|
placeholder="Enter Password">
|
||||||
|
<button onclick="init()"
|
||||||
|
class="bg-blue-300 hover:bg-blue-400 text-white font-bold py-2 px-4 rounded h-10 min-w-max sm:w-auto sm:ml-2">Login</button>
|
||||||
|
</div>
|
||||||
|
<div class="mb-5 flex space-x-2">
|
||||||
|
<input id="url" class="flex-grow p-2 rounded shadow h-10 w-full sm:w-auto" type="text"
|
||||||
|
placeholder="Enter URL">
|
||||||
|
<button onclick="startDownload()"
|
||||||
|
class="bg-blue-300 hover:bg-blue-400 text-white font-bold py-2 px-4 rounded h-10 min-w-max sm:w-auto sm:ml-2">⬇</button>
|
||||||
|
</div>
|
||||||
|
<div id="downloads"></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,136 @@
|
||||||
|
from fastapi import FastAPI, WebSocket, HTTPException, Request, Depends, status
|
||||||
|
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from starlette.websockets import WebSocketDisconnect
|
||||||
|
from typing import Optional
|
||||||
|
from pydantic import BaseModel
|
||||||
|
import aiohttp, asyncio
|
||||||
|
import os, tqdm, json
|
||||||
|
from collections import defaultdict
|
||||||
|
from base64 import b64encode
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
connected_clients = defaultdict(list)
|
||||||
|
downloads = defaultdict(dict)
|
||||||
|
completed_downloads = defaultdict(dict)
|
||||||
|
canceled_downloads = defaultdict(dict)
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadRequest(BaseModel):
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
|
security = HTTPBasic()
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_username(credentials: HTTPBasicCredentials = Depends(security)):
|
||||||
|
return credentials.username
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def get(request: Request):
|
||||||
|
with open("index.html", "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
return HTMLResponse(content=content)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/file_exists/")
|
||||||
|
async def file_exists(filename: str):
|
||||||
|
if os.path.isfile(filename):
|
||||||
|
os.rename(filename, filename + "+")
|
||||||
|
return {"exists": os.path.isfile(filename)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/downloads/")
|
||||||
|
async def get_downloads(username: str = Depends(get_current_username)):
|
||||||
|
return {
|
||||||
|
"in_progress": list(downloads[username].keys()),
|
||||||
|
"completed": list(completed_downloads[username].keys()),
|
||||||
|
"canceled": list(canceled_downloads[username].keys()),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/download/")
|
||||||
|
async def download_file(
|
||||||
|
request: DownloadRequest, username: str = Depends(get_current_username)
|
||||||
|
):
|
||||||
|
url = request.url
|
||||||
|
filename = url.split("/")[-1]
|
||||||
|
|
||||||
|
if url in downloads[username]:
|
||||||
|
raise HTTPException(status_code=400, detail="Download already in progress")
|
||||||
|
|
||||||
|
download_task = asyncio.create_task(do_download(url, filename, username))
|
||||||
|
downloads[username][url] = download_task
|
||||||
|
|
||||||
|
return {"message": "Download started"}
|
||||||
|
|
||||||
|
|
||||||
|
async def do_download(url, filename, username):
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(url) as resp:
|
||||||
|
file_size = int(resp.headers["Content-Length"])
|
||||||
|
pbar = tqdm.tqdm(
|
||||||
|
total=(file_size / (1024 * 128)),
|
||||||
|
unit="Mb",
|
||||||
|
ascii=True,
|
||||||
|
unit_scale=True,
|
||||||
|
)
|
||||||
|
with open(filename, "wb") as f:
|
||||||
|
chunk_size = 1024
|
||||||
|
downloaded_size = 0
|
||||||
|
last_progress = 0
|
||||||
|
async for chunk in resp.content.iter_any():
|
||||||
|
pbar.update(len(chunk) / (1024 * 128))
|
||||||
|
if url not in downloads[username]:
|
||||||
|
pbar.close()
|
||||||
|
return
|
||||||
|
f.write(chunk)
|
||||||
|
downloaded_size += len(chunk)
|
||||||
|
# Notify the client about the progress
|
||||||
|
progress = int((downloaded_size / file_size) * 100)
|
||||||
|
# Check if the integer percentage has changed
|
||||||
|
if progress != last_progress:
|
||||||
|
last_progress = progress
|
||||||
|
await notify_clients(progress, url, username)
|
||||||
|
pbar.close()
|
||||||
|
finally:
|
||||||
|
if url in downloads[username]:
|
||||||
|
if url not in canceled_downloads[username]:
|
||||||
|
completed_downloads[username][url] = downloads[username][url]
|
||||||
|
del downloads[username][url]
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/cancel/")
|
||||||
|
async def cancel_download(
|
||||||
|
request: DownloadRequest, username: str = Depends(get_current_username)
|
||||||
|
):
|
||||||
|
url = request.url
|
||||||
|
if url in downloads[username]:
|
||||||
|
canceled_downloads[username][url] = downloads[username][url]
|
||||||
|
downloads[username][url].cancel()
|
||||||
|
return {"message": "Download canceled"}
|
||||||
|
if url in completed_downloads[username]:
|
||||||
|
del completed_downloads[username][url]
|
||||||
|
return {"message": "Download removed"}
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail="No such download")
|
||||||
|
|
||||||
|
|
||||||
|
@app.websocket("/ws/{username}")
|
||||||
|
async def websocket_endpoint(websocket: WebSocket, username: str):
|
||||||
|
await websocket.accept()
|
||||||
|
connected_clients[username].append(websocket)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
await websocket.receive_text()
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
connected_clients[username].remove(websocket)
|
||||||
|
|
||||||
|
|
||||||
|
async def notify_clients(progress, url, username):
|
||||||
|
for client in connected_clients[username]:
|
||||||
|
await client.send_text(json.dumps({"progress": progress, "url": url}))
|
Binary file not shown.
After Width: | Height: | Size: 7.7 KiB |
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
Binary file not shown.
After Width: | Height: | Size: 99 KiB |
|
@ -0,0 +1,33 @@
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.responses import FileResponse, HTMLResponse
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
app.mount("/icons", StaticFiles(directory="icons"), name="icons")
|
||||||
|
|
||||||
|
reds = {"live-calendar": "https://outlook.live.com/calendar/0/view/month",
|
||||||
|
"twitter": "https://twitter.com"}
|
||||||
|
|
||||||
|
@app.get("/{icon_name}")
|
||||||
|
async def redirect_html(icon_name: str):
|
||||||
|
redirect_url = reds.get(icon_name)
|
||||||
|
html_content = f"""<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="refresh" content="0;url={redirect_url}" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
Redirecting...
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
return HTMLResponse(content=html_content)
|
||||||
|
|
||||||
|
@app.get("/{icon_name}/favicon.ico")
|
||||||
|
async def get_icon(icon_name: str):
|
||||||
|
icon_path = Path(f"icons/{icon_name}.ico")
|
||||||
|
if icon_path.exists():
|
||||||
|
return FileResponse(icon_path)
|
||||||
|
else:
|
||||||
|
return {"error": "Icon not found"}
|
|
@ -0,0 +1,28 @@
|
||||||
|
// ==UserScript==
|
||||||
|
// @name Twitter Remove Ad
|
||||||
|
// @namespace http://tampermonkey.net/
|
||||||
|
// @version 0.1
|
||||||
|
// @description try to take over the world!
|
||||||
|
// @author You
|
||||||
|
// @match https://twitter.com/home
|
||||||
|
// @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.com
|
||||||
|
// @grant none
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
let removead = setInterval( function(){
|
||||||
|
console.log("Checking ad..");
|
||||||
|
[...document.querySelectorAll("div[data-testid=cellInnerDiv]")].forEach(e => {
|
||||||
|
var ad = 0;
|
||||||
|
[...e.getElementsByTagName("span")].forEach(f => {
|
||||||
|
if (f.innerText == "Ad") {
|
||||||
|
ad = 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if( ad ) {
|
||||||
|
console.log(e.querySelectorAll("div[data-testid=User-Name]")[0].innerText);
|
||||||
|
e.innerHTML = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
})();
|
Loading…
Reference in New Issue