diff --git a/alembic/versions/467235201c70_download_sub_downloader.py b/alembic/versions/467235201c70_download_sub_downloader.py
new file mode 100644
index 00000000..c8f11c34
--- /dev/null
+++ b/alembic/versions/467235201c70_download_sub_downloader.py
@@ -0,0 +1,29 @@
+"""Download.sub_downloader.
+
+Revision ID: 467235201c70
+Revises: 1a70196c05bb
+Create Date: 2022-05-20 11:51:58.064020
+
+"""
+import os
+
+from alembic import op
+from sqlalchemy.orm import Session
+
+# revision identifiers, used by Alembic.
+revision = '467235201c70'
+down_revision = '1a70196c05bb'
+branch_labels = None
+depends_on = None
+
+DOCKERIZED = True if os.environ.get('DOCKER', '').lower().startswith('t') else False
+
+
+def upgrade():
+ session = Session(bind=op.get_bind())
+ session.execute('ALTER TABLE download ADD COLUMN IF NOT EXISTS sub_downloader TEXT')
+
+
+def downgrade():
+ session = Session(bind=op.get_bind())
+ session.execute('ALTER TABLE download DROP COLUMN IF EXISTS sub_downloader')
diff --git a/app/src/App.js b/app/src/App.js
index 30e47f40..8ab5d1d8 100644
--- a/app/src/App.js
+++ b/app/src/App.js
@@ -11,7 +11,7 @@ import 'react-semantic-toasts/styles/react-semantic-alert.css';
import {AppsRoute} from "./components/Apps";
import {InventoryRoute} from "./components/Inventory";
import {ArchiveRoute, ArchivesList} from "./components/Archive";
-import {Saver} from "./components/Upload";
+import {Downloads, Saver} from "./components/Upload";
import {MoreButton, PageContainer, SearchInput} from "./components/Common";
import {FilesRoute} from "./components/Files";
import {useSearch, useUp, useVersion} from "./hooks/customHooks";
@@ -23,10 +23,7 @@ function Welcome() {
useUp();
let body = (
- <>
-
-
- >
+
);
if (searchStr) {
diff --git a/app/src/api.js b/app/src/api.js
index c24ef47f..cd4e3a90 100644
--- a/app/src/api.js
+++ b/app/src/api.js
@@ -445,8 +445,11 @@ export async function getArchive(archiveId) {
}
}
-export async function postDownload(urls, downloader) {
- let body = {urls: urls, downloader: downloader};
+export async function postDownload(urls, downloader, frequency, sub_downloader) {
+ let body = {urls: urls, downloader: downloader, frequency: frequency || null};
+ if (sub_downloader) {
+ body['sub_downloader'] = sub_downloader;
+ }
let response = await apiPost(`${API_URI}/download`, body);
return response;
}
diff --git a/app/src/components/Admin.js b/app/src/components/Admin.js
index 090f1fe9..9898582e 100644
--- a/app/src/components/Admin.js
+++ b/app/src/components/Admin.js
@@ -3,7 +3,8 @@ import {
Button,
Checkbox,
Confirm,
- Container, Dimmer,
+ Container,
+ Dimmer,
Divider,
Form,
Header,
@@ -337,20 +338,46 @@ function ClearFailedDownloads({callback}) {
}
class DownloadRow extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ errorModalOpen: false,
+ }
+ }
+
render() {
- let {url, frequency, last_successful_download, status, location, next_download} = this.props;
+ let {url, frequency, last_successful_download, status, location, next_download, error} = this.props;
+ const {errorModalOpen} = this.state;
+
let positive = false;
if (status === 'pending') {
positive = true;
}
+
+ const errorModal = this.setState({errorModalOpen: false})}
+ onOpen={() => this.setState({errorModalOpen: true})}
+ open={errorModalOpen}
+ trigger={}
+ >
+ Download Error
+
+ {error}
+
+
+
+
+ ;
+
return (
{textEllipsis(url, 50)}
{secondsToFrequency(frequency)}
{last_successful_download ? secondsToDate(last_successful_download) : null}
{secondsToDate(next_download)}
- {location && View}
+ {error && errorModal}{location && View}
);
}
diff --git a/app/src/components/Common.js b/app/src/components/Common.js
index bc227f3f..48bc2dae 100644
--- a/app/src/components/Common.js
+++ b/app/src/components/Common.js
@@ -221,9 +221,21 @@ export const frequencyOptions = [
{key: '90days', text: '90 Days', value: 7776000},
];
+export const rssFrequencyOptions = [
+ {key: 'once', text: 'Once', value: 0},
+ {key: 'hourly', text: 'Hourly', value: 3600},
+ {key: '3hours', text: '3 hours', value: 10800},
+ {key: '12hours', text: '12 hours', value: 43200},
+ {key: 'daily', text: 'Daily', value: 86400},
+ {key: 'weekly', text: 'Weekly', value: 604800},
+ {key: 'biweekly', text: 'Biweekly', value: 1209600},
+ {key: '30days', text: '30 Days', value: 2592000},
+ {key: '90days', text: '90 Days', value: 7776000},
+];
+
export function secondsToFrequency(seconds) {
- for (let i = 0; i < Object.keys(frequencyOptions).length; i++) {
- let d = frequencyOptions[i];
+ for (let i = 0; i < Object.keys(rssFrequencyOptions).length; i++) {
+ let d = rssFrequencyOptions[i];
if (d.value === seconds) {
return d.text;
}
diff --git a/app/src/components/Upload.js b/app/src/components/Upload.js
index 321a3e37..14250fc5 100644
--- a/app/src/components/Upload.js
+++ b/app/src/components/Upload.js
@@ -1,8 +1,9 @@
-import React from "react";
-import {Button, Dropdown, Form, Header, Segment, TextArea} from "semantic-ui-react";
+import React, {useState} from "react";
+import {Button, Form, Header, Loader, TextArea} from "semantic-ui-react";
import {getDownloaders, postDownload} from "../api";
-import {WROLModeMessage} from "./Common";
-import {Link} from "react-router-dom";
+import {frequencyOptions, rssFrequencyOptions, WROLModeMessage} from "./Common";
+import Icon from "semantic-ui-react/dist/commonjs/elements/Icon";
+import Message from "semantic-ui-react/dist/commonjs/collections/Message";
const validUrl = /^(http|https):\/\/[^ "]+$/;
@@ -13,23 +14,10 @@ class Downloader extends React.Component {
urls: '',
valid: true,
pending: false,
- downloaders: [],
- downloader: null,
+ downloader: props.downloader,
};
}
- async componentDidMount() {
- await this.fetchDownloaders();
- }
-
- fetchDownloaders = async () => {
- let {downloaders} = await getDownloaders();
- downloaders = downloaders.map((i) => {
- return {key: i.name, text: i.pretty_name || i.name, value: i.name}
- })
- this.setState({downloaders});
- }
-
submitDownload = async () => {
let {urls, downloader} = this.state;
if (urls) {
@@ -64,46 +52,265 @@ class Downloader extends React.Component {
}
render() {
- let textareaPlaceholder = 'Enter one URL per line';
let disabled = !this.state.urls || !this.state.valid || this.state.pending || !this.state.downloader;
+ const {header} = this.props;
return (
-
-
-
-
+