diff --git a/browser_extension/README.md b/browser_extension/README.md
new file mode 100644
index 0000000..9737e6e
--- /dev/null
+++ b/browser_extension/README.md
@@ -0,0 +1,26 @@
+# Tube Archivist Companion
+A browser extension to directly add videos from YouTube to Tube Archivist.
+
+## MVP or better *bearly viable product*
+This is a proof of concept with the following functionality:
+- Add your Tube Archivist connection details in the addon popup
+- Inject a download button into youtube search results page
+- Clicking the button will automatically add the video to the your download queue
+
+## Test this extension
+- Firefox
+ - Open `about:debugging#/runtime/this-firefox`
+ - Click on *Load Temporary Add-on*
+ - Select the *manifest.json* file to load the addon.
+- Chrome / Chromium
+ - Open `chrome://extensions/`
+ - Toggle *Developer mode* on top right
+ - Click on *Load unpacked*
+ - Open the folder containing the *manifest.json* file.
+
+## Help needed
+This is only minimally useful in this state. Join us on our Discord and please help us improve that.
+
+## Note:
+- For mysterious reasons sometimes the download buttons will only load when refreshing the YouTube search page and not on first load... Hence: Help needed!
+- For your testing environment only for now: Point the extension to the newest *unstable* build.
diff --git a/browser_extension/extension/background.js b/browser_extension/extension/background.js
new file mode 100644
index 0000000..9e73291
--- /dev/null
+++ b/browser_extension/extension/background.js
@@ -0,0 +1,94 @@
+/*
+extension background script listening for events
+*/
+
+console.log("running background.js");
+
+let browserType = getBrowser();
+
+
+// boilerplate to dedect browser type api
+function getBrowser() {
+ if (typeof chrome !== "undefined") {
+ if (typeof browser !== "undefined") {
+ console.log("detected firefox");
+ return browser;
+ } else {
+ console.log("detected chrome");
+ return chrome;
+ }
+ } else {
+ console.log("failed to dedect browser");
+ throw "browser detection error"
+ };
+}
+
+
+// send post request to API backend
+async function sendPayload(url, token, payload) {
+
+ const rawResponse = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Accept": "application/json",
+ "Content-Type": "application/json",
+ "Authorization": token,
+ "mode": "no-cors"
+ },
+ body: JSON.stringify(payload)
+ });
+
+ const content = await rawResponse.json();
+ return content;
+}
+
+
+// read access storage and send
+function forwardRequest(payload) {
+
+ console.log("running forwardRequest");
+
+ function onGot(item) {
+ console.log(item.access);
+
+ const url = `${item.access.url}:${item.access.port}/api/download/`;
+ console.log(`sending to ${url}`);
+ const token = `Token ${item.access.apiKey}`;
+
+ sendPayload(url, token, payload).then(content => {
+ console.log(content);
+ })
+
+ };
+
+ function onError(error) {
+ console.local("failed to get access details");
+ console.log(`Error: ${error}`);
+ };
+
+ browserType.storage.local.get("access", function(result) {
+ onGot(result)
+ });
+
+}
+
+
+// listen for messages
+browserType.runtime.onMessage.addListener(
+ function(request, sender, sendResponse) {
+ console.log("responding from background.js listener");
+ console.log(JSON.stringify(request));
+ if (request.download) {
+ console.log("found new download task");
+ let payload = {
+ "data": [
+ {
+ "youtube_id": request.download["videoId"],
+ "status": "pending",
+ }
+ ]
+ }
+ forwardRequest(payload);
+ };
+ }
+);
diff --git a/browser_extension/extension/images/icon.png b/browser_extension/extension/images/icon.png
new file mode 100644
index 0000000..33c0c5b
Binary files /dev/null and b/browser_extension/extension/images/icon.png differ
diff --git a/browser_extension/extension/images/icon128.png b/browser_extension/extension/images/icon128.png
new file mode 100644
index 0000000..2c08551
Binary files /dev/null and b/browser_extension/extension/images/icon128.png differ
diff --git a/browser_extension/extension/images/icon16.png b/browser_extension/extension/images/icon16.png
new file mode 100644
index 0000000..a18ae31
Binary files /dev/null and b/browser_extension/extension/images/icon16.png differ
diff --git a/browser_extension/extension/images/logo.svg b/browser_extension/extension/images/logo.svg
new file mode 100644
index 0000000..3f900d4
--- /dev/null
+++ b/browser_extension/extension/images/logo.svg
@@ -0,0 +1,102 @@
+
+
+
diff --git a/browser_extension/extension/images/question.svg b/browser_extension/extension/images/question.svg
new file mode 100644
index 0000000..872deef
--- /dev/null
+++ b/browser_extension/extension/images/question.svg
@@ -0,0 +1,16 @@
+
+
+
diff --git a/browser_extension/extension/images/social/discord.svg b/browser_extension/extension/images/social/discord.svg
new file mode 100644
index 0000000..dd07931
--- /dev/null
+++ b/browser_extension/extension/images/social/discord.svg
@@ -0,0 +1,24 @@
+
+
+
diff --git a/browser_extension/extension/images/social/github.svg b/browser_extension/extension/images/social/github.svg
new file mode 100644
index 0000000..c693abf
--- /dev/null
+++ b/browser_extension/extension/images/social/github.svg
@@ -0,0 +1,23 @@
+
+
+
diff --git a/browser_extension/extension/images/social/reddit.svg b/browser_extension/extension/images/social/reddit.svg
new file mode 100644
index 0000000..e6c615c
--- /dev/null
+++ b/browser_extension/extension/images/social/reddit.svg
@@ -0,0 +1,35 @@
+
+
+
diff --git a/browser_extension/extension/index.html b/browser_extension/extension/index.html
new file mode 100644
index 0000000..f8c254a
--- /dev/null
+++ b/browser_extension/extension/index.html
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+ TubeArchivist Companion
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/browser_extension/extension/manifest.json b/browser_extension/extension/manifest.json
new file mode 100644
index 0000000..a260861
--- /dev/null
+++ b/browser_extension/extension/manifest.json
@@ -0,0 +1,25 @@
+{
+ "manifest_version": 2,
+ "name": "TubeArchivist Companion",
+ "description": "Interact with your selhosted TA server.",
+ "version": "0.0.1",
+ "icons": {
+ "128": "/images/icon128.png"
+ },
+ "browser_action": {
+ "default_icon": "/images/icon.png",
+ "default_popup": "index.html"
+ },
+ "permissions": [
+ "storage"
+ ],
+ "content_scripts": [
+ {
+ "matches": ["https://www.youtube.com/results*"],
+ "js": ["script.js"]
+ }
+ ],
+ "background": {
+ "scripts": ["background.js"]
+ }
+}
diff --git a/browser_extension/extension/popup.js b/browser_extension/extension/popup.js
new file mode 100644
index 0000000..84c21da
--- /dev/null
+++ b/browser_extension/extension/popup.js
@@ -0,0 +1,63 @@
+/*
+Loaded into popup index.html
+*/
+
+let browserType = getBrowser();
+
+// boilerplate to dedect browser type api
+function getBrowser() {
+ if (typeof chrome !== "undefined") {
+ if (typeof browser !== "undefined") {
+ console.log("detected firefox");
+ return browser;
+ } else {
+ console.log("detected chrome");
+ return chrome;
+ }
+ } else {
+ console.log("failed to dedect browser");
+ throw "browser detection error"
+ };
+}
+
+// store access details
+document.getElementById("save-login").addEventListener("click", function () {
+ console.log("save form");
+ let toStore = {
+ "access": {
+ "url": document.getElementById("url").value,
+ "port": document.getElementById("port").value,
+ "apiKey": document.getElementById("api-key").value
+ }
+ };
+ console.log(toStore);
+ browserType.storage.local.set(toStore, function() {
+ console.log("Stored connection details: " + JSON.stringify(toStore));
+ });
+})
+
+// fill in form
+document.addEventListener("DOMContentLoaded", async () => {
+
+ console.log("executing dom loader");
+
+ function onGot(item) {
+ if (!item.access) {
+ console.log("no access details found");
+ return
+ }
+ console.log(item.access);
+ document.getElementById("url").value = item.access.url;
+ document.getElementById("port").value = item.access.port;
+ document.getElementById("api-key").value = item.access.apiKey;
+ };
+
+ function onError(error) {
+ console.log(`Error: ${error}`);
+ };
+
+ browserType.storage.local.get("access", function(result) {
+ onGot(result)
+ });
+
+})
diff --git a/browser_extension/extension/script.js b/browser_extension/extension/script.js
new file mode 100644
index 0000000..803e094
--- /dev/null
+++ b/browser_extension/extension/script.js
@@ -0,0 +1,72 @@
+/*
+content script running on youtube.com
+*/
+
+console.log("running script.js");
+
+let browserType = getBrowser();
+
+setTimeout(function(){
+ console.log("running setimeout")
+ linkFinder();
+ return false;
+}, 2000);
+
+
+// boilerplate to dedect browser type api
+function getBrowser() {
+ if (typeof chrome !== "undefined") {
+ if (typeof browser !== "undefined") {
+ console.log("detected firefox");
+ return browser;
+ } else {
+ console.log("detected chrome");
+ return chrome;
+ }
+ } else {
+ console.log("failed to dedect browser");
+ throw "browser detection error"
+ };
+}
+
+
+// event handler for download task
+function addToDownload(videoId) {
+
+ console.log(`downloading ${videoId}`);
+ let payload = {
+ "download": {
+ "videoId": videoId
+ }
+ };
+
+ browserType.runtime.sendMessage(payload);
+
+}
+
+
+// find relevant links to add a button to
+function linkFinder() {
+
+ console.log("running link finder");
+
+ var allLinks = document.links;
+ for (let i = 0; i < allLinks.length; i++) {
+
+ const linkItem = allLinks[i];
+ const linkDest = linkItem.getAttribute("href");
+
+ if (linkDest.startsWith("/watch?v=") && linkItem.id == "video-title") {
+ var dlButton = document.createElement("button");
+ dlButton.innerText = "download";
+ var videoId = linkDest.split("=")[1];
+ dlButton.setAttribute("data-id", videoId);
+ dlButton.setAttribute("id", "ta-dl-" + videoId);
+ dlButton.onclick = function(event) {
+ var videoId = this.getAttribute("data-id");
+ addToDownload(videoId);
+ };
+ linkItem.parentElement.appendChild(dlButton);
+ }
+ }
+}
diff --git a/tubearchivist/api/README.md b/tubearchivist/api/README.md
index 73dd4fc..0e74a11 100644
--- a/tubearchivist/api/README.md
+++ b/tubearchivist/api/README.md
@@ -23,6 +23,10 @@ response = requests.get(url, headers=headers)
## Video Item View
/api/video/\/
+## Video Player View
+returns all relevant information to create video player
+/api/video/\/player
+
## Channel List View
/api/channel/
diff --git a/tubearchivist/api/urls.py b/tubearchivist/api/urls.py
index d39dc30..a6c6801 100644
--- a/tubearchivist/api/urls.py
+++ b/tubearchivist/api/urls.py
@@ -6,6 +6,7 @@ from api.views import (
DownloadApiListView,
DownloadApiView,
PlaylistApiView,
+ VideoApiPlayerView,
VideoApiView,
)
from django.urls import path
@@ -16,6 +17,11 @@ urlpatterns = [
VideoApiView.as_view(),
name="api-video",
),
+ path(
+ "video//player/",
+ VideoApiPlayerView.as_view(),
+ name="api-video-player",
+ ),
path(
"channel/",
ChannelApiListView.as_view(),
diff --git a/tubearchivist/api/views.py b/tubearchivist/api/views.py
index ccbc22f..165ca60 100644
--- a/tubearchivist/api/views.py
+++ b/tubearchivist/api/views.py
@@ -3,6 +3,7 @@
import requests
from home.src.config import AppConfig
from home.src.helper import UrlListParser
+from home.src.thumbnails import ThumbManager
from home.tasks import extrac_dl, subscribe_to
from rest_framework.authentication import (
SessionAuthentication,
@@ -77,6 +78,38 @@ class VideoApiView(ApiBaseView):
return Response(self.response, status=self.status_code)
+class VideoApiPlayerView(ApiBaseView):
+ """resolves to /api/video//player
+ GET: returns dict of video to build player
+ """
+
+ 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)
+ player = self.process_response()
+ return Response(player, status=self.status_code)
+
+ def process_response(self):
+ """build all needed vars for player"""
+ vid_data = self.response["data"]
+ youtube_id = vid_data["youtube_id"]
+ vid_thumb_url = ThumbManager().vid_thumb_path(youtube_id)
+ player = {
+ "youtube_id": youtube_id,
+ "media_url": "/media/" + vid_data["media_url"],
+ "vid_thumb_url": "/cache/" + vid_thumb_url,
+ "title": vid_data["title"],
+ "channel_name": vid_data["channel"]["channel_name"],
+ "channel_id": vid_data["channel"]["channel_id"],
+ "is_watched": vid_data["player"]["watched"],
+ }
+ return player
+
+
class ChannelApiView(ApiBaseView):
"""resolves to /api/channel//
GET: returns metadata dict of channel
@@ -178,6 +211,7 @@ class DownloadApiListView(ApiBaseView):
@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"]
diff --git a/tubearchivist/config/settings.py b/tubearchivist/config/settings.py
index cb12ad6..c1eb432 100644
--- a/tubearchivist/config/settings.py
+++ b/tubearchivist/config/settings.py
@@ -14,6 +14,7 @@ import hashlib
from os import environ, path
from pathlib import Path
+from corsheaders.defaults import default_headers
from home.src.config import AppConfig
# Build paths inside the project like this: BASE_DIR / 'subdir'.
@@ -41,6 +42,7 @@ INSTALLED_APPS = [
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
+ "corsheaders",
"whitenoise.runserver_nostatic",
"django.contrib.staticfiles",
"django.contrib.humanize",
@@ -52,6 +54,7 @@ INSTALLED_APPS = [
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
+ "corsheaders.middleware.CorsMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
@@ -140,3 +143,11 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
LOGIN_URL = "/login/"
LOGOUT_REDIRECT_URL = "/login/"
+
+# Cors needed for browser extension
+# background.js makes the request so HTTP_ORIGIN will be from extension
+CORS_ALLOWED_ORIGIN_REGEXES = [r"moz-extension://*", r"chrome-extension://*"]
+
+CORS_ALLOW_HEADERS = list(default_headers) + [
+ "mode",
+]
diff --git a/tubearchivist/requirements.txt b/tubearchivist/requirements.txt
index 3ebbf26..2bf1310 100644
--- a/tubearchivist/requirements.txt
+++ b/tubearchivist/requirements.txt
@@ -1,5 +1,6 @@
beautifulsoup4==4.10.0
celery==5.2.3
+django-cors-headers==3.11.0
Django==4.0.1
djangorestframework==3.13.1
Pillow==9.0.0