You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
526 lines
16 KiB
526 lines
16 KiB
"""all API views""" |
|
|
|
from api.src.search_processor import SearchProcess |
|
from api.src.task_processor import TaskHandler |
|
from home.src.download.queue import PendingInteract |
|
from home.src.download.yt_dlp_base import CookieHandler |
|
from home.src.es.connect import ElasticWrap |
|
from home.src.index.generic import Pagination |
|
from home.src.index.video import SponsorBlock |
|
from home.src.ta.config import AppConfig |
|
from home.src.ta.helper import UrlListParser |
|
from home.src.ta.ta_redis import RedisArchivist, RedisQueue |
|
from home.tasks import extrac_dl, subscribe_to |
|
from rest_framework.authentication import ( |
|
SessionAuthentication, |
|
TokenAuthentication, |
|
) |
|
from rest_framework.authtoken.models import Token |
|
from rest_framework.authtoken.views import ObtainAuthToken |
|
from rest_framework.permissions import IsAuthenticated |
|
from rest_framework.response import Response |
|
from rest_framework.views import APIView |
|
|
|
|
|
class ApiBaseView(APIView): |
|
"""base view to inherit from""" |
|
|
|
authentication_classes = [SessionAuthentication, TokenAuthentication] |
|
permission_classes = [IsAuthenticated] |
|
search_base = False |
|
data = False |
|
|
|
def __init__(self): |
|
super().__init__() |
|
self.response = {"data": False, "config": AppConfig().config} |
|
self.data = {"query": {"match_all": {}}} |
|
self.status_code = False |
|
self.context = False |
|
self.pagination_handler = False |
|
|
|
def get_document(self, document_id): |
|
"""get single document from es""" |
|
path = f"{self.search_base}{document_id}" |
|
print(path) |
|
response, status_code = ElasticWrap(path).get() |
|
try: |
|
self.response["data"] = SearchProcess(response).process() |
|
except KeyError: |
|
print(f"item not found: {document_id}") |
|
self.response["data"] = False |
|
self.status_code = status_code |
|
|
|
def initiate_pagination(self, request): |
|
"""set initial pagination values""" |
|
user_id = request.user.id |
|
page_get = int(request.GET.get("page", 0)) |
|
self.pagination_handler = Pagination(page_get, user_id) |
|
self.data.update( |
|
{ |
|
"size": self.pagination_handler.pagination["page_size"], |
|
"from": self.pagination_handler.pagination["page_from"], |
|
} |
|
) |
|
|
|
def get_document_list(self, request): |
|
"""get a list of results""" |
|
print(self.search_base) |
|
self.initiate_pagination(request) |
|
es_handler = ElasticWrap(self.search_base) |
|
response, status_code = es_handler.get(data=self.data) |
|
self.response["data"] = SearchProcess(response).process() |
|
if self.response["data"]: |
|
self.status_code = status_code |
|
else: |
|
self.status_code = 404 |
|
|
|
self.pagination_handler.validate(response["hits"]["total"]["value"]) |
|
self.response["paginate"] = self.pagination_handler.pagination |
|
|
|
|
|
class VideoApiView(ApiBaseView): |
|
"""resolves to /api/video/<video_id>/ |
|
GET: returns metadata dict of video |
|
""" |
|
|
|
search_base = "ta_video/_doc/" |
|
|
|
def get(self, request, video_id): |
|
# pylint: disable=unused-argument |
|
"""get request""" |
|
self.get_document(video_id) |
|
return Response(self.response, status=self.status_code) |
|
|
|
|
|
class VideoApiListView(ApiBaseView): |
|
"""resolves to /api/video/ |
|
GET: returns list of videos |
|
""" |
|
|
|
search_base = "ta_video/_search/" |
|
|
|
def get(self, request): |
|
"""get request""" |
|
self.data.update({"sort": [{"published": {"order": "desc"}}]}) |
|
self.get_document_list(request) |
|
|
|
return Response(self.response) |
|
|
|
|
|
class VideoProgressView(ApiBaseView): |
|
"""resolves to /api/video/<video_id>/ |
|
handle progress status for video |
|
""" |
|
|
|
def get(self, request, video_id): |
|
"""get progress for a single video""" |
|
user_id = request.user.id |
|
key = f"{user_id}:progress:{video_id}" |
|
video_progress = RedisArchivist().get_message(key) |
|
position = video_progress.get("position", 0) |
|
|
|
self.response = { |
|
"youtube_id": video_id, |
|
"user_id": user_id, |
|
"position": position, |
|
} |
|
return Response(self.response) |
|
|
|
def post(self, request, video_id): |
|
"""set progress position in redis""" |
|
position = request.data.get("position", 0) |
|
key = f"{request.user.id}:progress:{video_id}" |
|
message = {"position": position, "youtube_id": video_id} |
|
RedisArchivist().set_message(key, message) |
|
self.response = request.data |
|
|
|
return Response(self.response) |
|
|
|
def delete(self, request, video_id): |
|
"""delete progress position""" |
|
key = f"{request.user.id}:progress:{video_id}" |
|
RedisArchivist().del_message(key) |
|
self.response = {"progress-reset": video_id} |
|
|
|
return Response(self.response) |
|
|
|
|
|
class VideoSponsorView(ApiBaseView): |
|
"""resolves to /api/video/<video_id>/sponsor/ |
|
handle sponsor block integration |
|
""" |
|
|
|
search_base = "ta_video/_doc/" |
|
|
|
def get(self, request, video_id): |
|
"""get sponsor info""" |
|
# pylint: disable=unused-argument |
|
|
|
self.get_document(video_id) |
|
sponsorblock = self.response["data"].get("sponsorblock") |
|
|
|
return Response(sponsorblock) |
|
|
|
def post(self, request, video_id): |
|
"""post verification and timestamps""" |
|
if "segment" in request.data: |
|
response, status_code = self._create_segment(request, video_id) |
|
elif "vote" in request.data: |
|
response, status_code = self._vote_on_segment(request) |
|
|
|
return Response(response, status=status_code) |
|
|
|
@staticmethod |
|
def _create_segment(request, video_id): |
|
"""create segment in API""" |
|
start_time = request.data["segment"]["startTime"] |
|
end_time = request.data["segment"]["endTime"] |
|
response, status_code = SponsorBlock(request.user.id).post_timestamps( |
|
video_id, start_time, end_time |
|
) |
|
|
|
return response, status_code |
|
|
|
@staticmethod |
|
def _vote_on_segment(request): |
|
"""validate on existing segment""" |
|
user_id = request.user.id |
|
uuid = request.data["vote"]["uuid"] |
|
vote = request.data["vote"]["yourVote"] |
|
response, status_code = SponsorBlock(user_id).vote_on_segment( |
|
uuid, vote |
|
) |
|
|
|
return response, status_code |
|
|
|
|
|
class ChannelApiView(ApiBaseView): |
|
"""resolves to /api/channel/<channel_id>/ |
|
GET: returns metadata dict of channel |
|
""" |
|
|
|
search_base = "ta_channel/_doc/" |
|
|
|
def get(self, request, channel_id): |
|
# pylint: disable=unused-argument |
|
"""get request""" |
|
self.get_document(channel_id) |
|
return Response(self.response, status=self.status_code) |
|
|
|
|
|
class ChannelApiListView(ApiBaseView): |
|
"""resolves to /api/channel/ |
|
GET: returns list of channels |
|
POST: edit a list of channels |
|
""" |
|
|
|
search_base = "ta_channel/_search/" |
|
|
|
def get(self, request): |
|
"""get request""" |
|
self.get_document_list(request) |
|
self.data.update( |
|
{"sort": [{"channel_name.keyword": {"order": "asc"}}]} |
|
) |
|
|
|
return Response(self.response) |
|
|
|
@staticmethod |
|
def post(request): |
|
"""subscribe to list of channels""" |
|
data = request.data |
|
try: |
|
to_add = data["data"] |
|
except KeyError: |
|
message = "missing expected data key" |
|
print(message) |
|
return Response({"message": message}, status=400) |
|
|
|
pending = [i["channel_id"] for i in to_add if i["channel_subscribed"]] |
|
url_str = " ".join(pending) |
|
subscribe_to.delay(url_str) |
|
|
|
return Response(data) |
|
|
|
|
|
class ChannelApiVideoView(ApiBaseView): |
|
"""resolves to /api/channel/<channel-id>/video |
|
GET: returns a list of videos of channel |
|
""" |
|
|
|
search_base = "ta_video/_search/" |
|
|
|
def get(self, request, channel_id): |
|
"""handle get request""" |
|
self.data.update( |
|
{ |
|
"query": { |
|
"term": {"channel.channel_id": {"value": channel_id}} |
|
}, |
|
"sort": [{"published": {"order": "desc"}}], |
|
} |
|
) |
|
self.get_document_list(request) |
|
|
|
return Response(self.response, status=self.status_code) |
|
|
|
|
|
class PlaylistApiListView(ApiBaseView): |
|
"""resolves to /api/playlist/ |
|
GET: returns list of indexed playlists |
|
""" |
|
|
|
search_base = "ta_playlist/_search/" |
|
|
|
def get(self, request): |
|
"""handle get request""" |
|
self.data.update( |
|
{"sort": [{"playlist_name.keyword": {"order": "asc"}}]} |
|
) |
|
self.get_document_list(request) |
|
return Response(self.response) |
|
|
|
|
|
class PlaylistApiView(ApiBaseView): |
|
"""resolves to /api/playlist/<playlist_id>/ |
|
GET: returns metadata dict of playlist |
|
""" |
|
|
|
search_base = "ta_playlist/_doc/" |
|
|
|
def get(self, request, playlist_id): |
|
# pylint: disable=unused-argument |
|
"""get request""" |
|
self.get_document(playlist_id) |
|
return Response(self.response, status=self.status_code) |
|
|
|
|
|
class PlaylistApiVideoView(ApiBaseView): |
|
"""resolves to /api/playlist/<playlist_id>/video |
|
GET: returns list of videos in playlist |
|
""" |
|
|
|
search_base = "ta_video/_search/" |
|
|
|
def get(self, request, playlist_id): |
|
"""handle get request""" |
|
self.data["query"] = { |
|
"term": {"playlist.keyword": {"value": playlist_id}} |
|
} |
|
self.data.update({"sort": [{"published": {"order": "desc"}}]}) |
|
|
|
self.get_document_list(request) |
|
return Response(self.response, status=self.status_code) |
|
|
|
|
|
class DownloadApiView(ApiBaseView): |
|
"""resolves to /api/download/<video_id>/ |
|
GET: returns metadata dict of an item in the download queue |
|
POST: update status of item to pending or ignore |
|
DELETE: forget from download queue |
|
""" |
|
|
|
search_base = "ta_download/_doc/" |
|
valid_status = ["pending", "ignore"] |
|
|
|
def get(self, request, video_id): |
|
# pylint: disable=unused-argument |
|
"""get request""" |
|
self.get_document(video_id) |
|
return Response(self.response, status=self.status_code) |
|
|
|
def post(self, request, video_id): |
|
"""post to video to change status""" |
|
item_status = request.data["status"] |
|
if item_status not in self.valid_status: |
|
message = f"{video_id}: invalid status {item_status}" |
|
print(message) |
|
return Response({"message": message}, status=400) |
|
|
|
print(f"{video_id}: change status to {item_status}") |
|
PendingInteract(video_id=video_id, status=item_status).update_status() |
|
RedisQueue().clear_item(video_id) |
|
|
|
return Response(request.data) |
|
|
|
@staticmethod |
|
def delete(request, video_id): |
|
# pylint: disable=unused-argument |
|
"""delete single video from queue""" |
|
print(f"{video_id}: delete from queue") |
|
PendingInteract(video_id=video_id).delete_item() |
|
|
|
return Response({"success": True}) |
|
|
|
|
|
class DownloadApiListView(ApiBaseView): |
|
"""resolves to /api/download/ |
|
GET: returns latest videos in the download queue |
|
POST: add a list of videos to download queue |
|
DELETE: remove items based on query filter |
|
""" |
|
|
|
search_base = "ta_download/_search/" |
|
valid_filter = ["pending", "ignore"] |
|
|
|
def get(self, request): |
|
"""get request""" |
|
query_filter = request.GET.get("filter", False) |
|
self.data.update({"sort": [{"timestamp": {"order": "asc"}}]}) |
|
if query_filter: |
|
if query_filter not in self.valid_filter: |
|
message = f"invalid url query filder: {query_filter}" |
|
print(message) |
|
return Response({"message": message}, status=400) |
|
|
|
self.data["query"] = {"term": {"status": {"value": query_filter}}} |
|
|
|
self.get_document_list(request) |
|
return Response(self.response) |
|
|
|
@staticmethod |
|
def post(request): |
|
"""add list of videos to download queue""" |
|
print(f"request meta data: {request.META}") |
|
data = request.data |
|
try: |
|
to_add = data["data"] |
|
except KeyError: |
|
message = "missing expected data key" |
|
print(message) |
|
return Response({"message": message}, status=400) |
|
|
|
pending = [i["youtube_id"] for i in to_add if i["status"] == "pending"] |
|
url_str = " ".join(pending) |
|
try: |
|
youtube_ids = UrlListParser(url_str).process_list() |
|
except ValueError: |
|
message = f"failed to parse: {url_str}" |
|
print(message) |
|
return Response({"message": message}, status=400) |
|
|
|
extrac_dl.delay(youtube_ids) |
|
|
|
return Response(data) |
|
|
|
def delete(self, request): |
|
"""delete download queue""" |
|
query_filter = request.GET.get("filter", False) |
|
if query_filter not in self.valid_filter: |
|
message = f"invalid url query filter: {query_filter}" |
|
print(message) |
|
return Response({"message": message}, status=400) |
|
|
|
message = f"delete queue by status: {query_filter}" |
|
print(message) |
|
PendingInteract(status=query_filter).delete_by_status() |
|
|
|
return Response({"message": message}) |
|
|
|
|
|
class PingView(ApiBaseView): |
|
"""resolves to /api/ping/ |
|
GET: test your connection |
|
""" |
|
|
|
@staticmethod |
|
def get(request): |
|
"""get pong""" |
|
data = {"response": "pong", "user": request.user.id} |
|
return Response(data) |
|
|
|
|
|
class LoginApiView(ObtainAuthToken): |
|
"""resolves to /api/login/ |
|
POST: return token and username after successful login |
|
""" |
|
|
|
def post(self, request, *args, **kwargs): |
|
"""post data""" |
|
# pylint: disable=no-member |
|
serializer = self.serializer_class( |
|
data=request.data, context={"request": request} |
|
) |
|
serializer.is_valid(raise_exception=True) |
|
user = serializer.validated_data["user"] |
|
token, _ = Token.objects.get_or_create(user=user) |
|
|
|
print(f"returning token for user with id {user.pk}") |
|
|
|
return Response({"token": token.key, "user_id": user.pk}) |
|
|
|
|
|
class TaskApiView(ApiBaseView): |
|
"""resolves to /api/task/ |
|
GET: check if ongoing background task |
|
POST: start a new background task |
|
""" |
|
|
|
@staticmethod |
|
def get(request): |
|
"""handle get request""" |
|
# pylint: disable=unused-argument |
|
response = {"rescan": False, "downloading": False} |
|
for key in response.keys(): |
|
response[key] = RedisArchivist().is_locked(key) |
|
|
|
return Response(response) |
|
|
|
def post(self, request): |
|
"""handle post request""" |
|
|
|
data = request.data |
|
print(data) |
|
response = TaskHandler(data).run_task() |
|
|
|
return Response(response) |
|
|
|
|
|
class CookieView(ApiBaseView): |
|
"""resolves to /api/cookie/ |
|
GET: check if cookie is enabled |
|
POST: verify validity of cookie |
|
PUT: import cookie |
|
""" |
|
|
|
@staticmethod |
|
def get(request): |
|
"""handle get request""" |
|
# pylint: disable=unused-argument |
|
config = AppConfig().config |
|
cookie_enabled = config["downloads"]["cookie_import"] |
|
|
|
return Response({"cookie_enabled": cookie_enabled}) |
|
|
|
@staticmethod |
|
def post(request): |
|
"""handle post request""" |
|
# pylint: disable=unused-argument |
|
config = AppConfig().config |
|
validated = CookieHandler(config).validate() |
|
|
|
return Response({"cookie_validated": validated}) |
|
|
|
@staticmethod |
|
def put(request): |
|
"""handle put request""" |
|
# pylint: disable=unused-argument |
|
config = AppConfig().config |
|
cookie = request.data.get("cookie") |
|
if not cookie: |
|
message = "missing cookie key in request data" |
|
print(message) |
|
return Response({"message": message}, status=400) |
|
|
|
print(f"cookie preview:\n\n{cookie[:300]}") |
|
handler = CookieHandler(config) |
|
handler.set_cookie(cookie) |
|
validated = handler.validate() |
|
if not validated: |
|
handler.revoke() |
|
message = {"cookie_import": "fail", "cookie_validated": validated} |
|
print(f"cookie: {message}") |
|
return Response({"message": message}, status=400) |
|
|
|
message = {"cookie_import": "done", "cookie_validated": validated} |
|
return Response(message)
|
|
|