parent
da9832552f
commit
4d953c373f
@ -323,6 +323,22 @@
|
||||
"description": "Error Message; select/single window",
|
||||
"message": "Dots (.) in subfolders are not supported by browsers"
|
||||
},
|
||||
"export": {
|
||||
"description": "menu text",
|
||||
"message": "Export To File"
|
||||
},
|
||||
"export_aria2": {
|
||||
"description": "menu text",
|
||||
"message": "Export As aria2 List"
|
||||
},
|
||||
"export_metalink": {
|
||||
"description": "menu text",
|
||||
"message": "Export As Metalink"
|
||||
},
|
||||
"export_text": {
|
||||
"description": "menu text",
|
||||
"message": "Export As Text"
|
||||
},
|
||||
"extensionDescription": {
|
||||
"description": "DownThemAll! tagline, displayed in about:addons; Please do NOT refer to a specific browser such as firefox, as we will probably support more than one",
|
||||
"message": "The Mass Downloader for your browser"
|
||||
@ -371,6 +387,10 @@
|
||||
"description": "Menu text",
|
||||
"message": "Force Start"
|
||||
},
|
||||
"import": {
|
||||
"description": "menu text",
|
||||
"message": "Import From File"
|
||||
},
|
||||
"information_title": {
|
||||
"description": "Used in message boxes",
|
||||
"message": "Information"
|
||||
|
247
lib/imex.ts
Normal file
247
lib/imex.ts
Normal file
@ -0,0 +1,247 @@
|
||||
"use strict";
|
||||
// License: MIT
|
||||
|
||||
import { getTextLinks } from "./textlinks";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { BaseItem } from "./item";
|
||||
import { ALLOWED_SCHEMES } from "./constants";
|
||||
|
||||
export const NS_METALINK_RFC5854 = "urn:ietf:params:xml:ns:metalink";
|
||||
export const NS_DTA = "http://www.downthemall.net/properties#";
|
||||
|
||||
function parseNum(
|
||||
file: Element,
|
||||
attr: string,
|
||||
defaultValue: number,
|
||||
ns = NS_METALINK_RFC5854) {
|
||||
const val = file.getAttributeNS(ns, attr);
|
||||
if (!val) {
|
||||
return defaultValue + 1;
|
||||
}
|
||||
const num = parseInt(val, 10);
|
||||
if (isFinite(num)) {
|
||||
return num;
|
||||
}
|
||||
return defaultValue + 1;
|
||||
}
|
||||
|
||||
function importMeta4(data: string) {
|
||||
const parser = new DOMParser();
|
||||
const document = parser.parseFromString(data, "text/xml");
|
||||
const {documentElement} = document;
|
||||
const items: BaseItem[] = [];
|
||||
let batch = 0;
|
||||
for (const file of documentElement.querySelectorAll("file")) {
|
||||
try {
|
||||
const url = Array.from(file.querySelectorAll("url")).map(u => {
|
||||
try {
|
||||
const {textContent} = u;
|
||||
if (!textContent) {
|
||||
return null;
|
||||
}
|
||||
const url = new URL(textContent);
|
||||
if (!ALLOWED_SCHEMES.has(url.protocol)) {
|
||||
return null;
|
||||
}
|
||||
const prio = parseNum(u, "priority", 0);
|
||||
return {
|
||||
url,
|
||||
prio
|
||||
};
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}).filter(u => !!u).reduce((p, c) => {
|
||||
if (!c) {
|
||||
return null;
|
||||
}
|
||||
if (!p || p.prio < c.prio) {
|
||||
return c;
|
||||
}
|
||||
return p;
|
||||
});
|
||||
if (!url) {
|
||||
continue;
|
||||
}
|
||||
batch = parseNum(file, "num", batch, NS_DTA);
|
||||
const idx = parseNum(file, "idx", 0, NS_DTA);
|
||||
const item: BaseItem = {
|
||||
url: url.url.toString(),
|
||||
usable: decodeURIComponent(url.url.toString()),
|
||||
batch,
|
||||
idx
|
||||
};
|
||||
const ref = file.getAttributeNS(NS_DTA, "referrer");
|
||||
if (ref) {
|
||||
item.referrer = ref;
|
||||
item.usableReferrer = decodeURIComponent(ref);
|
||||
}
|
||||
const mask = file.getAttributeNS(NS_DTA, "mask");
|
||||
if (mask) {
|
||||
item.mask = mask;
|
||||
}
|
||||
items.push(item);
|
||||
}
|
||||
catch (ex) {
|
||||
console.error("Failed to import file", ex);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function parseKV(current: BaseItem, line: string) {
|
||||
const [k, v] = line.split("=", 2);
|
||||
switch (k.toLocaleLowerCase().trim()) {
|
||||
case "referer": {
|
||||
const rurls = getTextLinks(v);
|
||||
if (rurls && rurls.length) {
|
||||
current.referrer = rurls.pop();
|
||||
current.usableReferrer = decodeURIComponent(current.referrer || "");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function importText(data: string) {
|
||||
if (data.includes(NS_METALINK_RFC5854)) {
|
||||
return importMeta4(data);
|
||||
}
|
||||
const splitter = /(.+)\n|(.+)$/g;
|
||||
const spacer = /^\s+/;
|
||||
let match;
|
||||
let current: BaseItem | undefined = undefined;
|
||||
let idx = 0;
|
||||
const items = [];
|
||||
while ((match = splitter.exec(data)) !== null) {
|
||||
try {
|
||||
const line = match[0].trimRight();
|
||||
if (!line) {
|
||||
continue;
|
||||
}
|
||||
if (spacer.test(line)) {
|
||||
if (!current) {
|
||||
continue;
|
||||
}
|
||||
parseKV(current, line);
|
||||
continue;
|
||||
}
|
||||
const urls = getTextLinks(line);
|
||||
if (!urls || !urls.length) {
|
||||
continue;
|
||||
}
|
||||
current = {
|
||||
url: urls[0],
|
||||
usable: decodeURIComponent(urls[0]),
|
||||
idx: ++idx
|
||||
};
|
||||
items.push(current);
|
||||
}
|
||||
catch (ex) {
|
||||
current = undefined;
|
||||
console.error("Failed to import", ex);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
export interface Exporter {
|
||||
fileName: string;
|
||||
getText(items: BaseItem[]): string;
|
||||
}
|
||||
|
||||
class TextExporter {
|
||||
readonly fileName: string;
|
||||
|
||||
constructor() {
|
||||
this.fileName = "links.txt";
|
||||
}
|
||||
|
||||
getText(items: BaseItem[]) {
|
||||
const lines = [];
|
||||
for (const item of items) {
|
||||
lines.push(item.url);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
}
|
||||
|
||||
class Aria2Exporter {
|
||||
readonly fileName: string;
|
||||
|
||||
constructor() {
|
||||
this.fileName = "links.aria2.txt";
|
||||
}
|
||||
|
||||
getText(items: BaseItem[]) {
|
||||
const lines = [];
|
||||
for (const item of items) {
|
||||
lines.push(item.url);
|
||||
if (item.referrer) {
|
||||
lines.push(` referer=${item.referrer}`);
|
||||
}
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
}
|
||||
|
||||
class MetalinkExporter {
|
||||
readonly fileName: string;
|
||||
|
||||
constructor() {
|
||||
this.fileName = "links.meta4";
|
||||
}
|
||||
|
||||
getText(items: BaseItem[]) {
|
||||
const document = window.document.implementation.
|
||||
createDocument(NS_METALINK_RFC5854, "metalink", null);
|
||||
const root = document.documentElement;
|
||||
root.setAttributeNS(NS_DTA, "generator", "DownThemAll!");
|
||||
root.appendChild(document.createComment(
|
||||
"metalink as exported by DownThemAll!",
|
||||
));
|
||||
|
||||
for (const item of items) {
|
||||
const aitem = item as any;
|
||||
const f = document.createElementNS(NS_METALINK_RFC5854, "file");
|
||||
f.setAttribute("name", aitem.currentName);
|
||||
if (item.batch) {
|
||||
f.setAttributeNS(NS_DTA, "num", item.batch.toString());
|
||||
}
|
||||
if (item.idx) {
|
||||
f.setAttributeNS(NS_DTA, "idx", item.idx.toString());
|
||||
}
|
||||
if (item.referrer) {
|
||||
f.setAttributeNS(NS_DTA, "referrer", item.referrer);
|
||||
}
|
||||
if (item.mask) {
|
||||
f.setAttributeNS(NS_DTA, "mask", item.mask);
|
||||
}
|
||||
|
||||
if (item.description) {
|
||||
const n = document.createElementNS(NS_METALINK_RFC5854, "description");
|
||||
n.textContent = item.description;
|
||||
f.appendChild(n);
|
||||
}
|
||||
|
||||
const u = document.createElementNS(NS_METALINK_RFC5854, "url");
|
||||
u.textContent = item.url;
|
||||
f.appendChild(u);
|
||||
|
||||
if (aitem.totalSize > 0) {
|
||||
const s = document.createElementNS(NS_METALINK_RFC5854, "size");
|
||||
s.textContent = aitem.totalSize.toString();
|
||||
f.appendChild(s);
|
||||
}
|
||||
root.appendChild(f);
|
||||
}
|
||||
let xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
|
||||
xml += root.outerHTML;
|
||||
return xml;
|
||||
}
|
||||
}
|
||||
|
||||
export const textExporter = new TextExporter();
|
||||
export const aria2Exporter = new Aria2Exporter();
|
||||
export const metalinkExporter = new MetalinkExporter();
|
@ -28,7 +28,7 @@ function computeSelection(
|
||||
items: BaseMatchedItem[],
|
||||
onlyFast: boolean): ItemDelta[] {
|
||||
let ws = items.map((item, idx: number) => {
|
||||
item.idx = idx;
|
||||
item.idx = item.idx || idx;
|
||||
const {matched = null} = item;
|
||||
item.prevMatched = matched;
|
||||
item.matched = null;
|
||||
|
@ -110,6 +110,15 @@
|
||||
<li id="ctx-select-all" data-key="ACCEL-KeyA" data-i18n="select-all">Select All</li>
|
||||
<li id="ctx-select-invert" data-key="ACCEL-KeyI" data-i18n="invert-selection">Invert Selection</li>
|
||||
<li>-</li>
|
||||
<li id="ctx-import" data-icon="icon-import" data-i18n="import"></li>
|
||||
<li id="ctx-export" data-icon="icon-download" data-i18n="export">
|
||||
<ul class="table-context">
|
||||
<li id="ctx-export-text" data-i18n="export-text"></li>
|
||||
<li id="ctx-export-aria2" data-i18n="export-aria2"></li>
|
||||
<li id="ctx-export-metalink" data-i18n="export-metalink"></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>-</li>
|
||||
<li id="ctx-move-top" data-key="ALT-Home" data-i18n="move-top" data-icon="icon-top">Top</li>
|
||||
<li id="ctx-move-up" data-key="ALT-PageUp" data-i18n="move-up" data-icon="icon-up">Up</li>
|
||||
<li id="ctx-move-down" data-key="ALT-PageDown" data-i18n="move-down" data-icon="icon-down">down</li>
|
||||
|
@ -38,6 +38,10 @@ import { $ } from "../winutil";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { TableConfig } from "../../uikit/lib/config";
|
||||
import { IconCache } from "../../lib/iconcache";
|
||||
import * as imex from "../../lib/imex";
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { BaseItem } from "../../lib/item";
|
||||
import { API } from "../../lib/api";
|
||||
|
||||
const TREE_CONFIG_VERSION = 2;
|
||||
const RUNNING_TIMEOUT = 1000;
|
||||
@ -556,6 +560,12 @@ export class DownloadTable extends VirtualTable {
|
||||
ctx.on("ctx-remove-paused", () => this.removePausedDownloads());
|
||||
ctx.on("ctx-remove-batch", () => this.removeBatchDownloads());
|
||||
|
||||
ctx.on("ctx-import", () => this.importDownloads());
|
||||
ctx.on("ctx-export-text", () => this.exportDownloads(imex.textExporter));
|
||||
ctx.on("ctx-export-aria2", () => this.exportDownloads(imex.aria2Exporter));
|
||||
ctx.on("ctx-export-metalink",
|
||||
() => this.exportDownloads(imex.metalinkExporter));
|
||||
|
||||
ctx.on("dismissed", () => this.table.focus());
|
||||
|
||||
this.on("contextmenu", (tree, event) => {
|
||||
@ -1177,6 +1187,49 @@ export class DownloadTable extends VirtualTable {
|
||||
this.selection.toggle(0, this.rowCount - 1);
|
||||
}
|
||||
|
||||
importDownloads() {
|
||||
const picker = document.createElement("input");
|
||||
picker.setAttribute("type", "file");
|
||||
picker.setAttribute("accept", "text/*,.txt,.lst,.metalink,.meta4");
|
||||
picker.onchange = () => {
|
||||
if (!picker.files || !picker.files.length) {
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
if (!reader.result) {
|
||||
return;
|
||||
}
|
||||
const items = imex.importText(reader.result as string);
|
||||
if (!items || !items.length) {
|
||||
return;
|
||||
}
|
||||
API.regular(items, []);
|
||||
};
|
||||
reader.readAsText(picker.files[0], "utf-8");
|
||||
};
|
||||
picker.click();
|
||||
}
|
||||
|
||||
exportDownloads(exporter: imex.Exporter) {
|
||||
const items = this.getSelectedItems();
|
||||
if (!items.length) {
|
||||
return;
|
||||
}
|
||||
const text = exporter.getText(items as unknown as BaseItem[]);
|
||||
const enc = new TextEncoder();
|
||||
const data = enc.encode(text);
|
||||
const url = URL.createObjectURL(new Blob([data], {type: "text/plain"}));
|
||||
const link = document.createElement("a");
|
||||
link.setAttribute("href", url);
|
||||
link.setAttribute("download", exporter.fileName);
|
||||
link.style.display = "none";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
getRowClasses(rowid: number) {
|
||||
const item = this.downloads.filtered[rowid];
|
||||
if (!item) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user