diff --git a/Dockerfile b/Dockerfile
index 6b2a3a22..929967ec 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,7 +3,7 @@
# First stage to build python wheel
-FROM python:3.10.7-slim-bullseye AS builder
+FROM python:3.10.8-slim-bullseye AS builder
ARG TARGETPLATFORM
RUN apt-get update
diff --git a/tubearchivist/home/src/download/thumbnails.py b/tubearchivist/home/src/download/thumbnails.py
index ff556c09..e91a97db 100644
--- a/tubearchivist/home/src/download/thumbnails.py
+++ b/tubearchivist/home/src/download/thumbnails.py
@@ -14,7 +14,7 @@ from home.src.download import queue # partial import
from home.src.es.connect import IndexPaginate
from home.src.ta.config import AppConfig
from mutagen.mp4 import MP4, MP4Cover
-from PIL import Image, ImageFile, ImageFilter
+from PIL import Image, ImageFile, ImageFilter, UnidentifiedImageError
ImageFile.LOAD_TRUNCATED_IMAGES = True
@@ -42,7 +42,12 @@ class ThumbManagerBase:
try:
response = requests.get(url, stream=True, timeout=5)
if response.ok:
- return Image.open(response.raw)
+ try:
+ return Image.open(response.raw)
+ except UnidentifiedImageError:
+ print(f"failed to open thumbnail: {url}")
+ return self.get_fallback()
+
if response.status_code == 404:
return self.get_fallback()
diff --git a/tubearchivist/home/src/frontend/watched.py b/tubearchivist/home/src/frontend/watched.py
index 85aa1abe..88f694f8 100644
--- a/tubearchivist/home/src/frontend/watched.py
+++ b/tubearchivist/home/src/frontend/watched.py
@@ -57,6 +57,14 @@ class WatchState:
print(response)
raise ValueError("failed to mark video as watched")
+ def _get_source(self):
+ """build source line for update_by_query script"""
+ source = [
+ "ctx._source.player['watched'] = true",
+ f"ctx._source.player['watched_date'] = {self.stamp}",
+ ]
+ return "; ".join(source)
+
def mark_channel_watched(self):
"""change watched status of every video in channel"""
path = "ta_video/_update_by_query"
@@ -67,7 +75,7 @@ class WatchState:
data = {
"query": {"bool": {"must": must_list}},
"script": {
- "source": "ctx._source.player['watched'] = true",
+ "source": self._get_source(),
"lang": "painless",
},
}
@@ -87,7 +95,7 @@ class WatchState:
data = {
"query": {"bool": {"must": must_list}},
"script": {
- "source": "ctx._source.player['watched'] = true",
+ "source": self._get_source(),
"lang": "painless",
},
}
diff --git a/tubearchivist/home/templates/home/downloads.html b/tubearchivist/home/templates/home/downloads.html
index 85d61e7c..22dd6fdf 100644
--- a/tubearchivist/home/templates/home/downloads.html
+++ b/tubearchivist/home/templates/home/downloads.html
@@ -41,6 +41,15 @@
+ {% if channel_agg_list|length > 1 %}
+
Filter:
+
+ {% endif %}
{% if view_style == "grid" %}
{% if grid_items < 7 %}
diff --git a/tubearchivist/home/views.py b/tubearchivist/home/views.py
index 9d44399d..ba5af2f1 100644
--- a/tubearchivist/home/views.py
+++ b/tubearchivist/home/views.py
@@ -364,6 +364,7 @@ class DownloadView(ArchivistResultsView):
{
"title": "Downloads",
"add_form": AddToQueueForm(),
+ "channel_agg_list": self._get_channel_agg(),
}
)
return render(request, "home/downloads.html", self.context)
@@ -399,6 +400,38 @@ class DownloadView(ArchivistResultsView):
}
)
+ def _get_channel_agg(self):
+ """get pending channel with count"""
+ data = {
+ "size": 0,
+ "query": {"term": {"status": {"value": "pending"}}},
+ "aggs": {
+ "channel_downloads": {
+ "multi_terms": {
+ "size": 30,
+ "terms": [
+ {"field": "channel_name.keyword"},
+ {"field": "channel_id"},
+ ],
+ "order": {"_count": "desc"},
+ }
+ }
+ },
+ }
+ response, _ = ElasticWrap(self.es_search).get(data=data)
+ buckets = response["aggregations"]["channel_downloads"]["buckets"]
+
+ buckets_sorted = []
+ for i in buckets:
+ bucket = {
+ "name": i["key"][0],
+ "id": i["key"][1],
+ "count": i["doc_count"],
+ }
+ buckets_sorted.append(bucket)
+
+ return buckets_sorted
+
@staticmethod
def post(request):
"""handle post requests"""
diff --git a/tubearchivist/static/css/style.css b/tubearchivist/static/css/style.css
index e4f90472..7a3872e3 100644
--- a/tubearchivist/static/css/style.css
+++ b/tubearchivist/static/css/style.css
@@ -344,6 +344,7 @@ button:hover {
.grid-count {
display: flex;
justify-content: end;
+ align-items: center;
}
.view-icons img {
diff --git a/tubearchivist/static/script.js b/tubearchivist/static/script.js
index 7e375c17..a1205d68 100644
--- a/tubearchivist/static/script.js
+++ b/tubearchivist/static/script.js
@@ -1102,12 +1102,12 @@ function textReveal() {
function textExpand() {
var textBox = document.getElementById("text-expand");
var button = document.getElementById("text-expand-button");
- var textBoxLineClamp = textBox.style["-webkit-line-clamp"];
- if (textBoxLineClamp === "none") {
+ var style = window.getComputedStyle(textBox)
+ if (style.webkitLineClamp === "none") {
textBox.style["-webkit-line-clamp"] = "4";
button.innerText = "Show more";
} else {
- textBox.style["-webkit-line-clamp"] = "none";
+ textBox.style["-webkit-line-clamp"] = "unset";
button.innerText = "Show less";
}
}
@@ -1119,8 +1119,9 @@ function textExpandButtonVisibilityUpdate() {
if (!textBox || !button)
return;
- var textBoxLineClamp = textBox.style["-webkit-line-clamp"];
- if (textBoxLineClamp === "none")
+ var styles = window.getComputedStyle(textBox);
+ var textBoxLineClamp = styles.webkitLineClamp;
+ if (textBoxLineClamp === "unset")
return; // text box is in revealed state
if (textBox.offsetHeight < textBox.scrollHeight
@@ -1147,6 +1148,14 @@ function showForm() {
animate('animate-icon', 'pulse-img');
}
+function channelFilterDownload(value) {
+ if (value === "all") {
+ window.location = "/downloads/";
+ } else {
+ window.location.search = "?channel=" + value;
+ }
+}
+
function showOverwrite() {
var overwriteDiv = document.getElementById("overwrite-form");
if (overwriteDiv.classList.contains("hidden-overwrite")) {