Compare commits
10 Commits
91242164f5
...
49be7a313b
| Author | SHA1 | Date | |
|---|---|---|---|
| 49be7a313b | |||
| f3e2001cd9 | |||
| 410efad4ac | |||
| 2d8b1485a0 | |||
| 79c238b912 | |||
| c718ebe07f | |||
| ec8a0cd84e | |||
| c4fce38086 | |||
| 3afe3cd1a6 | |||
| 048077722e |
2
.gitignore
vendored
2
.gitignore
vendored
@ -5,6 +5,8 @@ tsconfig.tsbuildinfo
|
||||
|
||||
# Python
|
||||
env/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Output
|
||||
dist/
|
||||
|
||||
@ -7,7 +7,7 @@ require 'json'
|
||||
|
||||
current = `git rev-parse HEAD`.strip
|
||||
latest = current
|
||||
api_url = URI.parse 'https://lesbian.ddns.net/api/v1/repos/tomas/freestuff/commits?limit=1'
|
||||
api_url = URI.parse 'https://lesbian.ddns.net/api/v1/repos/tomas/webdl/commits?limit=1'
|
||||
|
||||
puts '[Daemon] '.red + 'Running WebDL daemon'
|
||||
while true do
|
||||
@ -27,7 +27,7 @@ while true do
|
||||
end
|
||||
|
||||
while true do
|
||||
sleep 10
|
||||
sleep 600
|
||||
|
||||
print '[Daemon] '.red + 'Checking for updates...'
|
||||
res = Net::HTTP.get_response api_url
|
||||
@ -39,7 +39,6 @@ while true do
|
||||
else
|
||||
puts ' Update available!'.cyan
|
||||
puts '[Daemon] '.red + 'Signaled server to shutdown'
|
||||
puts 'Signal'
|
||||
io.puts 'shutdown'
|
||||
io.flush
|
||||
break
|
||||
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@ -0,0 +1,8 @@
|
||||
certifi==2026.1.4
|
||||
charset-normalizer==3.4.4
|
||||
cloudscraper==1.2.71
|
||||
idna==3.11
|
||||
pyparsing==3.3.1
|
||||
requests==2.32.5
|
||||
requests-toolbelt==1.0.0
|
||||
urllib3==2.6.2
|
||||
@ -1,5 +1,6 @@
|
||||
import type { IDownload } from "./download.ts";
|
||||
import { Download } from "./download.js";
|
||||
import path from "node:path";
|
||||
import { Readable } from "node:stream";
|
||||
import { v4 as v4uuid } from "uuid";
|
||||
|
||||
@ -20,6 +21,11 @@ interface DownloadOrder
|
||||
__download: ()=>IDownload;
|
||||
}
|
||||
|
||||
interface DownloadError extends DownloadStatus
|
||||
{
|
||||
reason: string;
|
||||
}
|
||||
|
||||
interface DownloadEntity
|
||||
{
|
||||
dl: IDownload;
|
||||
@ -54,7 +60,7 @@ export class DownloadManager
|
||||
let id = uuid();
|
||||
|
||||
// Ensure we don't collide, just in that 0.0000000001% case :)
|
||||
while (id in this.__downloads && this.__orders.some(o => o.id == id))
|
||||
while (id in this.__errors || id in this.__downloads || this.__orders.some(o => o.id == id))
|
||||
id = uuid();
|
||||
|
||||
let resolve: (en: DownloadEntity) => void;
|
||||
@ -77,11 +83,29 @@ export class DownloadManager
|
||||
return id;
|
||||
}
|
||||
|
||||
getError(id: string): DownloadError
|
||||
{
|
||||
const err = this.__errors[id];
|
||||
if (!err)
|
||||
throw new Error("Error does not exist");
|
||||
const out: DownloadError = {
|
||||
reason: err.reason,
|
||||
url: err.url,
|
||||
finished: false,
|
||||
total: 0,
|
||||
received: 0,
|
||||
speed: 0,
|
||||
};
|
||||
if (err.filename)
|
||||
out.filename = err.filename;
|
||||
return out;
|
||||
}
|
||||
|
||||
getDownload(id: string): DownloadStatus
|
||||
{
|
||||
const en = this.__downloads[id];
|
||||
if (!en)
|
||||
throw "Download does not exist";
|
||||
throw new Error("Download does not exist");
|
||||
const out: DownloadStatus = {
|
||||
url: en.dl.getUrl(),
|
||||
finished: en.fin,
|
||||
@ -91,7 +115,27 @@ export class DownloadManager
|
||||
};
|
||||
const filename = en.dl.getFilename();
|
||||
if (filename)
|
||||
out.filename = filename;
|
||||
out.filename = path.basename(filename);
|
||||
return out;
|
||||
}
|
||||
|
||||
remove(id: string): void
|
||||
{
|
||||
if (id in this.__errors)
|
||||
delete this.__errors[id];
|
||||
else if (id in this.__downloads)
|
||||
delete this.__downloads[id];
|
||||
else if (this.__orders.some(o => o.id == id))
|
||||
this.__orders = this.__orders.filter(o => o.id != id);
|
||||
else
|
||||
throw new Error("Download does not exist");
|
||||
}
|
||||
|
||||
getErrors(): Record<string, DownloadError>
|
||||
{
|
||||
const out: Record<string, DownloadError> = {};
|
||||
for (const id of Object.keys(this.__errors))
|
||||
out[id] = this.getError(id);
|
||||
return out;
|
||||
}
|
||||
|
||||
@ -116,21 +160,36 @@ export class DownloadManager
|
||||
speed: 0,
|
||||
};
|
||||
if (or.outfile)
|
||||
out[or.id]!.filename = or.outfile;
|
||||
out[or.id]!.filename = path.basename(or.outfile);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
inQueue(id: string): boolean
|
||||
isOrder(id: string): boolean
|
||||
{
|
||||
return this.__orders.some(item=>item.id==id);
|
||||
}
|
||||
|
||||
existsDownload(id: string): boolean
|
||||
isDownload(id: string): boolean
|
||||
{
|
||||
return id in this.__downloads;
|
||||
}
|
||||
|
||||
isError(id: string): boolean
|
||||
{
|
||||
return id in this.__errors;
|
||||
}
|
||||
|
||||
exists(id: string): boolean
|
||||
{
|
||||
return (
|
||||
id in this.__errors ||
|
||||
id in this.__downloads ||
|
||||
this.__orders.some(o => o.id == id) ||
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
async getData(id: string): Promise<Readable>
|
||||
{
|
||||
let en: DownloadEntity | undefined = this.__downloads[id];
|
||||
@ -142,7 +201,7 @@ export class DownloadManager
|
||||
break;
|
||||
}
|
||||
if (!en)
|
||||
throw "Download does not exist";
|
||||
throw new Error("Download does not exist");
|
||||
return await en.dl.getData();
|
||||
}
|
||||
|
||||
@ -172,12 +231,30 @@ export class DownloadManager
|
||||
this.__update();
|
||||
});
|
||||
|
||||
dl.onError((error: Error)=>{
|
||||
en.fin = true;
|
||||
delete this.__downloads[or.id];
|
||||
const err: DownloadError = {
|
||||
reason: error.message,
|
||||
url: or.url,
|
||||
finished: false,
|
||||
total: 0,
|
||||
received: 0,
|
||||
speed: 0,
|
||||
};
|
||||
if (or.outfile)
|
||||
err.filename = path.basename(or.outfile);
|
||||
this.__errors[or.id] = err;
|
||||
this.__update();
|
||||
});
|
||||
|
||||
or.__en(en);
|
||||
this.__downloads[or.id] = en;
|
||||
}
|
||||
}
|
||||
|
||||
__max_downloads: number;
|
||||
__errors: Record<string, DownloadError> = {};
|
||||
__downloads: Record<string, DownloadEntity> = {};
|
||||
__orders: DownloadOrder[] = [];
|
||||
}
|
||||
|
||||
104
src/download.ts
104
src/download.ts
@ -25,6 +25,7 @@ export interface IDownload
|
||||
getData: ()=>Promise<Readable>;
|
||||
|
||||
onFinish: (callback: ()=>any)=>void;
|
||||
onError: (callback: (err: Error)=>any)=>void;
|
||||
}
|
||||
|
||||
|
||||
@ -38,65 +39,61 @@ export class Download extends EventEmitter implements IDownload
|
||||
if (outfile)
|
||||
this.filename = outfile
|
||||
|
||||
let dlp = this;
|
||||
function main(resolve: (a?: undefined)=>void, fail: (reason: string)=>void)
|
||||
{
|
||||
let proc;
|
||||
if (!outfile)
|
||||
proc = child_process.spawn("python", ["-m", "util.dlp", url, "-"]);
|
||||
else
|
||||
proc = child_process.spawn("python", ["-m", "util.dlp", url, outfile]);
|
||||
this.__trigger = new Promise((resolve: (a?: undefined)=>any)=>{
|
||||
this.on("end", resolve);
|
||||
});
|
||||
|
||||
let stdout: Buffer[] = [];
|
||||
let stderr = "";
|
||||
let proc;
|
||||
if (!outfile)
|
||||
proc = child_process.spawn("python", ["-m", "util.dlp", url, "-"]);
|
||||
else
|
||||
proc = child_process.spawn("python", ["-m", "util.dlp", url, outfile]);
|
||||
|
||||
let stdout_fin = false;
|
||||
let stderr_fin = false;
|
||||
let stdout: Buffer[] = [];
|
||||
let stderr = "";
|
||||
|
||||
proc.stderr.on("data", (chunk=>{
|
||||
stderr += chunk;
|
||||
if (stderr.includes("\n"))
|
||||
let stdout_fin = false;
|
||||
let stderr_fin = false;
|
||||
|
||||
proc.stderr.on("data", (chunk=>{
|
||||
stderr += chunk;
|
||||
if (stderr.includes("\n"))
|
||||
{
|
||||
let lines = stderr.split("\n");
|
||||
stderr = lines.pop()!;
|
||||
|
||||
for (const line of lines)
|
||||
{
|
||||
let lines = stderr.split("\n");
|
||||
stderr = lines.pop()!;
|
||||
|
||||
for (const line of lines)
|
||||
let msg = JSON.parse(line) as DLStatus;
|
||||
switch (msg.status)
|
||||
{
|
||||
let msg = JSON.parse(line) as DLStatus;
|
||||
switch (msg.status)
|
||||
{
|
||||
case "success":
|
||||
stderr_fin = true;
|
||||
if (stdout_fin)
|
||||
resolve()
|
||||
break;
|
||||
case "failure":
|
||||
dlp.error = msg.message!;
|
||||
fail(msg.message!);
|
||||
break;
|
||||
case "progress":
|
||||
dlp.total = msg.total!;
|
||||
dlp.received = msg.received!;
|
||||
dlp.speed = msg.speed!;
|
||||
break;
|
||||
}
|
||||
case "success":
|
||||
stderr_fin = true;
|
||||
if (stdout_fin)
|
||||
this.emit("end");
|
||||
break;
|
||||
case "failure":
|
||||
this.error = msg.message!;
|
||||
proc.stderr.removeAllListeners();
|
||||
this.emit("error", new Error(msg.message!));
|
||||
break;
|
||||
case "progress":
|
||||
this.total = msg.total!;
|
||||
this.received = msg.received!;
|
||||
this.speed = msg.speed!;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}));
|
||||
|
||||
proc.stdout.on("data", (chunk=>{stdout.push(chunk)}));
|
||||
proc.stdout.on("end", ()=>{
|
||||
if (!outfile)
|
||||
dlp.__buffer = Buffer.concat(stdout);
|
||||
stdout_fin = true;
|
||||
if (stderr_fin)
|
||||
resolve()
|
||||
})
|
||||
}
|
||||
|
||||
this.__trigger = new Promise(main);
|
||||
this.__trigger.then(()=>{
|
||||
dlp.emit("end");
|
||||
proc.stdout.on("data", (chunk=>{stdout.push(chunk)}));
|
||||
proc.stdout.on("end", ()=>{
|
||||
if (!outfile)
|
||||
this.__buffer = Buffer.concat(stdout);
|
||||
stdout_fin = true;
|
||||
if (stderr_fin)
|
||||
this.emit("end");
|
||||
});
|
||||
}
|
||||
|
||||
@ -121,6 +118,11 @@ export class Download extends EventEmitter implements IDownload
|
||||
this.on("end", callback);
|
||||
}
|
||||
|
||||
onError(callback: (err: Error)=>any)
|
||||
{
|
||||
this.on("error", callback);
|
||||
}
|
||||
|
||||
async getData(): Promise<Readable>
|
||||
{
|
||||
await this.__trigger;
|
||||
|
||||
109
src/index.ts
109
src/index.ts
@ -1,11 +1,9 @@
|
||||
import { DownloadManager } from "./dlmanager.js";
|
||||
import express from "express";
|
||||
import { detectBufferMime } from "mime-detect";
|
||||
import { downloadMovie } from "./movies.js";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import readline from "node:readline";
|
||||
import { Readable } from "node:stream";
|
||||
import toml from "toml";
|
||||
|
||||
|
||||
@ -16,8 +14,6 @@ if (!fs.existsSync(config.download.path))
|
||||
|
||||
const dlmgr = new DownloadManager(config.download.parallel);
|
||||
const app = express();
|
||||
// const mov_url = "https://kinogo.biz/122738-zveropolis-2.html";
|
||||
// dlmgr.enqueue(await downloadMovie(mov_url), mov_url);
|
||||
|
||||
|
||||
interface API_PostDownload
|
||||
@ -26,23 +22,6 @@ interface API_PostDownload
|
||||
filename: string,
|
||||
}
|
||||
|
||||
app.get("/download/:filename", (req,res)=>{
|
||||
if (req.params.filename.includes("/"))
|
||||
return res.status(400).send({error:"filename can not contain directories"});
|
||||
|
||||
const filename = path.join(config.download.path, req.params.filename);
|
||||
|
||||
const parts = req.url.split("?");
|
||||
if (parts.length == 1)
|
||||
return res.status(400).send("Missing download query");
|
||||
parts.shift();
|
||||
const url = parts.join("?");
|
||||
|
||||
const id = dlmgr.download(url, filename);
|
||||
|
||||
return res.status(303).header("Location", "/#"+id).send(id);
|
||||
});
|
||||
|
||||
app.use("/api/", express.json());
|
||||
|
||||
app.post("/api/download", (req,res)=>{
|
||||
@ -58,29 +37,93 @@ app.post("/api/download", (req,res)=>{
|
||||
});
|
||||
|
||||
app.get("/api/download/:id", (req, res)=>{
|
||||
if (!dlmgr.existsDownload(req.params.id))
|
||||
if (!dlmgr.isDownload(req.params.id))
|
||||
return res.status(404).send({error:"Download does not exist"});
|
||||
res.send(dlmgr.getDownload(req.params.id));
|
||||
});
|
||||
|
||||
app.get("/api/errors", (_req, res)=>{
|
||||
res.send(dlmgr.getErrors());
|
||||
});
|
||||
|
||||
app.get("/api/downloads", (_req, res)=>{
|
||||
res.send(dlmgr.getDownloads());
|
||||
});
|
||||
|
||||
app.get("/api/queue", (_req, res)=>{
|
||||
res.send(dlmgr.getQueue());
|
||||
});
|
||||
|
||||
app.get("/api/data/:id", async (req, res)=>{
|
||||
if (dlmgr.inQueue(req.params.id))
|
||||
return res.status(400).send("Download has not yet started");
|
||||
if (!dlmgr.existsDownload(req.params.id))
|
||||
return res.status(404).send("Download does not exist");
|
||||
if (dlmgr.isError(req.params.id))
|
||||
return res.status(400).send({error:"Download has failed", reason:dlmgr.getError(req.params.id)});
|
||||
if (dlmgr.isOrder(req.params.id))
|
||||
return res.status(400).send({error:"Download has not yet started"});
|
||||
if (!dlmgr.isDownload(req.params.id))
|
||||
return res.status(404).send({error:"Download does not exist"});
|
||||
const dl = dlmgr.getDownload(req.params.id)
|
||||
if (!dl.finished)
|
||||
return res.status(400).send("Download has not finished");
|
||||
return res.status(400).send({error:"Download has not finished"});
|
||||
|
||||
const data = await dlmgr.getData(req.params.id);
|
||||
const head = data.read(2048);
|
||||
const mime = await detectBufferMime(head);
|
||||
const stream = await dlmgr.getData(req.params.id);
|
||||
|
||||
res.contentType(mime);
|
||||
res.write(head);
|
||||
data.pipe(res);
|
||||
let head_size: number = 0;
|
||||
const head_chunks: Buffer[] = [];
|
||||
function tee(chunk: Buffer)
|
||||
{
|
||||
head_size += chunk.length;
|
||||
head_chunks.push(chunk);
|
||||
if (head_size >= 2048)
|
||||
{
|
||||
stream.off("data", tee);
|
||||
stream.pause();
|
||||
const head = Buffer.concat(head_chunks);
|
||||
detectBufferMime(head).then((mime)=>{
|
||||
res.contentType(mime);
|
||||
res.write(head);
|
||||
stream.pipe(res);
|
||||
stream.resume();
|
||||
});
|
||||
}
|
||||
}
|
||||
stream.on("data", tee);
|
||||
});
|
||||
|
||||
app.use("/api/", (_req,res)=>{
|
||||
res.status(404).send({"error": "Invalid API"});
|
||||
});
|
||||
|
||||
app.get("/download/:filename", (req,res)=>{
|
||||
if (req.params.filename.includes("/"))
|
||||
return res.status(400).send({error:"filename can not contain directories"});
|
||||
|
||||
const filename = path.join(config.download.path, req.params.filename);
|
||||
|
||||
const parts = req.url.split("?");
|
||||
if (parts.length == 1)
|
||||
return res.status(400).send("Missing download query");
|
||||
parts.shift();
|
||||
const url = parts.join("?");
|
||||
|
||||
for (const dl of Object.entries(dlmgr.getDownloads()))
|
||||
if (dl[1].url == url)
|
||||
return res.status(303).header("Location", "/#"+dl[0]).send(dl[0]);
|
||||
|
||||
for (const dl of Object.entries(dlmgr.getQueue()))
|
||||
if (dl[1].url == url)
|
||||
return res.status(303).header("Location", "/#"+dl[0]).send(dl[0]);
|
||||
|
||||
const id = dlmgr.download(url, filename);
|
||||
|
||||
return res.status(303).header("Location", "/#"+id).send(id);
|
||||
});
|
||||
|
||||
app.use(express.static("www"));
|
||||
|
||||
app.use((_req,res)=>{
|
||||
res.status(404).sendFile(path.resolve("www/404.html"));
|
||||
})
|
||||
|
||||
app.listen(config.server.port);
|
||||
console.log("Server running on :"+config.server.port);
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ export type Driver = (url: string) => ()=>IDownload;
|
||||
const __promises: Promise<any>[] = [];
|
||||
const __drivers: Record<string, Driver> = {};
|
||||
|
||||
for (const ent of fs.readdirSync(path.join(import.meta.dirname, "movies")))
|
||||
for (const ent of fs.readdirSync(path.join(path.dirname(new URL(import.meta.url).pathname), "movies")))
|
||||
{
|
||||
if (!ent.endsWith(".js"))
|
||||
continue;
|
||||
|
||||
@ -53,6 +53,10 @@ class KinogoDownload implements IDownload
|
||||
onFinish(callback: ()=>any)
|
||||
{
|
||||
}
|
||||
|
||||
onError(callback: (err: Error)=>any)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
9
www/404.html
Normal file
9
www/404.html
Normal file
@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>404 - Not Found</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>404 - Not Found</h1>
|
||||
</body>
|
||||
</html>
|
||||
33
www/api.js
Normal file
33
www/api.js
Normal file
@ -0,0 +1,33 @@
|
||||
API = {
|
||||
__get: async function(url)
|
||||
{
|
||||
const res = await fetch(url);
|
||||
if (res.status != 200)
|
||||
throw new Error(await res.json().error);
|
||||
return res;
|
||||
},
|
||||
|
||||
getDownload: async function(id)
|
||||
{
|
||||
const res = await this.__get("/api/download/"+id);
|
||||
return await res.json();
|
||||
},
|
||||
|
||||
getErrors: async function()
|
||||
{
|
||||
const res = await this.__get("/api/errors");
|
||||
return await res.json();
|
||||
},
|
||||
|
||||
getDownloads: async function()
|
||||
{
|
||||
const res = await this.__get("/api/downloads");
|
||||
return await res.json();
|
||||
},
|
||||
|
||||
getQueue: async function()
|
||||
{
|
||||
const res = await this.__get("/api/queue");
|
||||
return await res.json();
|
||||
},
|
||||
}
|
||||
53
www/index.html
Normal file
53
www/index.html
Normal file
@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<script src="/api.js"></script>
|
||||
<script src="https://lesbian.ddns.net/Tomas/-/files/lib/web/alpine.js" defer></script>
|
||||
<title>WebDL</title>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div id="menu" x-data="{errs: API.getErrors(), dls: API.getDownloads(), que: API.getQueue()}">
|
||||
<div id="errors" class="section" x-init="setInterval(async ()=>{errs = await API.getErrors();}, 500);">
|
||||
<template x-for="(err, id) in errs">
|
||||
<div class="entry">
|
||||
<div>
|
||||
<span x-text="(err.filename || err.url) + ' - ' + err.reason"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<hr>
|
||||
<div id="downloads" class="section" x-init="setInterval(async ()=>{dls = await API.getDownloads();}, 500);">
|
||||
<template x-for="(dl, id) in dls">
|
||||
<div class="entry">
|
||||
<template x-if="dl.received == dl.total">
|
||||
<div>
|
||||
<a x-bind:href="'/api/data/'+id" x-text="dl.filename || dl.url"></a>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="dl.received != dl.total">
|
||||
<div>
|
||||
<span style="flex:1" x-text="dl.filename || dl.url"></span>
|
||||
<progress style="flex:1" x-bind:max="dl.total" x-bind:value="dl.received"></progress>
|
||||
<span class="progress-text" x-text="`${(dl.received/dl.total*100).toFixed(1)}%`"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<hr>
|
||||
<div id="queue" class="section" x-init="setInterval(async ()=>{que = await API.getQueue();}, 500);">
|
||||
<template x-for="qu in que">
|
||||
<div class="entry">
|
||||
<div>
|
||||
<span x-text="qu.filename || qu.url"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
71
www/style.css
Normal file
71
www/style.css
Normal file
@ -0,0 +1,71 @@
|
||||
:root
|
||||
{
|
||||
--fg1: black;
|
||||
--fg2: gray;
|
||||
--bg1: white;
|
||||
--bg2: lightgray;
|
||||
|
||||
--fge: red;
|
||||
--bge: yellow;
|
||||
}
|
||||
|
||||
main
|
||||
{
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
min-width: 100vw;
|
||||
min-height: 100vh;
|
||||
max-width: 100vw;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
#menu
|
||||
{
|
||||
flex: 1;
|
||||
margin: 1em;
|
||||
padding: 1em;
|
||||
border-radius: 1em;
|
||||
background-color: var(--bg2);
|
||||
}
|
||||
|
||||
.entry
|
||||
{
|
||||
border-radius: 1em;
|
||||
background-color: var(--bg1);
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
.entry > div
|
||||
{
|
||||
padding: 1em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#errors > div
|
||||
{
|
||||
color: var(--fge);
|
||||
background-color: var(--bge);
|
||||
}
|
||||
|
||||
/*
|
||||
#downloads
|
||||
{
|
||||
}
|
||||
*/
|
||||
|
||||
#queue
|
||||
{
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.progress-text
|
||||
{
|
||||
text-align: right;
|
||||
min-width: 4em;
|
||||
}
|
||||
Reference in New Issue
Block a user