Major refactor of many things.

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

View File

@ -5,7 +5,7 @@ import sys
from django.conf import settings as dj_settings
from YtManagerApp.scheduler.jobs.synchronize_job import SynchronizeJob
from YtManagerApp.services.scheduler.jobs.synchronize_job import SynchronizeJob
from YtManagerApp.services import Services
from django.db.utils import OperationalError
@ -35,8 +35,8 @@ def main():
__initialize_logger()
try:
if Services.appConfig.initialized:
Services.scheduler.initialize()
if Services.appConfig().initialized:
Services.scheduler().initialize()
SynchronizeJob.schedule_global_job()
except OperationalError:
# Settings table is not created when running migrate or makemigrations;

View File

@ -7,8 +7,6 @@ from django.contrib.auth.models import User
from django.db import models
from django.db.models.functions import Lower
from YtManagerApp.utils import youtube
# help_text = user shown text
# verbose_name = user shown name
# null = nullable, blank = user is allowed to set value to empty
@ -101,15 +99,16 @@ class SubscriptionFolder(models.Model):
class Subscription(models.Model):
name = models.CharField(null=False, max_length=1024)
parent_folder = models.ForeignKey(SubscriptionFolder, on_delete=models.CASCADE, null=True, blank=True)
playlist_id = models.CharField(null=False, max_length=128)
description = models.TextField()
channel_id = models.CharField(max_length=128)
channel_name = models.CharField(max_length=1024)
original_url = models.CharField(null=False, max_length=1024)
thumbnail = models.CharField(max_length=1024)
provider = models.CharField(null=False, max_length=64)
provider_id = models.CharField(null=False, max_length=64)
provider_data = models.CharField(null=True, max_length=1024)
parent_folder = models.ForeignKey(SubscriptionFolder, on_delete=models.CASCADE, null=True, blank=True)
user = models.ForeignKey(User, on_delete=models.CASCADE)
# youtube adds videos to the 'Uploads' playlist at the top instead of the bottom
rewrite_playlist_indices = models.BooleanField(null=False, default=False)
# overrides
auto_download = models.BooleanField(null=True, blank=True)
@ -126,81 +125,37 @@ class Subscription(models.Model):
def __repr__(self):
return f'subscription {self.id}, name="{self.name}", playlist_id="{self.playlist_id}"'
def fill_from_playlist(self, info_playlist: youtube.Playlist):
self.name = info_playlist.title
self.playlist_id = info_playlist.id
self.description = info_playlist.description
self.channel_id = info_playlist.channel_id
self.channel_name = info_playlist.channel_title
self.thumbnail = youtube.best_thumbnail(info_playlist).url
def copy_from_channel(self, info_channel: youtube.Channel):
# No point in storing info about the 'uploads from X' playlist
self.name = info_channel.title
self.playlist_id = info_channel.uploads_playlist.id
self.description = info_channel.description
self.channel_id = info_channel.id
self.channel_name = info_channel.title
self.thumbnail = youtube.best_thumbnail(info_channel).url
self.rewrite_playlist_indices = True
def fetch_from_url(self, url, yt_api: youtube.YoutubeAPI):
url_parsed = yt_api.parse_url(url)
if 'playlist' in url_parsed:
info_playlist = yt_api.playlist(url=url)
if info_playlist is None:
raise ValueError('Invalid playlist ID!')
self.fill_from_playlist(info_playlist)
else:
info_channel = yt_api.channel(url=url)
if info_channel is None:
raise ValueError('Cannot find channel!')
self.copy_from_channel(info_channel)
def delete_subscription(self, keep_downloaded_videos: bool):
self.delete()
class Video(models.Model):
video_id = models.CharField(null=False, max_length=12)
name = models.TextField(null=False)
description = models.TextField()
watched = models.BooleanField(default=False, null=False)
new = models.BooleanField(default=True, null=False)
downloaded_path = models.TextField(null=True, blank=True)
subscription = models.ForeignKey(Subscription, on_delete=models.CASCADE)
playlist_index = models.IntegerField(null=False)
publish_date = models.DateTimeField(null=False)
thumbnail = models.TextField()
uploader_name = models.CharField(null=False, max_length=255)
provider_id = models.CharField(null=False, max_length=64)
provider_data = models.CharField(null=True, max_length=1024)
playlist_index = models.IntegerField(null=False)
downloaded_path = models.TextField(null=True, blank=True)
subscription = models.ForeignKey(Subscription, on_delete=models.CASCADE)
watched = models.BooleanField(default=False, null=False)
new = models.BooleanField(default=True, null=False)
views = models.IntegerField(null=False, default=0)
rating = models.FloatField(null=False, default=0.5)
@staticmethod
def create(playlist_item: youtube.PlaylistItem, subscription: Subscription):
video = Video()
video.video_id = playlist_item.resource_video_id
video.name = playlist_item.title
video.description = playlist_item.description
video.watched = False
video.new = True
video.downloaded_path = None
video.subscription = subscription
video.playlist_index = playlist_item.position
video.publish_date = playlist_item.published_at
video.thumbnail = youtube.best_thumbnail(playlist_item).url
video.save()
return video
def mark_watched(self):
self.watched = True
self.save()
if self.downloaded_path is not None:
from YtManagerApp.management.appconfig import appconfig
from YtManagerApp.scheduler.jobs import DeleteVideoJob
from YtManagerApp.scheduler.jobs import SynchronizeJob
from YtManagerApp.management.scheduler.jobs import DeleteVideoJob
from YtManagerApp.management.scheduler.jobs import SynchronizeJob
if appconfig.for_sub(self.subscription, 'automatically_delete_watched'):
DeleteVideoJob.schedule(self)
@ -209,7 +164,7 @@ class Video(models.Model):
def mark_unwatched(self):
self.watched = False
self.save()
from YtManagerApp.scheduler.jobs.synchronize_job import SynchronizeJob
from YtManagerApp.management.scheduler.jobs.synchronize_job import SynchronizeJob
SynchronizeJob.schedule_now_for_subscription(self.subscription)
def get_files(self):
@ -234,9 +189,9 @@ class Video(models.Model):
def delete_files(self):
if self.downloaded_path is not None:
from YtManagerApp.scheduler.jobs import DeleteVideoJob
from YtManagerApp.management.scheduler.jobs import DeleteVideoJob
from YtManagerApp.management.appconfig import appconfig
from YtManagerApp.scheduler.jobs import SynchronizeJob
from YtManagerApp.management.scheduler.jobs import SynchronizeJob
DeleteVideoJob.schedule(self)
@ -247,7 +202,7 @@ class Video(models.Model):
def download(self):
if not self.downloaded_path:
from YtManagerApp.scheduler.jobs.download_video_job import DownloadVideoJob
from YtManagerApp.management.scheduler.jobs import DownloadVideoJob
DownloadVideoJob.schedule(self)
def __str__(self):

View File

@ -1,13 +0,0 @@
import dependency_injector.containers as containers
import dependency_injector.providers as providers
from dynamic_preferences.registries import global_preferences_registry
from YtManagerApp.management.appconfig import AppConfig
from YtManagerApp.management.youtube_dl_manager import YoutubeDlManager
from YtManagerApp.scheduler.scheduler import YtsmScheduler
class Services(containers.DeclarativeContainer):
globalPreferencesRegistry = providers.Object(global_preferences_registry.manager())
appConfig = providers.Singleton(AppConfig, globalPreferencesRegistry)
scheduler = providers.Singleton(YtsmScheduler, appConfig)
youtubeDLManager = providers.Singleton(YoutubeDlManager)

View File

@ -0,0 +1,15 @@
import dependency_injector.containers as di_containers
import dependency_injector.providers as di_providers
from dynamic_preferences.registries import global_preferences_registry
from YtManagerApp.services.appconfig import AppConfig
from YtManagerApp.services.youtube_dl_manager import YoutubeDlManager
from YtManagerApp.services.scheduler.scheduler import YtsmScheduler
from YtManagerApp.services.video_provider_manager import VideoProviderManager
class Services(di_containers.DeclarativeContainer):
globalPreferencesRegistry = di_providers.Object(global_preferences_registry.manager())
appConfig = di_providers.Singleton(AppConfig, globalPreferencesRegistry)
scheduler = di_providers.Singleton(YtsmScheduler, appConfig)
youtubeDLManager = di_providers.Singleton(YoutubeDlManager)
providerManager = di_providers.Singleton(VideoProviderManager)

View File

@ -1,4 +1,4 @@
from YtManagerApp.scheduler.jobs.download_video_job import DownloadVideoJob
from YtManagerApp.services.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

View File

@ -0,0 +1,88 @@
from abc import abstractmethod, ABC
from typing import Iterable, ClassVar
from django import forms
from YtManagerApp.models import Subscription, Video
from YtManagerApp.services.scheduler.progress_tracker import ProgressTracker
class VideoProvider(ABC):
"""
Represents a video hosting service that provides videos and playlists (e.g. YouTube, Vimeo).
Note: the method implementations should be thread safe, as they may be called from multiple jobs running in
parallel.
"""
@abstractmethod
def update_configuration(self, **kwargs):
"""
Updates the configuration options of this video provider.
This method is called first when the provider is registered using the configuration stored in the
database. After that, the method will be called when the user changes any configuration options.
:param kwargs: Configuration arguments
"""
pass
@abstractmethod
def get_display_name(self) -> str:
"""
Returns an user friendly name for this provider.
:return:
"""
pass
@abstractmethod
def get_provider_id(self) -> str:
"""
Returns an identifier that uniquely identifies this provider.
:return:
"""
pass
@abstractmethod
def validate_playlist_url(self, url: str) -> bool:
"""
Validates that the given playlist URL is valid for the given video provider service.
:param url:
:return:
"""
pass
@abstractmethod
def fetch_playlist(self, url: str) -> Subscription:
"""
Gets metadata about the playlist identified by the given URL.
:param url:
:return:
"""
pass
@abstractmethod
def fetch_videos(self, subscription: Subscription) -> Iterable[Video]:
"""
Gets metadata about the videos in the given playlist.
:param subscription:
:return:
"""
pass
@abstractmethod
def update_videos(self, videos: Iterable[Video], progress_tracker: ProgressTracker, update_info: bool = True, update_stats: bool = False):
"""
Updates metadata about given videos.
:param update_info: If set to true, basic information such as title, description will be updated
:param update_stats: If set to true, video statistics (such as rating, view counts) will be updated
:param videos: Videos to be updated.
:param progress_tracker: Used to track the progress of the update process
:return:
"""
pass
@abstractmethod
def get_config_form(self) -> ClassVar[forms.Form]:
"""
Gets the configuration form
:return:
"""
pass

View File

@ -0,0 +1,167 @@
import json
from typing import ClassVar, Iterable, Optional
from django import forms
from external.pytaw.pytaw.utils import iterate_chunks
from external.pytaw.pytaw.youtube import YouTube, Thumbnail, InvalidURL, Resource, Video
from YtManagerApp.models import Video, Subscription
from YtManagerApp.services.providers.video_provider import VideoProvider
from YtManagerApp.services.scheduler.progress_tracker import ProgressTracker
class YouTubeConfigForm(forms.Form):
api_key = forms.CharField(label="YouTube API Key:")
class YouTubeProvider(VideoProvider):
def __init__(self):
self._apiKey: str = None
self._api: YouTube = None
def _sanity_check(self):
if self._apiKey is None:
raise ValueError("The YouTube API key is not set!")
@staticmethod
def _best_thumbnail(resource: Resource) -> Optional[Thumbnail]:
"""
Gets the best thumbnail available for a resource.
:param resource:
:return:
"""
thumbs = getattr(resource, 'thumbnails', None)
if thumbs is None or len(thumbs) <= 0:
return None
return max(thumbs, key=lambda t: t.width * t.height)
def update_configuration(self, **kwargs):
self._apiKey = kwargs.get('apiKey')
self._sanity_check()
self._api = YouTube(key=self._apiKey)
def get_display_name(self) -> str:
return "YouTube API Provider"
def get_provider_id(self) -> str:
return 'youtube'
def validate_playlist_url(self, url: str) -> bool:
try:
parsed_url = self._api.parse_url(url)
except InvalidURL:
return False
is_playlist = 'playlist' in parsed_url
is_channel = parsed_url['type'] in ('channel', 'user', 'channel_custom')
return is_playlist or is_channel
def fetch_playlist(self, url: str) -> Subscription:
if not self.validate_playlist_url(url):
raise ValueError("Invalid playlist or channel URL")
parsed_url = self._api.parse_url(url)
sub = Subscription()
if 'playlist' in parsed_url:
info = self._api.playlist(url=url)
if info is None:
raise ValueError('Invalid playlist ID!')
provider_data = {
'channel_id': None,
'rewrite_indices': False
}
sub.provider_id = info.id
else:
info = self._api.channel(url=url)
if info is None:
raise ValueError('Cannot find channel!')
provider_data = {
'channel_id': info.id,
'rewrite_indices': True
}
sub.provider_id = info.uploads_playlist.id
sub.name = info.title
sub.description = info.description
sub.original_url = url
sub.thumbnail = YouTubeProvider._best_thumbnail(info).url
sub.provider = self.get_provider_id()
sub.provider_data = json.dumps(provider_data)
return sub
def fetch_videos(self, subscription: Subscription) -> Iterable[Video]:
provider_data = json.loads(subscription.provider_data)
playlist_items = self._api.playlist_items(subscription.provider_id)
if provider_data.get('rewrite_indices'):
playlist_items = sorted(playlist_items, key=lambda x: x.published_at)
else:
playlist_items = sorted(playlist_items, key=lambda x: x.position)
i = 1
for playlist_item in playlist_items:
video = Video()
video.name = playlist_item.title
video.description = playlist_item.description
video.publish_date = playlist_item.published_at
video.thumbnail = YouTubeProvider._best_thumbnail(playlist_item).url
video.uploader_name = ""
video.provider_id = playlist_item.resource_video_id
video.provider_data = None
if provider_data.get('rewrite_indices'):
video.playlist_index = i
i += 1
else:
video.playlist_index = playlist_item.position
video.downloaded_path = None
video.subscription = subscription
video.watched = False
video.new = True
yield video
def update_videos(self, videos: Iterable[Video], progress_tracker: ProgressTracker, update_info: bool = True, update_stats: bool = False):
videos_list = list(videos)
progress_tracker.total_steps = len(videos_list)
parts = 'id'
if update_info:
parts += ',snippet'
if update_stats:
parts += ',statistics'
for batch in iterate_chunks(videos_list, 50):
batch_ids = [video.video_id for video in batch]
videos_new = {v.id: v for v in self._api.videos(batch_ids, part=parts)}
for video in batch:
progress_tracker.advance(1, "Updating video " + video.name)
video_new = videos_new.get(video.provider_id)
if video_new is None:
continue
if update_info:
video.name = video_new.title
video.description = video_new.description
if update_stats:
if video_new.n_likes is not None \
and video_new.n_dislikes is not None \
and video_new.n_likes + video_new.n_dislikes > 0:
video.rating = video_new.n_likes / (video_new.n_likes + video_new.n_dislikes)
video.views = video_new.n_views
def get_config_form(self) -> ClassVar[forms.Form]:
return YouTubeConfigForm

View File

@ -1,12 +1,12 @@
import logging
from abc import abstractmethod
from abc import abstractmethod, ABC
from typing import Optional
from YtManagerApp.models import JOB_MESSAGE_LEVELS_MAP, JobMessage
from .progress_tracker import ProgressTracker
class Job(object):
class Job(ABC):
name = 'GenericJob'
"""

View File

@ -1,7 +1,7 @@
import os
from YtManagerApp.models import Video
from YtManagerApp.scheduler.job import Job
from YtManagerApp.services.scheduler.job import Job
class DeleteVideoJob(Job):
@ -28,13 +28,13 @@ class DeleteVideoJob(Job):
except OSError as e:
self.log.error("Failed to delete video %d [%s %s]. Error: %s", self._video.id,
self._video.video_id, self._video.name, e)
self._video.provider_id, self._video.name, e)
self._video.downloaded_path = None
self._video.save()
self.log.info('Deleted video %d successfully! (%d files) [%s %s]', self._video.id, count,
self._video.video_id, self._video.name)
self._video.provider_id, self._video.name)
@staticmethod
def schedule(video: Video):
@ -44,4 +44,4 @@ class DeleteVideoJob(Job):
:return:
"""
from YtManagerApp.services import Services
Services.scheduler.add_job(DeleteVideoJob, args=[video])
Services.scheduler().add_job(DeleteVideoJob, args=[video])

View File

@ -6,7 +6,7 @@ from threading import Lock
import youtube_dl
from YtManagerApp.models import Video
from YtManagerApp.scheduler.job import Job
from YtManagerApp.services.scheduler.job import Job
class DownloadVideoJob(Job):
@ -132,5 +132,5 @@ class DownloadVideoJob(Job):
:param attempt:
:return:
"""
from YtManagerApp.services import Services
from YtManagerApp.management.services import Services
Services.scheduler.add_job(DownloadVideoJob, args=[video, attempt])

View File

@ -6,11 +6,10 @@ from apscheduler.triggers.cron import CronTrigger
from django.db.models import Max
from django.conf import settings
from YtManagerApp.management.downloader import fetch_thumbnail, downloader_process_subscription
from YtManagerApp.services.downloader import fetch_thumbnail, downloader_process_subscription
from YtManagerApp.models import *
from YtManagerApp.scheduler.job import Job
from YtManagerApp.services.scheduler.job import Job
from YtManagerApp.services import Services
from YtManagerApp.utils import youtube
from external.pytaw.pytaw.utils import iterate_chunks
_ENABLE_UPDATE_STATS = True
@ -115,7 +114,7 @@ class SynchronizeJob(Job):
if isinstance(obj, Subscription):
obj.thumbnail = fetch_thumbnail(obj.thumbnail, 'sub', obj.playlist_id, settings.THUMBNAIL_SIZE_SUBSCRIPTION)
elif isinstance(obj, Video):
obj.thumbnail = fetch_thumbnail(obj.thumbnail, 'video', obj.video_id, settings.THUMBNAIL_SIZE_VIDEO)
obj.thumbnail = fetch_thumbnail(obj.thumbnail, 'video', obj.provider_id, settings.THUMBNAIL_SIZE_VIDEO)
obj.save()
def check_video_deleted(self, video: Video):
@ -138,7 +137,7 @@ class SynchronizeJob(Job):
# Video not found, we can safely assume that the video was deleted.
if not found_video:
self.log.info("Video %d was deleted! [%s %s]", video.id, video.video_id, video.name)
self.log.info("Video %d was deleted! [%s %s]", video.id, video.provider_id, video.name)
# Clean up
for file in files:
try:
@ -166,11 +165,11 @@ class SynchronizeJob(Job):
@staticmethod
def schedule_global_job():
trigger = CronTrigger.from_crontab(Services.appConfig.sync_schedule)
trigger = CronTrigger.from_crontab(Services.appConfig().sync_schedule)
if SynchronizeJob.__global_sync_job is None:
trigger = CronTrigger.from_crontab(Services.appConfig.sync_schedule)
SynchronizeJob.__global_sync_job = Services.scheduler.add_job(SynchronizeJob, trigger, max_instances=1, coalesce=True)
trigger = CronTrigger.from_crontab(Services.appConfig().sync_schedule)
SynchronizeJob.__global_sync_job = Services.scheduler().add_job(SynchronizeJob, trigger, max_instances=1, coalesce=True)
else:
SynchronizeJob.__global_sync_job.reschedule(trigger, max_instances=1, coalesce=True)

View File

@ -0,0 +1,18 @@
from YtManagerApp.models import Video
from YtManagerApp.services.scheduler.job import Job
class YouTubeDLUpdateJob(Job):
name = "YouTubeDLUpdateJob"
def __init__(self, job_execution):
super().__init__(job_execution)
def get_description(self):
return f"Updating youtube-dl runtime"
def run(self):
from YtManagerApp.services import Services
self.set_total_steps(1)
Services.youtubeDLManager.install()
self.progress_advance(1)

View File

@ -6,11 +6,13 @@ from typing import Type, Union, Optional
import pytz
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.base import BaseTrigger
from apscheduler.triggers.interval import IntervalTrigger
from django.contrib.auth.models import User
from YtManagerApp.management.appconfig import AppConfig
from YtManagerApp.services.appconfig import AppConfig
from YtManagerApp.models import JobExecution, JOB_STATES_MAP
from YtManagerApp.scheduler.job import Job
from YtManagerApp.services.scheduler.job import Job
from YtManagerApp.services.scheduler.jobs.youtubedl_update_job import YouTubeDLUpdateJob
class YtsmScheduler(object):
@ -27,6 +29,10 @@ class YtsmScheduler(object):
self._configure_scheduler()
self._ap_scheduler.start()
self._schedule_main_jobs()
def _schedule_main_jobs(self):
self.add_job(YouTubeDLUpdateJob, trigger=IntervalTrigger(days=1))
def _configure_scheduler(self):
logger = logging.getLogger('scheduler')

View File

@ -0,0 +1,11 @@
from YtManagerApp.services.providers.video_provider import VideoProvider
class VideoProviderManager(object):
def __init__(self):
self._providers: Dict[str, VideoProvider] = {}
def register_provider(self, provider: VideoProvider):
pid = provider.get_provider_id()
self._providers[pid] = provider

View File

@ -28,12 +28,10 @@ class YoutubeDlManager(object):
def __init__(self):
self.verbose = False
self.progress = False
self._install_path = os.path.join(dj_settings.DATA_DIR, 'youtube-dl')
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 _check_installed(self):
return os.path.isfile(self._install_path) and os.access(self._install_path, os.X_OK)
def _get_run_args(self):
run_args = []
@ -47,13 +45,12 @@ class YoutubeDlManager(object):
return run_args
def run(self, *args):
path = self._get_path()
if not self._check_installed(path):
if not self._check_installed():
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)
ret = subprocess.run([sys.executable, self._install_path, *run_args, *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout = ret.stdout.decode('utf-8')
if len(stdout) > 0:
@ -93,15 +90,14 @@ class YoutubeDlManager(object):
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:
with open(self._install_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)
os.unlink(self._install_path)
os.rename(self._install_path + ".tmp", self._install_path)
os.chmod(self._install_path, 555)
# Test run
newver = self.get_installed_version()

View File

@ -1,49 +0,0 @@
from django.conf import settings
from external.pytaw.pytaw.youtube import YouTube, Channel, Playlist, PlaylistItem, Thumbnail, InvalidURL, Resource, Video
from typing import Optional
class YoutubeAPI(YouTube):
@staticmethod
def build_public() -> 'YoutubeAPI':
from YtManagerApp.management.appconfig import appconfig
return YoutubeAPI(key=appconfig.youtube_api_key)
# @staticmethod
# def build_oauth() -> 'YoutubeAPI':
# flow =
# credentials =
# service = build(API_SERVICE_NAME, API_VERSION, credentials)
def default_thumbnail(resource: Resource) -> Optional[Thumbnail]:
"""
Gets the default thumbnail for a resource.
Searches in the list of thumbnails for one with the label 'default', or takes the first one.
:param resource:
:return:
"""
thumbs = getattr(resource, 'thumbnails', None)
if thumbs is None or len(thumbs) <= 0:
return None
return next(
(i for i in thumbs if i.id == 'default'),
thumbs[0]
)
def best_thumbnail(resource: Resource) -> Optional[Thumbnail]:
"""
Gets the best thumbnail available for a resource.
:param resource:
:return:
"""
thumbs = getattr(resource, 'thumbnails', None)
if thumbs is None or len(thumbs) <= 0:
return None
return max(thumbs, key=lambda t: t.width * t.height)

View File

@ -3,7 +3,7 @@ from django.http import JsonResponse
from django.views.generic import View
from YtManagerApp.models import Video
from YtManagerApp.scheduler.jobs.synchronize_job import SynchronizeJob
from YtManagerApp.services.scheduler.jobs.synchronize_job import SynchronizeJob
class SyncNowView(LoginRequiredMixin, View):

View File

@ -36,7 +36,7 @@ class RegisterView(FormView):
return context
def post(self, request, *args, **kwargs):
if not Services.appConfig.allow_registrations:
if not Services.appConfig().allow_registrations:
return HttpResponseForbidden("Registrations are disabled!")
return super().post(request, *args, **kwargs)

View File

@ -8,7 +8,7 @@ from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.views.generic import FormView
from YtManagerApp.scheduler.jobs.synchronize_job import SynchronizeJob
from YtManagerApp.services.scheduler.jobs.synchronize_job import SynchronizeJob
from YtManagerApp.services import Services
from YtManagerApp.views.forms.first_time import WelcomeForm, ApiKeyForm, PickAdminUserForm, ServerConfigForm, DoneForm, \
UserCreationForm, LoginForm
@ -24,7 +24,7 @@ class WizardStepMixin:
def get(self, request, *args, **kwargs):
# Prevent access if application is already initialized
if Services.appConfig.initialized:
if Services.appConfig().initialized:
logger.debug(f"Attempted to access {request.path}, but first time setup already run. Redirected to home "
f"page.")
return redirect('home')
@ -32,7 +32,7 @@ class WizardStepMixin:
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
if Services.appConfig.initialized:
if Services.appConfig().initialized:
logger.debug(f"Attempted to post {request.path}, but first time setup already run.")
return HttpResponseForbidden()
return super().post(request, *args, **kwargs)
@ -65,14 +65,14 @@ class Step1ApiKeyView(WizardStepMixin, FormView):
def get_initial(self):
initial = super().get_initial()
initial['api_key'] = Services.appConfig.youtube_api_key
initial['api_key'] = Services.appConfig().youtube_api_key
return initial
def form_valid(self, form):
key = form.cleaned_data['api_key']
# TODO: validate key
if key is not None and len(key) > 0:
Services.appConfig.youtube_api_key = key
Services.appConfig().youtube_api_key = key
return super().form_valid(form)
@ -149,8 +149,8 @@ class Step3ConfigureView(WizardStepMixin, FormView):
def get_initial(self):
initial = super().get_initial()
initial['allow_registrations'] = Services.appConfig.allow_registrations
initial['sync_schedule'] = Services.appConfig.sync_schedule
initial['allow_registrations'] = Services.appConfig().allow_registrations
initial['sync_schedule'] = Services.appConfig().sync_schedule
initial['auto_download'] = self.request.user.preferences['auto_download']
initial['download_location'] = self.request.user.preferences['download_path']
return initial
@ -158,11 +158,11 @@ class Step3ConfigureView(WizardStepMixin, FormView):
def form_valid(self, form):
allow_registrations = form.cleaned_data['allow_registrations']
if allow_registrations is not None:
Services.appConfig.allow_registrations = allow_registrations
Services.appConfig().allow_registrations = allow_registrations
sync_schedule = form.cleaned_data['sync_schedule']
if sync_schedule is not None and len(sync_schedule) > 0:
Services.appConfig.sync_schedule = sync_schedule
Services.appConfig().sync_schedule = sync_schedule
auto_download = form.cleaned_data['auto_download']
if auto_download is not None:
@ -173,7 +173,7 @@ class Step3ConfigureView(WizardStepMixin, FormView):
self.request.user.preferences['download_path'] = download_location
# Set initialized to true
Services.appConfig.initialized = True
Services.appConfig().initialized = True
# Start scheduler if not started
Services.scheduler.initialize()

View File

@ -7,7 +7,7 @@ from YtManagerApp.dynamic_preferences_registry import MarkDeletedAsWatched, Auto
DownloadPath, DownloadFilePattern, DownloadFormat, DownloadSubtitles, DownloadAutogeneratedSubtitles, \
DownloadAllSubtitles, DownloadSubtitlesLangs, DownloadSubtitlesFormat
from YtManagerApp.models import VIDEO_ORDER_CHOICES
from YtManagerApp.services import Services
from YtManagerApp.management.services import Services
class SettingsForm(forms.Form):

View File

@ -12,7 +12,7 @@ from django.conf import settings
from django.core.paginator import Paginator
from YtManagerApp.management.videos import get_videos
from YtManagerApp.models import Subscription, SubscriptionFolder, VIDEO_ORDER_CHOICES, VIDEO_ORDER_MAPPING
from YtManagerApp.services import Services
from YtManagerApp.management.services import Services
from YtManagerApp.utils import youtube, subscription_file_parser
from YtManagerApp.views.controls.modal import ModalMixin

View File

@ -3,7 +3,7 @@ from django.http import HttpResponseForbidden
from django.urls import reverse_lazy
from django.views.generic import FormView
from YtManagerApp.scheduler.jobs.synchronize_job import SynchronizeJob
from YtManagerApp.management.scheduler.jobs.synchronize_job import SynchronizeJob
from YtManagerApp.views.forms.settings import SettingsForm, AdminSettingsForm