Add Gluetun Support

This commit is contained in:
Tyler Flowers 2026-03-21 21:11:19 -04:00
parent b6942c8352
commit 79387f5f65
6 changed files with 271 additions and 31 deletions

View File

@ -55,6 +55,12 @@ class AppConfigDownloadsSerializer(
)
cookie_import = serializers.BooleanField()
pot_provider_url = serializers.CharField(allow_null=True)
gluetun_url = serializers.CharField(allow_null=True)
gluetun_control_url = serializers.CharField(allow_null=True)
gluetun_control_key = serializers.CharField(allow_null=True)
gluetun_swap = serializers.BooleanField()
gluetun_attempts = serializers.IntegerField(allow_null=True)
gluetun_sleep = serializers.IntegerField(allow_null=True)
throttledratelimit = serializers.IntegerField(allow_null=True)
extractor_lang = serializers.CharField(allow_null=True)
integrate_ryd = serializers.BooleanField()

View File

@ -42,6 +42,8 @@ class DownloadsConfigType(TypedDict):
comment_sort: Literal["top", "new"] | None
cookie_import: bool
pot_provider_url: str | None
gluetun_url: str | None
gluetun_swap: bool
throttledratelimit: int | None
extractor_lang: str | None
integrate_ryd: bool
@ -91,6 +93,12 @@ class AppConfig:
"comment_sort": "top",
"cookie_import": False,
"pot_provider_url": None,
"gluetun_url": None,
"gluetun_control_url": None,
"gluetun_control_key": None,
"gluetun_swap": False,
"gluetun_attempts": 0,
"gluetun_sleep": 0,
"throttledratelimit": None,
"extractor_lang": None,
"integrate_ryd": False,

View File

@ -4,19 +4,20 @@ functionality:
- handle yt-dlp errors
"""
import time
from datetime import datetime
from http import cookiejar
from io import StringIO
from os import path
import yt_dlp
import requests
from appsettings.src.config import AppConfig
from common.src.env_settings import EnvironmentSettings
from common.src.helper import deep_merge, rand_sleep
from common.src.ta_redis import RedisArchivist
from django.conf import settings
class YtWrap:
"""wrap calls to yt"""
@ -81,23 +82,65 @@ class YtWrap:
if EnvironmentSettings.APP_DIR == "/app":
# container internal only
self.obs["plugin_dirs"].append("/opt/yt_plugins/bgutil")
def _add_gluetun_url(self):
"""add gluetun proxy url"""
if gluetun_url := self.config["downloads"].get("gluetun_url"):
deep_merge(
self.obs,
{
"proxy": [gluetun_url]
},
)
def download(self, url):
"""make download request"""
self.obs.update({"check_formats": "selected"})
with yt_dlp.YoutubeDL(self.obs) as ydl:
try:
ydl.download([url])
except yt_dlp.utils.DownloadError as err:
print(f"{url}: failed to download with message {err}")
if "Temporary failure in name resolution" in str(err):
raise ConnectionError("lost the internet, abort!") from err
if any(m in str(err) for m in self.BOT_MESSAGES):
print(self.BOT_ERROR_LOG)
rand_sleep(self.config)
raise ConnectionError(self.BOT_ERROR_LOG) from err
max_attempts = self.config["downloads"].get("gluetun_attempts")
inf = False
if max_attempts == 0 or max_attempts == None:
inf = True
max_attempts = 0
attempt = 0
while attempt < max_attempts or inf:
try:
ydl.download([url])
break
except yt_dlp.utils.DownloadError as err:
print(f"{url}: failed to download with message {err}")
if "Temporary failure in name resolution" in str(err):
raise ConnectionError("lost the internet, abort!") from err
if any(m in str(err) for m in self.BOT_MESSAGES):
print(self.BOT_ERROR_LOG)
if self.config["downloads"].get("gluetun_swap"):
control_url=f"{self.config['downloads'].get('gluetun_control_url')}/v1/vpn/status"
headers={
"Content-Type": "application/json",
"X-API-Key": self.config['downloads'].get('gluetun_control_key')
}
requests.put(
url=control_url,
headers=headers,
json={"status": "stopped"}
)
requests.put(
url=control_url,
headers=headers,
json={"status": "running"}
)
sleep_amount = self.config['downloads'].get('gluetun_sleep')
if sleep_amount == 0:
sleep_amount = 30
time.sleep(sleep_amount)
attempt += 1
if (attempt == max_attempts):
raise ConnectionError("Reached maximum IP attempts!") from err
continue
rand_sleep(self.config)
raise ConnectionError(self.BOT_ERROR_LOG) from err
return False, str(err)
return False, str(err)
self._validate_cookie()
@ -109,27 +152,59 @@ class YtWrap:
returns response, error
"""
with yt_dlp.YoutubeDL(self.obs) as ydl:
try:
response = ydl.extract_info(url)
except cookiejar.LoadError as err:
print(f"cookie file is invalid: {err}")
return None, str(err)
except yt_dlp.utils.ExtractorError as err:
print(f"{url}: failed to extract: {err}, continue...")
return None, str(err)
except yt_dlp.utils.DownloadError as err:
if "This channel does not have a" in str(err):
return None, None
max_attempts = self.config["downloads"].get("gluetun_attempts")
inf = False
if max_attempts == 0 or max_attempts == None:
inf = True
max_attempts = 0
attempt = 0
while attempt < max_attempts or inf:
try:
response = ydl.extract_info(url)
break
except cookiejar.LoadError as err:
print(f"cookie file is invalid: {err}")
return None, str(err)
except yt_dlp.utils.ExtractorError as err:
print(f"{url}: failed to extract: {err}, continue...")
return None, str(err)
except yt_dlp.utils.DownloadError as err:
if "This channel does not have a" in str(err):
return None, None
print(f"{url}: failed to get info from youtube: {err}")
if "Temporary failure in name resolution" in str(err):
raise ConnectionError("lost the internet, abort!") from err
if any(m in str(err) for m in self.BOT_MESSAGES):
print(self.BOT_ERROR_LOG)
rand_sleep(self.config)
raise ConnectionError(self.BOT_ERROR_LOG) from err
print(f"{url}: failed to get info from youtube: {err}")
if "Temporary failure in name resolution" in str(err):
raise ConnectionError("lost the internet, abort!") from err
if any(m in str(err) for m in self.BOT_MESSAGES):
print(self.BOT_ERROR_LOG)
if self.config["downloads"].get("gluetun_swap"):
control_url=f"{self.config['downloads'].get('gluetun_control_url')}/v1/vpn/status"
headers={
"Content-Type": "application/json",
"X-API-Key": self.config['downloads'].get('gluetun_control_key')
}
requests.put(
url=control_url,
headers=headers,
json={"status": "stopped"}
)
requests.put(
url=control_url,
headers=headers,
json={"status": "running"}
)
sleep_amount = self.config['downloads'].get('gluetun_sleep')
if sleep_amount == 0:
sleep_amount = 30
time.sleep(sleep_amount)
attempt += 1
if (attempt == max_attempts):
raise ConnectionError("Reached maximum IP attempts!") from err
continue
rand_sleep(self.config)
raise ConnectionError(self.BOT_ERROR_LOG) from err
return None, str(err)
return None, str(err)
self._validate_cookie()

View File

@ -23,6 +23,12 @@ export type AppSettingsConfigType = {
comment_sort: string;
cookie_import: boolean;
pot_provider_url: string | null;
gluetun_url: string | null;
gluetun_control_url: string | null;
gluetun_control_key: string | null;
gluetun_swap: boolean;
gluetun_attempts: number | null;
gluetun_sleep: number | null;
throttledratelimit: number | null;
extractor_lang: string | null;
integrate_ryd: boolean;

View File

@ -71,6 +71,14 @@ const SettingsApplication = () => {
const [showCookieForm, setShowCookieForm] = useState<boolean>(false);
const [potProviderUrl, setPotProviderUrl] = useState<string | null>(null);
// Gluetun
const [gluetunUrl, setGluetunUrl] = useState<string | null>(null);
const [gluetunControlUrl, setGluetunControlUrl] = useState<string | null>(null);
const [gluetunControlKey, setGluetunControlKey] = useState<string | null>(null);
const [gluetunSwap, setGluetunSwap] = useState<boolean>(false);
const [gluetunAttempts, setGluetunAttempts] = useState<number | null>(null);
const [gluetunSleep, setGluetunSleep] = useState<number | null>(null);
// Integrations
const [showApiToken, setShowApiToken] = useState(false);
const [downloadDislikes, setDownloadDislikes] = useState(false);
@ -125,6 +133,14 @@ const SettingsApplication = () => {
// Cookie
setPotProviderUrl(appSettingsConfigData?.downloads.pot_provider_url || null);
// Gluetun
setGluetunUrl(appSettingsConfigData?.downloads.gluetun_url || null);
setGluetunControlUrl(appSettingsConfigData?.downloads.gluetun_control_url || null);
setGluetunControlKey(appSettingsConfigData?.downloads.gluetun_control_key || null);
setGluetunSwap(appSettingsConfigData?.downloads.gluetun_swap || false);
setGluetunAttempts(appSettingsConfigData?.downloads.gluetun_attempts || null);
setGluetunSleep(appSettingsConfigData?.downloads.gluetun_sleep || null);
// Integrations
setDownloadDislikes(appSettingsConfigData?.downloads.integrate_ryd || false);
setEnableSponsorBlock(appSettingsConfigData?.downloads.integrate_sponsorblock || false);
@ -753,6 +769,129 @@ const SettingsApplication = () => {
/>
</div>
</div>
<div className="info-box-item">
<h2 id="gluetun">Gluetun</h2>
{userConfig.show_help_text && (
<div className="help-text">
<p>
Use a VPN for downloads via Gluetun
</p>
<ul>
<li>
Using a VPN can help get work around blocked requests if you don't
have or want to use an account.
</li>
<li>
This expects you to use and setup{' '}
<a
target="_blank"
href="https://github.com/qdm12/gluetun"
>
Gluetun.
</a>
</li>
<li>
Enabling Swap IPs will change the VPN's IP if it's blocked from
downloading regular videos.
</li>
<li>
Using Swap IPs requires you to configure a control server and use
an API key, see{' '}
<a
target="_blank"
href="https://github.com/qdm12/gluetun-wiki/blob/main/setup/advanced/control-server.md"
>
Gluetun's Documentation.
</a>
</li>
<li>
Setting "Swap IP attempts" to 0 or leaving it empty will try
swapping the IP infinitely until it can download. Otherwise, it
will attempt to swap the IP only for a certain number of attempts.
</li>
<li>
Setting "Sleep Interval" will wait that amount of time after
swapping the IP before attempting to download again. Default is 30.
</li>
</ul>
</div>
)}
<div className="settings-box-wrapper">
<div>
<p>Gluetun Proxy URL</p>
</div>
<InputConfig
type="text"
name="downloads.gluetun_url"
value={gluetunUrl}
setValue={setGluetunUrl}
oldValue={appSettingsConfig.downloads.gluetun_url}
updateCallback={handleUpdateConfig}
/>
</div>
<div className="settings-box-wrapper">
<div>
<p>Gluetun Control Server URL</p>
</div>
<InputConfig
type="text"
name="downloads.gluetun_control_url"
value={gluetunControlUrl}
setValue={setGluetunControlUrl}
oldValue={appSettingsConfig.downloads.gluetun_control_url}
updateCallback={handleUpdateConfig}
/>
</div>
<div className="settings-box-wrapper">
<div>
<p>Gluetun Control Server API Key</p>
</div>
<InputConfig
type="text"
name="downloads.gluetun_control_key"
value={gluetunControlKey}
setValue={setGluetunControlKey}
oldValue={appSettingsConfig.downloads.gluetun_control_key}
updateCallback={handleUpdateConfig}
/>
</div>
<div className="settings-box-wrapper">
<div>
<p>Swap IPs</p>
</div>
<ToggleConfig
name="downloads.gluetun_swap"
value={gluetunSwap}
updateCallback={handleUpdateConfig}
/>
</div>
<div className="settings-box-wrapper">
<div>
<p>Swap IP Attempts</p>
</div>
<InputConfig
type="number"
name="downloads.gluetun_attempts"
value={gluetunAttempts}
setValue={setGluetunAttempts}
oldValue={appSettingsConfig?.downloads.gluetun_attempts}
updateCallback={handleUpdateConfig}
/>
</div>
<div className="settings-box-wrapper">
<div>
<p>Sleep Interval</p>
</div>
<InputConfig
type="number"
name="downloads.gluetun_sleep"
value={gluetunSleep}
setValue={setGluetunSleep}
oldValue={appSettingsConfig?.downloads.gluetun_sleep}
updateCallback={handleUpdateConfig}
/>
</div>
</div>
<div className="info-box-item">
<h2 id="sntegrations">Integrations</h2>
{userConfig.show_help_text && (

View File

@ -30,6 +30,12 @@ export const useAppSettingsStore = create<AppSettingsState>(set => ({
comment_sort: 'asc',
cookie_import: false,
pot_provider_url: null,
gluetun_url: null,
gluetun_control_url: null,
gluetun_control_key: null,
gluetun_swap: false,
gluetun_attempts: null,
gluetun_sleep: null,
throttledratelimit: null,
extractor_lang: null,
integrate_ryd: false,