implement cookie import, #build

Changed:
- fix arm64 ffmpeg issue
- added cookie import
- use cookie for all yt-dlp calls
- API: add cookie validation view
This commit is contained in:
simon 2022-04-30 19:34:10 +07:00
commit 8f72c5f42c
No known key found for this signature in database
GPG Key ID: 2C15AA5E89985DD4
13 changed files with 217 additions and 30 deletions

View File

@ -7,21 +7,7 @@ FROM python:3.10.4-slim-bullseye AS builder
ARG TARGETPLATFORM ARG TARGETPLATFORM
RUN apt-get update RUN apt-get update
RUN apt-get install -y --no-install-recommends build-essential gcc curl RUN apt-get install -y --no-install-recommends build-essential gcc
# get newest patched ffmpeg and ffprobe builds for amd64 fall back to repo ffmpeg for arm64
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ] ; then \
curl -s https://api.github.com/repos/yt-dlp/FFmpeg-Builds/releases/latest \
| grep browser_download_url \
| grep ".*master.*linux64.*tar.xz" \
| cut -d '"' -f 4 \
| xargs curl -L --output ffmpeg.tar.xz && \
tar -xf ffmpeg.tar.xz --strip-components=2 --no-anchored -C /usr/bin/ "ffmpeg" && \
tar -xf ffmpeg.tar.xz --strip-components=2 --no-anchored -C /usr/bin/ "ffprobe" && \
rm ffmpeg.tar.xz \
; elif [ "$TARGETPLATFORM" = "linux/arm64" ] ; then \
apt-get -y update && apt-get -y install --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/* \
; fi
# install requirements # install requirements
COPY ./tubearchivist/requirements.txt /requirements.txt COPY ./tubearchivist/requirements.txt /requirements.txt
@ -37,15 +23,28 @@ ENV PYTHONUNBUFFERED 1
# copy build requirements # copy build requirements
COPY --from=builder /root/.local /root/.local COPY --from=builder /root/.local /root/.local
COPY --from=builder /usr/bin/ffmpeg /usr/bin/ffmpeg
COPY --from=builder /usr/bin/ffprobe /usr/bin/ffprobe
ENV PATH=/root/.local/bin:$PATH ENV PATH=/root/.local/bin:$PATH
# install distro packages needed # install distro packages needed
RUN apt-get clean && apt-get -y update && apt-get -y install --no-install-recommends \ RUN apt-get clean && apt-get -y update && apt-get -y install --no-install-recommends \
nginx \ nginx \
atomicparsley \ atomicparsley \
curl && rm -rf /var/lib/apt/lists/* curl \
xz-utils && rm -rf /var/lib/apt/lists/*
# get newest patched ffmpeg and ffprobe builds for amd64 fall back to repo ffmpeg for arm64
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ] ; then \
curl -s https://api.github.com/repos/yt-dlp/FFmpeg-Builds/releases/latest \
| grep browser_download_url \
| grep ".*master.*linux64.*tar.xz" \
| cut -d '"' -f 4 \
| xargs curl -L --output ffmpeg.tar.xz && \
tar -xf ffmpeg.tar.xz --strip-components=2 --no-anchored -C /usr/bin/ "ffmpeg" && \
tar -xf ffmpeg.tar.xz --strip-components=2 --no-anchored -C /usr/bin/ "ffprobe" && \
rm ffmpeg.tar.xz \
; elif [ "$TARGETPLATFORM" = "linux/arm64" ] ; then \
apt-get -y update && apt-get -y install --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/* \
; fi
# install debug tools for testing environment # install debug tools for testing environment
RUN if [ "$INSTALL_DEBUG" ] ; then \ RUN if [ "$INSTALL_DEBUG" ] ; then \

View File

@ -213,3 +213,21 @@ POST /api/task/
List of valid task names: List of valid task names:
- **download_pending**: Start the download queue - **download_pending**: Start the download queue
- **rescan_pending**: Rescan your subscriptions - **rescan_pending**: Rescan your subscriptions
## Cookie View
Check your youtube cookie settings
GET /api/cookie/
```json
{
"cookie_enabled": true
}
```
POST /api/cookie/
Send empty post request to validate cookie.
```json
{
"cookie_validated": true
}
```

View File

@ -4,6 +4,7 @@ from api.views import (
ChannelApiListView, ChannelApiListView,
ChannelApiVideoView, ChannelApiVideoView,
ChannelApiView, ChannelApiView,
CookieView,
DownloadApiListView, DownloadApiListView,
DownloadApiView, DownloadApiView,
LoginApiView, LoginApiView,
@ -87,4 +88,9 @@ urlpatterns = [
TaskApiView.as_view(), TaskApiView.as_view(),
name="api-task", name="api-task",
), ),
path(
"cookie/",
CookieView.as_view(),
name="api-cookie",
),
] ]

View File

@ -3,6 +3,7 @@
from api.src.search_processor import SearchProcess from api.src.search_processor import SearchProcess
from api.src.task_processor import TaskHandler from api.src.task_processor import TaskHandler
from home.src.download.queue import PendingInteract from home.src.download.queue import PendingInteract
from home.src.download.yt_cookie import CookieHandler
from home.src.es.connect import ElasticWrap from home.src.es.connect import ElasticWrap
from home.src.index.generic import Pagination from home.src.index.generic import Pagination
from home.src.index.video import SponsorBlock from home.src.index.video import SponsorBlock
@ -462,3 +463,27 @@ class TaskApiView(ApiBaseView):
response = TaskHandler(data).run_task() response = TaskHandler(data).run_task()
return Response(response) return Response(response)
class CookieView(ApiBaseView):
"""resolves to /api/cookie/
GET: check if cookie is enabled
POST: verify validity of 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
validated = CookieHandler().validate()
return Response({"cookie_validated": validated})

View File

@ -26,6 +26,7 @@
"subtitle": false, "subtitle": false,
"subtitle_source": false, "subtitle_source": false,
"subtitle_index": false, "subtitle_index": false,
"cookie_import": false,
"throttledratelimit": false, "throttledratelimit": false,
"integrate_ryd": false, "integrate_ryd": false,
"integrate_sponsorblock": false "integrate_sponsorblock": false

View File

@ -13,8 +13,10 @@ from home.src.download.subscriptions import (
PlaylistSubscription, PlaylistSubscription,
) )
from home.src.download.thumbnails import ThumbManager from home.src.download.thumbnails import ThumbManager
from home.src.download.yt_cookie import CookieHandler
from home.src.es.connect import ElasticWrap, IndexPaginate from home.src.es.connect import ElasticWrap, IndexPaginate
from home.src.index.playlist import YoutubePlaylist from home.src.index.playlist import YoutubePlaylist
from home.src.ta.config import AppConfig
from home.src.ta.helper import DurationConverter from home.src.ta.helper import DurationConverter
from home.src.ta.ta_redis import RedisArchivist from home.src.ta.ta_redis import RedisArchivist
@ -119,12 +121,29 @@ class PendingInteract:
class PendingList(PendingIndex): class PendingList(PendingIndex):
"""manage the pending videos list""" """manage the pending videos list"""
yt_obs = {
"default_search": "ytsearch",
"quiet": True,
"check_formats": "selected",
"noplaylist": True,
"writethumbnail": True,
"simulate": True,
}
def __init__(self, youtube_ids=False): def __init__(self, youtube_ids=False):
super().__init__() super().__init__()
self.process_config()
self.youtube_ids = youtube_ids self.youtube_ids = youtube_ids
self.to_skip = False self.to_skip = False
self.missing_videos = False self.missing_videos = False
def process_config(self):
"""add user config to yt_obs"""
config = AppConfig().config
if config["downloads"]["cookie_import"]:
cookie_path = CookieHandler().use()
self.yt_obs.update({"cookiefile": cookie_path})
def parse_url_list(self): def parse_url_list(self):
"""extract youtube ids from list""" """extract youtube ids from list"""
self.missing_videos = [] self.missing_videos = []
@ -223,16 +242,8 @@ class PendingList(PendingIndex):
def get_youtube_details(self, youtube_id): def get_youtube_details(self, youtube_id):
"""get details from youtubedl for single pending video""" """get details from youtubedl for single pending video"""
obs = {
"default_search": "ytsearch",
"quiet": True,
"check_formats": "selected",
"noplaylist": True,
"writethumbnail": True,
"simulate": True,
}
try: try:
vid = yt_dlp.YoutubeDL(obs).extract_info(youtube_id) vid = yt_dlp.YoutubeDL(self.yt_obs).extract_info(youtube_id)
except yt_dlp.utils.DownloadError: except yt_dlp.utils.DownloadError:
print("failed to extract info for: " + youtube_id) print("failed to extract info for: " + youtube_id)
return False return False

View File

@ -0,0 +1,85 @@
"""
functionality:
- import yt cookie from filesystem
- make cookie available for yt-dlp
"""
import os
import yt_dlp
from home.src.ta.config import AppConfig
from home.src.ta.ta_redis import RedisArchivist
class CookieHandler:
"""handle youtube cookie for yt-dlp"""
CONFIG = AppConfig().config
CACHE_PATH = CONFIG["application"]["cache_dir"]
COOKIE_FILE_NAME = "cookies.google.txt"
COOKIE_KEY = "cookie"
COOKIE_PATH = "cookie.txt"
def import_cookie(self):
"""import cookie from file"""
import_path = os.path.join(
self.CACHE_PATH, "import", self.COOKIE_FILE_NAME
)
with open(import_path, encoding="utf-8") as cookie_file:
cookie = cookie_file.read()
RedisArchivist().set_message(self.COOKIE_KEY, cookie, expire=False)
os.remove(import_path)
print("cookie: import successfully")
def use(self):
"""make cookie available in FS"""
cookie = RedisArchivist().get_message(self.COOKIE_KEY)
if isinstance(cookie, dict):
print("no cookie imported")
raise FileNotFoundError
with open(self.COOKIE_PATH, "w", encoding="utf-8") as cookie_file:
cookie_file.write(cookie)
print("cookie: made available")
return self.COOKIE_PATH
def hide(self):
"""hide cookie file if not in use"""
try:
os.remove(self.COOKIE_PATH)
except FileNotFoundError:
print("cookie: not available")
return
print("cookie: hidden")
def revoke(self):
"""revoke cookie"""
self.hide()
RedisArchivist().del_message(self.COOKIE_KEY)
print("cookie: revoked")
def validate(self):
"""validate cookie using the liked videos playlist"""
try:
_ = self.use()
except FileNotFoundError:
return False
url = "https://www.youtube.com/playlist?list=LL"
yt_obs = {
"quiet": True,
"skip_download": True,
"extract_flat": True,
"cookiefile": self.COOKIE_PATH,
}
try:
response = yt_dlp.YoutubeDL(yt_obs).extract_info(url)
except yt_dlp.utils.DownloadError:
print("failed to validate cookie")
response = False
return bool(response)

View File

@ -14,6 +14,7 @@ from time import sleep
import yt_dlp import yt_dlp
from home.src.download.queue import PendingList from home.src.download.queue import PendingList
from home.src.download.subscriptions import PlaylistSubscription from home.src.download.subscriptions import PlaylistSubscription
from home.src.download.yt_cookie import CookieHandler
from home.src.es.connect import ElasticWrap, IndexPaginate from home.src.es.connect import ElasticWrap, IndexPaginate
from home.src.index.channel import YoutubeChannel from home.src.index.channel import YoutubeChannel
from home.src.index.playlist import YoutubePlaylist from home.src.index.playlist import YoutubePlaylist
@ -290,6 +291,9 @@ class VideoDownloader:
self.obs["ratelimit"] = ( self.obs["ratelimit"] = (
self.config["downloads"]["limit_speed"] * 1024 self.config["downloads"]["limit_speed"] * 1024
) )
if self.config["downloads"]["cookie_import"]:
cookie_path = CookieHandler().use()
self.obs["cookiefile"] = cookie_path
throttle = self.config["downloads"]["throttledratelimit"] throttle = self.config["downloads"]["throttledratelimit"]
if throttle: if throttle:

View File

@ -86,6 +86,12 @@ class ApplicationSettingsForm(forms.Form):
("1", "enable subtitle index"), ("1", "enable subtitle index"),
] ]
COOKIE_IMPORT_CHOICES = [
("", "-- change cookie settings"),
("0", "disable cookie"),
("1", "enable cookie"),
]
subscriptions_channel_size = forms.IntegerField(required=False) subscriptions_channel_size = forms.IntegerField(required=False)
downloads_limit_count = forms.IntegerField(required=False) downloads_limit_count = forms.IntegerField(required=False)
downloads_limit_speed = forms.IntegerField(required=False) downloads_limit_speed = forms.IntegerField(required=False)
@ -106,6 +112,9 @@ class ApplicationSettingsForm(forms.Form):
downloads_subtitle_index = forms.ChoiceField( downloads_subtitle_index = forms.ChoiceField(
widget=forms.Select, choices=SUBTITLE_INDEX_CHOICES, required=False widget=forms.Select, choices=SUBTITLE_INDEX_CHOICES, required=False
) )
downloads_cookie_import = forms.ChoiceField(
widget=forms.Select, choices=COOKIE_IMPORT_CHOICES, required=False
)
downloads_integrate_ryd = forms.ChoiceField( downloads_integrate_ryd = forms.ChoiceField(
widget=forms.Select, choices=RYD_CHOICES, required=False widget=forms.Select, choices=RYD_CHOICES, required=False
) )

View File

@ -6,6 +6,7 @@ functionality:
import math import math
import yt_dlp import yt_dlp
from home.src.download.yt_cookie import CookieHandler
from home.src.es.connect import ElasticWrap from home.src.es.connect import ElasticWrap
from home.src.ta.config import AppConfig from home.src.ta.config import AppConfig
from home.src.ta.ta_redis import RedisArchivist from home.src.ta.ta_redis import RedisArchivist
@ -37,6 +38,9 @@ class YouTubeItem:
"""read user conf""" """read user conf"""
self.config = AppConfig().config self.config = AppConfig().config
self.app_conf = self.config["application"] self.app_conf = self.config["application"]
if self.config["downloads"]["cookie_import"]:
cookie_path = CookieHandler().use()
self.yt_obs.update({"cookiefile": cookie_path})
def get_from_youtube(self): def get_from_youtube(self):
"""use yt-dlp to get meta data from youtube""" """use yt-dlp to get meta data from youtube"""

View File

@ -83,6 +83,7 @@ class AppConfig:
def update_config(self, form_post): def update_config(self, form_post):
"""update config values from settings form""" """update config values from settings form"""
updated = []
for key, value in form_post.items(): for key, value in form_post.items():
if not value and not isinstance(value, int): if not value and not isinstance(value, int):
continue continue
@ -96,8 +97,10 @@ class AppConfig:
config_dict, config_value = key.split("_", maxsplit=1) config_dict, config_value = key.split("_", maxsplit=1)
self.config[config_dict][config_value] = to_write self.config[config_dict][config_value] = to_write
updated.append((config_value, to_write))
RedisArchivist().set_message("config", self.config, expire=False) RedisArchivist().set_message("config", self.config, expire=False)
return updated
@staticmethod @staticmethod
def set_user_config(form_post, user_id): def set_user_config(form_post, user_id):

View File

@ -114,6 +114,14 @@
{{ app_form.downloads_subtitle_index }} {{ app_form.downloads_subtitle_index }}
</div> </div>
</div> </div>
<div class="settings-group">
<h2 id="format">Cookie</h2>
<div class="settings-item">
<p>Import YouTube cookie: <span class="settings-current">{{ config.downloads.cookie_import }}</span><br></p>
<i>Place your cookie file named <span class="settings-current">cookies.google.txt</span> in /cache/import before enabling.</i><br>
{{ app_form.downloads_cookie_import }}
</div>
</div>
<div class="settings-group"> <div class="settings-group">
<h2 id="integrations">Integrations</h2> <h2 id="integrations">Integrations</h2>
<div class="settings-item"> <div class="settings-item">

View File

@ -14,6 +14,7 @@ from django.contrib.auth.forms import AuthenticationForm
from django.http import JsonResponse from django.http import JsonResponse
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.views import View from django.views import View
from home.src.download.yt_cookie import CookieHandler
from home.src.es.index_setup import get_available_backups from home.src.es.index_setup import get_available_backups
from home.src.frontend.api_calls import PostData from home.src.frontend.api_calls import PostData
from home.src.frontend.forms import ( from home.src.frontend.forms import (
@ -791,8 +792,7 @@ class SettingsView(View):
token = Token.objects.get_or_create(user=request.user)[0] token = Token.objects.get_or_create(user=request.user)[0]
return token return token
@staticmethod def post(self, request):
def post(request):
"""handle form post to update settings""" """handle form post to update settings"""
user_form = UserSettingsForm(request.POST) user_form = UserSettingsForm(request.POST)
if user_form.is_valid(): if user_form.is_valid():
@ -805,7 +805,8 @@ class SettingsView(View):
app_form_post = app_form.cleaned_data app_form_post = app_form.cleaned_data
if app_form_post: if app_form_post:
print(app_form_post) print(app_form_post)
AppConfig().update_config(app_form_post) updated = AppConfig().update_config(app_form_post)
self.post_process_updated(updated)
scheduler_form = SchedulerSettingsForm(request.POST) scheduler_form = SchedulerSettingsForm(request.POST)
if scheduler_form.is_valid(): if scheduler_form.is_valid():
@ -817,6 +818,19 @@ class SettingsView(View):
sleep(1) sleep(1)
return redirect("settings", permanent=True) return redirect("settings", permanent=True)
@staticmethod
def post_process_updated(updated):
"""apply changes for config"""
if not updated:
return
for config_value, updated_value in updated:
if config_value == "cookie_import":
if updated_value:
CookieHandler().import_cookie()
else:
CookieHandler().revoke()
def progress(request): def progress(request):
# pylint: disable=unused-argument # pylint: disable=unused-argument