mirror of
https://github.com/tubearchivist/tubearchivist-frontend.git
synced 2024-11-22 03:40:14 +00:00
commit
9b4b75a11a
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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": {
|
||||||
|
"term": {"channel.channel_id": {"value": channel_id}}
|
||||||
|
},
|
||||||
|
"sort": [{"published": {"order": "desc"}}],
|
||||||
}
|
}
|
||||||
self.get_document_list(data)
|
)
|
||||||
self.get_paginate()
|
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
|
||||||
|
@ -147,3 +147,4 @@ class Pagination:
|
|||||||
]
|
]
|
||||||
|
|
||||||
self.pagination["next_pages"] = next_pages
|
self.pagination["next_pages"] = next_pages
|
||||||
|
self.pagination["total_hits"] = total_hits
|
||||||
|
@ -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
|
||||||
|
88
tubearchivist/www/src/lib/getDownloads.ts
Normal file → Executable file
88
tubearchivist/www/src/lib/getDownloads.ts
Normal file → Executable 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();
|
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;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
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();
|
||||||
};
|
};
|
@ -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>
|
||||||
|
@ -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,23 +51,28 @@ 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);
|
||||||
@ -71,16 +80,48 @@ const Download: NextPage = () => {
|
|||||||
|
|
||||||
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(() => {
|
||||||
|
handleSetErrorMessage(null);
|
||||||
handleSetFormHidden(true);
|
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,33 +243,20 @@ 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(false)}
|
onChange={() => handleSetIgnoredStatus(!ignoredStatus)}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked
|
checked={ignoredStatus}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="" className="onbtn">
|
<label htmlFor="" className={ignoredStatus ? "onbtn" : "ofbtn"}>
|
||||||
On
|
{ignoredStatus ? "On" : "Off"}
|
||||||
</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">
|
||||||
|
<div className="view-icons-margin">
|
||||||
<NextImage
|
<NextImage
|
||||||
width={30}
|
width={30}
|
||||||
height={34}
|
height={34}
|
||||||
@ -177,7 +265,9 @@ const Download: NextPage = () => {
|
|||||||
title="Switch to grid view"
|
title="Switch to grid view"
|
||||||
onClick={() => handleSetViewstyle("grid")}
|
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"> */}
|
||||||
|
<div className="view-icons-margin">
|
||||||
<NextImage
|
<NextImage
|
||||||
width={30}
|
width={30}
|
||||||
height={34}
|
height={34}
|
||||||
@ -186,36 +276,30 @@ const Download: NextPage = () => {
|
|||||||
title="Switch to list view"
|
title="Switch to list view"
|
||||||
onClick={() => handleSetViewstyle("list")}
|
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)) {
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
<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}`}>
|
<div className={`dl-list ${viewStyle}`}>
|
||||||
{downloads.data &&
|
{!isLoading && !error && !downloads?.message &&
|
||||||
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> < </span> }
|
||||||
|
<span> Page {pageNumber}</span>
|
||||||
|
{downloads?.paginate?.last_page && <span> > </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> */}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user