[API] add watched state endpoints

This commit is contained in:
simon 2022-12-23 20:39:14 +07:00
parent a2e2fd1b89
commit 6ed2308f99
No known key found for this signature in database
GPG Key ID: 2C15AA5E89985DD4
6 changed files with 112 additions and 74 deletions

View File

@ -41,6 +41,7 @@ Note:
- [Refresh](#refresh-view) - [Refresh](#refresh-view)
- [Cookie](#cookie-view) - [Cookie](#cookie-view)
- [Search](#search-view) - [Search](#search-view)
- [Watched](#watched-view)
- [Ping](#ping-view) - [Ping](#ping-view)
## Authentication ## Authentication
@ -404,6 +405,19 @@ GET /api/search/?query=\<query>
Returns search results from your 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 ## Ping View
Validate your connection with the API Validate your connection with the API
GET /api/ping/ GET /api/ping/

View File

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

View File

@ -7,6 +7,7 @@ from home.src.download.yt_dlp_base import CookieHandler
from home.src.es.connect import ElasticWrap from home.src.es.connect import ElasticWrap
from home.src.es.snapshot import ElasticSnapshot from home.src.es.snapshot import ElasticSnapshot
from home.src.frontend.searching import SearchForm 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.channel import YoutubeChannel
from home.src.index.generic import Pagination from home.src.index.generic import Pagination
from home.src.index.reindex import ReindexProgress from home.src.index.reindex import ReindexProgress
@ -703,6 +704,24 @@ class CookieView(ApiBaseView):
return Response(message) 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): class SearchView(ApiBaseView):
"""resolves to /api/search/ """resolves to /api/search/
GET: run a search with the string in the ?query parameter GET: run a search with the string in the ?query parameter

View File

@ -74,12 +74,12 @@ class PostData:
def _watched(self): def _watched(self):
"""mark as watched""" """mark as watched"""
WatchState(self.exec_val).mark_as_watched() WatchState(self.exec_val, is_watched=True).change()
return {"success": True} return {"success": True}
def _un_watched(self): def _un_watched(self):
"""mark as unwatched""" """mark as unwatched"""
WatchState(self.exec_val).mark_as_unwatched() WatchState(self.exec_val, is_watched=False).change()
return {"success": True} return {"success": True}
def _change_view(self): def _change_view(self):

View File

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

View File

@ -24,14 +24,13 @@ function updateVideoWatchStatus(input1, videoCurrentWatchStatus) {
removeProgressBar(videoId); removeProgressBar(videoId);
let watchStatusIndicator, payload; let watchStatusIndicator, payload;
let apiEndpoint = '/api/watched/'
if (videoCurrentWatchStatus === 'watched') { if (videoCurrentWatchStatus === 'watched') {
watchStatusIndicator = createWatchStatusIndicator(videoId, 'unwatched'); watchStatusIndicator = createWatchStatusIndicator(videoId, 'unwatched');
payload = JSON.stringify({ un_watched: videoId }); apiRequest(apiEndpoint, 'POST', {id: videoId, "is_watched": false})
sendPost(payload);
} else if (videoCurrentWatchStatus === 'unwatched') { } else if (videoCurrentWatchStatus === 'unwatched') {
watchStatusIndicator = createWatchStatusIndicator(videoId, 'watched'); watchStatusIndicator = createWatchStatusIndicator(videoId, 'watched');
payload = JSON.stringify({ watched: videoId }); apiRequest(apiEndpoint, 'POST', {id: videoId, "is_watched": true})
sendPost(payload);
} }
let watchButtons = document.getElementsByClassName('watch-button'); let watchButtons = document.getElementsByClassName('watch-button');
@ -76,9 +75,10 @@ function removeProgressBar(videoId) {
function isWatchedButton(button) { function isWatchedButton(button) {
let youtube_id = button.getAttribute('data-id'); 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(); button.remove();
sendPost(payload); apiRequest(apiEndpoint, 'POST', data);
setTimeout(function () { setTimeout(function () {
location.reload(); location.reload();
}, 1000); }, 1000);