mirror of
https://git.vectorsigma.ru/public/tubearchivist.git
synced 2026-08-09 09:09:32 +00:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0874b2d02 | ||
|
|
baacd3ee39 | ||
|
|
9cd23c3666 | ||
|
|
0e17e2a6cf | ||
|
|
2dea0aa57b | ||
|
|
ba1c8c15c4 | ||
|
|
f16915be11 | ||
|
|
858d437f3f | ||
|
|
de30ac302a | ||
|
|
dd8597307c | ||
|
|
608403c113 | ||
|
|
40eff8e30e | ||
|
|
0bba36cbc3 | ||
|
|
a5788117de | ||
|
|
3f1075d0b2 | ||
|
|
dea2688b49 | ||
|
|
4f1daeb18c | ||
|
|
927e6fa909 | ||
|
|
bd7cdb3942 | ||
|
|
9dfd967a32 | ||
|
|
6eee762d3a | ||
|
|
a8c5773f81 | ||
|
|
fbb52dc93f | ||
|
|
c9e936da21 | ||
|
|
c825e67f69 | ||
|
|
bcb7b9443b | ||
|
|
896d166dcf |
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
|
|
||||||
# First stage to build python wheel
|
# 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
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
@@ -14,7 +14,7 @@ COPY ./tubearchivist/requirements.txt /requirements.txt
|
|||||||
RUN pip install --user -r requirements.txt
|
RUN pip install --user -r requirements.txt
|
||||||
|
|
||||||
# build final image
|
# build final image
|
||||||
FROM python:3.10.7-slim-bullseye as tubearchivist
|
FROM python:3.10.8-slim-bullseye as tubearchivist
|
||||||
|
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
ARG INSTALL_DEBUG
|
ARG INSTALL_DEBUG
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ Once your YouTube video collection grows, it becomes hard to search and find a s
|
|||||||
## Installing and updating
|
## Installing and updating
|
||||||
Take a look at the example `docker-compose.yml` file provided. Use the *latest* or the named semantic version tag. The *unstable* tag is for intermediate testing and as the name implies, is **unstable** and not be used on your main installation but in a [testing environment](CONTRIBUTING.md).
|
Take a look at the example `docker-compose.yml` file provided. Use the *latest* or the named semantic version tag. The *unstable* tag is for intermediate testing and as the name implies, is **unstable** and not be used on your main installation but in a [testing environment](CONTRIBUTING.md).
|
||||||
|
|
||||||
|
For minimal system requirements, the Tube Archivist stack needs around 2GB of available memory for a small testing setup and around 4GB of available memory for a mid to large sized installation.
|
||||||
|
|
||||||
Tube Archivist depends on three main components split up into separate docker containers:
|
Tube Archivist depends on three main components split up into separate docker containers:
|
||||||
|
|
||||||
### Tube Archivist
|
### Tube Archivist
|
||||||
|
|||||||
@@ -18,11 +18,7 @@ set -e
|
|||||||
|
|
||||||
function sync_blackhole {
|
function sync_blackhole {
|
||||||
|
|
||||||
# docker commands need sudo, only build amd64
|
|
||||||
host="blackhole.local"
|
host="blackhole.local"
|
||||||
|
|
||||||
read -sp 'Password: ' remote_pw
|
|
||||||
export PASS=$remote_pw
|
|
||||||
|
|
||||||
rsync -a --progress --delete-after \
|
rsync -a --progress --delete-after \
|
||||||
--exclude ".git" \
|
--exclude ".git" \
|
||||||
@@ -32,8 +28,8 @@ function sync_blackhole {
|
|||||||
--exclude "db.sqlite3" \
|
--exclude "db.sqlite3" \
|
||||||
. -e ssh "$host":tubearchivist
|
. -e ssh "$host":tubearchivist
|
||||||
|
|
||||||
echo "$PASS" | ssh "$host" 'sudo -S docker buildx build --platform linux/amd64 -t bbilly1/tubearchivist:latest tubearchivist --load 2>/dev/null'
|
ssh "$host" 'docker build -t bbilly1/tubearchivist --build-arg TARGETPLATFORM="linux/amd64" tubearchivist'
|
||||||
echo "$PASS" | ssh "$host" 'sudo -S docker compose up -d 2>/dev/null'
|
ssh "$host" 'docker compose up -d'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- archivist-es
|
- archivist-es
|
||||||
archivist-es:
|
archivist-es:
|
||||||
image: bbilly1/tubearchivist-es # only for amd64, or use official es 8.3.3
|
image: bbilly1/tubearchivist-es # only for amd64, or use official es 8.4.3
|
||||||
container_name: archivist-es
|
container_name: archivist-es
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Each channel will get a dedicated channel detail page accessible at `/channel/<c
|
|||||||
|
|
||||||
Additionally there is a *Channel Playlist* page, accessible at `/channel/<channel-id>/playlist/` to show all indexed playlists from this channel.
|
Additionally there is a *Channel Playlist* page, accessible at `/channel/<channel-id>/playlist/` to show all indexed playlists from this channel.
|
||||||
|
|
||||||
On the *Channel About* page you can see additional metadata.
|
On the *Channel About* page, accessible at `/channel/<channel-id>/about/`, you can see additional metadata.
|
||||||
- The button **Delete Channel** will delete the channel plus all videos of this channel, both media files and metadata additionally this will also delete playlists metadata belonging to that channel.
|
- The button **Delete Channel** will delete the channel plus all videos of this channel, both media files and metadata additionally this will also delete playlists metadata belonging to that channel.
|
||||||
|
|
||||||
The channel customize form gives options to change settings on a per channel basis. Any configurations here will overwrite your configurations from the [settings](Settings) page.
|
The channel customize form gives options to change settings on a per channel basis. Any configurations here will overwrite your configurations from the [settings](Settings) page.
|
||||||
@@ -32,3 +32,5 @@ The channel customize form gives options to change settings on a per channel bas
|
|||||||
- **Auto Delete**: Automatically delete watched videos from this channel after selected days.
|
- **Auto Delete**: Automatically delete watched videos from this channel after selected days.
|
||||||
- **Index Playlists**: Automatically add all Playlists with at least a video downloaded to your index. Only do this for channels where you care about playlists as this will slow down indexing new videos for having to check which playlist this belongs to.
|
- **Index Playlists**: Automatically add all Playlists with at least a video downloaded to your index. Only do this for channels where you care about playlists as this will slow down indexing new videos for having to check which playlist this belongs to.
|
||||||
- **SponsorBlock**: Using [SponsorBlock](https://sponsor.ajay.app/) to get and skip sponsored content. Customize per channel: You can *disable* or *enable* SponsorBlock for certain channels only to overwrite the behavior set on the [Settings](settings) page. Selecting *unset* will remove the overwrite and your setting will fall back to the default on the settings page.
|
- **SponsorBlock**: Using [SponsorBlock](https://sponsor.ajay.app/) to get and skip sponsored content. Customize per channel: You can *disable* or *enable* SponsorBlock for certain channels only to overwrite the behavior set on the [Settings](settings) page. Selecting *unset* will remove the overwrite and your setting will fall back to the default on the settings page.
|
||||||
|
|
||||||
|
If you have any videos pending in the download queue, a *Downloads* link will show, bringing you directly to the [downloads](Downloads) page, filtering the list by the selected channel.
|
||||||
@@ -29,6 +29,8 @@ The **Add to Download Queue** icon <img src="assets/icon-add.png?raw=true" alt="
|
|||||||
## The Download Queue
|
## The Download Queue
|
||||||
Below the three buttons you find the download queue. New items will get added at the bottom of the queue, the next video to download once you click on **Start Download** will be the first in the list.
|
Below the three buttons you find the download queue. New items will get added at the bottom of the queue, the next video to download once you click on **Start Download** will be the first in the list.
|
||||||
|
|
||||||
|
You can filter the download queue with the **filter** dropdown box, the filter will show once you have more than one channel in the download queue. Select the channel to filter by name, the number in parentheses indicates how many videos you have pending from this channel. Reset the filter by selecting *all* from the dropdown. This will generate links for the top 30 channels with pending videos.
|
||||||
|
|
||||||
Every video in the download queue has two buttons:
|
Every video in the download queue has two buttons:
|
||||||
- **Ignore**: This will remove that video from the download queue and this video will not get added again, even when you **Rescan Subscriptions**.
|
- **Ignore**: This will remove that video from the download queue and this video will not get added again, even when you **Rescan Subscriptions**.
|
||||||
- **Download now**: This will give priority to this video. If the download process is already running, the prioritized video will get downloaded as soon as the current video is finished. If there is no download process running, this will start downloading this single video and stop after that.
|
- **Download now**: This will give priority to this video. If the download process is already running, the prioritized video will get downloaded as soon as the current video is finished. If there is no download process running, this will start downloading this single video and stop after that.
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ The list views return a paginate object with the following keys:
|
|||||||
- prev_pages: *array of ints* of previous pages, if available
|
- prev_pages: *array of ints* of previous pages, if available
|
||||||
- current_page: *int* current page from query
|
- current_page: *int* current page from query
|
||||||
- max_hits: *bool* if max of 10k results is reached
|
- max_hits: *bool* if max of 10k results is reached
|
||||||
|
- params: *str* additional url encoded query parameters
|
||||||
- last_page: *int* of last page link
|
- last_page: *int* of last page link
|
||||||
- next_pages: *array of ints* of next pages
|
- next_pages: *array of ints* of next pages
|
||||||
- total_hits: *int* total results
|
- total_hits: *int* total results
|
||||||
@@ -169,6 +170,7 @@ GET /api/download/
|
|||||||
|
|
||||||
Parameter:
|
Parameter:
|
||||||
- filter: pending, ignore
|
- filter: pending, ignore
|
||||||
|
- channel: channel-id
|
||||||
|
|
||||||
### Add list of videos to download queue
|
### Add list of videos to download queue
|
||||||
POST /api/download/
|
POST /api/download/
|
||||||
|
|||||||
@@ -53,9 +53,7 @@ class ApiBaseView(APIView):
|
|||||||
|
|
||||||
def initiate_pagination(self, request):
|
def initiate_pagination(self, request):
|
||||||
"""set initial pagination values"""
|
"""set initial pagination values"""
|
||||||
user_id = request.user.id
|
self.pagination_handler = Pagination(request)
|
||||||
page_get = int(request.GET.get("page", 0))
|
|
||||||
self.pagination_handler = Pagination(page_get, user_id)
|
|
||||||
self.data.update(
|
self.data.update(
|
||||||
{
|
{
|
||||||
"size": self.pagination_handler.pagination["page_size"],
|
"size": self.pagination_handler.pagination["page_size"],
|
||||||
@@ -368,13 +366,23 @@ class DownloadApiListView(ApiBaseView):
|
|||||||
"""get request"""
|
"""get request"""
|
||||||
query_filter = request.GET.get("filter", False)
|
query_filter = request.GET.get("filter", False)
|
||||||
self.data.update({"sort": [{"timestamp": {"order": "asc"}}]})
|
self.data.update({"sort": [{"timestamp": {"order": "asc"}}]})
|
||||||
|
|
||||||
|
must_list = []
|
||||||
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)
|
||||||
|
|
||||||
self.data["query"] = {"term": {"status": {"value": query_filter}}}
|
must_list.append({"term": {"status": {"value": query_filter}}})
|
||||||
|
|
||||||
|
filter_channel = request.GET.get("channel", False)
|
||||||
|
if filter_channel:
|
||||||
|
must_list.append(
|
||||||
|
{"term": {"channel_id": {"value": filter_channel}}}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.data["query"] = {"bool": {"must": must_list}}
|
||||||
|
|
||||||
self.get_document_list(request)
|
self.get_document_list(request)
|
||||||
return Response(self.response)
|
return Response(self.response)
|
||||||
|
|||||||
@@ -207,4 +207,4 @@ CORS_ALLOW_HEADERS = list(default_headers) + [
|
|||||||
|
|
||||||
# TA application settings
|
# TA application settings
|
||||||
TA_UPSTREAM = "https://github.com/tubearchivist/tubearchivist"
|
TA_UPSTREAM = "https://github.com/tubearchivist/tubearchivist"
|
||||||
TA_VERSION = "v0.2.2"
|
TA_VERSION = "v0.2.3"
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ class PendingList(PendingIndex):
|
|||||||
print(f"{youtube_id}: skipping premium video, id not matching")
|
print(f"{youtube_id}: skipping premium video, id not matching")
|
||||||
return False
|
return False
|
||||||
# stop if video is streaming live now
|
# stop if video is streaming live now
|
||||||
if vid["is_live"]:
|
if vid["live_status"] in ["is_upcoming", "is_live"]:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return self._parse_youtube_details(vid)
|
return self._parse_youtube_details(vid)
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ class ChannelSubscription:
|
|||||||
|
|
||||||
for idx, channel in enumerate(all_channels):
|
for idx, channel in enumerate(all_channels):
|
||||||
channel_id = channel["channel_id"]
|
channel_id = channel["channel_id"]
|
||||||
|
print(f"{channel_id}: find missing videos.")
|
||||||
last_videos = self.get_last_youtube_videos(channel_id)
|
last_videos = self.get_last_youtube_videos(channel_id)
|
||||||
|
|
||||||
if last_videos:
|
if last_videos:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from home.src.download import queue # partial import
|
|||||||
from home.src.es.connect import IndexPaginate
|
from home.src.es.connect import IndexPaginate
|
||||||
from home.src.ta.config import AppConfig
|
from home.src.ta.config import AppConfig
|
||||||
from mutagen.mp4 import MP4, MP4Cover
|
from mutagen.mp4 import MP4, MP4Cover
|
||||||
from PIL import Image, ImageFile, ImageFilter
|
from PIL import Image, ImageFile, ImageFilter, UnidentifiedImageError
|
||||||
|
|
||||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||||
|
|
||||||
@@ -42,7 +42,12 @@ class ThumbManagerBase:
|
|||||||
try:
|
try:
|
||||||
response = requests.get(url, stream=True, timeout=5)
|
response = requests.get(url, stream=True, timeout=5)
|
||||||
if response.ok:
|
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:
|
if response.status_code == 404:
|
||||||
return self.get_fallback()
|
return self.get_fallback()
|
||||||
|
|
||||||
|
|||||||
@@ -175,6 +175,14 @@ class VideoDownloader:
|
|||||||
if not success:
|
if not success:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
mess_dict = {
|
||||||
|
"status": self.MSG,
|
||||||
|
"level": "info",
|
||||||
|
"title": "Indexing....",
|
||||||
|
"message": "Add video metadata to index.",
|
||||||
|
}
|
||||||
|
RedisArchivist().set_message(self.MSG, mess_dict, expire=60)
|
||||||
|
|
||||||
vid_dict = index_new_video(
|
vid_dict = index_new_video(
|
||||||
youtube_id, video_overwrites=self.video_overwrites
|
youtube_id, video_overwrites=self.video_overwrites
|
||||||
)
|
)
|
||||||
@@ -187,12 +195,17 @@ class VideoDownloader:
|
|||||||
}
|
}
|
||||||
RedisArchivist().set_message(self.MSG, mess_dict)
|
RedisArchivist().set_message(self.MSG, mess_dict)
|
||||||
|
|
||||||
|
if queue.has_item():
|
||||||
|
message = "Continue with next video."
|
||||||
|
else:
|
||||||
|
message = "Download queue is finished."
|
||||||
|
|
||||||
self.move_to_archive(vid_dict)
|
self.move_to_archive(vid_dict)
|
||||||
mess_dict = {
|
mess_dict = {
|
||||||
"status": self.MSG,
|
"status": self.MSG,
|
||||||
"level": "info",
|
"level": "info",
|
||||||
"title": "Completed",
|
"title": "Completed",
|
||||||
"message": "",
|
"message": message,
|
||||||
}
|
}
|
||||||
RedisArchivist().set_message(self.MSG, mess_dict, expire=10)
|
RedisArchivist().set_message(self.MSG, mess_dict, expire=10)
|
||||||
self._delete_from_pending(youtube_id)
|
self._delete_from_pending(youtube_id)
|
||||||
|
|||||||
@@ -57,6 +57,14 @@ class WatchState:
|
|||||||
print(response)
|
print(response)
|
||||||
raise ValueError("failed to mark video as watched")
|
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):
|
def mark_channel_watched(self):
|
||||||
"""change watched status of every video in channel"""
|
"""change watched status of every video in channel"""
|
||||||
path = "ta_video/_update_by_query"
|
path = "ta_video/_update_by_query"
|
||||||
@@ -67,7 +75,7 @@ class WatchState:
|
|||||||
data = {
|
data = {
|
||||||
"query": {"bool": {"must": must_list}},
|
"query": {"bool": {"must": must_list}},
|
||||||
"script": {
|
"script": {
|
||||||
"source": "ctx._source.player['watched'] = true",
|
"source": self._get_source(),
|
||||||
"lang": "painless",
|
"lang": "painless",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -87,7 +95,7 @@ class WatchState:
|
|||||||
data = {
|
data = {
|
||||||
"query": {"bool": {"must": must_list}},
|
"query": {"bool": {"must": must_list}},
|
||||||
"script": {
|
"script": {
|
||||||
"source": "ctx._source.player['watched'] = true",
|
"source": self._get_source(),
|
||||||
"lang": "painless",
|
"lang": "painless",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,16 +73,25 @@ class Pagination:
|
|||||||
figure out the pagination based on page size and total_hits
|
figure out the pagination based on page size and total_hits
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, page_get, user_id, search_get=False):
|
def __init__(self, request):
|
||||||
self.user_id = user_id
|
self.request = request
|
||||||
|
self.page_get = False
|
||||||
|
self.params = False
|
||||||
|
self.get_params()
|
||||||
self.page_size = self.get_page_size()
|
self.page_size = self.get_page_size()
|
||||||
self.page_get = page_get
|
|
||||||
self.search_get = search_get
|
|
||||||
self.pagination = self.first_guess()
|
self.pagination = self.first_guess()
|
||||||
|
|
||||||
|
def get_params(self):
|
||||||
|
"""process url query parameters"""
|
||||||
|
query_dict = self.request.GET.copy()
|
||||||
|
self.page_get = int(query_dict.get("page", 0))
|
||||||
|
|
||||||
|
_ = query_dict.pop("page", False)
|
||||||
|
self.params = query_dict.urlencode()
|
||||||
|
|
||||||
def get_page_size(self):
|
def get_page_size(self):
|
||||||
"""get default or user modified page_size"""
|
"""get default or user modified page_size"""
|
||||||
key = f"{self.user_id}:page_size"
|
key = f"{self.request.user.id}:page_size"
|
||||||
page_size = RedisArchivist().get_message(key)["status"]
|
page_size = RedisArchivist().get_message(key)["status"]
|
||||||
if not page_size:
|
if not page_size:
|
||||||
config = AppConfig().config
|
config = AppConfig().config
|
||||||
@@ -108,9 +117,9 @@ class Pagination:
|
|||||||
"prev_pages": prev_pages,
|
"prev_pages": prev_pages,
|
||||||
"current_page": page_get,
|
"current_page": page_get,
|
||||||
"max_hits": False,
|
"max_hits": False,
|
||||||
|
"params": self.params,
|
||||||
}
|
}
|
||||||
if self.search_get:
|
|
||||||
pagination.update({"search_get": self.search_get})
|
|
||||||
return pagination
|
return pagination
|
||||||
|
|
||||||
def validate(self, total_hits):
|
def validate(self, total_hits):
|
||||||
|
|||||||
@@ -45,13 +45,19 @@ class YoutubePlaylist(YouTubeItem):
|
|||||||
|
|
||||||
def process_youtube_meta(self):
|
def process_youtube_meta(self):
|
||||||
"""extract relevant fields from youtube"""
|
"""extract relevant fields from youtube"""
|
||||||
|
try:
|
||||||
|
playlist_thumbnail = self.youtube_meta["thumbnails"][-1]["url"]
|
||||||
|
except IndexError:
|
||||||
|
print(f"{self.youtube_id}: thumbnail extraction failed")
|
||||||
|
playlist_thumbnail = False
|
||||||
|
|
||||||
self.json_data = {
|
self.json_data = {
|
||||||
"playlist_id": self.youtube_id,
|
"playlist_id": self.youtube_id,
|
||||||
"playlist_active": True,
|
"playlist_active": True,
|
||||||
"playlist_name": self.youtube_meta["title"],
|
"playlist_name": self.youtube_meta["title"],
|
||||||
"playlist_channel": self.youtube_meta["channel"],
|
"playlist_channel": self.youtube_meta["channel"],
|
||||||
"playlist_channel_id": self.youtube_meta["channel_id"],
|
"playlist_channel_id": self.youtube_meta["channel_id"],
|
||||||
"playlist_thumbnail": self.youtube_meta["thumbnails"][-1]["url"],
|
"playlist_thumbnail": playlist_thumbnail,
|
||||||
"playlist_description": self.youtube_meta["description"] or False,
|
"playlist_description": self.youtube_meta["description"] or False,
|
||||||
"playlist_last_refresh": int(datetime.now().strftime("%s")),
|
"playlist_last_refresh": int(datetime.now().strftime("%s")),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,9 +48,17 @@ class SponsorBlock:
|
|||||||
url = f"{self.API}/skipSegments?videoID={youtube_id}"
|
url = f"{self.API}/skipSegments?videoID={youtube_id}"
|
||||||
headers = {"User-Agent": self.user_agent}
|
headers = {"User-Agent": self.user_agent}
|
||||||
print(f"{youtube_id}: get sponsorblock timestamps")
|
print(f"{youtube_id}: get sponsorblock timestamps")
|
||||||
response = requests.get(url, headers=headers)
|
try:
|
||||||
|
response = requests.get(url, headers=headers, timeout=10)
|
||||||
|
except requests.ReadTimeout:
|
||||||
|
print(f"{youtube_id}: sponsorblock API timeout")
|
||||||
|
return False
|
||||||
|
|
||||||
if not response.ok:
|
if not response.ok:
|
||||||
print(f"{youtube_id}: sponsorblock failed: {response.text}")
|
print(f"{youtube_id}: sponsorblock failed: {response.status_code}")
|
||||||
|
if response.status_code == 503:
|
||||||
|
return False
|
||||||
|
|
||||||
sponsor_dict = {
|
sponsor_dict = {
|
||||||
"last_refresh": self.last_refresh,
|
"last_refresh": self.last_refresh,
|
||||||
"is_enabled": True,
|
"is_enabled": True,
|
||||||
|
|||||||
@@ -143,3 +143,8 @@ class RedisQueue(RedisBase):
|
|||||||
def trim(self, size):
|
def trim(self, size):
|
||||||
"""trim the queue based on settings amount"""
|
"""trim the queue based on settings amount"""
|
||||||
self.conn.execute_command("LTRIM", self.key, 0, size)
|
self.conn.execute_command("LTRIM", self.key, 0, size)
|
||||||
|
|
||||||
|
def has_item(self):
|
||||||
|
"""check if queue as at least one pending item"""
|
||||||
|
result = self.conn.execute_command("LRANGE", self.key, 0, 0)
|
||||||
|
return bool(result)
|
||||||
|
|||||||
@@ -79,16 +79,16 @@
|
|||||||
<div class="pagination">
|
<div class="pagination">
|
||||||
{% if pagination %}
|
{% if pagination %}
|
||||||
{% if pagination.current_page > 1 %}
|
{% if pagination.current_page > 1 %}
|
||||||
{% if pagination.search_get %}
|
{% if pagination.params %}
|
||||||
<a class="pagination-item" href="{{ request.path }}?search={{ pagination.search_get }}">First</a>
|
<a class="pagination-item" href="{{ request.path }}?{{ pagination.params }}">First</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="pagination-item" href="{{ request.path }}">First</a>
|
<a class="pagination-item" href="{{ request.path }}">First</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if pagination.prev_pages %}
|
{% if pagination.prev_pages %}
|
||||||
{% for page in pagination.prev_pages %}
|
{% for page in pagination.prev_pages %}
|
||||||
{% if pagination.search_get %}
|
{% if pagination.params %}
|
||||||
<a class="pagination-item" href="?page={{ page }}&search={{ pagination.search_get }}">{{ page }}</a>
|
<a class="pagination-item" href="?page={{ page }}&{{ pagination.params }}">{{ page }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="pagination-item" href="?page={{ page }}">{{ page }}</a>
|
<a class="pagination-item" href="?page={{ page }}">{{ page }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -100,16 +100,16 @@
|
|||||||
{% if pagination.next_pages %}
|
{% if pagination.next_pages %}
|
||||||
<span> ></span>
|
<span> ></span>
|
||||||
{% for page in pagination.next_pages %}
|
{% for page in pagination.next_pages %}
|
||||||
{% if pagination.search_get %}
|
{% if pagination.params %}
|
||||||
<a class="pagination-item" href="?page={{ page }}&search={{ pagination.search_get }}">{{ page }}</a>
|
<a class="pagination-item" href="?page={{ page }}&{{ pagination.params }}">{{ page }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a class="pagination-item" href="?page={{ page }}">{{ page }}</a>
|
<a class="pagination-item" href="?page={{ page }}">{{ page }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if pagination.last_page > 0 %}
|
{% if pagination.last_page > 0 %}
|
||||||
{% if pagination.search_get %}
|
{% if pagination.params %}
|
||||||
<a class="pagination-item" href="?page={{ pagination.last_page }}&search={{ pagination.search_get }}">
|
<a class="pagination-item" href="?page={{ pagination.last_page }}&{{ pagination.params }}">
|
||||||
{% if pagination.max_hits %}
|
{% if pagination.max_hits %}
|
||||||
Max ({{ pagination.last_page }})
|
Max ({{ pagination.last_page }})
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
<a href="{% url 'channel_id' channel_info.channel_id %}"><h3>Videos</h3></a>
|
<a href="{% url 'channel_id' channel_info.channel_id %}"><h3>Videos</h3></a>
|
||||||
<a href="{% url 'channel_id_playlist' channel_info.channel_id %}"><h3>Playlists</h3></a>
|
<a href="{% url 'channel_id_playlist' channel_info.channel_id %}"><h3>Playlists</h3></a>
|
||||||
<a href="{% url 'channel_id_about' channel_info.channel_id %}"><h3>About</h3></a>
|
<a href="{% url 'channel_id_about' channel_info.channel_id %}"><h3>About</h3></a>
|
||||||
|
{% if has_pending %}
|
||||||
|
<a href="{% url 'downloads' %}?channel={{ channel_info.channel_id }}"><h3>Downloads</h3></a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div id="notifications" data="channel_id"></div>
|
<div id="notifications" data="channel_id"></div>
|
||||||
<div class="info-box info-box-2">
|
<div class="info-box info-box-2">
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
<a href="{% url 'channel_id' channel_info.channel_id %}"><h3>Videos</h3></a>
|
<a href="{% url 'channel_id' channel_info.channel_id %}"><h3>Videos</h3></a>
|
||||||
<a href="{% url 'channel_id_playlist' channel_info.channel_id %}"><h3>Playlists</h3></a>
|
<a href="{% url 'channel_id_playlist' channel_info.channel_id %}"><h3>Playlists</h3></a>
|
||||||
<a href="{% url 'channel_id_about' channel_info.channel_id %}"><h3>About</h3></a>
|
<a href="{% url 'channel_id_about' channel_info.channel_id %}"><h3>About</h3></a>
|
||||||
|
{% if has_pending %}
|
||||||
|
<a href="{% url 'downloads' %}?channel={{ channel_info.channel_id }}"><h3>Downloads</h3></a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="info-box info-box-3">
|
<div class="info-box info-box-3">
|
||||||
<div class="info-box-item">
|
<div class="info-box-item">
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
<a href="{% url 'channel_id' channel_info.channel_id %}"><h3>Videos</h3></a>
|
<a href="{% url 'channel_id' channel_info.channel_id %}"><h3>Videos</h3></a>
|
||||||
<a href="{% url 'channel_id_playlist' channel_info.channel_id %}"><h3>Playlists</h3></a>
|
<a href="{% url 'channel_id_playlist' channel_info.channel_id %}"><h3>Playlists</h3></a>
|
||||||
<a href="{% url 'channel_id_about' channel_info.channel_id %}"><h3>About</h3></a>
|
<a href="{% url 'channel_id_about' channel_info.channel_id %}"><h3>About</h3></a>
|
||||||
|
{% if has_pending %}
|
||||||
|
<a href="{% url 'downloads' %}?channel={{ channel_info.channel_id }}"><h3>Downloads</h3></a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="view-controls">
|
<div class="view-controls">
|
||||||
<div class="toggle">
|
<div class="toggle">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="boxed-content">
|
<div class="boxed-content">
|
||||||
<div class="title-bar">
|
<div class="title-bar">
|
||||||
<h1>Downloads</h1>
|
<h1>Downloads {% if channel_filter_id %} for {{ channel_filter_name }}{% endif %}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div id="notifications" data="download"></div>
|
<div id="notifications" data="download"></div>
|
||||||
<div id="downloadControl"></div>
|
<div id="downloadControl"></div>
|
||||||
@@ -41,6 +41,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="view-icons">
|
<div class="view-icons">
|
||||||
|
{% if channel_agg_list|length > 1 %}
|
||||||
|
<span>Filter:</span>
|
||||||
|
<select name="channel_filter" id="channel_filter" onchange="channelFilterDownload(this.value)">
|
||||||
|
<option value="all" {% if not channel_filter_id %}selected{% endif %}>all</option>
|
||||||
|
{% for channel in channel_agg_list %}
|
||||||
|
<option {% if channel_filter_id == channel.id %}selected{% endif %} value="{{ channel.id }}">{{ channel.name }} ({{channel.count}})</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
{% endif %}
|
||||||
{% if view_style == "grid" %}
|
{% if view_style == "grid" %}
|
||||||
<div class="grid-count">
|
<div class="grid-count">
|
||||||
{% if grid_items < 7 %}
|
{% if grid_items < 7 %}
|
||||||
@@ -55,7 +64,7 @@
|
|||||||
<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>
|
||||||
<h3>Total videos: {{ max_hits }}{% if max_hits == 10000 %}+{% endif %}</h3>
|
<h3>Total videos: {{ max_hits }}{% if max_hits == 10000 %}+{% endif %} {% if channel_filter_id %} - from channel <i>{{ channel_filter_name }}</i>{% endif %}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="boxed-content {% if view_style == "grid" %}boxed-{{ grid_items }}{% endif %}">
|
<div class="boxed-content {% if view_style == "grid" %}boxed-{{ grid_items }}{% endif %}">
|
||||||
<div class="video-list {{ view_style }} {% if view_style == "grid" %}grid-{{ grid_items }}{% endif %}">
|
<div class="video-list {{ view_style }} {% if view_style == "grid" %}grid-{{ grid_items }}{% endif %}">
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ from home.src.frontend.forms import (
|
|||||||
UserSettingsForm,
|
UserSettingsForm,
|
||||||
)
|
)
|
||||||
from home.src.frontend.searching import SearchHandler
|
from home.src.frontend.searching import SearchHandler
|
||||||
from home.src.index.channel import channel_overwrites
|
from home.src.index.channel import YoutubeChannel, channel_overwrites
|
||||||
from home.src.index.generic import Pagination
|
from home.src.index.generic import Pagination
|
||||||
from home.src.index.playlist import YoutubePlaylist
|
from home.src.index.playlist import YoutubePlaylist
|
||||||
from home.src.ta.config import AppConfig, ScheduleBuilder
|
from home.src.ta.config import AppConfig, ScheduleBuilder
|
||||||
@@ -237,14 +237,10 @@ class ArchivistResultsView(ArchivistViewConfig):
|
|||||||
|
|
||||||
def initiate_vars(self, request):
|
def initiate_vars(self, request):
|
||||||
"""search in es for vidoe hits"""
|
"""search in es for vidoe hits"""
|
||||||
page_get = int(request.GET.get("page", 0))
|
|
||||||
self.user_id = request.user.id
|
self.user_id = request.user.id
|
||||||
self.config_builder(self.user_id)
|
self.config_builder(self.user_id)
|
||||||
self.search_get = request.GET.get("search", False)
|
self.search_get = request.GET.get("search", False)
|
||||||
search_encoded = self._url_encode(self.search_get)
|
self.pagination_handler = Pagination(request)
|
||||||
self.pagination_handler = Pagination(
|
|
||||||
page_get=page_get, user_id=self.user_id, search_get=search_encoded
|
|
||||||
)
|
|
||||||
self.sort_by = self._sort_by_overwrite()
|
self.sort_by = self._sort_by_overwrite()
|
||||||
self._initial_data()
|
self._initial_data()
|
||||||
|
|
||||||
@@ -362,29 +358,80 @@ class DownloadView(ArchivistResultsView):
|
|||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""handle get request"""
|
"""handle get request"""
|
||||||
self.initiate_vars(request)
|
self.initiate_vars(request)
|
||||||
self._update_view_data()
|
self._update_view_data(request)
|
||||||
self.find_results()
|
self.find_results()
|
||||||
self.context.update(
|
self.context.update(
|
||||||
{
|
{
|
||||||
"title": "Downloads",
|
"title": "Downloads",
|
||||||
"add_form": AddToQueueForm(),
|
"add_form": AddToQueueForm(),
|
||||||
|
"channel_agg_list": self._get_channel_agg(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return render(request, "home/downloads.html", self.context)
|
return render(request, "home/downloads.html", self.context)
|
||||||
|
|
||||||
def _update_view_data(self):
|
def _update_view_data(self, request):
|
||||||
"""update downloads view specific data dict"""
|
"""update downloads view specific data dict"""
|
||||||
if self.context["show_ignored_only"]:
|
if self.context["show_ignored_only"]:
|
||||||
filter_view = "ignore"
|
filter_view = "ignore"
|
||||||
else:
|
else:
|
||||||
filter_view = "pending"
|
filter_view = "pending"
|
||||||
|
|
||||||
|
must_list = [{"term": {"status": {"value": filter_view}}}]
|
||||||
|
|
||||||
|
channel_filter = request.GET.get("channel", False)
|
||||||
|
if channel_filter:
|
||||||
|
must_list.append(
|
||||||
|
{"term": {"channel_id": {"value": channel_filter}}}
|
||||||
|
)
|
||||||
|
|
||||||
|
channel = YoutubeChannel(channel_filter)
|
||||||
|
channel.get_from_es()
|
||||||
|
self.context.update(
|
||||||
|
{
|
||||||
|
"channel_filter_id": channel_filter,
|
||||||
|
"channel_filter_name": channel.json_data["channel_name"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
self.data.update(
|
self.data.update(
|
||||||
{
|
{
|
||||||
"query": {"term": {"status": {"value": filter_view}}},
|
"query": {"bool": {"must": must_list}},
|
||||||
"sort": [{"timestamp": {"order": "asc"}}],
|
"sort": [{"timestamp": {"order": "asc"}}],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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
|
@staticmethod
|
||||||
def post(request):
|
def post(request):
|
||||||
"""handle post requests"""
|
"""handle post requests"""
|
||||||
@@ -414,7 +461,37 @@ class DownloadView(ArchivistResultsView):
|
|||||||
return redirect("downloads", permanent=True)
|
return redirect("downloads", permanent=True)
|
||||||
|
|
||||||
|
|
||||||
class ChannelIdView(ArchivistResultsView):
|
class ChannelIdBaseView(ArchivistResultsView):
|
||||||
|
"""base class for all channel-id views"""
|
||||||
|
|
||||||
|
def get_channel_meta(self, channel_id):
|
||||||
|
"""get metadata for channel"""
|
||||||
|
path = f"ta_channel/_doc/{channel_id}"
|
||||||
|
response, _ = ElasticWrap(path).get()
|
||||||
|
channel_info = SearchProcess(response).process()
|
||||||
|
|
||||||
|
return channel_info
|
||||||
|
|
||||||
|
def channel_has_pending(self, channel_id):
|
||||||
|
"""check if channel has pending videos in queue"""
|
||||||
|
path = "ta_download/_search"
|
||||||
|
data = {
|
||||||
|
"size": 1,
|
||||||
|
"query": {
|
||||||
|
"bool": {
|
||||||
|
"must": [
|
||||||
|
{"term": {"status": {"value": "pending"}}},
|
||||||
|
{"term": {"channel_id": {"value": channel_id}}},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response, _ = ElasticWrap(path).get(data=data)
|
||||||
|
|
||||||
|
self.context.update({"has_pending": bool(response["hits"]["hits"])})
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelIdView(ChannelIdBaseView):
|
||||||
"""resolves to /channel/<channel-id>/
|
"""resolves to /channel/<channel-id>/
|
||||||
display single channel page from channel_id
|
display single channel page from channel_id
|
||||||
"""
|
"""
|
||||||
@@ -428,6 +505,7 @@ class ChannelIdView(ArchivistResultsView):
|
|||||||
self._update_view_data(channel_id)
|
self._update_view_data(channel_id)
|
||||||
self.find_results()
|
self.find_results()
|
||||||
self.match_progress()
|
self.match_progress()
|
||||||
|
self.channel_has_pending(channel_id)
|
||||||
|
|
||||||
if self.context["results"]:
|
if self.context["results"]:
|
||||||
channel_info = self.context["results"][0]["source"]["channel"]
|
channel_info = self.context["results"][0]["source"]["channel"]
|
||||||
@@ -478,7 +556,7 @@ class ChannelIdView(ArchivistResultsView):
|
|||||||
return redirect("channel_id", channel_id, permanent=True)
|
return redirect("channel_id", channel_id, permanent=True)
|
||||||
|
|
||||||
|
|
||||||
class ChannelIdAboutView(ArchivistResultsView):
|
class ChannelIdAboutView(ChannelIdBaseView):
|
||||||
"""resolves to /channel/<channel-id>/about/
|
"""resolves to /channel/<channel-id>/about/
|
||||||
show metadata, handle per channel conf
|
show metadata, handle per channel conf
|
||||||
"""
|
"""
|
||||||
@@ -488,6 +566,7 @@ class ChannelIdAboutView(ArchivistResultsView):
|
|||||||
def get(self, request, channel_id):
|
def get(self, request, channel_id):
|
||||||
"""handle get request"""
|
"""handle get request"""
|
||||||
self.initiate_vars(request)
|
self.initiate_vars(request)
|
||||||
|
self.channel_has_pending(channel_id)
|
||||||
|
|
||||||
path = f"ta_channel/_doc/{channel_id}"
|
path = f"ta_channel/_doc/{channel_id}"
|
||||||
response, _ = ElasticWrap(path).get()
|
response, _ = ElasticWrap(path).get()
|
||||||
@@ -521,7 +600,7 @@ class ChannelIdAboutView(ArchivistResultsView):
|
|||||||
return redirect("channel_id_about", channel_id, permanent=True)
|
return redirect("channel_id_about", channel_id, permanent=True)
|
||||||
|
|
||||||
|
|
||||||
class ChannelIdPlaylistView(ArchivistResultsView):
|
class ChannelIdPlaylistView(ChannelIdBaseView):
|
||||||
"""resolves to /channel/<channel-id>/playlist/
|
"""resolves to /channel/<channel-id>/playlist/
|
||||||
show all playlists of channel
|
show all playlists of channel
|
||||||
"""
|
"""
|
||||||
@@ -534,8 +613,9 @@ class ChannelIdPlaylistView(ArchivistResultsView):
|
|||||||
self.initiate_vars(request)
|
self.initiate_vars(request)
|
||||||
self._update_view_data(channel_id)
|
self._update_view_data(channel_id)
|
||||||
self.find_results()
|
self.find_results()
|
||||||
|
self.channel_has_pending(channel_id)
|
||||||
|
|
||||||
channel_info = self._get_channel_meta(channel_id)
|
channel_info = self.get_channel_meta(channel_id)
|
||||||
channel_name = channel_info["channel_name"]
|
channel_name = channel_info["channel_name"]
|
||||||
self.context.update(
|
self.context.update(
|
||||||
{
|
{
|
||||||
@@ -556,14 +636,6 @@ class ChannelIdPlaylistView(ArchivistResultsView):
|
|||||||
|
|
||||||
self.data["query"] = {"bool": {"must": must_list}}
|
self.data["query"] = {"bool": {"must": must_list}}
|
||||||
|
|
||||||
def _get_channel_meta(self, channel_id):
|
|
||||||
"""get metadata for channel"""
|
|
||||||
path = f"ta_channel/_doc/{channel_id}"
|
|
||||||
response, _ = ElasticWrap(path).get()
|
|
||||||
channel_info = SearchProcess(response).process()
|
|
||||||
|
|
||||||
return channel_info
|
|
||||||
|
|
||||||
|
|
||||||
class ChannelView(ArchivistResultsView):
|
class ChannelView(ArchivistResultsView):
|
||||||
"""resolves to /channel/
|
"""resolves to /channel/
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
beautifulsoup4==4.11.1
|
beautifulsoup4==4.11.1
|
||||||
celery==5.2.7
|
celery==5.2.7
|
||||||
Django==4.0.6
|
Django==4.1.2
|
||||||
django-auth-ldap==4.1.0
|
django-auth-ldap==4.1.0
|
||||||
django-cors-headers==3.13.0
|
django-cors-headers==3.13.0
|
||||||
djangorestframework==3.13.1
|
djangorestframework==3.14.0
|
||||||
Pillow==9.2.0
|
Pillow==9.2.0
|
||||||
redis==4.3.4
|
redis==4.3.4
|
||||||
requests==2.28.1
|
requests==2.28.1
|
||||||
ryd-client==0.0.6
|
ryd-client==0.0.6
|
||||||
uWSGI==2.0.20
|
uWSGI==2.0.20
|
||||||
whitenoise==6.2.0
|
whitenoise==6.2.0
|
||||||
yt_dlp==2022.9.1
|
yt_dlp==2022.10.4
|
||||||
|
|||||||
@@ -344,6 +344,7 @@ button:hover {
|
|||||||
.grid-count {
|
.grid-count {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: end;
|
justify-content: end;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-icons img {
|
.view-icons img {
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ function toggleCheckbox(checkbox) {
|
|||||||
var payload = JSON.stringify(payloadDict);
|
var payload = JSON.stringify(payloadDict);
|
||||||
sendPost(payload);
|
sendPost(payload);
|
||||||
setTimeout(function(){
|
setTimeout(function(){
|
||||||
var currPage = window.location.pathname;
|
var currPage = window.location.pathname + window.location.search;
|
||||||
window.location.replace(currPage);
|
window.location.replace(currPage);
|
||||||
return false;
|
return false;
|
||||||
}, 500);
|
}, 500);
|
||||||
@@ -1102,12 +1102,12 @@ function textReveal() {
|
|||||||
function textExpand() {
|
function textExpand() {
|
||||||
var textBox = document.getElementById("text-expand");
|
var textBox = document.getElementById("text-expand");
|
||||||
var button = document.getElementById("text-expand-button");
|
var button = document.getElementById("text-expand-button");
|
||||||
var textBoxLineClamp = textBox.style["-webkit-line-clamp"];
|
var style = window.getComputedStyle(textBox)
|
||||||
if (textBoxLineClamp === "none") {
|
if (style.webkitLineClamp === "none") {
|
||||||
textBox.style["-webkit-line-clamp"] = "4";
|
textBox.style["-webkit-line-clamp"] = "4";
|
||||||
button.innerText = "Show more";
|
button.innerText = "Show more";
|
||||||
} else {
|
} else {
|
||||||
textBox.style["-webkit-line-clamp"] = "none";
|
textBox.style["-webkit-line-clamp"] = "unset";
|
||||||
button.innerText = "Show less";
|
button.innerText = "Show less";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1119,8 +1119,9 @@ function textExpandButtonVisibilityUpdate() {
|
|||||||
if (!textBox || !button)
|
if (!textBox || !button)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var textBoxLineClamp = textBox.style["-webkit-line-clamp"];
|
var styles = window.getComputedStyle(textBox);
|
||||||
if (textBoxLineClamp === "none")
|
var textBoxLineClamp = styles.webkitLineClamp;
|
||||||
|
if (textBoxLineClamp === "unset")
|
||||||
return; // text box is in revealed state
|
return; // text box is in revealed state
|
||||||
|
|
||||||
if (textBox.offsetHeight < textBox.scrollHeight
|
if (textBox.offsetHeight < textBox.scrollHeight
|
||||||
@@ -1147,6 +1148,14 @@ function showForm() {
|
|||||||
animate('animate-icon', 'pulse-img');
|
animate('animate-icon', 'pulse-img');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function channelFilterDownload(value) {
|
||||||
|
if (value === "all") {
|
||||||
|
window.location = "/downloads/";
|
||||||
|
} else {
|
||||||
|
window.location.search = "?channel=" + value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function showOverwrite() {
|
function showOverwrite() {
|
||||||
var overwriteDiv = document.getElementById("overwrite-form");
|
var overwriteDiv = document.getElementById("overwrite-form");
|
||||||
if (overwriteDiv.classList.contains("hidden-overwrite")) {
|
if (overwriteDiv.classList.contains("hidden-overwrite")) {
|
||||||
|
|||||||
Reference in New Issue
Block a user