From 6ed2308f99d3d4b81b1feefb2e33a6c74a6a5a9b Mon Sep 17 00:00:00 2001 From: simon Date: Fri, 23 Dec 2022 20:39:14 +0700 Subject: [PATCH] [API] add watched state endpoints --- tubearchivist/api/README.md | 14 ++ tubearchivist/api/urls.py | 6 + tubearchivist/api/views.py | 19 +++ tubearchivist/home/src/frontend/api_calls.py | 4 +- tubearchivist/home/src/frontend/watched.py | 131 +++++++++---------- tubearchivist/static/script.js | 12 +- 6 files changed, 112 insertions(+), 74 deletions(-) diff --git a/tubearchivist/api/README.md b/tubearchivist/api/README.md index f6c5aea..1a95fb8 100644 --- a/tubearchivist/api/README.md +++ b/tubearchivist/api/README.md @@ -41,6 +41,7 @@ Note: - [Refresh](#refresh-view) - [Cookie](#cookie-view) - [Search](#search-view) +- [Watched](#watched-view) - [Ping](#ping-view) ## Authentication @@ -404,6 +405,19 @@ GET /api/search/?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/ diff --git a/tubearchivist/api/urls.py b/tubearchivist/api/urls.py index 0476719..7cf5368 100644 --- a/tubearchivist/api/urls.py +++ b/tubearchivist/api/urls.py @@ -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(), diff --git a/tubearchivist/api/views.py b/tubearchivist/api/views.py index e24c2ef..c99871d 100644 --- a/tubearchivist/api/views.py +++ b/tubearchivist/api/views.py @@ -7,6 +7,7 @@ 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 @@ -703,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 diff --git a/tubearchivist/home/src/frontend/api_calls.py b/tubearchivist/home/src/frontend/api_calls.py index 48df366..3ed129f 100644 --- a/tubearchivist/home/src/frontend/api_calls.py +++ b/tubearchivist/home/src/frontend/api_calls.py @@ -74,12 +74,12 @@ class PostData: def _watched(self): """mark as watched""" - WatchState(self.exec_val).mark_as_watched() + WatchState(self.exec_val, is_watched=True).change() return {"success": True} def _un_watched(self): """mark as unwatched""" - WatchState(self.exec_val).mark_as_unwatched() + WatchState(self.exec_val, is_watched=False).change() return {"success": True} def _change_view(self): diff --git a/tubearchivist/home/src/frontend/watched.py b/tubearchivist/home/src/frontend/watched.py index e8b3e15..8978b96 100644 --- a/tubearchivist/home/src/frontend/watched.py +++ b/tubearchivist/home/src/frontend/watched.py @@ -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() diff --git a/tubearchivist/static/script.js b/tubearchivist/static/script.js index 1883c01..cb993f4 100644 --- a/tubearchivist/static/script.js +++ b/tubearchivist/static/script.js @@ -24,14 +24,13 @@ function updateVideoWatchStatus(input1, videoCurrentWatchStatus) { removeProgressBar(videoId); let watchStatusIndicator, payload; + 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);