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 + + + + +
+ ta-logo +
+
+ + + + + + +
+
+ +
+
+
+ + reddit-icon + + + discord-icon + + + github-icon + +
+
+ + question-icon + +
+
+
+ + + \ 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