Major refactor of many things.

This commit is contained in:
2019-12-19 00:27:06 +02:00
parent fd5d05232f
commit 6b843f1fc2
28 changed files with 374 additions and 181 deletions

View File

@ -1,36 +0,0 @@
from YtManagerApp.dynamic_preferences_registry import Initialized, YouTubeAPIKey, AllowRegistrations, SyncSchedule, SchedulerConcurrency
class AppConfig(object):
# Properties
props = {
'initialized': Initialized,
'youtube_api_key': YouTubeAPIKey,
'allow_registrations': AllowRegistrations,
'sync_schedule': SyncSchedule,
'concurrency': SchedulerConcurrency
}
# Init
def __init__(self, pref_manager):
self.__pref_manager = pref_manager
def __getattr__(self, item):
prop_class = AppConfig.props[item]
prop_full_name = prop_class.section.name + "__" + prop_class.name
return self.__pref_manager[prop_full_name]
def __setattr__(self, key, value):
if key in AppConfig.props:
prop_class = AppConfig.props[key]
prop_full_name = prop_class.section.name + "__" + prop_class.name
self.__pref_manager[prop_full_name] = value
else:
super().__setattr__(key, value)
def for_sub(self, subscription, pref: str):
value = getattr(subscription, pref)
if value is None:
value = subscription.user.preferences[pref]
return value

View File

@ -1,111 +0,0 @@
from YtManagerApp.scheduler.jobs.download_video_job import DownloadVideoJob
from YtManagerApp.models import Video, Subscription, VIDEO_ORDER_MAPPING
from YtManagerApp.utils import first_non_null
from django.conf import settings as srv_settings
import logging
import requests
import mimetypes
import os
import PIL.Image
import PIL.ImageOps
from urllib.parse import urljoin
log = logging.getLogger('downloader')
def __get_subscription_config(sub: Subscription):
user = sub.user
enabled = first_non_null(sub.auto_download, user.preferences['auto_download'])
global_limit = user.preferences['download_global_limit']
limit = first_non_null(sub.download_limit, user.preferences['download_subscription_limit'])
order = first_non_null(sub.download_order, user.preferences['download_order'])
order = VIDEO_ORDER_MAPPING[order]
return enabled, global_limit, limit, order
def downloader_process_subscription(sub: Subscription):
log.info('Processing subscription %d [%s %s]', sub.id, sub.playlist_id, sub.id)
enabled, global_limit, limit, order = __get_subscription_config(sub)
log.info('Determined settings enabled=%s global_limit=%d limit=%d order="%s"', enabled, global_limit, limit, order)
if enabled:
videos_to_download = Video.objects\
.filter(subscription=sub, downloaded_path__isnull=True, watched=False)\
.order_by(order)
log.info('%d download candidates.', len(videos_to_download))
if global_limit > 0:
global_downloaded = Video.objects.filter(subscription__user=sub.user, downloaded_path__isnull=False).count()
allowed_count = max(global_limit - global_downloaded, 0)
videos_to_download = videos_to_download[0:allowed_count]
log.info('Global limit is set, can only download up to %d videos.', allowed_count)
if limit > 0:
sub_downloaded = Video.objects.filter(subscription=sub, downloaded_path__isnull=False).count()
allowed_count = max(limit - sub_downloaded, 0)
videos_to_download = videos_to_download[0:allowed_count]
log.info('Limit is set, can only download up to %d videos.', allowed_count)
# enqueue download
for video in videos_to_download:
log.info('Enqueuing video %d [%s %s] index=%d', video.id, video.video_id, video.name, video.playlist_index)
DownloadVideoJob.schedule(video)
log.info('Finished processing subscription %d [%s %s]', sub.id, sub.playlist_id, sub.id)
def downloader_process_all():
for subscription in Subscription.objects.all():
downloader_process_subscription(subscription)
def fetch_thumbnail(url, object_type, identifier, thumb_size):
log.info('Fetching thumbnail url=%s object_type=%s identifier=%s', url, object_type, identifier)
# Make request to obtain mime type
try:
response = requests.get(url, stream=True)
except requests.exceptions.RequestException as e:
log.error('Failed to fetch thumbnail %s. Error: %s', url, e)
return url
ext = mimetypes.guess_extension(response.headers['Content-Type'])
# Build file path
file_name = f"{identifier}{ext}"
abs_path_dir = os.path.join(srv_settings.MEDIA_ROOT, "thumbs", object_type)
abs_path = os.path.join(abs_path_dir, file_name)
abs_path_tmp = file_name + '.tmp'
# Store image
try:
os.makedirs(abs_path_dir, exist_ok=True)
with open(abs_path_tmp, "wb") as f:
for chunk in response.iter_content(chunk_size=1024):
if chunk:
f.write(chunk)
# Resize and crop to thumbnail size
image = PIL.Image.open(abs_path_tmp)
image = PIL.ImageOps.fit(image, thumb_size)
image.save(abs_path)
image.close()
# Delete temp file
os.unlink(abs_path_tmp)
except requests.exceptions.RequestException as e:
log.error('Error while downloading stream for thumbnail %s. Error: %s', url, e)
return url
except OSError as e:
log.error('Error while writing to file %s for thumbnail %s. Error: %s', abs_path, url, e)
return url
# Return
media_url = urljoin(srv_settings.MEDIA_URL, f"thumbs/{object_type}/{file_name}")
return media_url

View File

@ -1,57 +0,0 @@
import re
from typing import Optional
from django.contrib.auth.models import User
from django.db.models import Q
from YtManagerApp.models import Subscription, Video, SubscriptionFolder
def get_videos(user: User,
sort_order: Optional[str],
query: Optional[str] = None,
subscription_id: Optional[int] = None,
folder_id: Optional[int] = None,
only_watched: Optional[bool] = None,
only_downloaded: Optional[bool] = None,
):
filter_args = []
filter_kwargs = {
'subscription__user': user
}
# Process query string - basically, we break it down into words,
# and then search for the given text in the name, description, uploader name and subscription name
if query is not None:
for match in re.finditer(r'\w+', query):
word = match[0]
filter_args.append(Q(name__icontains=word)
| Q(description__icontains=word)
| Q(uploader_name__icontains=word)
| Q(subscription__name__icontains=word))
# Subscription id
if subscription_id is not None:
filter_kwargs['subscription_id'] = subscription_id
# Folder id
if folder_id is not None:
# Visit function - returns only the subscription IDs
def visit(node):
if isinstance(node, Subscription):
return node.id
return None
filter_kwargs['subscription_id__in'] = SubscriptionFolder.traverse(folder_id, user, visit)
# Only watched
if only_watched is not None:
filter_kwargs['watched'] = only_watched
# Only downloaded
# - not downloaded (False) -> is null (True)
# - downloaded (True) -> is not null (False)
if only_downloaded is not None:
filter_kwargs['downloaded_path__isnull'] = not only_downloaded
return Video.objects.filter(*filter_args, **filter_kwargs).order_by(sort_order)

View File

@ -1,111 +0,0 @@
import logging
import os
import subprocess
import sys
import requests
from django.conf import settings as dj_settings
LATEST_URL = "https://yt-dl.org/downloads/latest/youtube-dl"
GITHUB_API_LATEST_RELEASE = "https://api.github.com/repos/ytdl-org/youtube-dl/releases/latest"
log = logging.getLogger("YoutubeDlManager")
class YoutubeDlException(Exception):
pass
class YoutubeDlNotInstalledException(YoutubeDlException):
pass
class YoutubeDlRuntimeException(YoutubeDlException):
pass
class YoutubeDlManager(object):
def __init__(self):
self.verbose = False
self.progress = False
def _get_path(self):
return os.path.join(dj_settings.DATA_DIR, 'youtube-dl')
def _check_installed(self, path):
return os.path.isfile(path) and os.access(path, os.X_OK)
def _get_run_args(self):
run_args = []
if self.verbose:
run_args.append('-v')
if self.progress:
run_args.append('--newline')
else:
run_args.append('--no-progress')
return run_args
def run(self, *args):
path = self._get_path()
if not self._check_installed(path):
log.error("Cannot run youtube-dl, it is not installed!")
raise YoutubeDlNotInstalledException
run_args = self._get_run_args()
ret = subprocess.run([sys.executable, path, *run_args, *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout = ret.stdout.decode('utf-8')
if len(stdout) > 0:
log.info("YoutubeDL: " + stdout)
stderr = ret.stderr.decode('utf-8')
if len(stderr) > 0:
log.error("YoutubeDL: " + stderr)
if ret.returncode != 0:
raise YoutubeDlRuntimeException()
return stdout
def get_installed_version(self):
return self.run('--version')
def get_latest_version(self):
resp = requests.get(GITHUB_API_LATEST_RELEASE, allow_redirects=True)
resp.raise_for_status()
info = resp.json()
return info['tag_name']
def install(self):
# Check if we are running the latest version
latest = self.get_latest_version()
try:
current = self.get_installed_version()
except YoutubeDlNotInstalledException:
current = None
if latest == current:
log.info(f"Running latest youtube-dl version ({current})!")
return
# Download latest
resp = requests.get(LATEST_URL, allow_redirects=True, stream=True)
resp.raise_for_status()
path = self._get_path()
with open(path + ".tmp", "wb") as f:
for chunk in resp.iter_content(10 * 1024):
f.write(chunk)
# Replace
os.unlink(path)
os.rename(path + ".tmp", path)
os.chmod(path, 555)
# Test run
newver = self.get_installed_version()
if current is None:
log.info(f"Installed youtube-dl version {newver}.")
else:
log.info(f"Upgraded youtube-dl from version {current} to {newver}.")