Merge pull request #3 from n8detar/feat/react-frontend

Downloads Page
This commit is contained in:
Sean 2022-04-22 12:30:07 -05:00 committed by GitHub
commit 9b4b75a11a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 369 additions and 135 deletions

View File

@ -134,7 +134,9 @@ chown 1000:0 /path/to/mount/point
This will match the permissions with the **UID** and **GID** of elasticsearch within the container and should fix the issue. This will match the permissions with the **UID** and **GID** of elasticsearch within the container and should fix the issue.
### Disk usage ### Disk usage
The Elasticsearch index will turn to *read only* if the disk usage of the container goes above 95% until the usage drops below 90% again. Similar to that, TubeArchivist will become all sorts of messed up when running out of disk space. There are some error messages in the logs when that happens, but it's best to make sure to have enough disk space before starting to download. The Elasticsearch index will turn to *read only* if the disk usage of the container goes above 95% until the usage drops below 90% again, you will see error messages like `disk usage exceeded flood-stage watermark`, [link](https://github.com/tubearchivist/tubearchivist#disk-usage).
Similar to that, TubeArchivist will become all sorts of messed up when running out of disk space. There are some error messages in the logs when that happens, but it's best to make sure to have enough disk space before starting to download.
## Getting Started ## Getting Started
1. Go through the **settings** page and look at the available options. Particularly set *Download Format* to your desired video quality before downloading. **Tube Archivist** downloads the best available quality by default. To support iOS or MacOS and some other browsers a compatible format must be specified. For example: 1. Go through the **settings** page and look at the available options. Particularly set *Download Format* to your desired video quality before downloading. **Tube Archivist** downloads the best available quality by default. To support iOS or MacOS and some other browsers a compatible format must be specified. For example:

View File

@ -20,6 +20,19 @@ headers = {"Authorization": "Token xxxxxxxxxx"}
response = requests.get(url, headers=headers) response = requests.get(url, headers=headers)
``` ```
## Pagination
The list views return a paginate object with the following keys:
- page_size: int current page size set in config
- page_from: int first result idx
- prev_pages: array of ints of previous pages, if available
- current_page: int current page from query
- max_hits: reached: bool if max of 10k results is reached
- last_page: int of last page link
- next_pages: array of ints of next pages
- total_hits: int total results
Pass page number as a query parameter: `page=2`. Defaults to *0*, `page=1` is redundant and falls back to *0*. If a page query doesn't return any results, you'll get `HTTP 404 Not Found`.
## Login View ## Login View
Return token and user ID for username and password: Return token and user ID for username and password:
POST /api/login POST /api/login

View File

@ -3,6 +3,7 @@
from api.src.search_processor import SearchProcess from api.src.search_processor import SearchProcess
from home.src.download.queue import PendingInteract from home.src.download.queue import PendingInteract
from home.src.es.connect import ElasticWrap from home.src.es.connect import ElasticWrap
from home.src.index.generic import Pagination
from home.src.index.video import SponsorBlock from home.src.index.video import SponsorBlock
from home.src.ta.config import AppConfig from home.src.ta.config import AppConfig
from home.src.ta.helper import UrlListParser from home.src.ta.helper import UrlListParser
@ -25,12 +26,15 @@ class ApiBaseView(APIView):
authentication_classes = [SessionAuthentication, TokenAuthentication] authentication_classes = [SessionAuthentication, TokenAuthentication]
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
search_base = False search_base = False
data = False
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.response = {"data": False, "config": AppConfig().config} self.response = {"data": False, "config": AppConfig().config}
self.data = {"query": {"match_all": {}}}
self.status_code = False self.status_code = False
self.context = False self.context = False
self.pagination_handler = False
def get_document(self, document_id): def get_document(self, document_id):
"""get single document from es""" """get single document from es"""
@ -44,20 +48,33 @@ class ApiBaseView(APIView):
self.response["data"] = False self.response["data"] = False
self.status_code = status_code self.status_code = status_code
def get_paginate(self): def initiate_pagination(self, request):
"""add pagination detail to response""" """set initial pagination values"""
self.response["paginate"] = False user_id = request.user.id
page_get = int(request.GET.get("page", 0))
self.pagination_handler = Pagination(page_get, user_id)
self.data.update(
{
"size": self.pagination_handler.pagination["page_size"],
"from": self.pagination_handler.pagination["page_from"],
}
)
def get_document_list(self, data): def get_document_list(self, request):
"""get a list of results""" """get a list of results"""
print(self.search_base) print(self.search_base)
response, status_code = ElasticWrap(self.search_base).get(data=data) self.initiate_pagination(request)
es_handler = ElasticWrap(self.search_base)
response, status_code = es_handler.get(data=self.data)
self.response["data"] = SearchProcess(response).process() self.response["data"] = SearchProcess(response).process()
if self.response["data"]: if self.response["data"]:
self.status_code = status_code self.status_code = status_code
else: else:
self.status_code = 404 self.status_code = 404
self.pagination_handler.validate(response["hits"]["total"]["value"])
self.response["paginate"] = self.pagination_handler.pagination
class VideoApiView(ApiBaseView): class VideoApiView(ApiBaseView):
"""resolves to /api/video/<video_id>/ """resolves to /api/video/<video_id>/
@ -81,11 +98,9 @@ class VideoApiListView(ApiBaseView):
search_base = "ta_video/_search/" search_base = "ta_video/_search/"
def get(self, request): def get(self, request):
# pylint: disable=unused-argument
"""get request""" """get request"""
data = {"query": {"match_all": {}}} self.data.update({"sort": [{"published": {"order": "desc"}}]})
self.get_document_list(data) self.get_document_list(request)
self.get_paginate()
return Response(self.response) return Response(self.response)
@ -200,11 +215,11 @@ class ChannelApiListView(ApiBaseView):
search_base = "ta_channel/_search/" search_base = "ta_channel/_search/"
def get(self, request): def get(self, request):
# pylint: disable=unused-argument
"""get request""" """get request"""
data = {"query": {"match_all": {}}} self.get_document_list(request)
self.get_document_list(data) self.data.update(
self.get_paginate() {"sort": [{"channel_name.keyword": {"order": "asc"}}]}
)
return Response(self.response) return Response(self.response)
@ -234,13 +249,16 @@ class ChannelApiVideoView(ApiBaseView):
search_base = "ta_video/_search/" search_base = "ta_video/_search/"
def get(self, request, channel_id): def get(self, request, channel_id):
# pylint: disable=unused-argument
"""handle get request""" """handle get request"""
data = { self.data.update(
"query": {"term": {"channel.channel_id": {"value": channel_id}}} {
} "query": {
self.get_document_list(data) "term": {"channel.channel_id": {"value": channel_id}}
self.get_paginate() },
"sort": [{"published": {"order": "desc"}}],
}
)
self.get_document_list(request)
return Response(self.response, status=self.status_code) return Response(self.response, status=self.status_code)
@ -253,11 +271,11 @@ class PlaylistApiListView(ApiBaseView):
search_base = "ta_playlist/_search/" search_base = "ta_playlist/_search/"
def get(self, request): def get(self, request):
# pylint: disable=unused-argument
"""handle get request""" """handle get request"""
data = {"query": {"match_all": {}}} self.data.update(
self.get_document_list(data) {"sort": [{"playlist_name.keyword": {"order": "asc"}}]}
self.get_paginate() )
self.get_document_list(request)
return Response(self.response) return Response(self.response)
@ -283,13 +301,13 @@ class PlaylistApiVideoView(ApiBaseView):
search_base = "ta_video/_search/" search_base = "ta_video/_search/"
def get(self, request, playlist_id): def get(self, request, playlist_id):
# pylint: disable=unused-argument
"""handle get request""" """handle get request"""
data = { self.data["query"] = {
"query": {"term": {"playlist.keyword": {"value": playlist_id}}} "term": {"playlist.keyword": {"value": playlist_id}}
} }
self.get_document_list(data) self.data.update({"sort": [{"published": {"order": "desc"}}]})
self.get_paginate()
self.get_document_list(request)
return Response(self.response, status=self.status_code) return Response(self.response, status=self.status_code)
@ -344,23 +362,18 @@ class DownloadApiListView(ApiBaseView):
valid_filter = ["pending", "ignore"] valid_filter = ["pending", "ignore"]
def get(self, request): def get(self, request):
# pylint: disable=unused-argument
"""get request""" """get request"""
query_filter = request.GET.get("filter", False) query_filter = request.GET.get("filter", False)
data = { self.data.update({"sort": [{"timestamp": {"order": "asc"}}]})
"query": {"match_all": {}},
"sort": [{"timestamp": {"order": "asc"}}],
}
if query_filter: if query_filter:
if query_filter not in self.valid_filter: if query_filter not in self.valid_filter:
message = f"invalid url query filder: {query_filter}" message = f"invalid url query filder: {query_filter}"
print(message) print(message)
return Response({"message": message}, status=400) return Response({"message": message}, status=400)
data["query"] = {"term": {"status": {"value": query_filter}}} self.data["query"] = {"term": {"status": {"value": query_filter}}}
self.get_document_list(data) self.get_document_list(request)
self.get_paginate()
return Response(self.response) return Response(self.response)
@staticmethod @staticmethod

View File

@ -147,3 +147,4 @@ class Pagination:
] ]
self.pagination["next_pages"] = next_pages self.pagination["next_pages"] = next_pages
self.pagination["total_hits"] = total_hits

View File

@ -195,6 +195,11 @@ class SubtitleParser:
if flatten: if flatten:
# fix overlapping retiming issue # fix overlapping retiming issue
if "dDurationMs" not in flatten[-1]:
# some events won't have a duration
print(f"failed to parse event without duration: {event}")
continue
last_end = flatten[-1]["tStartMs"] + flatten[-1]["dDurationMs"] last_end = flatten[-1]["tStartMs"] + flatten[-1]["dDurationMs"]
if event["tStartMs"] < last_end: if event["tStartMs"] < last_end:
joined = flatten[-1]["segs"][0]["utf8"] + "\n" + text joined = flatten[-1]["segs"][0]["utf8"] + "\n" + text

90
tubearchivist/www/src/lib/getDownloads.ts Normal file → Executable file
View File

@ -1,9 +1,10 @@
import { Download } from "../types/download"; import { Download } from "../types/download";
import { DownloadResponse } from "../types/download"; import { getTAUrl } from "./constants";
import { TA_BASE_URL } from "./constants";
export const getDownloads = async (token: string): Promise<Download> => { const TA_BASE_URL = getTAUrl();
const response = await fetch(`${TA_BASE_URL}/api/download/`, {
export const getDownloads = async (token: string, filter: boolean, pageNumber: number): Promise<Download> => {
const response = await fetch(`${TA_BASE_URL.server}/api/download/?filter=${filter ? 'ignore' : 'pending'}&page=${pageNumber}`, {
headers: { headers: {
Accept: "application/json", Accept: "application/json",
"Content-Type": "application/json", "Content-Type": "application/json",
@ -11,20 +12,87 @@ export const getDownloads = async (token: string): Promise<Download> => {
mode: "no-cors", mode: "no-cors",
}, },
}); });
if (!response.ok) { if (response.ok) {
throw new Error("Error getting download queue information"); return response.json();
} else {
// var error: Download = {
// data: null,
// config: null,
// paginate: null,
// message: response.statusText + " (" + response.status + ")",
// }
// error = response.statusText + " (" + response.status + ");
throw new Error(response.statusText + " (" + response.status + ")");
// return error;
} }
return response.json();
}; };
export const sendDownloads = async (token: string, input: string): Promise<DownloadResponse> => { export const sendDownloads = async (token: string, input: string): Promise<Download> => {
var data = { var data = {
"data": [{ "data": [{
"youtube_id": input, "youtube_id": input,
"status": "pending" "status": "pending"
}] }]
}; };
const response = await fetch(`${TA_BASE_URL}/api/download/`, { const response = await fetch(`${TA_BASE_URL.server}/api/download/`, {
body: JSON.stringify(data),
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Token ${token}`,
mode: "no-cors",
},
method: "POST"
});
if (response.ok) {
return response.json();
} else if (response.status == 400) {
throw new Error("Failed to extract links. Please input IDs or URLs for videos, channels, or playlists.");
} else {
throw new Error("Failed to connect to the API.");
}
};
export const sendDeleteAllQueuedIgnored = async (token: string, filter: string): Promise<Download> => {
const response = await fetch(`${TA_BASE_URL.server}/api/download/?filter=${filter}`, {
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Token ${token}`,
mode: "no-cors",
},
method: "DELETE"
});
if (!response.ok) {
throw new Error("Error removing all videos.");
// return response.json();
}
return response.json();
};
export const sendDeleteVideoQueuedIgnored = async (token: string, videoId: string): Promise<Download> => {
const response = await fetch(`${TA_BASE_URL.server}/api/download/${videoId}/`, {
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Token ${token}`,
mode: "no-cors",
},
method: "DELETE"
});
if (!response.ok) {
throw new Error("Error removing video.");
// return response.json();
}
return response.json();
};
export const sendMoveVideoQueuedIgnored = async (token: string, videoId: string, status: string): Promise<Download> => {
var data = {
"status": status
};
const response = await fetch(`${TA_BASE_URL.server}/api/download/${videoId}/`, {
body: JSON.stringify(data), body: JSON.stringify(data),
headers: { headers: {
Accept: "application/json", Accept: "application/json",
@ -35,7 +103,7 @@ export const sendDownloads = async (token: string, input: string): Promise<Downl
method: "POST" method: "POST"
}); });
if (!response.ok) { if (!response.ok) {
throw new Error("Error adding content to the download queue."); throw new Error("Error moving video to" + status + ".");
} }
return response.json(); return response.json();
}; };

View File

@ -6,6 +6,7 @@ import { CustomHead } from "../components/CustomHead";
import { Layout } from "../components/Layout"; import { Layout } from "../components/Layout";
import { getTAUrl } from "../lib/constants"; import { getTAUrl } from "../lib/constants";
import { getChannels } from "../lib/getChannels"; import { getChannels } from "../lib/getChannels";
import { formatNumbers } from "../lib/utils";
const TA_BASE_URL = getTAUrl(); const TA_BASE_URL = getTAUrl();
@ -170,7 +171,7 @@ const Channel: NextPage = () => {
</a> </a>
</h3> </h3>
{/* {% if channel.source.channel_subs >= 1000000 %} */} {/* {% if channel.source.channel_subs >= 1000000 %} */}
<p>Subscribers: {channel?.channel_subs} </p> <p>Subscribers: {formatNumbers(channel?.channel_subs)} </p>
{/* {% else %} */} {/* {% else %} */}
</div> </div>
</div> </div>
@ -184,20 +185,20 @@ const Channel: NextPage = () => {
className="unsubscribe" className="unsubscribe"
type="button" type="button"
id="{{ channel.source.channel_id }}" id="{{ channel.source.channel_id }}"
onClick={() => console.log("unsubscribe(this.id)")} onClick={() => console.log("unsubscribe(this.id) -> toggleSubscribe()")}
title="Unsubscribe from {{ channel.source.channel_name }}" title={`${channel?.channel_subscribed ? "Unsubscribe from" : "Subscribe to"} ${channel?.channel_name}`}
> >
Unsubscribe {channel?.channel_subscribed ? "Unsubscribe" : "Subscribe"}
</button> </button>
{/* {% else %} */} {/* {% else %} */}
<button {/* <button
type="button" type="button"
id="{{ channel.source.channel_id }}" id="{{ channel.source.channel_id }}"
onClick={() => console.log("subscribe(this.id)")} onClick={() => console.log("subscribe(this.id)")}
title="Subscribe to {{ channel.source.channel_name }}" title="Subscribe to {{ channel.source.channel_name }}"
> >
Subscribe Subscribe
</button> </button> */}
{/* {% endif %} */} {/* {% endif %} */}
</div> </div>
</div> </div>

View File

@ -5,20 +5,24 @@ import { dehydrate, QueryClient, useQuery } from "react-query";
import { CustomHead } from "../components/CustomHead"; import { CustomHead } from "../components/CustomHead";
import { Layout } from "../components/Layout"; import { Layout } from "../components/Layout";
import NextImage from "next/image"; import NextImage from "next/image";
import { TA_BASE_URL } from "../lib/constants"; import { getDownloads, sendDeleteAllQueuedIgnored, sendDeleteVideoQueuedIgnored, sendMoveVideoQueuedIgnored } from "../lib/getDownloads";
import { getDownloads } from "../lib/getDownloads";
import { sendDownloads } from "../lib/getDownloads"; import { sendDownloads } from "../lib/getDownloads";
import RescanIcon from "../images/icon-rescan.svg"; import RescanIcon from "../images/icon-rescan.svg";
import DownloadIcon from "../images/icon-download.svg"; import DownloadIcon from "../images/icon-download.svg";
import AddIcon from "../images/icon-add.svg"; import AddIcon from "../images/icon-add.svg";
import GridViewIcon from "../images/icon-gridview.svg"; import GridViewIcon from "../images/icon-gridview.svg";
import ListViewIcon from "../images/icon-listview.svg"; import ListViewIcon from "../images/icon-listview.svg";
import StopIcon from "../images/icon-stop.svg";
import CloseIcon from "../images/icon-close.svg";
import { getTAUrl } from "../lib/constants";
const TA_BASE_URL = getTAUrl();
type ViewStyle = "grid" | "list"; type ViewStyle = "grid" | "list";
type IgnoredStatus = boolean; type IgnoredStatus = boolean;
type FormHidden = boolean; type FormHidden = boolean;
type ErrorMessage = string;
type PageNumber = number;
export const getServerSideProps: GetServerSideProps = async (context) => { export const getServerSideProps: GetServerSideProps = async (context) => {
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@ -33,8 +37,8 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
}; };
} }
await queryClient.prefetchQuery(["downloads", session.ta_token.token], () => await queryClient.prefetchQuery(["downloads", session.ta_token.token, false, 1], () =>
getDownloads(session.ta_token.token) getDownloads(session.ta_token.token, false, 1)
); );
return { return {
@ -47,40 +51,77 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
const Download: NextPage = () => { const Download: NextPage = () => {
const { data: session } = useSession(); const { data: session } = useSession();
const [ignoredStatus, setIgnoredStatus] = useState<IgnoredStatus>(false);
const [pageNumber, setPageNumber] = useState<PageNumber>(1);
const { const {
data: downloads, data: downloads,
error, error,
isLoading, isLoading,
refetch,
} = useQuery( } = useQuery(
["downloads", session.ta_token.token], ["downloads", session.ta_token.token, ignoredStatus, pageNumber],
() => getDownloads(session.ta_token.token), () => getDownloads(session.ta_token.token, ignoredStatus, pageNumber),
{ {
enabled: !!session?.ta_token?.token, enabled: !!session?.ta_token?.token,
refetchInterval: 1500,
refetchIntervalInBackground: false,
} }
); );
var count = 0;
const [viewStyle, setViewStyle] = useState<ViewStyle>(downloads?.config?.default_view?.downloads);
const [ignoredStatus, setIgnoredStatus] = useState<IgnoredStatus>(false);
const [formHidden, setFormHidden] = useState<FormHidden>(true); const [formHidden, setFormHidden] = useState<FormHidden>(true);
const [viewStyle, setViewStyle] = useState<ViewStyle>(downloads?.config?.default_view?.downloads);
const [errorMessage, setErrorMessage] = useState<ErrorMessage>(null);
const handleSetViewstyle = (selectedViewStyle: ViewStyle) => { const handleSetViewstyle = (selectedViewStyle: ViewStyle) => {
setViewStyle(selectedViewStyle); setViewStyle(selectedViewStyle);
}; };
const handleSetIgnoredStatus = (selectedIgnoredStatus: IgnoredStatus) => { const handleSetIgnoredStatus = (selectedIgnoredStatus: IgnoredStatus) => {
setIgnoredStatus(selectedIgnoredStatus); setIgnoredStatus(selectedIgnoredStatus);
refetch();
handleSetPageNumber(1);
}; };
const handleSetFormHidden = (selectedFormHidden: FormHidden) => { const handleSetFormHidden = (selectedFormHidden: FormHidden) => {
setFormHidden(selectedFormHidden); setFormHidden(selectedFormHidden);
}; };
const handleSetErrorMessage = (selectedErrorMessage: ErrorMessage) => {
setErrorMessage(selectedErrorMessage);
};
const handleSetPageNumber = (selectedPageNumber: PageNumber) => {
setPageNumber(selectedPageNumber);
};
const addToDownloadQueue = event => { const addToDownloadQueue = event => {
event.preventDefault(); event.preventDefault();
sendDownloads(session.ta_token.token, event.target.vid_url.value); sendDownloads(session.ta_token.token, event.target.vid_url.value).then(() => {
handleSetFormHidden(true); handleSetErrorMessage(null);
handleSetFormHidden(true);
})
.catch(error => handleSetErrorMessage(error.message));
}
const handleMoveVideoQueuedIgnored = (session: string, youtube_id: string, status: string) => {
sendMoveVideoQueuedIgnored(session, youtube_id, status).then(() => {
handleSetErrorMessage(null);
})
.catch(error => handleSetErrorMessage(error.message));
}
const handleDeleteVideoQueuedIgnored = (session: string, youtube_id: string) => {
sendDeleteVideoQueuedIgnored(session, youtube_id).then(() => {
handleSetErrorMessage(null);
})
.catch(error => handleSetErrorMessage(error.message));
}
const handleDeleteAllQueuedIgnored = (session: string, filter: string) => {
sendDeleteAllQueuedIgnored(session, filter).then(() => {
handleSetErrorMessage(null);
})
.catch(error => handleSetErrorMessage(error.message));
} }
return ( return (
@ -92,8 +133,66 @@ const Download: NextPage = () => {
<div className="title-bar"> <div className="title-bar">
<h1>Downloads</h1> <h1>Downloads</h1>
</div> </div>
<div id="notifications"></div> <div id="notifications">
<div id="downloadControl"></div> {(error || !downloads?.data) && !isLoading &&
<div className="error notification">
<h3>API Connection Error</h3>
<p></p>
</div>
}
{errorMessage &&
<div className="error notification">
<h3>Failed to add downloads to the queue.</h3>
<p>{errorMessage}</p>
</div>
}
{
// <div className="info notification">
// <h3>Adding new videos to download queue.</h3>
// <p>Extracting lists</p>
// <p>Progress: 0/0</p>
// </div>
}
{
// <div className="info notification">
// <h3>Rescanning channels and playlists.</h3>
// <p>Looking for new videos.</p>
// </div>
}
{
// <div className="info notification">
// <h3>Downloading: `VIDEO_TITLE`</h3>
// <p>processing</p>
// <p>`DOWNLOADED_PERCENTAGE`% of `VIDEO_SIVE``VIDEO_SIZE_UNIT` at `DOWNLOAD_SPEED``DOWNLOAD_SPEED_UNIT` - time left: `DOWNLOAD_TIME_LEFT`</p>
// <p>processing</p>
// <p>Moving</p>
// <p>Completed</p>
// </div>
}
</div>
<div id="downloadControl">
{/* Appears when video is downloading */}
{/* <div className="dl-control-icons">
<NextImage
width={30}
height={30}
src={StopIcon}
alt="stop icon"
title="Stop Download Queue"
id="stop-icon"
onClick={() => console.log("stopQueue()")}
/>
<NextImage
width={30}
height={30}
src={CloseIcon}
alt="kill icon"
title="Kill Download Queue"
id="kill-icon"
onClick={() => console.log("killQueue()")}
/>
</div> */}
</div>
<div className="info-box info-box-3"> <div className="info-box info-box-3">
<div className="icon-text"> <div className="icon-text">
<NextImage <NextImage
@ -102,6 +201,7 @@ const Download: NextPage = () => {
src={RescanIcon} src={RescanIcon}
alt="rescan-icon" alt="rescan-icon"
title="Rescan subscriptions" title="Rescan subscriptions"
// className="rotate-img" // Set when rescanning
onClick={() => console.log("rescanPending()")} onClick={() => console.log("rescanPending()")}
/> />
{/* <img id="rescan-icon" onclick="rescanPending()" src="{% static 'img/icon-rescan.svg' %}" alt="rescan-icon"></img> */} {/* <img id="rescan-icon" onclick="rescanPending()" src="{% static 'img/icon-rescan.svg' %}" alt="rescan-icon"></img> */}
@ -114,6 +214,7 @@ const Download: NextPage = () => {
src={DownloadIcon} src={DownloadIcon}
alt="download-icon" alt="download-icon"
title="Start download" title="Start download"
// className="bounce-img" // Set when video is downloading
onClick={() => console.log("dlPending()")} onClick={() => console.log("dlPending()")}
/> />
{/* <img id="download-icon" onclick="dlPending()" src="{% static 'img/icon-download.svg' %}" alt="download-icon"></img> */} {/* <img id="download-icon" onclick="dlPending()" src="{% static 'img/icon-download.svg' %}" alt="download-icon"></img> */}
@ -142,80 +243,63 @@ const Download: NextPage = () => {
<div className="view-controls"> <div className="view-controls">
<div className="toggle"> <div className="toggle">
<span>Show only ignored videos:</span> <span>Show only ignored videos:</span>
{ignoredStatus && <div className="toggleBox">
<div className="toggleBox"> <input
<input id="show_ignored_only"
id="show_ignored_only" onChange={() => handleSetIgnoredStatus(!ignoredStatus)}
onChange={() => handleSetIgnoredStatus(false)} type="checkbox"
type="checkbox" checked={ignoredStatus}
checked />
/> <label htmlFor="" className={ignoredStatus ? "onbtn" : "ofbtn"}>
<label htmlFor="" className="onbtn"> {ignoredStatus ? "On" : "Off"}
On </label>
</label> </div>
</div>
}
{!ignoredStatus &&
<div className="toggleBox">
<input
id="show_ignored_only"
onChange={() => handleSetIgnoredStatus(true)}
type="checkbox"
/>
<label htmlFor="" className="ofbtn">
Off
</label>
</div>
}
</div> </div>
<div className="view-icons"> <div className="view-icons">
<NextImage <div className="view-icons-margin">
width={30} <NextImage
height={34} width={30}
src={GridViewIcon} height={34}
alt="grid view" src={GridViewIcon}
title="Switch to grid view" alt="grid view"
onClick={() => handleSetViewstyle("grid")} title="Switch to grid view"
/> onClick={() => handleSetViewstyle("grid")}
/>
</div>
{/* <img src="{% static 'img/icon-gridview.svg' %}" onclick="changeView(this)" data-origin="downloads" data-value="grid" alt="grid view"> */} {/* <img src="{% static 'img/icon-gridview.svg' %}" onclick="changeView(this)" data-origin="downloads" data-value="grid" alt="grid view"> */}
<NextImage <div className="view-icons-margin">
width={30} <NextImage
height={34} width={30}
src={ListViewIcon} height={34}
alt="list view" src={ListViewIcon}
title="Switch to list view" alt="list view"
onClick={() => handleSetViewstyle("list")} title="Switch to list view"
/> onClick={() => handleSetViewstyle("list")}
/>
</div>
{/* <img src="{% static 'img/icon-listview.svg' %}" onclick="changeView(this)" data-origin="downloads" data-value="list" alt="list view"> */} {/* <img src="{% static 'img/icon-listview.svg' %}" onclick="changeView(this)" data-origin="downloads" data-value="list" alt="list view"> */}
</div> </div>
</div> </div>
{ignoredStatus && {ignoredStatus &&
<div className="title-split"> <div className="title-split">
<h2>Ignored from download</h2> <h2>Ignored from download</h2>
<button onClick={() => console.log("deleteQueue(this)")} data-id="ignore" title="Delete all previously ignored videos from the queue">Delete all ignored</button> <button onClick={() => handleDeleteAllQueuedIgnored(session.ta_token.token, "ignore")} title="Remove all ignored videos.">Remove all ignored</button>
</div> </div>
} }
{!ignoredStatus && {!ignoredStatus &&
<div className="title-split"> <div className="title-split">
<h2>Download queue</h2> <h2>Download queue</h2>
<button onClick={() => console.log("deleteQueue(this)")} data-id="pending" title="Delete all pending videos from the queue">Delete all queued</button> <button onClick={() => handleDeleteAllQueuedIgnored(session.ta_token.token, "pending")} title="Remove all videos from the queue.">Remove all queued</button>
</div> </div>
} }
{downloads?.data?.forEach((video) => { <h3>Total videos: {downloads?.paginate?.total_hits} {!downloads && !downloads?.message && !ignoredStatus && <p>No videos queued for download. Press rescan subscriptions to check if there are any new videos.</p>}</h3>
if ((video?.status == "ignore" && ignoredStatus) || (video?.status == "pending" && !ignoredStatus)) { <div className={`dl-list ${viewStyle}`}>
count++; {!isLoading && !error && !downloads?.message &&
}
})}
<h3>Total videos: {count} {!count && <p>No videos queued for download. Press rescan subscriptions to check if there are any new videos.</p>}</h3>
<div className={`dl-list ${viewStyle}`}>
{downloads.data &&
downloads?.data?.map((video) => { downloads?.data?.map((video) => {
count++;
if ((video?.status == "ignore" && ignoredStatus) || (video?.status == "pending" && !ignoredStatus)) {
return ( return (
<div key={video?.youtube_id} className={`dl-item ${viewStyle}`}> <div key={video?.youtube_id} className={`dl-item ${viewStyle}`}>
<div className={`dl-thumb ${viewStyle}`}> <div className={`dl-thumb ${viewStyle}`}>
<img src={`${TA_BASE_URL}${video?.vid_thumb_url}`} alt="video_thumb"></img> <img src={`${TA_BASE_URL.server}${video?.vid_thumb_url}`} alt="video_thumb"></img>
{ignoredStatus && <span>ignored</span>} {ignoredStatus && <span>ignored</span>}
{/* {% if show_ignored_only %} */} {/* {% if show_ignored_only %} */}
{/* <span>ignored</span> */} {/* <span>ignored</span> */}
@ -237,8 +321,8 @@ const Download: NextPage = () => {
{/* <p>Published: {{ video.source.published }} | Duration: {{ video.source.duration }} | {{ video.source.youtube_id }}</p> */} {/* <p>Published: {{ video.source.published }} | Duration: {{ video.source.duration }} | {{ video.source.youtube_id }}</p> */}
{ignoredStatus && {ignoredStatus &&
<div> <div>
<button data-id={`${video?.youtube_id}`} onClick={() => console.log("forgetIgnore(this)")}>Forget</button> <button className="button-padding" title="Move this video to the download queue." onClick={() => handleMoveVideoQueuedIgnored(session.ta_token.token, video?.youtube_id, "pending")}>Add to queue</button>
<button data-id={`${video?.youtube_id}`} onClick={() => console.log("addSingle(this)")}>Add to queue</button> <button className="button-padding" title="Remove this video from the ignored list." onClick={() => handleDeleteVideoQueuedIgnored(session.ta_token.token, video?.youtube_id)}>Remove</button>
</div> </div>
} }
{/* {% if show_ignored_only %} */} {/* {% if show_ignored_only %} */}
@ -246,8 +330,9 @@ const Download: NextPage = () => {
{/* <button data-id="{{ video.source.youtube_id }}" onclick="addSingle(this)">Add to queue</button> */} {/* <button data-id="{{ video.source.youtube_id }}" onclick="addSingle(this)">Add to queue</button> */}
{!ignoredStatus && {!ignoredStatus &&
<div> <div>
<button data-id={`${video?.youtube_id}`} onClick={() => console.log("toIgnore(this)")}>Ignore</button> <button className="button-padding" title="Ignore this video." onClick={() => handleMoveVideoQueuedIgnored(session.ta_token.token, video?.youtube_id, "ignore")}>Ignore</button>
<button id={`${video?.youtube_id}`} data-id={`${video?.youtube_id}`} onClick={() => console.log("downloadNow(this)")}>Download now</button> <button className="button-padding" title="Download this video now." onClick={() => console.log("downloadNow(this)")}>Download now</button>
<button className="button-padding" title="Remove this video from the queue." onClick={() => handleDeleteVideoQueuedIgnored(session.ta_token.token, video?.youtube_id)}>Remove</button>
</div> </div>
} }
{/* {% else %} */} {/* {% else %} */}
@ -257,7 +342,6 @@ const Download: NextPage = () => {
</div> </div>
</div> </div>
); );
}
}) })
} }
{/* {% if results %} */} {/* {% if results %} */}
@ -290,7 +374,29 @@ const Download: NextPage = () => {
{/* </div> */} {/* </div> */}
{/* {% endfor %} */} {/* {% endfor %} */}
{/* {% endif %} */} {/* {% endif %} */}
</div>
</div>
<div className="boxed-content">
<div className="pagination">
{pageNumber != 1 ? <a className="pagination-item" onClick={() => handleSetPageNumber(1)}>First</a> : ``}
{downloads?.paginate?.prev_pages &&
downloads?.paginate?.prev_pages?.map((page) => {
return (
<a key={`${page}`} className="pagination-item" onClick={() => handleSetPageNumber(page)}>{page}</a>
)})
}
{pageNumber != 1 && <span> &lt; </span> }
<span> Page {pageNumber}</span>
{downloads?.paginate?.last_page && <span> &gt; </span> }
{downloads?.paginate?.next_pages &&
downloads?.paginate?.next_pages?.map((page) => {
return (
<a key={`${page}`} className="pagination-item" onClick={() => handleSetPageNumber(page)}>{page}</a>
)})
}
{downloads?.paginate?.last_page &&
<a className="pagination-item" onClick={() => handleSetPageNumber(downloads?.paginate?.last_page)}> Last ({downloads?.paginate?.last_page}) </a>
}
</div> </div>
</div> </div>
{/* <script type="text/javascript" src="{% static 'progress.js' %}"></script> */} {/* <script type="text/javascript" src="{% static 'progress.js' %}"></script> */}

View File

@ -98,6 +98,10 @@ textarea {
width: 100%; width: 100%;
} }
.button-padding {
margin: 0 4px 0 0;
}
button { button {
border-radius: 0; border-radius: 0;
padding: 5px 13px; padding: 5px 13px;
@ -326,6 +330,13 @@ button:hover {
justify-content: flex-end; justify-content: flex-end;
} }
.view-icons-margin {
/* width: 30px; */
margin: 5px 10px;
/* cursor: pointer;
filter: var(--img-filter); */
}
.view-icons img { .view-icons img {
width: 30px; width: 30px;
margin: 5px 10px; margin: 5px 10px;
@ -516,6 +527,8 @@ button:hover {
.pagination-item { .pagination-item {
padding: 5px; padding: 5px;
margin-right: 4px;
cursor: pointer;
border: 1px solid; border: 1px solid;
} }

View File

@ -1,14 +1,26 @@
export interface Download { export interface Download {
data: Datum[]; data: Datum[];
config: Config; config: Config;
paginate: boolean; paginate: Paginate;
}
export interface DownloadResponse {
data: Datum[];
message: string; message: string;
} }
export interface Paginate {
page_size: number;
page_from: number;
prev_pages: number[];
current_page: number;
max_hits: boolean;
last_page: number;
next_pages: number[];
total_hits: number;
}
// export interface DownloadResponse {
// data: Datum[];
// message: string;
// }
export interface Config { export interface Config {
archive: Archive; archive: Archive;
default_view: DefaultView; default_view: DefaultView;