API endpoints, #build

Changed:
- [API] Added delete video endpoint
- [API] Added delete channel endpoint
- [API] Added watched state endpoint
- Changed download queue interaction to existing endpoints
- Added update notification task
This commit is contained in:
simon 2022-12-23 23:01:28 +07:00
commit 14e0429758
No known key found for this signature in database
GPG Key ID: 2C15AA5E89985DD4
19 changed files with 643 additions and 315 deletions

348
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -41,6 +41,7 @@ Note:
- [Refresh](#refresh-view)
- [Cookie](#cookie-view)
- [Search](#search-view)
- [Watched](#watched-view)
- [Ping](#ping-view)
## Authentication
@ -79,7 +80,8 @@ Pass page number as a query parameter: `page=2`. Defaults to *0*, `page=1` is re
/api/video/
## Video Item View
/api/video/\<video_id>/
GET: /api/video/\<video_id>/
DELETE: /api/video/\<video_id>/
## Video Comment View
/api/video/\<video_id>/comment/
@ -88,12 +90,12 @@ Pass page number as a query parameter: `page=2`. Defaults to *0*, `page=1` is re
/api/video/\<video_id>/similar/
## Video Progress View
/api/video/\<video_id>/progress
/api/video/\<video_id>/progress/
Progress is stored for each user.
### Get last player position of a video
GET /api/video/\<video_id>/progress
GET /api/video/\<video_id>/progress/
```json
{
"youtube_id": "<video_id>",
@ -103,7 +105,7 @@ GET /api/video/\<video_id>/progress
```
### Post player position of video
POST /api/video/\<video_id>/progress
POST /api/video/\<video_id>/progress/
```json
{
"position": 100
@ -111,7 +113,7 @@ POST /api/video/\<video_id>/progress
```
### Delete player position of video
DELETE /api/video/\<video_id>/progress
DELETE /api/video/\<video_id>/progress/
## Sponsor Block View
@ -164,7 +166,9 @@ POST /api/channel/
```
## Channel Item View
/api/channel/\<channel_id>/
GET: /api/channel/\<channel_id>/
DELETE: /api/channel/\<channel_id>/
- Will delete channel with all it's videos
## Channel Videos View
/api/channel/\<channel_id>/video/
@ -264,7 +268,7 @@ Remove this snapshot from index
## Login View
Return token and user ID for username and password:
POST /api/login
POST /api/login/
```json
{
"username": "tubearchivist",
@ -401,9 +405,22 @@ GET /api/search/?query=\<query>
Returns search results from your query.
## Watched View
POST /api/watched/
Change watched state, where the `id` can be a single video, or channel/playlist to change all videos belonging to that channel/playlist.
```json
{
"id": "xxxxxxx",
"is_watched": True
}
```
## Ping View
Validate your connection with the API
GET /api/ping
GET /api/ping/
When valid returns message with user id:
```json

View File

@ -23,6 +23,7 @@ from api.views import (
VideoProgressView,
VideoSimilarView,
VideoSponsorView,
WatchedView,
)
from django.urls import path
@ -124,6 +125,11 @@ urlpatterns = [
CookieView.as_view(),
name="api-cookie",
),
path(
"watched/",
WatchedView.as_view(),
name="api-watched",
),
path(
"search/",
SearchView.as_view(),

View File

@ -7,9 +7,11 @@ from home.src.download.yt_dlp_base import CookieHandler
from home.src.es.connect import ElasticWrap
from home.src.es.snapshot import ElasticSnapshot
from home.src.frontend.searching import SearchForm
from home.src.frontend.watched import WatchState
from home.src.index.channel import YoutubeChannel
from home.src.index.generic import Pagination
from home.src.index.reindex import ReindexProgress
from home.src.index.video import SponsorBlock
from home.src.index.video import SponsorBlock, YoutubeVideo
from home.src.ta.config import AppConfig
from home.src.ta.helper import UrlListParser
from home.src.ta.ta_redis import RedisArchivist, RedisQueue
@ -95,6 +97,20 @@ class VideoApiView(ApiBaseView):
self.get_document(video_id)
return Response(self.response, status=self.status_code)
def delete(self, request, video_id):
# pylint: disable=unused-argument
"""delete single video"""
message = {"video": video_id}
try:
YoutubeVideo(video_id).delete_media_file()
status_code = 200
message.update({"state": "delete"})
except FileNotFoundError:
status_code = 404
message.update({"state": "not found"})
return Response(message, status=status_code)
class VideoApiListView(ApiBaseView):
"""resolves to /api/video/
@ -251,6 +267,20 @@ class ChannelApiView(ApiBaseView):
self.get_document(channel_id)
return Response(self.response, status=self.status_code)
def delete(self, request, channel_id):
# pylint: disable=unused-argument
"""delete channel"""
message = {"channel": channel_id}
try:
YoutubeChannel(channel_id).delete_channel()
status_code = 200
message.update({"state": "delete"})
except FileNotFoundError:
status_code = 404
message.update({"state": "not found"})
return Response(message, status=status_code)
class ChannelApiListView(ApiBaseView):
"""resolves to /api/channel/
@ -375,7 +405,7 @@ class DownloadApiView(ApiBaseView):
def post(self, request, video_id):
"""post to video to change status"""
item_status = request.data["status"]
item_status = request.data.get("status")
if item_status not in self.valid_status:
message = f"{video_id}: invalid status {item_status}"
print(message)
@ -674,6 +704,24 @@ class CookieView(ApiBaseView):
return Response(message)
class WatchedView(ApiBaseView):
"""resolves to /api/watched/
POST: change watched state of video, channel or playlist
"""
def post(self, request):
"""change watched state"""
youtube_id = request.data.get("id")
is_watched = request.data.get("is_watched")
if not youtube_id or is_watched is None:
message = {"message": "missing id or is_watched"}
return Response(message, status=400)
WatchState(youtube_id, is_watched).change()
return Response({"message": "success"}, status=200)
class SearchView(ApiBaseView):
"""resolves to /api/search/
GET: run a search with the string in the ?query parameter

View File

@ -262,4 +262,4 @@ CORS_ALLOW_HEADERS = list(default_headers) + [
# TA application settings
TA_UPSTREAM = "https://github.com/tubearchivist/tubearchivist"
TA_VERSION = "v0.3.0"
TA_VERSION = "v0.3.1-unstable"

View File

@ -8,6 +8,7 @@ from home.src.es.connect import ElasticWrap
from home.src.es.index_setup import ElasitIndexWrap
from home.src.es.snapshot import ElasticSnapshot
from home.src.ta.config import AppConfig as ArchivistConfig
from home.src.ta.config import ReleaseVersion
from home.src.ta.helper import clear_dl_cache
from home.src.ta.ta_redis import RedisArchivist
@ -34,6 +35,7 @@ class StartupCheck:
self.make_folders()
clear_dl_cache(self.config_handler.config)
self.snapshot_check()
self.ta_version_check()
self.set_has_run()
def get_has_run(self):
@ -120,6 +122,10 @@ class StartupCheck:
print("elasticsearch version check passed")
def ta_version_check(self):
"""remove key if updated now"""
ReleaseVersion().is_updated()
class HomeConfig(AppConfig):
"""call startup funcs"""

View File

@ -49,6 +49,7 @@
"check_reindex_days": 90,
"thumbnail_check": {"minute": "0", "hour": "17", "day_of_week": "*"},
"run_backup": {"minute": "0", "hour": "8", "day_of_week": "0"},
"run_backup_rotate": 5
"run_backup_rotate": 5,
"version_check": {"minute": "0", "hour": "11", "day_of_week": "*"}
}
}

View File

@ -15,7 +15,7 @@ from home.src.download.subscriptions import PlaylistSubscription
from home.src.download.yt_dlp_base import CookieHandler, YtWrap
from home.src.es.connect import ElasticWrap, IndexPaginate
from home.src.index.channel import YoutubeChannel
from home.src.index.comments import Comments
from home.src.index.comments import CommentList
from home.src.index.playlist import YoutubePlaylist
from home.src.index.video import YoutubeVideo, index_new_video
from home.src.ta.config import AppConfig
@ -143,25 +143,7 @@ class DownloadPostProcess:
def get_comments(self):
"""get comments from youtube"""
if not self.download.config["downloads"]["comment_max"]:
return
total_videos = len(self.download.videos)
for idx, video_id in enumerate(self.download.videos):
comment = Comments(video_id, config=self.download.config)
comment.build_json(notify=(idx, total_videos))
if comment.json_data:
comment.upload_comments()
key = "message:download"
message = {
"status": key,
"level": "info",
"title": "Download and index comments finished",
"message": f"added comments for {total_videos} videos",
}
RedisArchivist().set_message(key, message, expire=4)
CommentList(self.download.videos).index(notify=True)
class VideoDownloader:

View File

@ -4,15 +4,11 @@ Functionality:
- called via user input
"""
from home.src.download.queue import PendingInteract
from home.src.download.subscriptions import (
ChannelSubscription,
PlaylistSubscription,
)
from home.src.frontend.watched import WatchState
from home.src.index.channel import YoutubeChannel
from home.src.index.playlist import YoutubePlaylist
from home.src.index.video import YoutubeVideo
from home.src.ta.helper import UrlListParser
from home.src.ta.ta_redis import RedisArchivist, RedisQueue
from home.tasks import (
@ -50,12 +46,9 @@ class PostData:
def exec_map(self):
"""map dict key and return function to execute"""
exec_map = {
"watched": self._watched,
"un_watched": self._un_watched,
"change_view": self._change_view,
"change_grid": self._change_grid,
"rescan_pending": self._rescan_pending,
"ignore": self._ignore,
"dl_pending": self._dl_pending,
"queue": self._queue_handler,
"unsubscribe": self._unsubscribe,
@ -65,32 +58,17 @@ class PostData:
"show_subed_only": self._show_subed_only,
"dlnow": self._dlnow,
"show_ignored_only": self._show_ignored_only,
"forgetIgnore": self._forget_ignore,
"addSingle": self._add_single,
"deleteQueue": self._delete_queue,
"manual-import": self._manual_import,
"re-embed": self._re_embed,
"db-backup": self._db_backup,
"db-restore": self._db_restore,
"fs-rescan": self._fs_rescan,
"delete-video": self._delete_video,
"delete-channel": self._delete_channel,
"delete-playlist": self._delete_playlist,
"find-playlists": self._find_playlists,
}
return exec_map[self.to_exec]
def _watched(self):
"""mark as watched"""
WatchState(self.exec_val).mark_as_watched()
return {"success": True}
def _un_watched(self):
"""mark as unwatched"""
WatchState(self.exec_val).mark_as_unwatched()
return {"success": True}
def _change_view(self):
"""process view changes in home, channel, and downloads"""
origin, new_view = self.exec_val.split(":")
@ -117,15 +95,6 @@ class PostData:
update_subscribed.delay()
return {"success": True}
def _ignore(self):
"""ignore from download queue"""
video_id = self.exec_val
print(f"{video_id}: ignore video from download queue")
PendingInteract(video_id=video_id, status="ignore").update_status()
# also clear from redis queue
RedisQueue(queue_name="dl_queue").clear_item(video_id)
return {"success": True}
@staticmethod
def _dl_pending():
"""start the download queue"""
@ -228,27 +197,6 @@ class PostData:
RedisArchivist().set_message(key, value)
return {"success": True}
def _forget_ignore(self):
"""delete from ta_download index"""
video_id = self.exec_val
print(f"{video_id}: forget from download")
PendingInteract(video_id=video_id).delete_item()
return {"success": True}
def _add_single(self):
"""add single youtube_id to download queue"""
video_id = self.exec_val
print(f"{video_id}: add single vid to download queue")
PendingInteract(video_id=video_id, status="pending").update_status()
return {"success": True}
def _delete_queue(self):
"""delete download queue"""
status = self.exec_val
print("deleting from download queue: " + status)
PendingInteract(status=status).delete_by_status()
return {"success": True}
@staticmethod
def _manual_import():
"""run manual import from settings page"""
@ -284,18 +232,6 @@ class PostData:
rescan_filesystem.delay()
return {"success": True}
def _delete_video(self):
"""delete media file, metadata and thumb"""
youtube_id = self.exec_val
YoutubeVideo(youtube_id).delete_media_file()
return {"success": True}
def _delete_channel(self):
"""delete channel and all matching videos"""
channel_id = self.exec_val
YoutubeChannel(channel_id).delete_channel()
return {"success": True}
def _delete_playlist(self):
"""delete playlist, only metadata or incl all videos"""
playlist_dict = self.exec_val

View File

@ -12,95 +12,94 @@ from home.src.ta.helper import UrlListParser
class WatchState:
"""handle watched checkbox for videos and channels"""
def __init__(self, youtube_id):
def __init__(self, youtube_id, is_watched):
self.youtube_id = youtube_id
self.is_watched = is_watched
self.stamp = int(datetime.now().timestamp())
self.pipeline = f"_ingest/pipeline/watch_{youtube_id}"
def mark_as_watched(self):
"""update es with new watched value"""
url_type = self.dedect_type()
def change(self):
"""change watched state of item(s)"""
url_type = self._dedect_type()
if url_type == "video":
self.mark_vid_watched()
elif url_type == "channel":
self.mark_channel_watched()
elif url_type == "playlist":
self.mark_playlist_watched()
self.change_vid_state()
return
print(f"{self.youtube_id}: marked as watched")
self._add_pipeline()
path = f"ta_video/_update_by_query?pipeline=watch_{self.youtube_id}"
data = self._build_update_data(url_type)
_, _ = ElasticWrap(path).post(data)
self._delete_pipeline()
def mark_as_unwatched(self):
"""revert watched state to false"""
url_type = self.dedect_type()
if url_type == "video":
self.mark_vid_watched(revert=True)
print(f"{self.youtube_id}: revert as unwatched")
def dedect_type(self):
def _dedect_type(self):
"""find youtube id type"""
print(self.youtube_id)
url_process = UrlListParser(self.youtube_id).process_list()
url_type = url_process[0]["type"]
return url_type
def mark_vid_watched(self, revert=False):
"""change watched status of single video"""
def change_vid_state(self):
"""change watched state of video"""
path = f"ta_video/_update/{self.youtube_id}"
data = {
"doc": {"player": {"watched": True, "watched_date": self.stamp}}
"doc": {
"player": {
"watched": self.is_watched,
"watched_date": self.stamp,
}
}
}
if revert:
data["doc"]["player"]["watched"] = False
response, status_code = ElasticWrap(path).post(data=data)
if status_code != 200:
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 _build_update_data(self, url_type):
"""build update by query data based on url_type"""
term_key_map = {
"channel": "channel.channel_id",
"playlist": "playlist.keyword",
}
term_key = term_key_map.get(url_type)
def mark_channel_watched(self):
"""change watched status of every video in channel"""
path = "ta_video/_update_by_query"
must_list = [
{"term": {"channel.channel_id": {"value": self.youtube_id}}},
{"term": {"player.watched": {"value": False}}},
]
data = {
"query": {"bool": {"must": must_list}},
"script": {
"source": self._get_source(),
"lang": "painless",
},
return {
"query": {
"bool": {
"must": [
{"term": {term_key: {"value": self.youtube_id}}},
{
"term": {
"player.watched": {
"value": not self.is_watched
}
}
},
],
}
}
}
response, status_code = ElasticWrap(path).post(data=data)
if status_code != 200:
print(response)
raise ValueError("failed mark channel as watched")
def mark_playlist_watched(self):
"""change watched state of all videos in playlist"""
path = "ta_video/_update_by_query"
must_list = [
{"term": {"playlist.keyword": {"value": self.youtube_id}}},
{"term": {"player.watched": {"value": False}}},
]
def _add_pipeline(self):
"""add ingest pipeline"""
data = {
"query": {"bool": {"must": must_list}},
"script": {
"source": self._get_source(),
"lang": "painless",
},
"description": f"{self.youtube_id}: watched {self.is_watched}",
"processors": [
{
"set": {
"field": "player.watched",
"value": self.is_watched,
}
},
{
"set": {
"field": "player.watched_date",
"value": self.stamp,
}
},
],
}
_, _ = ElasticWrap(self.pipeline).put(data)
response, status_code = ElasticWrap(path).post(data=data)
if status_code != 200:
print(response)
raise ValueError("failed mark playlist as watched")
def _delete_pipeline(self):
"""delete pipeline"""
ElasticWrap(self.pipeline).delete()

View File

@ -298,6 +298,9 @@ class YoutubeChannel(YouTubeItem):
"""delete channel and all videos"""
print(f"{self.youtube_id}: delete channel")
self.get_from_es()
if not self.json_data:
raise FileNotFoundError
folder_path = self.get_folder_path()
print(f"{self.youtube_id}: delete all media files")
try:

View File

@ -14,7 +14,7 @@ from home.src.ta.ta_redis import RedisArchivist
class Comments:
"""hold all comments functionality"""
"""interact with comments per video"""
def __init__(self, youtube_id, config=False):
self.youtube_id = youtube_id
@ -146,6 +146,7 @@ class Comments:
if not self.is_activated:
return
print(f"{self.youtube_id}: upload comments")
_, _ = ElasticWrap(self.es_path).put(self.json_data)
vid_path = f"ta_video/_update/{self.youtube_id}"
@ -187,3 +188,40 @@ class Comments:
self.delete_comments()
self.upload_comments()
class CommentList:
"""interact with comments in group"""
def __init__(self, video_ids):
self.video_ids = video_ids
self.config = AppConfig().config
def index(self, notify=False):
"""index group of videos"""
if not self.config["downloads"].get("comment_max"):
return
total_videos = len(self.video_ids)
for idx, video_id in enumerate(self.video_ids):
comment = Comments(video_id, config=self.config)
if notify:
notify = (idx, total_videos)
comment.build_json(notify=notify)
if comment.json_data:
comment.upload_comments()
if notify:
self.notify_final(total_videos)
@staticmethod
def notify_final(total_videos):
"""send final notification"""
key = "message:download"
message = {
"status": key,
"level": "info",
"title": "Download and index comments finished",
"message": f"added comments for {total_videos} videos",
}
RedisArchivist().set_message(key, message, expire=4)

View File

@ -14,6 +14,7 @@ import subprocess
from home.src.download.queue import PendingList
from home.src.download.thumbnails import ThumbManager
from home.src.es.connect import ElasticWrap
from home.src.index.comments import CommentList
from home.src.index.video import YoutubeVideo, index_new_video
from home.src.ta.config import AppConfig
from home.src.ta.helper import clean_string, ignore_filelist
@ -601,6 +602,8 @@ def scan_filesystem():
filesystem_handler.delete_from_index()
if filesystem_handler.to_index:
print("index new videos")
for missing_vid in filesystem_handler.to_index:
youtube_id = missing_vid[2]
video_ids = [i[2] for i in filesystem_handler.to_index]
for youtube_id in video_ids:
index_new_video(youtube_id)
CommentList(video_ids).index()

View File

@ -292,6 +292,9 @@ class YoutubeVideo(YouTubeItem, YoutubeSubtitle):
"""delete video file, meta data"""
print(f"{self.youtube_id}: delete video")
self.get_from_es()
if not self.json_data:
raise FileNotFoundError
video_base = self.app_conf["videos"]
media_url = self.json_data.get("media_url")
file_path = os.path.join(video_base, media_url)

View File

@ -8,7 +8,9 @@ import json
import os
import re
import requests
from celery.schedules import crontab
from django.conf import settings
from home.src.ta.ta_redis import RedisArchivist
@ -154,6 +156,7 @@ class ScheduleBuilder:
"check_reindex": "0 12 *",
"thumbnail_check": "0 17 *",
"run_backup": "0 18 0",
"version_check": "0 11 *",
}
CONFIG = ["check_reindex_days", "run_backup_rotate"]
MSG = "message:setting"
@ -268,3 +271,74 @@ class ScheduleBuilder:
schedule_dict.update(to_add)
return schedule_dict
class ReleaseVersion:
"""compare local version with remote version"""
REMOTE_URL = "https://www.tubearchivist.com/api/release/latest/"
NEW_KEY = "versioncheck:new"
def __init__(self):
self.local_version = self._parse_version(settings.TA_VERSION)
self.is_unstable = settings.TA_VERSION.endswith("-unstable")
self.remote_version = False
self.is_breaking = False
self.response = False
def check(self):
"""check version"""
print(f"[{self.local_version}]: look for updates")
self.get_remote_version()
new_version, is_breaking = self._has_update()
if new_version:
message = {
"status": True,
"version": new_version,
"is_breaking": is_breaking,
}
RedisArchivist().set_message(self.NEW_KEY, message)
print(f"[{self.local_version}]: found new version {new_version}")
def get_remote_version(self):
"""read version from remote"""
self.response = requests.get(self.REMOTE_URL, timeout=20).json()
remote_version_str = self.response["release_version"]
self.remote_version = self._parse_version(remote_version_str)
self.is_breaking = self.response["breaking_changes"]
def _has_update(self):
"""check if there is an update"""
for idx, number in enumerate(self.local_version):
is_newer = self.remote_version[idx] > number
if is_newer:
return self.response["release_version"], self.is_breaking
if self.is_unstable and self.local_version == self.remote_version:
return self.response["release_version"], self.is_breaking
return False, False
@staticmethod
def _parse_version(version):
"""return version parts"""
clean = version.rstrip("-unstable").lstrip("v")
return tuple((int(i) for i in clean.split(".")))
def is_updated(self):
"""check if update happened in the mean time"""
message = self.get_update()
if not message:
return
if self._parse_version(message.get("version")) == self.local_version:
print(f"[{self.local_version}]: update completed")
RedisArchivist().del_message(self.NEW_KEY)
def get_update(self):
"""return new version dict if available"""
message = RedisArchivist().get_message(self.NEW_KEY)
if not message.get("status"):
return False
return message

View File

@ -22,7 +22,7 @@ from home.src.es.index_setup import ElasitIndexWrap
from home.src.index.channel import YoutubeChannel
from home.src.index.filesystem import ImportFolderScanner, scan_filesystem
from home.src.index.reindex import Reindex, ReindexManual, ReindexOutdated
from home.src.ta.config import AppConfig, ScheduleBuilder
from home.src.ta.config import AppConfig, ReleaseVersion, ScheduleBuilder
from home.src.ta.helper import UrlListParser, clear_dl_cache
from home.src.ta.ta_redis import RedisArchivist, RedisQueue
@ -290,9 +290,15 @@ def index_channel_playlists(channel_id):
channel.index_channel_playlists()
@shared_task(name="version_check")
def version_check():
"""check for new updates"""
ReleaseVersion().check()
try:
app.conf.beat_schedule = ScheduleBuilder().build_schedule()
except KeyError:
# update path from v0.0.8 to v0.0.9 to load new defaults
# update path to load new defaults
StartupCheck().sync_redis_state()
app.conf.beat_schedule = ScheduleBuilder().build_schedule()

View File

@ -132,7 +132,14 @@
</div>
<div class="footer">
<div class="boxed-content">
<span>© 2021 - <script type="text/javascript">document.write(new Date().getFullYear());</script> TubeArchivist {{ version }} </span><span><a href="{% url 'about' %}">About</a> | <a href="https://github.com/tubearchivist/tubearchivist" target="_blank">GitHub</a> | <a href="https://hub.docker.com/r/bbilly1/tubearchivist" target="_blank">Docker Hub</a> | <a href="https://www.tubearchivist.com/discord" target="_blank">Discord</a> | <a href="https://www.reddit.com/r/TubeArchivist/">Reddit</a></span>
<span>© 2021 - <script type="text/javascript">document.write(new Date().getFullYear());</script></span>
<span>TubeArchivist</span>
<span>{{ version }}</span>
{% if ta_update %}
<span class="danger-zone">{{ ta_update.version }} available{% if ta_update.is_breaking %}<span class="danger-zone">Breaking Changes!</span>{% endif %}</span>
<span><a href="https://github.com/tubearchivist/tubearchivist/releases/tag/{{ ta_update.version }}" target="_blank">Release Page</a> | </span>
{% endif %}
<span><a href="{% url 'about' %}">About</a> | <a href="https://github.com/tubearchivist/tubearchivist" target="_blank">GitHub</a> | <a href="https://hub.docker.com/r/bbilly1/tubearchivist" target="_blank">Docker Hub</a> | <a href="https://www.tubearchivist.com/discord" target="_blank">Discord</a> | <a href="https://www.reddit.com/r/TubeArchivist/">Reddit</a></span>
</div>
</div>
</body>

View File

@ -36,7 +36,7 @@ from home.src.index.channel import YoutubeChannel, channel_overwrites
from home.src.index.generic import Pagination
from home.src.index.playlist import YoutubePlaylist
from home.src.index.reindex import ReindexProgress
from home.src.ta.config import AppConfig, ScheduleBuilder
from home.src.ta.config import AppConfig, ReleaseVersion, ScheduleBuilder
from home.src.ta.helper import UrlListParser, time_parser
from home.src.ta.ta_redis import RedisArchivist
from home.tasks import extrac_dl, index_channel_playlists, subscribe_to
@ -138,6 +138,7 @@ class ArchivistViewConfig(View):
"show_ignored_only": self._get_show_ignore_only(),
"show_subed_only": self._get_show_subed_only(),
"version": settings.TA_VERSION,
"ta_update": ReleaseVersion().get_update(),
}
@ -257,6 +258,19 @@ class ArchivistResultsView(ArchivistViewConfig):
self.context["pagination"] = self.pagination_handler.pagination
class MinView(View):
"""to inherit from for minimal config vars"""
@staticmethod
def get_min_context(request):
"""build minimal vars for context"""
return {
"colors": AppConfig(request.user.id).colors,
"version": settings.TA_VERSION,
"ta_update": ReleaseVersion().get_update(),
}
class HomeView(ArchivistResultsView):
"""resolves to /
handle home page and video search post functionality
@ -298,20 +312,23 @@ class HomeView(ArchivistResultsView):
self.data["query"] = query
class LoginView(View):
class LoginView(MinView):
"""resolves to /login/
Greeting and login page
"""
SEC_IN_DAY = 60 * 60 * 24
@staticmethod
def get(request):
def get(self, request):
"""handle get requests"""
failed = bool(request.GET.get("failed"))
colors = AppConfig(request.user.id).colors
form = CustomAuthForm()
context = {"colors": colors, "form": form, "form_error": failed}
context = self.get_min_context(request)
context.update(
{
"form": CustomAuthForm(),
"form_error": bool(request.GET.get("failed")),
}
)
return render(request, "home/login.html", context)
def post(self, request):
@ -333,19 +350,15 @@ class LoginView(View):
return redirect("/login?failed=true")
class AboutView(View):
class AboutView(MinView):
"""resolves to /about/
show helpful how to information
"""
@staticmethod
def get(request):
def get(self, request):
"""handle http get"""
context = {
"title": "About",
"colors": AppConfig(request.user.id).colors,
"version": settings.TA_VERSION,
}
context = self.get_min_context(request)
context.update({"title": "About"})
return render(request, "home/about.html", context)
@ -843,7 +856,7 @@ class PlaylistView(ArchivistResultsView):
return redirect("playlist")
class VideoView(View):
class VideoView(MinView):
"""resolves to /video/<video-id>/
display details about a single video
"""
@ -869,17 +882,18 @@ class VideoView(View):
request_type="video", request_id=video_id
).get_progress()
context = {
"video": video_data,
"playlist_nav": playlist_nav,
"title": video_data.get("title"),
"colors": config_handler.colors,
"cast": config_handler.config["application"]["enable_cast"],
"version": settings.TA_VERSION,
"config": config_handler.config,
"position": time_parser(request.GET.get("t")),
"reindex": reindex.get("state"),
}
context = self.get_min_context(request)
context.update(
{
"video": video_data,
"playlist_nav": playlist_nav,
"title": video_data.get("title"),
"cast": config_handler.config["application"]["enable_cast"],
"config": config_handler.config,
"position": time_parser(request.GET.get("t")),
"reindex": reindex.get("state"),
}
)
return render(request, "home/video.html", context)
@staticmethod
@ -936,7 +950,7 @@ class SearchView(ArchivistResultsView):
return render(request, "home/search.html", self.context)
class SettingsView(View):
class SettingsView(MinView):
"""resolves to /settings/
handle the settings page, display current settings,
take post request from the form to update settings
@ -944,28 +958,19 @@ class SettingsView(View):
def get(self, request):
"""read and display current settings"""
config_handler = AppConfig(request.user.id)
colors = config_handler.colors
available_backups = ElasticBackup().get_all_backup_files()
user_form = UserSettingsForm()
app_form = ApplicationSettingsForm()
scheduler_form = SchedulerSettingsForm()
snapshots = ElasticSnapshot().get_snapshot_stats()
token = self.get_token(request)
context = {
"title": "Settings",
"config": config_handler.config,
"api_token": token,
"colors": colors,
"available_backups": available_backups,
"user_form": user_form,
"app_form": app_form,
"scheduler_form": scheduler_form,
"snapshots": snapshots,
"version": settings.TA_VERSION,
}
context = self.get_min_context(request)
context.update(
{
"title": "Settings",
"config": AppConfig(request.user.id).config,
"api_token": self.get_token(request),
"available_backups": ElasticBackup().get_all_backup_files(),
"user_form": UserSettingsForm(),
"app_form": ApplicationSettingsForm(),
"scheduler_form": SchedulerSettingsForm(),
"snapshots": ElasticSnapshot().get_snapshot_stats(),
}
)
return render(request, "home/settings.html", context)

View File

@ -23,15 +23,14 @@ function updateVideoWatchStatus(input1, videoCurrentWatchStatus) {
postVideoProgress(videoId, 0); // Reset video progress on watched/unwatched;
removeProgressBar(videoId);
let watchStatusIndicator, payload;
let watchStatusIndicator;
let apiEndpoint = '/api/watched/';
if (videoCurrentWatchStatus === 'watched') {
watchStatusIndicator = createWatchStatusIndicator(videoId, 'unwatched');
payload = JSON.stringify({ un_watched: videoId });
sendPost(payload);
apiRequest(apiEndpoint, 'POST', { id: videoId, is_watched: false });
} else if (videoCurrentWatchStatus === 'unwatched') {
watchStatusIndicator = createWatchStatusIndicator(videoId, 'watched');
payload = JSON.stringify({ watched: videoId });
sendPost(payload);
apiRequest(apiEndpoint, 'POST', { id: videoId, is_watched: true });
}
let watchButtons = document.getElementsByClassName('watch-button');
@ -76,9 +75,10 @@ function removeProgressBar(videoId) {
function isWatchedButton(button) {
let youtube_id = button.getAttribute('data-id');
let payload = JSON.stringify({ watched: youtube_id });
let apiEndpoint = '/api/watched/';
let data = { id: youtube_id, is_watched: true };
button.remove();
sendPost(payload);
apiRequest(apiEndpoint, 'POST', data);
setTimeout(function () {
location.reload();
}, 1000);
@ -186,8 +186,8 @@ function dlPending() {
function toIgnore(button) {
let youtube_id = button.getAttribute('data-id');
let payload = JSON.stringify({ ignore: youtube_id });
sendPost(payload);
let apiEndpoint = '/api/download/' + youtube_id + '/';
apiRequest(apiEndpoint, 'POST', { status: 'ignore' });
document.getElementById('dl-' + youtube_id).remove();
}
@ -203,15 +203,15 @@ function downloadNow(button) {
function forgetIgnore(button) {
let youtube_id = button.getAttribute('data-id');
let payload = JSON.stringify({ forgetIgnore: youtube_id });
sendPost(payload);
let apiEndpoint = '/api/download/' + youtube_id + '/';
apiRequest(apiEndpoint, 'DELETE');
document.getElementById('dl-' + youtube_id).remove();
}
function addSingle(button) {
let youtube_id = button.getAttribute('data-id');
let payload = JSON.stringify({ addSingle: youtube_id });
sendPost(payload);
let apiEndpoint = '/api/download/' + youtube_id + '/';
apiRequest(apiEndpoint, 'POST', { status: 'pending' });
document.getElementById('dl-' + youtube_id).remove();
setTimeout(function () {
checkMessages();
@ -220,8 +220,8 @@ function addSingle(button) {
function deleteQueue(button) {
let to_delete = button.getAttribute('data-id');
let payload = JSON.stringify({ deleteQueue: to_delete });
sendPost(payload);
let apiEndpoint = '/api/download/?filter=' + to_delete;
apiRequest(apiEndpoint, 'DELETE');
// clear button
let message = document.createElement('p');
message.innerText = 'deleting download queue: ' + to_delete;
@ -334,8 +334,8 @@ function deleteConfirm() {
function deleteVideo(button) {
let to_delete = button.getAttribute('data-id');
let to_redirect = button.getAttribute('data-redirect');
let payload = JSON.stringify({ 'delete-video': to_delete });
sendPost(payload);
let apiEndpoint = '/api/video/' + to_delete + '/';
apiRequest(apiEndpoint, 'DELETE');
setTimeout(function () {
let redirect = '/channel/' + to_redirect;
window.location.replace(redirect);
@ -344,8 +344,8 @@ function deleteVideo(button) {
function deleteChannel(button) {
let to_delete = button.getAttribute('data-id');
let payload = JSON.stringify({ 'delete-channel': to_delete });
sendPost(payload);
let apiEndpoint = '/api/channel/' + to_delete + '/';
apiRequest(apiEndpoint, 'DELETE');
setTimeout(function () {
window.location.replace('/channel/');
}, 1000);