mirror of
https://github.com/meeb/tubesync.git
synced 2025-06-28 01:36:56 +00:00
Merge branch 'main' into patch-12
This commit is contained in:
commit
06fbc7c7aa
34
Dockerfile
34
Dockerfile
@ -25,6 +25,7 @@ ARG TARGETARCH
|
|||||||
|
|
||||||
ENV DEBIAN_FRONTEND="noninteractive" \
|
ENV DEBIAN_FRONTEND="noninteractive" \
|
||||||
APT_KEEP_ARCHIVES=1 \
|
APT_KEEP_ARCHIVES=1 \
|
||||||
|
EDITOR="editor" \
|
||||||
HOME="/root" \
|
HOME="/root" \
|
||||||
LANGUAGE="en_US.UTF-8" \
|
LANGUAGE="en_US.UTF-8" \
|
||||||
LANG="en_US.UTF-8" \
|
LANG="en_US.UTF-8" \
|
||||||
@ -321,6 +322,8 @@ RUN --mount=type=cache,id=apt-lib-cache-${TARGETARCH},sharing=private,target=/va
|
|||||||
apt-get -y autoclean && \
|
apt-get -y autoclean && \
|
||||||
rm -v -f /var/cache/debconf/*.dat-old
|
rm -v -f /var/cache/debconf/*.dat-old
|
||||||
|
|
||||||
|
# The preference for openresty over nginx,
|
||||||
|
# is for the newer version.
|
||||||
FROM tubesync-openresty AS tubesync
|
FROM tubesync-openresty AS tubesync
|
||||||
|
|
||||||
ARG S6_VERSION
|
ARG S6_VERSION
|
||||||
@ -343,16 +346,30 @@ RUN --mount=type=cache,id=apt-lib-cache-${TARGETARCH},sharing=private,target=/va
|
|||||||
# Install required distro packages
|
# Install required distro packages
|
||||||
apt-get -y --no-install-recommends install \
|
apt-get -y --no-install-recommends install \
|
||||||
libmariadb3 \
|
libmariadb3 \
|
||||||
|
libonig5 \
|
||||||
pkgconf \
|
pkgconf \
|
||||||
python3 \
|
python3 \
|
||||||
python3-libsass \
|
python3-libsass \
|
||||||
python3-pip-whl \
|
python3-pip-whl \
|
||||||
python3-socks \
|
python3-socks \
|
||||||
curl \
|
curl \
|
||||||
|
indent \
|
||||||
less \
|
less \
|
||||||
|
lua-lpeg \
|
||||||
|
tre-agrep \
|
||||||
|
vis \
|
||||||
|
xxd \
|
||||||
&& \
|
&& \
|
||||||
# Link to the current python3 version
|
# Link to the current python3 version
|
||||||
ln -v -s -f -T "$(find /usr/local/lib -name 'python3.[0-9]*' -type d -printf '%P\n' | sort -r -V | head -n 1)" /usr/local/lib/python3 && \
|
ln -v -s -f -T "$(find /usr/local/lib -name 'python3.[0-9]*' -type d -printf '%P\n' | sort -r -V | head -n 1)" /usr/local/lib/python3 && \
|
||||||
|
# Configure the editor alternatives
|
||||||
|
touch /usr/local/bin/babi /bin/nano /usr/bin/vim.tiny && \
|
||||||
|
update-alternatives --install /usr/bin/editor editor /usr/local/bin/babi 50 && \
|
||||||
|
update-alternatives --install /usr/local/bin/nano nano /bin/nano 10 && \
|
||||||
|
update-alternatives --install /usr/local/bin/nano nano /usr/local/bin/babi 20 && \
|
||||||
|
update-alternatives --install /usr/local/bin/vim vim /usr/bin/vim.tiny 15 && \
|
||||||
|
update-alternatives --install /usr/local/bin/vim vim /usr/bin/vis 35 && \
|
||||||
|
rm -v /usr/local/bin/babi /bin/nano /usr/bin/vim.tiny && \
|
||||||
# Create a 'app' user which the application will run as
|
# Create a 'app' user which the application will run as
|
||||||
groupadd app && \
|
groupadd app && \
|
||||||
useradd -M -d /app -s /bin/false -g app app && \
|
useradd -M -d /app -s /bin/false -g app app && \
|
||||||
@ -404,6 +421,7 @@ RUN --mount=type=tmpfs,target=/cache \
|
|||||||
g++ \
|
g++ \
|
||||||
gcc \
|
gcc \
|
||||||
libjpeg-dev \
|
libjpeg-dev \
|
||||||
|
libonig-dev \
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
libwebp-dev \
|
libwebp-dev \
|
||||||
make \
|
make \
|
||||||
@ -447,6 +465,7 @@ RUN --mount=type=tmpfs,target=/cache \
|
|||||||
g++ \
|
g++ \
|
||||||
gcc \
|
gcc \
|
||||||
libjpeg-dev \
|
libjpeg-dev \
|
||||||
|
libonig-dev \
|
||||||
libpq-dev \
|
libpq-dev \
|
||||||
libwebp-dev \
|
libwebp-dev \
|
||||||
make \
|
make \
|
||||||
@ -456,8 +475,21 @@ RUN --mount=type=tmpfs,target=/cache \
|
|||||||
&& \
|
&& \
|
||||||
apt-get -y autopurge && \
|
apt-get -y autopurge && \
|
||||||
apt-get -y autoclean && \
|
apt-get -y autoclean && \
|
||||||
|
LD_LIBRARY_PATH=/usr/local/lib/python3/dist-packages/pillow.libs:/usr/local/lib/python3/dist-packages/psycopg_binary.libs \
|
||||||
|
find /usr/local/lib/python3/dist-packages/ \
|
||||||
|
-name '*.so*' -print \
|
||||||
|
-exec du -h '{}' ';' \
|
||||||
|
-exec ldd '{}' ';' \
|
||||||
|
>| /cache/python-shared-objects 2>&1 && \
|
||||||
rm -v -f /var/cache/debconf/*.dat-old && \
|
rm -v -f /var/cache/debconf/*.dat-old && \
|
||||||
rm -v -rf /tmp/*
|
rm -v -rf /tmp/* ; \
|
||||||
|
if grep >/dev/null -Fe ' => not found' /cache/python-shared-objects ; \
|
||||||
|
then \
|
||||||
|
cat -v /cache/python-shared-objects ; \
|
||||||
|
printf -- 1>&2 '%s\n' \
|
||||||
|
ERROR: ' An unresolved shared object was found.' ; \
|
||||||
|
exit 1 ; \
|
||||||
|
fi
|
||||||
|
|
||||||
# Copy root
|
# Copy root
|
||||||
COPY config/root /
|
COPY config/root /
|
||||||
|
1
Pipfile
1
Pipfile
@ -25,3 +25,4 @@ emoji = "*"
|
|||||||
brotli = "*"
|
brotli = "*"
|
||||||
html5lib = "*"
|
html5lib = "*"
|
||||||
bgutil-ytdlp-pot-provider = "*"
|
bgutil-ytdlp-pot-provider = "*"
|
||||||
|
babi = "*"
|
||||||
|
@ -7,9 +7,19 @@ import pstats
|
|||||||
import string
|
import string
|
||||||
import time
|
import time
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
|
from functools import partial
|
||||||
|
from operator import attrgetter, itemgetter
|
||||||
|
from pathlib import Path
|
||||||
from urllib.parse import urlunsplit, urlencode, urlparse
|
from urllib.parse import urlunsplit, urlencode, urlparse
|
||||||
from .errors import DatabaseConnectionError
|
from .errors import DatabaseConnectionError
|
||||||
|
|
||||||
|
def directory_and_stem(arg_path, /, all_suffixes=False):
|
||||||
|
filepath = Path(arg_path)
|
||||||
|
stem = Path(filepath.stem)
|
||||||
|
while all_suffixes and stem.suffixes and '' != stem.suffix:
|
||||||
|
stem = Path(stem.stem)
|
||||||
|
return (filepath.parent, str(stem),)
|
||||||
|
|
||||||
|
|
||||||
def getenv(key, default=None, /, *, integer=False, string=True):
|
def getenv(key, default=None, /, *, integer=False, string=True):
|
||||||
'''
|
'''
|
||||||
@ -46,6 +56,51 @@ def getenv(key, default=None, /, *, integer=False, string=True):
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def glob_quote(filestr, /):
|
||||||
|
_glob_specials = {
|
||||||
|
'?': '[?]',
|
||||||
|
'*': '[*]',
|
||||||
|
'[': '[[]',
|
||||||
|
']': '[]]', # probably not needed, but it won't hurt
|
||||||
|
}
|
||||||
|
|
||||||
|
if not isinstance(filestr, str):
|
||||||
|
raise TypeError(f'expected a str, got "{type(filestr)}"')
|
||||||
|
|
||||||
|
return filestr.translate(str.maketrans(_glob_specials))
|
||||||
|
|
||||||
|
|
||||||
|
def list_of_dictionaries(arg_list, /, arg_function=lambda x: x):
|
||||||
|
assert callable(arg_function)
|
||||||
|
if isinstance(arg_list, list):
|
||||||
|
_map_func = partial(lambda f, d: f(d) if isinstance(d, dict) else d, arg_function)
|
||||||
|
return (True, list(map(_map_func, arg_list)),)
|
||||||
|
return (False, arg_list,)
|
||||||
|
|
||||||
|
|
||||||
|
def mkdir_p(arg_path, /, *, mode=0o777):
|
||||||
|
'''
|
||||||
|
Reminder: mode only affects the last directory
|
||||||
|
'''
|
||||||
|
dirpath = Path(arg_path)
|
||||||
|
return dirpath.mkdir(mode=mode, parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def multi_key_sort(iterable, specs, /, use_reversed=False, *, item=False, attr=False, key_func=None):
|
||||||
|
result = list(iterable)
|
||||||
|
if key_func is None:
|
||||||
|
# itemgetter is the default
|
||||||
|
if item or not (item or attr):
|
||||||
|
key_func = itemgetter
|
||||||
|
elif attr:
|
||||||
|
key_func = attrgetter
|
||||||
|
for key, reverse in reversed(specs):
|
||||||
|
result.sort(key=key_func(key), reverse=reverse)
|
||||||
|
if use_reversed:
|
||||||
|
return list(reversed(result))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def parse_database_connection_string(database_connection_string):
|
def parse_database_connection_string(database_connection_string):
|
||||||
'''
|
'''
|
||||||
Parses a connection string in a URL style format, such as:
|
Parses a connection string in a URL style format, such as:
|
||||||
@ -167,6 +222,15 @@ def clean_emoji(s):
|
|||||||
return emoji.replace_emoji(s)
|
return emoji.replace_emoji(s)
|
||||||
|
|
||||||
|
|
||||||
|
def seconds_to_timestr(seconds):
|
||||||
|
seconds = seconds % (24 * 3600)
|
||||||
|
hour = seconds // 3600
|
||||||
|
seconds %= 3600
|
||||||
|
minutes = seconds // 60
|
||||||
|
seconds %= 60
|
||||||
|
return '{:02d}:{:02d}:{:02d}'.format(hour, minutes, seconds)
|
||||||
|
|
||||||
|
|
||||||
def time_func(func):
|
def time_func(func):
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
start = time.perf_counter()
|
start = time.perf_counter()
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
|
|
||||||
from .choices import Val, Fallback
|
from .choices import Val, Fallback
|
||||||
from .utils import multi_key_sort
|
from common.utils import multi_key_sort
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
from pathlib import Path
|
|
||||||
from ..choices import Val, YouTube_SourceType # noqa
|
from ..choices import Val, YouTube_SourceType # noqa
|
||||||
|
|
||||||
|
|
||||||
@ -11,11 +10,3 @@ def _nfo_element(nfo, label, text, /, *, attrs={}, tail='\n', char=' ', indent=2
|
|||||||
element.tail = tail + (char * indent)
|
element.tail = tail + (char * indent)
|
||||||
return element
|
return element
|
||||||
|
|
||||||
def directory_and_stem(arg_path, /, all_suffixes=False):
|
|
||||||
filepath = Path(arg_path)
|
|
||||||
stem = Path(filepath.stem)
|
|
||||||
while all_suffixes and stem.suffixes and '' != stem.suffix:
|
|
||||||
stem = Path(stem.stem)
|
|
||||||
stem = str(stem)
|
|
||||||
return (filepath.parent, stem,)
|
|
||||||
|
|
||||||
|
@ -17,15 +17,15 @@ from common.logger import log
|
|||||||
from common.errors import NoFormatException
|
from common.errors import NoFormatException
|
||||||
from common.json import JSONEncoder
|
from common.json import JSONEncoder
|
||||||
from common.utils import (
|
from common.utils import (
|
||||||
clean_filename, clean_emoji,
|
clean_filename, clean_emoji, directory_and_stem,
|
||||||
|
glob_quote, mkdir_p, multi_key_sort, seconds_to_timestr,
|
||||||
)
|
)
|
||||||
from ..youtube import (
|
from ..youtube import (
|
||||||
get_media_info as get_youtube_media_info,
|
get_media_info as get_youtube_media_info,
|
||||||
download_media as download_youtube_media,
|
download_media as download_youtube_media,
|
||||||
)
|
)
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
seconds_to_timestr, parse_media_format, filter_response,
|
filter_response, parse_media_format, write_text_file,
|
||||||
write_text_file, mkdir_p, glob_quote, multi_key_sort,
|
|
||||||
)
|
)
|
||||||
from ..matching import (
|
from ..matching import (
|
||||||
get_best_combined_format,
|
get_best_combined_format,
|
||||||
@ -38,7 +38,7 @@ from ..choices import (
|
|||||||
from ._migrations import (
|
from ._migrations import (
|
||||||
media_file_storage, get_media_thumb_path, get_media_file_path,
|
media_file_storage, get_media_thumb_path, get_media_file_path,
|
||||||
)
|
)
|
||||||
from ._private import _srctype_dict, _nfo_element, directory_and_stem
|
from ._private import _srctype_dict, _nfo_element
|
||||||
from .media__tasks import (
|
from .media__tasks import (
|
||||||
download_checklist, download_finished, wait_for_premiere,
|
download_checklist, download_finished, wait_for_premiere,
|
||||||
)
|
)
|
||||||
|
@ -10,6 +10,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from background_task.signals import task_failed
|
from background_task.signals import task_failed
|
||||||
from background_task.models import Task
|
from background_task.models import Task
|
||||||
from common.logger import log
|
from common.logger import log
|
||||||
|
from common.utils import glob_quote, mkdir_p
|
||||||
from .models import Source, Media, Metadata
|
from .models import Source, Media, Metadata
|
||||||
from .tasks import (delete_task_by_source, delete_task_by_media, index_source_task,
|
from .tasks import (delete_task_by_source, delete_task_by_media, index_source_task,
|
||||||
download_media_thumbnail, download_media_metadata,
|
download_media_thumbnail, download_media_metadata,
|
||||||
@ -17,7 +18,7 @@ from .tasks import (delete_task_by_source, delete_task_by_media, index_source_ta
|
|||||||
download_media, download_source_images,
|
download_media, download_source_images,
|
||||||
delete_all_media_for_source, save_all_media_for_source,
|
delete_all_media_for_source, save_all_media_for_source,
|
||||||
rename_media, get_media_metadata_task, get_media_download_task)
|
rename_media, get_media_metadata_task, get_media_download_task)
|
||||||
from .utils import delete_file, glob_quote, mkdir_p
|
from .utils import delete_file
|
||||||
from .filtering import filter_media
|
from .filtering import filter_media
|
||||||
from .choices import Val, YouTube_SourceType
|
from .choices import Val, YouTube_SourceType
|
||||||
|
|
||||||
|
@ -31,11 +31,11 @@ from common.errors import ( NoFormatException, NoMediaException,
|
|||||||
NoThumbnailException,
|
NoThumbnailException,
|
||||||
DownloadFailedException, )
|
DownloadFailedException, )
|
||||||
from common.utils import ( django_queryset_generator as qs_gen,
|
from common.utils import ( django_queryset_generator as qs_gen,
|
||||||
remove_enclosed, )
|
remove_enclosed, seconds_to_timestr, )
|
||||||
from .choices import Val, TaskQueue
|
from .choices import Val, TaskQueue
|
||||||
from .models import Source, Media, MediaServer
|
from .models import Source, Media, MediaServer
|
||||||
from .utils import ( get_remote_image, resize_image_to_height,
|
from .utils import ( get_remote_image, resize_image_to_height,
|
||||||
write_text_file, filter_response, seconds_to_timestr, )
|
write_text_file, filter_response, )
|
||||||
from .youtube import YouTubeError
|
from .youtube import YouTubeError
|
||||||
|
|
||||||
db_vendor = db.connection.vendor
|
db_vendor = db.connection.vendor
|
||||||
|
@ -2,11 +2,11 @@ import os
|
|||||||
import re
|
import re
|
||||||
import math
|
import math
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from operator import attrgetter, itemgetter
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
import requests
|
import requests
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
from common.utils import list_of_dictionaries
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from urllib.parse import urlsplit, parse_qs
|
from urllib.parse import urlsplit, parse_qs
|
||||||
from django.forms import ValidationError
|
from django.forms import ValidationError
|
||||||
@ -95,20 +95,6 @@ def resize_image_to_height(image, width, height):
|
|||||||
return image
|
return image
|
||||||
|
|
||||||
|
|
||||||
def glob_quote(filestr):
|
|
||||||
_glob_specials = {
|
|
||||||
'?': '[?]',
|
|
||||||
'*': '[*]',
|
|
||||||
'[': '[[]',
|
|
||||||
']': '[]]', # probably not needed, but it won't hurt
|
|
||||||
}
|
|
||||||
|
|
||||||
if not isinstance(filestr, str):
|
|
||||||
raise TypeError(f'filestr must be a str, got "{type(filestr)}"')
|
|
||||||
|
|
||||||
return filestr.translate(str.maketrans(_glob_specials))
|
|
||||||
|
|
||||||
|
|
||||||
def file_is_editable(filepath):
|
def file_is_editable(filepath):
|
||||||
'''
|
'''
|
||||||
Checks that a file exists and the file is in an allowed predefined tuple of
|
Checks that a file exists and the file is in an allowed predefined tuple of
|
||||||
@ -130,14 +116,6 @@ def file_is_editable(filepath):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def mkdir_p(arg_path, mode=0o777):
|
|
||||||
'''
|
|
||||||
Reminder: mode only affects the last directory
|
|
||||||
'''
|
|
||||||
dirpath = Path(arg_path)
|
|
||||||
return dirpath.mkdir(mode=mode, parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
|
|
||||||
def write_text_file(filepath, filedata):
|
def write_text_file(filepath, filedata):
|
||||||
if not isinstance(filedata, str):
|
if not isinstance(filedata, str):
|
||||||
raise TypeError(f'filedata must be a str, got "{type(filedata)}"')
|
raise TypeError(f'filedata must be a str, got "{type(filedata)}"')
|
||||||
@ -162,30 +140,6 @@ def delete_file(filepath):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def seconds_to_timestr(seconds):
|
|
||||||
seconds = seconds % (24 * 3600)
|
|
||||||
hour = seconds // 3600
|
|
||||||
seconds %= 3600
|
|
||||||
minutes = seconds // 60
|
|
||||||
seconds %= 60
|
|
||||||
return '{:02d}:{:02d}:{:02d}'.format(hour, minutes, seconds)
|
|
||||||
|
|
||||||
|
|
||||||
def multi_key_sort(iterable, specs, /, use_reversed=False, *, item=False, attr=False, key_func=None):
|
|
||||||
result = list(iterable)
|
|
||||||
if key_func is None:
|
|
||||||
# itemgetter is the default
|
|
||||||
if item or not (item or attr):
|
|
||||||
key_func = itemgetter
|
|
||||||
elif attr:
|
|
||||||
key_func = attrgetter
|
|
||||||
for key, reverse in reversed(specs):
|
|
||||||
result.sort(key=key_func(key), reverse=reverse)
|
|
||||||
if use_reversed:
|
|
||||||
return list(reversed(result))
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_codec(codec_str):
|
def normalize_codec(codec_str):
|
||||||
result = str(codec_str).upper()
|
result = str(codec_str).upper()
|
||||||
parts = result.split('.')
|
parts = result.split('.')
|
||||||
@ -201,17 +155,6 @@ def normalize_codec(codec_str):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def list_of_dictionaries(arg_list, arg_function=lambda x: x):
|
|
||||||
assert callable(arg_function)
|
|
||||||
if isinstance(arg_list, list):
|
|
||||||
def _call_func_with_dict(arg_dict):
|
|
||||||
if isinstance(arg_dict, dict):
|
|
||||||
return arg_function(arg_dict)
|
|
||||||
return arg_dict
|
|
||||||
return (True, list(map(_call_func_with_dict, arg_list)),)
|
|
||||||
return (False, arg_list,)
|
|
||||||
|
|
||||||
|
|
||||||
def _url_keys(arg_dict, filter_func):
|
def _url_keys(arg_dict, filter_func):
|
||||||
result = {}
|
result = {}
|
||||||
if isinstance(arg_dict, dict):
|
if isinstance(arg_dict, dict):
|
||||||
|
@ -20,13 +20,13 @@ from django.utils._os import safe_join
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from common.timestamp import timestamp_to_datetime
|
from common.timestamp import timestamp_to_datetime
|
||||||
from common.utils import append_uri_params
|
from common.utils import append_uri_params, mkdir_p, multi_key_sort
|
||||||
from background_task.models import Task, CompletedTask
|
from background_task.models import Task, CompletedTask
|
||||||
from .models import Source, Media, MediaServer
|
from .models import Source, Media, MediaServer
|
||||||
from .forms import (ValidateSourceForm, ConfirmDeleteSourceForm, RedownloadMediaForm,
|
from .forms import (ValidateSourceForm, ConfirmDeleteSourceForm, RedownloadMediaForm,
|
||||||
SkipMediaForm, EnableMediaForm, ResetTasksForm, ScheduleTaskForm,
|
SkipMediaForm, EnableMediaForm, ResetTasksForm, ScheduleTaskForm,
|
||||||
ConfirmDeleteMediaServerForm, SourceForm)
|
ConfirmDeleteMediaServerForm, SourceForm)
|
||||||
from .utils import validate_url, delete_file, multi_key_sort, mkdir_p
|
from .utils import delete_file, validate_url
|
||||||
from .tasks import (map_task_to_instance, get_error_message,
|
from .tasks import (map_task_to_instance, get_error_message,
|
||||||
get_source_completed_tasks, get_media_download_task,
|
get_source_completed_tasks, get_media_download_task,
|
||||||
delete_task_by_media, index_source_task,
|
delete_task_by_media, index_source_task,
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from common.logger import log
|
from common.logger import log
|
||||||
|
from common.utils import mkdir_p
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
@ -15,7 +16,6 @@ from urllib.parse import urlsplit, parse_qs
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from .choices import Val, FileExtension
|
from .choices import Val, FileExtension
|
||||||
from .hooks import postprocessor_hook, progress_hook
|
from .hooks import postprocessor_hook, progress_hook
|
||||||
from .utils import mkdir_p
|
|
||||||
import yt_dlp
|
import yt_dlp
|
||||||
import yt_dlp.patch.check_thumbnails
|
import yt_dlp.patch.check_thumbnails
|
||||||
import yt_dlp.patch.fatal_http_errors
|
import yt_dlp.patch.fatal_http_errors
|
||||||
|
Loading…
Reference in New Issue
Block a user