Merge api app from branch 'testing'

This commit is contained in:
simon 2022-01-11 18:43:03 +07:00
commit 51f63f7b71
15 changed files with 343 additions and 2 deletions

View File

@ -0,0 +1,59 @@
# TubeArchivist API
Documentation of available API endpoints.
**Note: This is very early alpha and will change!**
## Authentication
API token will get automatically created, accessible on the settings page. Token needs to be passed as an authorization header with every request. Additionally session based authentication is enabled too: When you are logged into your TubeArchivist instance, you'll have access to the api in the browser for testing.
Curl example:
```shell
curl -v /api/video/<video-id>/ \
-H "Authorization: Token xxxxxxxxxx"
```
Python requests example:
```python
import requests
url = "/api/video/<video-id>/"
headers = {"Authorization": "Token xxxxxxxxxx"}
response = requests.get(url, headers=headers)
```
## Video Item View
/api/video/\<video_id>/
## Channel List View
/api/channel/
### Subscribe to a list of channels
POST /api/channel/
```json
{
"data": [
{"channel_id": "UC9-y-6csu5WGm29I7JiwpnA", "channel_subscribed": true}
]
}
```
## Channel Item View
/api/channel/\<channel_id>/
## Playlists Item View
/api/playlist/\<playlist_id>/
## Download Queue List View
/api/download/
### Add list of videos to download queue
POST /api/download/
```json
{
"data": [
{"youtube_id": "NYj3DnI81AQ", "status": "pending"}
]
}
```
## Download Queue Item View
/api/download/\<video_id>/

View File

View File

@ -0,0 +1,3 @@
from django.contrib import admin # noqa: F401
# Register your models here.

10
tubearchivist/api/apps.py Normal file
View File

@ -0,0 +1,10 @@
"""apps file for api package"""
from django.apps import AppConfig
class ApiConfig(AppConfig):
"""app config"""
default_auto_field = "django.db.models.BigAutoField"
name = "api"

View File

View File

@ -0,0 +1,3 @@
"""api models"""
# from django.db import models

View File

View File

@ -0,0 +1,3 @@
from django.test import TestCase # noqa: F401
# Create your tests here.

44
tubearchivist/api/urls.py Normal file
View File

@ -0,0 +1,44 @@
"""all api urls"""
from api.views import (
ChannelApiListView,
ChannelApiView,
DownloadApiListView,
DownloadApiView,
PlaylistApiView,
VideoApiView,
)
from django.urls import path
urlpatterns = [
path(
"video/<slug:video_id>/",
VideoApiView.as_view(),
name="api-video",
),
path(
"channel/",
ChannelApiListView.as_view(),
name="api-channel-list",
),
path(
"channel/<slug:channel_id>/",
ChannelApiView.as_view(),
name="api-channel",
),
path(
"playlist/<slug:playlist_id>/",
PlaylistApiView.as_view(),
name="api-playlist",
),
path(
"download/",
DownloadApiListView.as_view(),
name="api-download-list",
),
path(
"download/<slug:video_id>/",
DownloadApiView.as_view(),
name="api-download",
),
]

200
tubearchivist/api/views.py Normal file
View File

@ -0,0 +1,200 @@
"""all API views"""
import requests
from home.src.config import AppConfig
from home.src.helper import UrlListParser
from home.tasks import extrac_dl, subscribe_to
from rest_framework.authentication import (
SessionAuthentication,
TokenAuthentication,
)
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
def __init__(self):
super().__init__()
self.response = {"data": False}
self.status_code = False
self.context = False
def config_builder(self):
"""build confic context"""
default_conf = AppConfig().config
self.context = {
"es_url": default_conf["application"]["es_url"],
"es_auth": default_conf["application"]["es_auth"],
}
def get_document(self, document_id):
"""get single document from es"""
es_url = self.context["es_url"]
url = f"{es_url}{self.search_base}{document_id}"
print(url)
response = requests.get(url, auth=self.context["es_auth"])
try:
self.response["data"] = response.json()["_source"]
except KeyError:
print(f"item not found: {document_id}")
self.response["data"] = False
self.status_code = response.status_code
def get_paginate(self):
"""add pagination detail to response"""
self.response["paginate"] = False
def get_document_list(self, data):
"""get a list of results"""
es_url = self.context["es_url"]
url = f"{es_url}{self.search_base}"
print(url)
response = requests.get(url, json=data, auth=self.context["es_auth"])
all_hits = response.json()["hits"]["hits"]
self.response["data"] = [i["_source"] for i in all_hits]
self.status_code = response.status_code
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.config_builder()
self.get_document(video_id)
return Response(self.response, status=self.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.config_builder()
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):
# pylint: disable=unused-argument
"""get request"""
data = {"query": {"match_all": {}}}
self.config_builder()
self.get_document_list(data)
self.get_paginate()
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 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.config_builder()
self.get_document(playlist_id)
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
"""
search_base = "/ta_download/_doc/"
def get(self, request, video_id):
# pylint: disable=unused-argument
"""get request"""
self.config_builder()
self.get_document(video_id)
return Response(self.response, status=self.status_code)
class DownloadApiListView(ApiBaseView):
"""resolves to /api/download/
GET: returns latest videos in the download queue
POST: add a list of videos to download queue
"""
search_base = "/ta_download/_search/"
def get(self, request):
# pylint: disable=unused-argument
"""get request"""
data = {"query": {"match_all": {}}}
self.config_builder()
self.get_document_list(data)
self.get_paginate()
return Response(self.response)
@staticmethod
def post(request):
"""add list of videos to download queue"""
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)

View File

@ -44,6 +44,9 @@ INSTALLED_APPS = [
"whitenoise.runserver_nostatic", "whitenoise.runserver_nostatic",
"django.contrib.staticfiles", "django.contrib.staticfiles",
"django.contrib.humanize", "django.contrib.humanize",
"rest_framework",
"rest_framework.authtoken",
"api",
] ]
MIDDLEWARE = [ MIDDLEWARE = [

View File

@ -18,5 +18,6 @@ from django.urls import include, path
urlpatterns = [ urlpatterns = [
path("", include("home.urls")), path("", include("home.urls")),
path("api/", include("api.urls")),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
] ]

View File

@ -97,6 +97,10 @@
</div> </div>
<div class="settings-group"> <div class="settings-group">
<h2 id="integrations">Integrations</h2> <h2 id="integrations">Integrations</h2>
<div class="settings-item">
<p>API token:</p>
<p>{{ api_token }}</p>
</div>
<div class="settings-item"> <div class="settings-item">
<p>Integrate with <a href="https://returnyoutubedislike.com/">returnyoutubedislike.com</a> to get dislikes and average ratings back: <span class="settings-current">{{ config.downloads.integrate_ryd }}</span></p> <p>Integrate with <a href="https://returnyoutubedislike.com/">returnyoutubedislike.com</a> to get dislikes and average ratings back: <span class="settings-current">{{ config.downloads.integrate_ryd }}</span></p>
<i>Before activating that, make sure you have a scraping sleep interval of at least 3 secs set to avoid ratelimiting issues.</i><br> <i>Before activating that, make sure you have a scraping sleep interval of at least 3 secs set to avoid ratelimiting issues.</i><br>

View File

@ -31,6 +31,7 @@ from home.src.index import YoutubePlaylist
from home.src.index_management import get_available_backups from home.src.index_management import get_available_backups
from home.src.searching import Pagination, SearchHandler from home.src.searching import Pagination, SearchHandler
from home.tasks import extrac_dl, subscribe_to from home.tasks import extrac_dl, subscribe_to
from rest_framework.authtoken.models import Token
class ArchivistViewConfig(View): class ArchivistViewConfig(View):
@ -683,8 +684,7 @@ class SettingsView(View):
take post request from the form to update settings take post request from the form to update settings
""" """
@staticmethod def get(self, request):
def get(request):
"""read and display current settings""" """read and display current settings"""
config_handler = AppConfig(request.user.id) config_handler = AppConfig(request.user.id)
colors = config_handler.colors colors = config_handler.colors
@ -693,10 +693,12 @@ class SettingsView(View):
user_form = UserSettingsForm() user_form = UserSettingsForm()
app_form = ApplicationSettingsForm() app_form = ApplicationSettingsForm()
scheduler_form = SchedulerSettingsForm() scheduler_form = SchedulerSettingsForm()
token = self.get_token(request)
context = { context = {
"title": "Settings", "title": "Settings",
"config": config_handler.config, "config": config_handler.config,
"api_token": token,
"colors": colors, "colors": colors,
"available_backups": available_backups, "available_backups": available_backups,
"user_form": user_form, "user_form": user_form,
@ -706,6 +708,14 @@ class SettingsView(View):
return render(request, "home/settings.html", context) return render(request, "home/settings.html", context)
@staticmethod
def get_token(request):
"""get existing or create new token of user"""
# pylint: disable=no-member
token = Token.objects.get_or_create(user=request.user)[0]
print(token)
return token
@staticmethod @staticmethod
def post(request): def post(request):
"""handle form post to update settings""" """handle form post to update settings"""

View File

@ -1,6 +1,7 @@
beautifulsoup4==4.10.0 beautifulsoup4==4.10.0
celery==5.2.3 celery==5.2.3
Django==4.0.1 Django==4.0.1
djangorestframework==3.13.1
Pillow==9.0.0 Pillow==9.0.0
redis==4.1.0 redis==4.1.0
requests==2.27.1 requests==2.27.1