03.推送aria2下载¶
首先安装依赖包
pip install -U ariarpc p115client
基础代码¶
下面的代码拉取 115 某个目录下的文件并把下载链接推送给某个 aria2 服务
from pathlib import Path
from ariarpc import AriaRPC
from p115client import P115Client
from p115client.tool import iter_download_files
# aria2 服务地址
origin_aria2 = "http://localhost:6800"
# 302 服务地址
origin_302 = "http://localhost:8000"
client = P115Client(Path("~/115-cookies.txt").expanduser())
rpc = AriaRPC(url=f"{origin_aria2}/jsonrpc")
# TODO: 待下载的目录 id
cid = 0
for info in iter_download_files(client, cid):
print(rpc.aria2.addUri(
[f"{origin_302}?pickcode={info['pickcode']}"],
{"dir": info["dirname"][1:]}
))
如果你想要批量删除任务,则可以参考
# 停掉等待中
while result := rpc.aria2.tellWaiting(0, 1000)["result"]:
for info in result:
print(rpc.aria2.remove(info["gid"]))
# 停掉活动中
while result := rpc.aria2.tellActive()["result"]:
for info in result:
print(rpc.aria2.remove(info["gid"]))
网页版¶
下面的脚本提供一个网页版界面,可以罗列目录树,然后推送下载链接到 aria2
#!/usr/bin/env python3
# encoding: utf-8
__author__ = "ChenyangGao <https://github.com/ChenyangGao>"
__version__ = (0, 0, 1)
__requirements__ = ["ariarpc", "fastapi", "p115client"]
from argparse import ArgumentParser
from posixpath import join as joinpath
from string import digits
try:
from ariarpc import AriaRPC
from fastapi import BackgroundTasks, FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.middleware.cors import CORSMiddleware
from p115client import P115Client, normalize_attr_simple
from p115client.tool import iter_download_files, iter_files_with_path, get_id_to_path
except ImportError:
from subprocess import run as prun
from sys import executable
prun([executable, "-m", "pip", "install", "-U", *__requirements__], check=True)
from ariarpc import AriaRPC
from fastapi import BackgroundTasks, FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.middleware.cors import CORSMiddleware
from p115client import P115Client, normalize_attr_simple
from p115client.tool import iter_download_files, iter_files_with_path, get_id_to_path
parser = ArgumentParser(description="115 推送 aria2 下载")
parser.add_argument("-c", "--cookies", default="", help="cookies 字符串,优先级高于 -cp/--cookies-path,如果有多个则一行写一个")
parser.add_argument("-cp", "--cookies-path", default="", help="cookies 文件保存路径,默认为当前工作目录下的 115-cookies.txt,如果有多个则一行写一个")
parser.add_argument("-H", "--host", default="0.0.0.0", help="ip 或 hostname,默认值:'0.0.0.0'")
parser.add_argument("-P", "--port", default=1234, type=int, help="端口号,默认值:1234")
args = parser.parse_args()
cookies = args.cookies.strip()
if cookies:
client = P115Client(cookies, check_for_relogin=True)
else:
from pathlib import Path
cookies_path = args.cookies_path.strip() or "115-cookies.txt"
client = P115Client(Path(cookies_path), check_for_relogin=True)
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/load")
async def load_file_tree(path: str = "0"):
path = path or "0"
if path == 0 or not (path.startswith("0") or path.strip(digits)):
cid = int(path)
else:
cid = await get_id_to_path(client, path, async_=True)
tree: dict = {
"id": "f_0",
"text": "/",
"state": { "opened": True },
"dirname": "",
"children": [],
}
id_to_dirnode: dict[int, dict] = {}
children: list[dict]
async for attr in iter_files_with_path(
client,
cid,
normalize_attr=normalize_attr_simple,
async_=True,
):
node = tree
for info in attr["ancestors"][1:-1]:
cid = info["id"]
if cid not in id_to_dirnode:
name = info["name"].replace("/", "|")
id_to_dirnode[cid] = {
"id": f"f_{cid}",
"text": name,
"state": {"opened": True },
"dirname": joinpath(node["dirname"], name),
"children": [],
}
node["children"].append(id_to_dirnode[cid])
node = id_to_dirnode[cid]
node["children"].append({
"id": attr["pickcode"],
"text": attr["name"].replace("/", "|"),
"type": "file",
})
return tree
async def batch_push(
cid: int = 0,
savedir: str = "",
origin_aria2: str = "http://localhost:6800",
origin_302: str = "http://localhost:8000",
):
rpc = AriaRPC(url=f"{origin_aria2}/jsonrpc")
async for info in iter_download_files(client, cid, async_=True):
await rpc.aria2.addUri(
[f"{origin_302}?pickcode={info['pickcode']}"],
{"dir": joinpath(savedir, info["dirname"][1:])},
async_=True,
)
@app.post("/push_all")
async def push_all(request: Request, background_tasks: BackgroundTasks):
data = await request.json()
print("payload =", data)
origin_aria2 = data["origin_aria2"]
origin_302 = data["origin_302"]
savedir = data["savedir"]
path = data["path"] or "0"
if path == "0" or not (path.startswith("0") or path.strip(digits)):
cid = int(path)
else:
cid = await get_id_to_path(client, path, async_=True)
background_tasks.add_task(
batch_push,
cid,
savedir=savedir,
origin_aria2=origin_aria2,
origin_302=origin_302,
)
return {"message": f"pushed backgroud task: push download links to aria2, cid={cid}"}
@app.post("/push_some")
async def push_some(request: Request):
data = await request.json()
print("payload =", data)
origin_aria2 = data["origin_aria2"]
origin_302 = data["origin_302"]
savedir = data["savedir"]
dirname = data["dirname"]
pickcodes = data["pickcodes"]
rpc = AriaRPC(url=f"{origin_aria2}/jsonrpc")
return await rpc.aria2.addUri(
[f"{origin_302}?pickcode={pc}" for pc in pickcodes],
{"dir": joinpath(savedir, dirname)},
async_=True,
)
async def aria2_clear_tasks(origin):
rpc = AriaRPC(url=f"{origin}/jsonrpc")
while result := rpc.aria2.tellWaiting(0, 1000)["result"]:
for info in result:
await rpc.aria2.remove(info["gid"], async_=True)
while result := rpc.aria2.tellActive()["result"]:
for info in result:
await rpc.aria2.remove(info["gid"], async_=True)
@app.post("/clear_tasks")
async def clear_tasks(request: Request, background_tasks: BackgroundTasks):
data = await request.json()
origin_aria2 = data["origin_aria2"]
background_tasks.add_task(aria2_clear_tasks, origin_aria2)
return {"message": f"pushed backgroud task: clear aria2 tasks"}
@app.get("/")
async def index():
html = """\
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width" />
<title>115toAria2</title>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jstree@3.3.17/dist/jstree.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jstree@3.3.17/dist/themes/default/style.min.css">
<style>
html { overflow-y:scroll; background:#D9E3CB }
body { background:transparent; line-height:1.6em; }
.list-group-item-text { line-height:1.6em; }
.container { min-width:320px; max-width:960px; }
#head { background:#333; border-bottom:4px solid #73796B; min-height:75px; }
#logo { margin:0; padding:0; height:60px; }
#logo span { position:absolute; right:0; top:1px; font-size:10px; background:#D9E3CB; box-shadow:0 0 4px black; padding:1px 6px 3px 6px; border-radius:4px; text-indent:0; color:black; font-weight:bold; }
#logo a { position:relative; display:block; margin:0 auto; height:100%; width:160px; overflow:hidden; text-indent:110%; white-space:nowrap; background:url('./images/logo.png') left 5px no-repeat; }
#content { box-shadow:0 20px 0px 20px rgba(255,255,255,0.3); }
#menu { text-align:center; vertical-align:top; }
#menu > li { margin:0 10px 0 0; display:inline-block; float:none; }
#menu > li > a { border-radius:5px; color:white; margin:12px 0 0 0; padding-top:8px; padding-bottom:8px; text-shadow:1px 1px 0 rgba(0,0,0,0.5); background:transparent; }
#menu > li > a:hover { background:#73796B; }
#menu > .active > a,
#menu > .active > a:hover { background:white; color:black; text-shadow:1px 1px 0 rgba(255,255,255,0.5); }
#head form { margin:14px auto; max-width:240px; }
#head input { border-radius:10px; 10px center no-repeat; padding-left:32px; }
.page { margin-top:-10px; background:white; border-radius:5px; box-shadow:0 0 10px rgba(0,0,0,0.7); padding-top:20px; padding-bottom:15px; display:none; }
h2 { margin:0 0 1em 0; padding:0 0 0.75em 0; text-align:center; color:#333; border-bottom:1px dotted #666; }
h3 { text-align:left; color:#73796B; font-family:Georgia, serif; font-style:italic; padding:0.5em; border-bottom:1px dotted; margin:0 0 1em 0; }
h3 > i { font-size:0.6em; }
h4 { margin-top:1em; }
#docs .nav {
margin:0 -15px 1em -15px; font-size:1.2em; padding-left:25px; text-align:center;
background-image: -webkit-gradient(linear, 0 100%, 0 0, from(#eee), color-stop(0.6, #fff));
background-image: -webkit-linear-gradient(bottom, #eee, #fff 60%);
background-image: -moz-linear-gradient(bottom, #eee, #fff 60%);
background-image: -o-linear-gradient(bottom, #eee, #fff 60%);
background-image: linear-gradient(bottom, #eee, #fff 60%);
}
#docs h3 { margin-left:-15px; margin-right:-15px; padding-left:25px; }
.spaced > li { margin-bottom:1.8em; }
.item { padding:12px 10px 0 10px; margin-bottom:10px; border-radius:5px; border:1px solid #eee; }
.item > .item-inner { display:none; }
.item:nth-child(2n) { background:#fcfcfc; }
.item > h4 { margin:0 0 10px 0; font-size:1em; overflow:hidden; cursor:pointer; }
.item > h4 > code { padding:5px 10px; font-size:1.1em; float:left; }
.item p { padding:10px 10px; margin:0; }
.params { margin:10px 10px; }
.params li { padding:10px 0; border-top:1px dotted silver; }
.params p code { font-size:14px; padding-left:6px; padding-right:6px; line-height:20px; display:inline-block; }
.param { display:inline-block; padding-left:10px; padding-right:10px; font-size:14px; line-height:20px; float:left; }
.return { color:white; background:#C7254E; float:left; font-size:14px; line-height:20px; }
.trigger { color:white; background:#286B1C; font-size:14px; line-height:20px; }
.type { color:white; background:silver; }
.params p { margin:0 0 0 190px; padding:0; }
.item > h4 > .meta { float:right; background:silver; color:white; cursor:auto; margin-left:10px; }
.item > h4 > .plugin { background:#d9e3cb; color:black; }
.private { opacity:0.5; transition:opacity 0.4s; }
.private:hover { opacity:1; }
.prop { background:#DCEAF4; color:navy; }
.func { background:#F4DCDF; color:#8b0000; }
.evnt { background:#CFF2C9; color:#286B1C; }
.func strong { text-shadow:1px 1px 0 white; }
.list-margin li { margin-bottom:10px; }
.list-margin strong { font-style:italic; }
pre code { color:#333; }
pre code strong { color:#000; }
.comment { color:#999; text-shadow:1px 1px 0 white; }
.comment strong { display:inline-block; width:15px; line-height:15px; font-size:10px; text-align:center; border-radius:8px; background:gray; padding:0; color:white; text-shadow:none; }
#main-buttons { text-align:center; padding-bottom:2em; }
#main-buttons > small { color:#666; }
#main-buttons > .btn { font-weight:bold; width:135px; text-shadow:1px 1px 0 #666; margin-right:10px; margin-bottom:10px; }
.features { margin:0 auto 2em auto; max-width:85%; }
.features > li { width:45%; padding:8px 0; }
.features > li > .glyphicon { margin-right:10px; }
.list-group-item-heading { font-weight:bold; font-size:16px; }
.list-group-item-text { color:#666; }
pre .point { display:inline-block; border-radius:5px; width:100%; padding:5px 0; }
#jstree1, #jstree2, .demo { max-width:100%; overflow:auto; font:10px Verdana, sans-serif; box-shadow:0 0 5px #ccc; padding:10px; border-radius:5px; }
#plugins .demo, #plugins pre { min-height:200px; }
#no_res { text-align:center; border:0 !important; }
@media (max-width: 740px) {
.param, .return, .trigger { float:none; }
.params p { margin-left:10px; margin-top:10px; line-height:26px; }
.features { max-width:100%; }
.features > li { width:auto; margin:0px 30px 0 0; }
.features > li > .glyphicon { margin-right:2px; }
}
.container {
margin-bottom: 10px;
}
input[type="text"] {
width: 100%;
padding: 8px;
box-sizing: border-box;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="container" id="content">
<div class="row page" id="demo" style="display:block;">
<div class="col-md-12">
<h3>115 推送 aria2 下载</h3>
<div class="row">
<div class="container">
<label for="origin_aria2">aria2 服务地址:</label>
<input class="input" type="text" id="origin_aria2" value="http://localhost:6800" >
</div>
</div>
<div class="row">
<div class="container">
<label for="origin_302">302 服务地址:</label>
<input class="input" type="text" id="origin_302" value="http://localhost:8000">
</div>
</div>
<div class="row">
<div class="container">
<label for="savedir">下载目录:</label>
<input class="input" type="text" id="savedir" value="">
</div>
</div>
<div class="row">
<div class="container">
<label for="output3">请输入 id 或 路径:</label>
<input class="input" type="text" id="path" value="0">
</div>
</div>
<div class="container">
<button class="btn btn-success btn-xs" onclick="loadData()"><i class="glyphicon glyphicon-refresh"></i> 拉取数据</button>
<button class="btn btn-primary btn-xs" onclick="pushAll()"><i class="glyphicon glyphicon-open"></i> 推送全部(🔔 无需拉取数据)</button>
<button class="btn btn-info btn-xs" onclick="clearTasks()"><i class="glyphicon glyphicon-open"></i> 清空 aria2 任务</button>
</div>
<div class="container">
<button type="button" class="btn btn-danger btn-xs" onclick="deleteSome();"><i class="glyphicon glyphicon-remove"></i> 删除所选</button>
<button type="button" class="btn btn-warning btn-xs" onclick="pushSome();"><i class="glyphicon glyphicon-cloud-download"></i> 推送所选</button>
</div>
<div class="row" style="padding: 3px; padding-left: 10px">
<input type="text" value="" style="box-shadow:inset 0 0 4px #eee; width:480px; margin:0; padding:6px 12px; border-radius:4px; border:1px solid silver; font-size:1.1em;" id="search" placeholder="Search" />
</div>
<div class="row">
<div class="col-md-12">
<div id="filetree" class="demo" style="margin-top:1em; min-height:200px;"></div>
<script>
for (const el of document.querySelectorAll("input.input")) {
el.addEventListener("input", function() {
localStorage.setItem(this.id, this.value);
});
}
window.onload = function() {
for (const el of document.querySelectorAll("input.input")) {
const val = localStorage.getItem(el.id);
if (val !== null) el.value = val;
}
}
function getConfig() {
return {
origin_aria2: document.getElementById('origin_aria2').value,
origin_302: document.getElementById('origin_302').value,
savedir: document.getElementById('savedir').value,
path: document.getElementById('path').value || 0,
}
}
function deleteSome() {
var ref = $('#filetree').jstree(true),
sel = ref.get_selected();
if(!sel.length) { return false; }
ref.delete_node(sel);
};
async function pushSome() {
var ref = $('#filetree').jstree(true),
sel = ref.get_selected();
if(!sel.length) { return false; }
const config = getConfig();
for (const pickcode of sel) {
if (pickcode.startsWith("f_"))
continue;
const node = ref.get_node(pickcode);
const dirname = ref.get_node(node.parent).original.dirname;
await fetch("/push_some", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
...config,
dirname: dirname,
name: node.text,
pickcodes: [pickcode],
})
});
}
};
async function pushAll() {
const config = getConfig();
await fetch("/push_all", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(config),
});
};
async function clearTasks() {
const config = getConfig();
await fetch("/clear_tasks", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(config),
});
}
function loadData() {
let to = false;
$('#search').keyup(function () {
if(to) { clearTimeout(to); }
to = setTimeout(function () {
let v = $('#search').val();
$('#filetree').jstree(true).search(v);
}, 250);
});
console.log(localStorage.getItem("path") || 0);
$('#filetree').jstree("destroy").empty();
$('#filetree')
.jstree({
"core": {
"animation" : 0,
"check_callback" : true,
"themes" : { "stripes" : true },
"data": {
"url": `/load?path=${localStorage.getItem("path") || 0}`,
"dataType": "json",
}
},
"types" : {
"file" : {
"icon" : "glyphicon glyphicon-file",
}
},
"plugins": [
"checkbox", "conditionalselect", "contextmenu", "massload", "search", "sort",
"state", "types",
],
});
}
</script>
</div>
</div>
</div>
</div>
</div>
</body>
</html>"""
return HTMLResponse(content=html)
if __name__ == "__main__":
from uvicorn import run
run(app, host=args.host, port=args.port)