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)