parent
da6c6bcf68
commit
4b09a0db67
4
TODO.md
4
TODO.md
@ -8,8 +8,6 @@ P2
|
|||||||
|
|
||||||
Planned for later.
|
Planned for later.
|
||||||
|
|
||||||
* Soft errors and retry logic
|
|
||||||
* Big caveat: When the server still responds, like 50x errors which would be recoverable, we actually have no way of knowing it did in respond in such a way. See P4 - Handle Errors remarks.
|
|
||||||
* Inter-addon API (basic)
|
* Inter-addon API (basic)
|
||||||
* Add downloads
|
* Add downloads
|
||||||
* vtable perf: cache column widths
|
* vtable perf: cache column widths
|
||||||
@ -44,8 +42,6 @@ Stuff that probably cannot be implemented due to WeberEension limitations.
|
|||||||
* Firefox helpfully keeps different lists of downloads. One for newly added downloads, and other ones for "previous" downloads. Turns out the WebExtension API only ever queries the "new" list.
|
* Firefox helpfully keeps different lists of downloads. One for newly added downloads, and other ones for "previous" downloads. Turns out the WebExtension API only ever queries the "new" list.
|
||||||
* Segmented downloads
|
* Segmented downloads
|
||||||
* Cannot be done with WebExtensions - downloads API has no support and manually downloading, storing in temporary add-on storage and reassmbling the downloaded parts later is not only efficient but does not reliabliy work due to storage limitations.
|
* Cannot be done with WebExtensions - downloads API has no support and manually downloading, storing in temporary add-on storage and reassmbling the downloaded parts later is not only efficient but does not reliabliy work due to storage limitations.
|
||||||
* Handle errors, 404 and such
|
|
||||||
* The Firefox download manager is too stupid and webRequest does not see Downloads, so cannot be done right now.
|
|
||||||
* Conflicts: ask when a file exists
|
* Conflicts: ask when a file exists
|
||||||
* Not supported by Firefox
|
* Not supported by Firefox
|
||||||
* Speed limiter
|
* Speed limiter
|
||||||
|
@ -617,6 +617,14 @@
|
|||||||
"description": "Preferences/General",
|
"description": "Preferences/General",
|
||||||
"message": "Remove missing downloads after a restart"
|
"message": "Remove missing downloads after a restart"
|
||||||
},
|
},
|
||||||
|
"pref_retries": {
|
||||||
|
"description": "pref text",
|
||||||
|
"message": "Number of retries of downloads on temporary errors"
|
||||||
|
},
|
||||||
|
"pref_retry_time": {
|
||||||
|
"description": "pref text",
|
||||||
|
"message": "Retry every (in minutes)"
|
||||||
|
},
|
||||||
"pref_show_urls": {
|
"pref_show_urls": {
|
||||||
"description": "Preferences/General",
|
"description": "Preferences/General",
|
||||||
"message": "Show URLs instead of Names"
|
"message": "Show URLs instead of Names"
|
||||||
@ -955,6 +963,20 @@
|
|||||||
"description": "Action for resuming a download",
|
"description": "Action for resuming a download",
|
||||||
"message": "Resume"
|
"message": "Resume"
|
||||||
},
|
},
|
||||||
|
"retrying": {
|
||||||
|
"description": "Status text",
|
||||||
|
"message": "Retrying"
|
||||||
|
},
|
||||||
|
"retrying_error": {
|
||||||
|
"description": "status text",
|
||||||
|
"message": "Retrying - $ERROR$",
|
||||||
|
"placeholders": {
|
||||||
|
"error": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Server Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"running": {
|
"running": {
|
||||||
"description": "Status text",
|
"description": "Status text",
|
||||||
"message": "Running"
|
"message": "Running"
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
"tooltip": true,
|
"tooltip": true,
|
||||||
"show-urls": false,
|
"show-urls": false,
|
||||||
"remove-missing-on-init": false,
|
"remove-missing-on-init": false,
|
||||||
|
"retries": 5,
|
||||||
|
"retry-time": 10,
|
||||||
"limits": [
|
"limits": [
|
||||||
{
|
{
|
||||||
"domain": "*",
|
"domain": "*",
|
||||||
|
15
lib/db.ts
15
lib/db.ts
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { BaseItem } from "./item";
|
import { BaseItem } from "./item";
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { Download } from "./manager/download";
|
||||||
|
import { RUNNING, QUEUED, RETRYING } from "./manager/state";
|
||||||
|
|
||||||
// License: MIT
|
// License: MIT
|
||||||
|
|
||||||
@ -69,7 +72,7 @@ export const DB = new class DB {
|
|||||||
return await new Promise(this.getAllInternal);
|
return await new Promise(this.getAllInternal);
|
||||||
}
|
}
|
||||||
|
|
||||||
saveItemsInternal(items: any[], resolve: Function, reject: Function) {
|
saveItemsInternal(items: Download[], resolve: Function, reject: Function) {
|
||||||
if (!items || !items.length || !this.db) {
|
if (!items || !items.length || !this.db) {
|
||||||
resolve();
|
resolve();
|
||||||
return;
|
return;
|
||||||
@ -83,9 +86,13 @@ export const DB = new class DB {
|
|||||||
if (item.private) {
|
if (item.private) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const req = store.put(item.toJSON());
|
const json = item.toJSON();
|
||||||
|
if (item.state === RUNNING || item.state === RETRYING) {
|
||||||
|
json.state = QUEUED;
|
||||||
|
}
|
||||||
|
const req = store.put(json);
|
||||||
if (!("dbId" in item) || item.dbId < 0) {
|
if (!("dbId" in item) || item.dbId < 0) {
|
||||||
req.onsuccess = () => item.dbId = req.result;
|
req.onsuccess = () => item.dbId = req.result as number;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -94,7 +101,7 @@ export const DB = new class DB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveItems(items: any[]) {
|
async saveItems(items: Download[]) {
|
||||||
await this.init();
|
await this.init();
|
||||||
return await new Promise(this.saveItemsInternal.bind(this, items));
|
return await new Promise(this.saveItemsInternal.bind(this, items));
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,9 @@ const DEFAULTS = {
|
|||||||
written: 0,
|
written: 0,
|
||||||
manId: 0,
|
manId: 0,
|
||||||
mime: "",
|
mime: "",
|
||||||
prerolled: false
|
prerolled: false,
|
||||||
|
retries: 0,
|
||||||
|
deadline: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
let sessionId = 0;
|
let sessionId = 0;
|
||||||
@ -105,6 +107,8 @@ export class BaseDownload {
|
|||||||
|
|
||||||
public prerolled: boolean;
|
public prerolled: boolean;
|
||||||
|
|
||||||
|
public retries: number;
|
||||||
|
|
||||||
constructor(options: any) {
|
constructor(options: any) {
|
||||||
Object.assign(this, DEFAULTS);
|
Object.assign(this, DEFAULTS);
|
||||||
this.assign(options);
|
this.assign(options);
|
||||||
@ -113,6 +117,7 @@ export class BaseDownload {
|
|||||||
}
|
}
|
||||||
this.sessionId = ++sessionId;
|
this.sessionId = ++sessionId;
|
||||||
this.renamer = new Renamer(this);
|
this.renamer = new Renamer(this);
|
||||||
|
this.retries = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
assign(options: any) {
|
assign(options: any) {
|
||||||
@ -182,6 +187,7 @@ export class BaseDownload {
|
|||||||
rv.currentName = this.browserName || rv.destName || rv.finalName;
|
rv.currentName = this.browserName || rv.destName || rv.finalName;
|
||||||
rv.error = this.error;
|
rv.error = this.error;
|
||||||
rv.ext = this.renamer.p_ext;
|
rv.ext = this.renamer.p_ext;
|
||||||
|
rv.retries = this.retries;
|
||||||
return rv;
|
return rv;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { CHROME, downloads, DownloadOptions } from "../browser";
|
import { CHROME, downloads, DownloadOptions } from "../browser";
|
||||||
import { Prefs } from "../prefs";
|
import { Prefs, PrefWatcher } from "../prefs";
|
||||||
import { PromiseSerializer } from "../pserializer";
|
import { PromiseSerializer } from "../pserializer";
|
||||||
import { filterInSitu, parsePath } from "../util";
|
import { filterInSitu, parsePath } from "../util";
|
||||||
import { BaseDownload } from "./basedownload";
|
import { BaseDownload } from "./basedownload";
|
||||||
@ -19,10 +19,27 @@ import {
|
|||||||
PAUSABLE,
|
PAUSABLE,
|
||||||
PAUSED,
|
PAUSED,
|
||||||
QUEUED,
|
QUEUED,
|
||||||
RUNNING
|
RUNNING,
|
||||||
|
RETRYING
|
||||||
} from "./state";
|
} from "./state";
|
||||||
import { Preroller } from "./preroller";
|
import { Preroller } from "./preroller";
|
||||||
|
|
||||||
|
function isRecoverable(error: string) {
|
||||||
|
switch (error) {
|
||||||
|
case "CRASH":
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case "SERVER_FAILED":
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return error.startsWith("NETWORK_");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const RETRIES = new PrefWatcher("retries", 5);
|
||||||
|
const RETRY_TIME = new PrefWatcher("retry-time", 5);
|
||||||
|
|
||||||
export class Download extends BaseDownload {
|
export class Download extends BaseDownload {
|
||||||
public manager: Manager;
|
public manager: Manager;
|
||||||
|
|
||||||
@ -34,6 +51,10 @@ export class Download extends BaseDownload {
|
|||||||
|
|
||||||
public error: string;
|
public error: string;
|
||||||
|
|
||||||
|
public dbId: number;
|
||||||
|
|
||||||
|
public deadline: number;
|
||||||
|
|
||||||
constructor(manager: Manager, options: any) {
|
constructor(manager: Manager, options: any) {
|
||||||
super(options);
|
super(options);
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
@ -86,6 +107,7 @@ export class Download extends BaseDownload {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
|
console.error("cannot resume", ex);
|
||||||
this.manager.removeManId(this.manId);
|
this.manager.removeManId(this.manId);
|
||||||
this.removeFromBrowser();
|
this.removeFromBrowser();
|
||||||
}
|
}
|
||||||
@ -182,8 +204,7 @@ export class Download extends BaseDownload {
|
|||||||
this.serverName = res.name;
|
this.serverName = res.name;
|
||||||
}
|
}
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
this.cancel();
|
this.cancelAccordingToError(res.error);
|
||||||
this.error = res.error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
@ -209,20 +230,32 @@ export class Download extends BaseDownload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async pause() {
|
async pause(retry?: boolean) {
|
||||||
if (!(PAUSABLE & this.state)) {
|
if (!(PAUSABLE & this.state)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!retry) {
|
||||||
|
this.retries = 0;
|
||||||
|
this.deadline = 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// eslint-disable-next-line no-magic-numbers
|
||||||
|
this.deadline = Date.now() + RETRY_TIME.value * 60 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.state === RUNNING && this.manId) {
|
if (this.state === RUNNING && this.manId) {
|
||||||
try {
|
try {
|
||||||
await downloads.pause(this.manId);
|
await downloads.pause(this.manId);
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
console.error("pause", ex.toString(), ex);
|
console.error("pause", ex.toString(), ex);
|
||||||
|
this.cancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.changeState(PAUSED);
|
|
||||||
|
this.changeState(retry ? RETRYING : PAUSED);
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
@ -230,6 +263,8 @@ export class Download extends BaseDownload {
|
|||||||
this.manId = 0;
|
this.manId = 0;
|
||||||
this.written = this.totalSize = 0;
|
this.written = this.totalSize = 0;
|
||||||
this.mime = this.serverName = this.browserName = "";
|
this.mime = this.serverName = this.browserName = "";
|
||||||
|
this.retries = 0;
|
||||||
|
this.deadline = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeFromBrowser() {
|
async removeFromBrowser() {
|
||||||
@ -262,6 +297,17 @@ export class Download extends BaseDownload {
|
|||||||
this.changeState(CANCELED);
|
this.changeState(CANCELED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async cancelAccordingToError(error: string) {
|
||||||
|
if (!isRecoverable(error) || ++this.retries > RETRIES.value) {
|
||||||
|
this.cancel();
|
||||||
|
this.error = error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.pause(true);
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
|
||||||
setMissing() {
|
setMissing() {
|
||||||
if (this.manId) {
|
if (this.manId) {
|
||||||
this.manager.removeManId(this.manId);
|
this.manager.removeManId(this.manId);
|
||||||
@ -318,8 +364,7 @@ export class Download extends BaseDownload {
|
|||||||
this.changeState(PAUSED);
|
this.changeState(PAUSED);
|
||||||
}
|
}
|
||||||
else if (error) {
|
else if (error) {
|
||||||
this.cancel();
|
this.cancelAccordingToError(error);
|
||||||
this.error = error;
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.changeState(RUNNING);
|
this.changeState(RUNNING);
|
||||||
@ -330,6 +375,9 @@ export class Download extends BaseDownload {
|
|||||||
if (state.paused) {
|
if (state.paused) {
|
||||||
this.changeState(PAUSED);
|
this.changeState(PAUSED);
|
||||||
}
|
}
|
||||||
|
else if (error) {
|
||||||
|
this.cancelAccordingToError(error);
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
this.cancel();
|
this.cancel();
|
||||||
this.error = error || "";
|
this.error = error || "";
|
||||||
|
@ -4,11 +4,11 @@
|
|||||||
import { EventEmitter } from "../events";
|
import { EventEmitter } from "../events";
|
||||||
import { Notification } from "../notifications";
|
import { Notification } from "../notifications";
|
||||||
import { DB } from "../db";
|
import { DB } from "../db";
|
||||||
import { QUEUED, CANCELED, RUNNING } from "./state";
|
import { QUEUED, CANCELED, RUNNING, RETRYING } from "./state";
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { Bus, Port } from "../bus";
|
import { Bus, Port } from "../bus";
|
||||||
import { sort } from "../sorting";
|
import { sort } from "../sorting";
|
||||||
import { Prefs } from "../prefs";
|
import { Prefs, PrefWatcher } from "../prefs";
|
||||||
import { _ } from "../i18n";
|
import { _ } from "../i18n";
|
||||||
import { CoalescedUpdate, mapFilterInSitu, filterInSitu } from "../util";
|
import { CoalescedUpdate, mapFilterInSitu, filterInSitu } from "../util";
|
||||||
import { PromiseSerializer } from "../pserializer";
|
import { PromiseSerializer } from "../pserializer";
|
||||||
@ -30,6 +30,9 @@ const setShelfEnabled = downloads.setShelfEnabled || function() {
|
|||||||
// ignored
|
// ignored
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const FINISH_NOTIFICATION = new PrefWatcher("finish-notification", true);
|
||||||
|
const SOUNDS = new PrefWatcher("sounds", false);
|
||||||
|
|
||||||
export class Manager extends EventEmitter {
|
export class Manager extends EventEmitter {
|
||||||
private items: Download[];
|
private items: Download[];
|
||||||
|
|
||||||
@ -49,10 +52,14 @@ export class Manager extends EventEmitter {
|
|||||||
|
|
||||||
private readonly running: Set<Download>;
|
private readonly running: Set<Download>;
|
||||||
|
|
||||||
|
private readonly retrying: Set<Download>;
|
||||||
|
|
||||||
private scheduler: Scheduler | null;
|
private scheduler: Scheduler | null;
|
||||||
|
|
||||||
private shouldReload: boolean;
|
private shouldReload: boolean;
|
||||||
|
|
||||||
|
private deadlineTimer: number;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.active = true;
|
this.active = true;
|
||||||
@ -63,11 +70,13 @@ export class Manager extends EventEmitter {
|
|||||||
AUTOSAVE_TIMEOUT, this.save.bind(this));
|
AUTOSAVE_TIMEOUT, this.save.bind(this));
|
||||||
this.dirty = new CoalescedUpdate(
|
this.dirty = new CoalescedUpdate(
|
||||||
DIRTY_TIMEOUT, this.processDirty.bind(this));
|
DIRTY_TIMEOUT, this.processDirty.bind(this));
|
||||||
|
this.processDeadlines = this.processDeadlines.bind(this);
|
||||||
this.sids = new Map();
|
this.sids = new Map();
|
||||||
this.manIds = new Map();
|
this.manIds = new Map();
|
||||||
this.ports = new Set();
|
this.ports = new Set();
|
||||||
this.scheduler = null;
|
this.scheduler = null;
|
||||||
this.running = new Set();
|
this.running = new Set();
|
||||||
|
this.retrying = new Set();
|
||||||
|
|
||||||
this.startNext = PromiseSerializer.wrapNew(1, this, this.startNext);
|
this.startNext = PromiseSerializer.wrapNew(1, this, this.startNext);
|
||||||
|
|
||||||
@ -188,14 +197,11 @@ export class Manager extends EventEmitter {
|
|||||||
this.notifiedFinished = false;
|
this.notifiedFinished = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async maybeRunFinishActions() {
|
maybeRunFinishActions() {
|
||||||
if (this.running.size) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.maybeNotifyFinished();
|
|
||||||
if (this.running.size) {
|
if (this.running.size) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.maybeNotifyFinished();
|
||||||
if (this.shouldReload) {
|
if (this.shouldReload) {
|
||||||
this.saveQueue.trigger();
|
this.saveQueue.trigger();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -208,20 +214,15 @@ export class Manager extends EventEmitter {
|
|||||||
setShelfEnabled(true);
|
setShelfEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async maybeNotifyFinished() {
|
maybeNotifyFinished() {
|
||||||
if (this.notifiedFinished || this.running.size) {
|
if (this.notifiedFinished || this.running.size || this.retrying.size) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const notification = await Prefs.get("finish-notification", true);
|
if (SOUNDS.value) {
|
||||||
const sounds = await Prefs.get("sounds", false);
|
|
||||||
if (this.notifiedFinished || this.running.size) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (sounds) {
|
|
||||||
const audio = new Audio(runtime.getURL("/style/done.opus"));
|
const audio = new Audio(runtime.getURL("/style/done.opus"));
|
||||||
audio.addEventListener("canplaythrough", () => audio.play());
|
audio.addEventListener("canplaythrough", () => audio.play());
|
||||||
}
|
}
|
||||||
if (notification) {
|
if (FINISH_NOTIFICATION.value) {
|
||||||
new Notification(null, _("queue-finished"));
|
new Notification(null, _("queue-finished"));
|
||||||
}
|
}
|
||||||
this.notifiedFinished = true;
|
this.notifiedFinished = true;
|
||||||
@ -323,6 +324,10 @@ export class Manager extends EventEmitter {
|
|||||||
if (oldState === RUNNING) {
|
if (oldState === RUNNING) {
|
||||||
this.running.delete(download);
|
this.running.delete(download);
|
||||||
}
|
}
|
||||||
|
else if (oldState === RETRYING) {
|
||||||
|
this.retrying.delete(download);
|
||||||
|
this.findDeadline();
|
||||||
|
}
|
||||||
if (newState === QUEUED) {
|
if (newState === QUEUED) {
|
||||||
this.resetScheduler();
|
this.resetScheduler();
|
||||||
this.startNext().catch(console.error);
|
this.startNext().catch(console.error);
|
||||||
@ -334,10 +339,56 @@ export class Manager extends EventEmitter {
|
|||||||
this.running.add(download);
|
this.running.add(download);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
if (newState === RETRYING) {
|
||||||
|
this.addRetry(download);
|
||||||
|
}
|
||||||
this.startNext().catch(console.error);
|
this.startNext().catch(console.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addRetry(download: Download) {
|
||||||
|
this.retrying.add(download);
|
||||||
|
this.findDeadline();
|
||||||
|
}
|
||||||
|
|
||||||
|
private findDeadline() {
|
||||||
|
let deadline = Array.from(this.retrying).
|
||||||
|
reduce<number>((deadline, item) => {
|
||||||
|
if (deadline) {
|
||||||
|
return item.deadline ? Math.min(deadline, item.deadline) : deadline;
|
||||||
|
}
|
||||||
|
return item.deadline;
|
||||||
|
}, 0);
|
||||||
|
if (deadline <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
deadline -= Date.now();
|
||||||
|
if (deadline <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.deadlineTimer) {
|
||||||
|
window.clearTimeout(this.deadlineTimer);
|
||||||
|
}
|
||||||
|
this.deadlineTimer = window.setTimeout(this.processDeadlines, deadline);
|
||||||
|
}
|
||||||
|
|
||||||
|
private processDeadlines() {
|
||||||
|
this.deadlineTimer = 0;
|
||||||
|
try {
|
||||||
|
const now = Date.now();
|
||||||
|
this.items.forEach(item => {
|
||||||
|
if (item.deadline && Math.abs(item.deadline - now) < 1000) {
|
||||||
|
this.retrying.delete(item);
|
||||||
|
item.resume(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
this.findDeadline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sorted(sids: number[]) {
|
sorted(sids: number[]) {
|
||||||
try {
|
try {
|
||||||
// Construct new items
|
// Construct new items
|
||||||
|
@ -8,8 +8,9 @@ export const PAUSED = 1 << 3;
|
|||||||
export const DONE = 1 << 4;
|
export const DONE = 1 << 4;
|
||||||
export const CANCELED = 1 << 5;
|
export const CANCELED = 1 << 5;
|
||||||
export const MISSING = 1 << 6;
|
export const MISSING = 1 << 6;
|
||||||
|
export const RETRYING = 1 << 7;
|
||||||
|
|
||||||
export const RESUMABLE = PAUSED | CANCELED;
|
export const RESUMABLE = PAUSED | CANCELED | RETRYING;
|
||||||
export const FORCABLE = PAUSED | QUEUED | CANCELED;
|
export const FORCABLE = PAUSED | QUEUED | CANCELED | RETRYING;
|
||||||
export const PAUSABLE = QUEUED | CANCELED | RUNNING;
|
export const PAUSABLE = QUEUED | CANCELED | RUNNING | RETRYING;
|
||||||
export const CANCELABLE = QUEUED | RUNNING | PAUSED | DONE | MISSING;
|
export const CANCELABLE = QUEUED | RUNNING | PAUSED | DONE | MISSING | RETRYING;
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
--add-color: navy;
|
--add-color: navy;
|
||||||
--queue-color: gray;
|
--queue-color: gray;
|
||||||
--pause-color: #ffa318;
|
--pause-color: #ffa318;
|
||||||
|
--retry-color: rgb(0, 112, 204);
|
||||||
--error-color: rgb(160, 13, 42);
|
--error-color: rgb(160, 13, 42);
|
||||||
--running-color: #aae061;
|
--running-color: #aae061;
|
||||||
--finishing-color: #57cc12;
|
--finishing-color: #57cc12;
|
||||||
|
@ -202,6 +202,23 @@ body > * {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.retrying .virtualtable-column-2 .virtualtable-icon {
|
||||||
|
color: var(--retry-color);
|
||||||
|
}
|
||||||
|
.retrying .virtualtable-column-2 .virtualtable-progress-bar {
|
||||||
|
background: var(--retry-color);
|
||||||
|
}
|
||||||
|
.retrying .virtualtable-column-2 .virtualtable-progress-undetermined {
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
45deg,
|
||||||
|
var(--retry-color),
|
||||||
|
var(--retry-color) 6px,
|
||||||
|
transparent 6px,
|
||||||
|
transparent 12px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.missing .virtualtable-column-2 .virtualtable-icon,
|
.missing .virtualtable-column-2 .virtualtable-icon,
|
||||||
.canceled .virtualtable-column-2 .virtualtable-icon {
|
.canceled .virtualtable-column-2 .virtualtable-icon {
|
||||||
color: var(--error-color);
|
color: var(--error-color);
|
||||||
|
@ -139,3 +139,10 @@ legend {
|
|||||||
background: rgba(128, 128, 128, 0.05);
|
background: rgba(128, 128, 128, 0.05);
|
||||||
box-shadow: 1px 1px 6px lightgray;
|
box-shadow: 1px 1px 6px lightgray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#network-general {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
grid-column-gap: 1em;
|
||||||
|
grid-row-gap: 1ex;
|
||||||
|
}
|
@ -10,6 +10,7 @@ export const StateTexts = locale.then(() => Object.freeze(new Map([
|
|||||||
[DownloadState.QUEUED, _("queued")],
|
[DownloadState.QUEUED, _("queued")],
|
||||||
[DownloadState.RUNNING, _("running")],
|
[DownloadState.RUNNING, _("running")],
|
||||||
[DownloadState.FINISHING, _("finishing")],
|
[DownloadState.FINISHING, _("finishing")],
|
||||||
|
[DownloadState.RETRYING, _("paused")],
|
||||||
[DownloadState.PAUSED, _("paused")],
|
[DownloadState.PAUSED, _("paused")],
|
||||||
[DownloadState.DONE, _("done")],
|
[DownloadState.DONE, _("done")],
|
||||||
[DownloadState.CANCELED, _("canceled")],
|
[DownloadState.CANCELED, _("canceled")],
|
||||||
@ -21,6 +22,7 @@ export const StateClasses = Object.freeze(new Map([
|
|||||||
[DownloadState.RUNNING, "running"],
|
[DownloadState.RUNNING, "running"],
|
||||||
[DownloadState.FINISHING, "finishing"],
|
[DownloadState.FINISHING, "finishing"],
|
||||||
[DownloadState.PAUSED, "paused"],
|
[DownloadState.PAUSED, "paused"],
|
||||||
|
[DownloadState.RETRYING, "retrying"],
|
||||||
[DownloadState.DONE, "done"],
|
[DownloadState.DONE, "done"],
|
||||||
[DownloadState.CANCELED, "canceled"],
|
[DownloadState.CANCELED, "canceled"],
|
||||||
[DownloadState.MISSING, "missing"],
|
[DownloadState.MISSING, "missing"],
|
||||||
@ -31,6 +33,7 @@ export const StateIcons = Object.freeze(new Map([
|
|||||||
[DownloadState.RUNNING, "icon-go"],
|
[DownloadState.RUNNING, "icon-go"],
|
||||||
[DownloadState.FINISHING, "icon-go"],
|
[DownloadState.FINISHING, "icon-go"],
|
||||||
[DownloadState.PAUSED, "icon-pause"],
|
[DownloadState.PAUSED, "icon-pause"],
|
||||||
|
[DownloadState.RETRYING, "icon-pause"],
|
||||||
[DownloadState.DONE, "icon-done"],
|
[DownloadState.DONE, "icon-done"],
|
||||||
[DownloadState.CANCELED, "icon-error"],
|
[DownloadState.CANCELED, "icon-error"],
|
||||||
[DownloadState.MISSING, "icon-failed"],
|
[DownloadState.MISSING, "icon-failed"],
|
||||||
|
@ -146,6 +146,8 @@ export class DownloadItem extends EventEmitter {
|
|||||||
|
|
||||||
public opening: boolean;
|
public opening: boolean;
|
||||||
|
|
||||||
|
public retries: number;
|
||||||
|
|
||||||
constructor(owner: DownloadTable, raw: any, stats?: Stats) {
|
constructor(owner: DownloadTable, raw: any, stats?: Stats) {
|
||||||
super();
|
super();
|
||||||
Object.assign(this, raw);
|
Object.assign(this, raw);
|
||||||
@ -247,6 +249,12 @@ export class DownloadItem extends EventEmitter {
|
|||||||
if (this.state === DownloadState.RUNNING) {
|
if (this.state === DownloadState.RUNNING) {
|
||||||
return this.eta;
|
return this.eta;
|
||||||
}
|
}
|
||||||
|
if (this.state === DownloadState.RETRYING) {
|
||||||
|
if (this.error) {
|
||||||
|
return _("retrying_error", _(this.error) || this.error);
|
||||||
|
}
|
||||||
|
return _("retrying");
|
||||||
|
}
|
||||||
if (this.error) {
|
if (this.error) {
|
||||||
return _(this.error) || this.error;
|
return _(this.error) || this.error;
|
||||||
}
|
}
|
||||||
|
@ -123,9 +123,14 @@
|
|||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article id="tab-network" class="tab">
|
<article id="tab-network" class="tab">
|
||||||
<fieldset>
|
<fieldset id="network-general">
|
||||||
<legend data-i18n="pref.netglobal"></legend>
|
<legend data-i18n="pref.netglobal"></legend>
|
||||||
<label><span data-i18n="pref-concurrent-downloads">Concurrent downloads</span> <input id="pref-concurrent-downloads" type="number" min="1" max="10"></label>
|
<label data-i18n="pref-concurrent-downloads">Concurrent downloads</label>
|
||||||
|
<input id="pref-concurrent-downloads" type="number" min="1" max="10">
|
||||||
|
<label data-i18n="pref-retries"></label>
|
||||||
|
<input id="pref-retries" type="number" min="0" max="100">
|
||||||
|
<label data-i18n="pref-retry-time"></label>
|
||||||
|
<input id="pref-retry-time" type="number" min="1" max="600">
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<table id="limits" data-singleselect="true">
|
<table id="limits" data-singleselect="true">
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -623,6 +623,8 @@ addEventListener("DOMContentLoaded", async () => {
|
|||||||
|
|
||||||
// Network
|
// Network
|
||||||
new IntPref("pref-concurrent-downloads", "concurrent");
|
new IntPref("pref-concurrent-downloads", "concurrent");
|
||||||
|
new IntPref("pref-retries", "retries");
|
||||||
|
new IntPref("pref-retry-time", "retry-time");
|
||||||
|
|
||||||
visible("#limits").then(() => new LimitsUI());
|
visible("#limits").then(() => new LimitsUI());
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user