mirror of
https://github.com/chibicitiberiu/ytsm.git
synced 2024-02-24 05:43:31 +00:00
Major refactor of many things.
This commit is contained in:
parent
fd5d05232f
commit
6b843f1fc2
@ -5,7 +5,7 @@ import sys
|
|||||||
|
|
||||||
from django.conf import settings as dj_settings
|
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 YtManagerApp.services import Services
|
||||||
from django.db.utils import OperationalError
|
from django.db.utils import OperationalError
|
||||||
|
|
||||||
@ -35,8 +35,8 @@ def main():
|
|||||||
__initialize_logger()
|
__initialize_logger()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if Services.appConfig.initialized:
|
if Services.appConfig().initialized:
|
||||||
Services.scheduler.initialize()
|
Services.scheduler().initialize()
|
||||||
SynchronizeJob.schedule_global_job()
|
SynchronizeJob.schedule_global_job()
|
||||||
except OperationalError:
|
except OperationalError:
|
||||||
# Settings table is not created when running migrate or makemigrations;
|
# Settings table is not created when running migrate or makemigrations;
|
||||||
|
@ -7,8 +7,6 @@ from django.contrib.auth.models import User
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.functions import Lower
|
from django.db.models.functions import Lower
|
||||||
|
|
||||||
from YtManagerApp.utils import youtube
|
|
||||||
|
|
||||||
# help_text = user shown text
|
# help_text = user shown text
|
||||||
# verbose_name = user shown name
|
# verbose_name = user shown name
|
||||||
# null = nullable, blank = user is allowed to set value to empty
|
# null = nullable, blank = user is allowed to set value to empty
|
||||||
@ -101,15 +99,16 @@ class SubscriptionFolder(models.Model):
|
|||||||
|
|
||||||
class Subscription(models.Model):
|
class Subscription(models.Model):
|
||||||
name = models.CharField(null=False, max_length=1024)
|
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()
|
description = models.TextField()
|
||||||
channel_id = models.CharField(max_length=128)
|
original_url = models.CharField(null=False, max_length=1024)
|
||||||
channel_name = models.CharField(max_length=1024)
|
|
||||||
thumbnail = models.CharField(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)
|
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
|
# overrides
|
||||||
auto_download = models.BooleanField(null=True, blank=True)
|
auto_download = models.BooleanField(null=True, blank=True)
|
||||||
@ -126,81 +125,37 @@ class Subscription(models.Model):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f'subscription {self.id}, name="{self.name}", playlist_id="{self.playlist_id}"'
|
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):
|
def delete_subscription(self, keep_downloaded_videos: bool):
|
||||||
self.delete()
|
self.delete()
|
||||||
|
|
||||||
|
|
||||||
class Video(models.Model):
|
class Video(models.Model):
|
||||||
video_id = models.CharField(null=False, max_length=12)
|
|
||||||
name = models.TextField(null=False)
|
name = models.TextField(null=False)
|
||||||
description = models.TextField()
|
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)
|
publish_date = models.DateTimeField(null=False)
|
||||||
thumbnail = models.TextField()
|
thumbnail = models.TextField()
|
||||||
uploader_name = models.CharField(null=False, max_length=255)
|
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)
|
views = models.IntegerField(null=False, default=0)
|
||||||
rating = models.FloatField(null=False, default=0.5)
|
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):
|
def mark_watched(self):
|
||||||
self.watched = True
|
self.watched = True
|
||||||
self.save()
|
self.save()
|
||||||
if self.downloaded_path is not None:
|
if self.downloaded_path is not None:
|
||||||
from YtManagerApp.management.appconfig import appconfig
|
from YtManagerApp.management.appconfig import appconfig
|
||||||
from YtManagerApp.scheduler.jobs import DeleteVideoJob
|
from YtManagerApp.management.scheduler.jobs import DeleteVideoJob
|
||||||
from YtManagerApp.scheduler.jobs import SynchronizeJob
|
from YtManagerApp.management.scheduler.jobs import SynchronizeJob
|
||||||
|
|
||||||
if appconfig.for_sub(self.subscription, 'automatically_delete_watched'):
|
if appconfig.for_sub(self.subscription, 'automatically_delete_watched'):
|
||||||
DeleteVideoJob.schedule(self)
|
DeleteVideoJob.schedule(self)
|
||||||
@ -209,7 +164,7 @@ class Video(models.Model):
|
|||||||
def mark_unwatched(self):
|
def mark_unwatched(self):
|
||||||
self.watched = False
|
self.watched = False
|
||||||
self.save()
|
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)
|
SynchronizeJob.schedule_now_for_subscription(self.subscription)
|
||||||
|
|
||||||
def get_files(self):
|
def get_files(self):
|
||||||
@ -234,9 +189,9 @@ class Video(models.Model):
|
|||||||
|
|
||||||
def delete_files(self):
|
def delete_files(self):
|
||||||
if self.downloaded_path is not None:
|
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.management.appconfig import appconfig
|
||||||
from YtManagerApp.scheduler.jobs import SynchronizeJob
|
from YtManagerApp.management.scheduler.jobs import SynchronizeJob
|
||||||
|
|
||||||
DeleteVideoJob.schedule(self)
|
DeleteVideoJob.schedule(self)
|
||||||
|
|
||||||
@ -247,7 +202,7 @@ class Video(models.Model):
|
|||||||
|
|
||||||
def download(self):
|
def download(self):
|
||||||
if not self.downloaded_path:
|
if not self.downloaded_path:
|
||||||
from YtManagerApp.scheduler.jobs.download_video_job import DownloadVideoJob
|
from YtManagerApp.management.scheduler.jobs import DownloadVideoJob
|
||||||
DownloadVideoJob.schedule(self)
|
DownloadVideoJob.schedule(self)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -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)
|
|
15
app/YtManagerApp/services/__init__.py
Normal file
15
app/YtManagerApp/services/__init__.py
Normal 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)
|
@ -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.models import Video, Subscription, VIDEO_ORDER_MAPPING
|
||||||
from YtManagerApp.utils import first_non_null
|
from YtManagerApp.utils import first_non_null
|
||||||
from django.conf import settings as srv_settings
|
from django.conf import settings as srv_settings
|
88
app/YtManagerApp/services/providers/video_provider.py
Normal file
88
app/YtManagerApp/services/providers/video_provider.py
Normal 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
|
167
app/YtManagerApp/services/providers/youtube_provider.py
Normal file
167
app/YtManagerApp/services/providers/youtube_provider.py
Normal 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
|
@ -1,12 +1,12 @@
|
|||||||
import logging
|
import logging
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod, ABC
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from YtManagerApp.models import JOB_MESSAGE_LEVELS_MAP, JobMessage
|
from YtManagerApp.models import JOB_MESSAGE_LEVELS_MAP, JobMessage
|
||||||
from .progress_tracker import ProgressTracker
|
from .progress_tracker import ProgressTracker
|
||||||
|
|
||||||
|
|
||||||
class Job(object):
|
class Job(ABC):
|
||||||
name = 'GenericJob'
|
name = 'GenericJob'
|
||||||
|
|
||||||
"""
|
"""
|
@ -1,7 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from YtManagerApp.models import Video
|
from YtManagerApp.models import Video
|
||||||
from YtManagerApp.scheduler.job import Job
|
from YtManagerApp.services.scheduler.job import Job
|
||||||
|
|
||||||
|
|
||||||
class DeleteVideoJob(Job):
|
class DeleteVideoJob(Job):
|
||||||
@ -28,13 +28,13 @@ class DeleteVideoJob(Job):
|
|||||||
|
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
self.log.error("Failed to delete video %d [%s %s]. Error: %s", self._video.id,
|
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.downloaded_path = None
|
||||||
self._video.save()
|
self._video.save()
|
||||||
|
|
||||||
self.log.info('Deleted video %d successfully! (%d files) [%s %s]', self._video.id, count,
|
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
|
@staticmethod
|
||||||
def schedule(video: Video):
|
def schedule(video: Video):
|
||||||
@ -44,4 +44,4 @@ class DeleteVideoJob(Job):
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
from YtManagerApp.services import Services
|
from YtManagerApp.services import Services
|
||||||
Services.scheduler.add_job(DeleteVideoJob, args=[video])
|
Services.scheduler().add_job(DeleteVideoJob, args=[video])
|
@ -6,7 +6,7 @@ from threading import Lock
|
|||||||
import youtube_dl
|
import youtube_dl
|
||||||
|
|
||||||
from YtManagerApp.models import Video
|
from YtManagerApp.models import Video
|
||||||
from YtManagerApp.scheduler.job import Job
|
from YtManagerApp.services.scheduler.job import Job
|
||||||
|
|
||||||
|
|
||||||
class DownloadVideoJob(Job):
|
class DownloadVideoJob(Job):
|
||||||
@ -132,5 +132,5 @@ class DownloadVideoJob(Job):
|
|||||||
:param attempt:
|
:param attempt:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
from YtManagerApp.services import Services
|
from YtManagerApp.management.services import Services
|
||||||
Services.scheduler.add_job(DownloadVideoJob, args=[video, attempt])
|
Services.scheduler.add_job(DownloadVideoJob, args=[video, attempt])
|
@ -6,11 +6,10 @@ from apscheduler.triggers.cron import CronTrigger
|
|||||||
from django.db.models import Max
|
from django.db.models import Max
|
||||||
from django.conf import settings
|
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.models import *
|
||||||
from YtManagerApp.scheduler.job import Job
|
from YtManagerApp.services.scheduler.job import Job
|
||||||
from YtManagerApp.services import Services
|
from YtManagerApp.services import Services
|
||||||
from YtManagerApp.utils import youtube
|
|
||||||
from external.pytaw.pytaw.utils import iterate_chunks
|
from external.pytaw.pytaw.utils import iterate_chunks
|
||||||
|
|
||||||
_ENABLE_UPDATE_STATS = True
|
_ENABLE_UPDATE_STATS = True
|
||||||
@ -115,7 +114,7 @@ class SynchronizeJob(Job):
|
|||||||
if isinstance(obj, Subscription):
|
if isinstance(obj, Subscription):
|
||||||
obj.thumbnail = fetch_thumbnail(obj.thumbnail, 'sub', obj.playlist_id, settings.THUMBNAIL_SIZE_SUBSCRIPTION)
|
obj.thumbnail = fetch_thumbnail(obj.thumbnail, 'sub', obj.playlist_id, settings.THUMBNAIL_SIZE_SUBSCRIPTION)
|
||||||
elif isinstance(obj, Video):
|
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()
|
obj.save()
|
||||||
|
|
||||||
def check_video_deleted(self, video: Video):
|
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.
|
# Video not found, we can safely assume that the video was deleted.
|
||||||
if not found_video:
|
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
|
# Clean up
|
||||||
for file in files:
|
for file in files:
|
||||||
try:
|
try:
|
||||||
@ -166,11 +165,11 @@ class SynchronizeJob(Job):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def schedule_global_job():
|
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:
|
if SynchronizeJob.__global_sync_job is None:
|
||||||
trigger = CronTrigger.from_crontab(Services.appConfig.sync_schedule)
|
trigger = CronTrigger.from_crontab(Services.appConfig().sync_schedule)
|
||||||
SynchronizeJob.__global_sync_job = Services.scheduler.add_job(SynchronizeJob, trigger, max_instances=1, coalesce=True)
|
SynchronizeJob.__global_sync_job = Services.scheduler().add_job(SynchronizeJob, trigger, max_instances=1, coalesce=True)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
SynchronizeJob.__global_sync_job.reschedule(trigger, max_instances=1, coalesce=True)
|
SynchronizeJob.__global_sync_job.reschedule(trigger, max_instances=1, coalesce=True)
|
@ -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)
|
@ -6,11 +6,13 @@ from typing import Type, Union, Optional
|
|||||||
import pytz
|
import pytz
|
||||||
from apscheduler.schedulers.background import BackgroundScheduler
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
from apscheduler.triggers.base import BaseTrigger
|
from apscheduler.triggers.base import BaseTrigger
|
||||||
|
from apscheduler.triggers.interval import IntervalTrigger
|
||||||
from django.contrib.auth.models import User
|
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.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):
|
class YtsmScheduler(object):
|
||||||
@ -27,6 +29,10 @@ class YtsmScheduler(object):
|
|||||||
|
|
||||||
self._configure_scheduler()
|
self._configure_scheduler()
|
||||||
self._ap_scheduler.start()
|
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):
|
def _configure_scheduler(self):
|
||||||
logger = logging.getLogger('scheduler')
|
logger = logging.getLogger('scheduler')
|
11
app/YtManagerApp/services/video_provider_manager.py
Normal file
11
app/YtManagerApp/services/video_provider_manager.py
Normal 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
|
@ -28,12 +28,10 @@ class YoutubeDlManager(object):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.verbose = False
|
self.verbose = False
|
||||||
self.progress = False
|
self.progress = False
|
||||||
|
self._install_path = os.path.join(dj_settings.DATA_DIR, 'youtube-dl')
|
||||||
|
|
||||||
def _get_path(self):
|
def _check_installed(self):
|
||||||
return os.path.join(dj_settings.DATA_DIR, 'youtube-dl')
|
return os.path.isfile(self._install_path) and os.access(self._install_path, os.X_OK)
|
||||||
|
|
||||||
def _check_installed(self, path):
|
|
||||||
return os.path.isfile(path) and os.access(path, os.X_OK)
|
|
||||||
|
|
||||||
def _get_run_args(self):
|
def _get_run_args(self):
|
||||||
run_args = []
|
run_args = []
|
||||||
@ -47,13 +45,12 @@ class YoutubeDlManager(object):
|
|||||||
return run_args
|
return run_args
|
||||||
|
|
||||||
def run(self, *args):
|
def run(self, *args):
|
||||||
path = self._get_path()
|
if not self._check_installed():
|
||||||
if not self._check_installed(path):
|
|
||||||
log.error("Cannot run youtube-dl, it is not installed!")
|
log.error("Cannot run youtube-dl, it is not installed!")
|
||||||
raise YoutubeDlNotInstalledException
|
raise YoutubeDlNotInstalledException
|
||||||
|
|
||||||
run_args = self._get_run_args()
|
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')
|
stdout = ret.stdout.decode('utf-8')
|
||||||
if len(stdout) > 0:
|
if len(stdout) > 0:
|
||||||
@ -93,15 +90,14 @@ class YoutubeDlManager(object):
|
|||||||
resp = requests.get(LATEST_URL, allow_redirects=True, stream=True)
|
resp = requests.get(LATEST_URL, allow_redirects=True, stream=True)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
|
|
||||||
path = self._get_path()
|
with open(self._install_path + ".tmp", "wb") as f:
|
||||||
with open(path + ".tmp", "wb") as f:
|
|
||||||
for chunk in resp.iter_content(10 * 1024):
|
for chunk in resp.iter_content(10 * 1024):
|
||||||
f.write(chunk)
|
f.write(chunk)
|
||||||
|
|
||||||
# Replace
|
# Replace
|
||||||
os.unlink(path)
|
os.unlink(self._install_path)
|
||||||
os.rename(path + ".tmp", path)
|
os.rename(self._install_path + ".tmp", self._install_path)
|
||||||
os.chmod(path, 555)
|
os.chmod(self._install_path, 555)
|
||||||
|
|
||||||
# Test run
|
# Test run
|
||||||
newver = self.get_installed_version()
|
newver = self.get_installed_version()
|
@ -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)
|
|
@ -3,7 +3,7 @@ from django.http import JsonResponse
|
|||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from YtManagerApp.models import Video
|
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):
|
class SyncNowView(LoginRequiredMixin, View):
|
||||||
|
@ -36,7 +36,7 @@ class RegisterView(FormView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
if not Services.appConfig.allow_registrations:
|
if not Services.appConfig().allow_registrations:
|
||||||
return HttpResponseForbidden("Registrations are disabled!")
|
return HttpResponseForbidden("Registrations are disabled!")
|
||||||
|
|
||||||
return super().post(request, *args, **kwargs)
|
return super().post(request, *args, **kwargs)
|
||||||
|
@ -8,7 +8,7 @@ from django.shortcuts import redirect
|
|||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.views.generic import FormView
|
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.services import Services
|
||||||
from YtManagerApp.views.forms.first_time import WelcomeForm, ApiKeyForm, PickAdminUserForm, ServerConfigForm, DoneForm, \
|
from YtManagerApp.views.forms.first_time import WelcomeForm, ApiKeyForm, PickAdminUserForm, ServerConfigForm, DoneForm, \
|
||||||
UserCreationForm, LoginForm
|
UserCreationForm, LoginForm
|
||||||
@ -24,7 +24,7 @@ class WizardStepMixin:
|
|||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
|
|
||||||
# Prevent access if application is already initialized
|
# 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 "
|
logger.debug(f"Attempted to access {request.path}, but first time setup already run. Redirected to home "
|
||||||
f"page.")
|
f"page.")
|
||||||
return redirect('home')
|
return redirect('home')
|
||||||
@ -32,7 +32,7 @@ class WizardStepMixin:
|
|||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
def post(self, 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.")
|
logger.debug(f"Attempted to post {request.path}, but first time setup already run.")
|
||||||
return HttpResponseForbidden()
|
return HttpResponseForbidden()
|
||||||
return super().post(request, *args, **kwargs)
|
return super().post(request, *args, **kwargs)
|
||||||
@ -65,14 +65,14 @@ class Step1ApiKeyView(WizardStepMixin, FormView):
|
|||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
initial = super().get_initial()
|
initial = super().get_initial()
|
||||||
initial['api_key'] = Services.appConfig.youtube_api_key
|
initial['api_key'] = Services.appConfig().youtube_api_key
|
||||||
return initial
|
return initial
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
key = form.cleaned_data['api_key']
|
key = form.cleaned_data['api_key']
|
||||||
# TODO: validate key
|
# TODO: validate key
|
||||||
if key is not None and len(key) > 0:
|
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)
|
return super().form_valid(form)
|
||||||
|
|
||||||
@ -149,8 +149,8 @@ class Step3ConfigureView(WizardStepMixin, FormView):
|
|||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
initial = super().get_initial()
|
initial = super().get_initial()
|
||||||
initial['allow_registrations'] = Services.appConfig.allow_registrations
|
initial['allow_registrations'] = Services.appConfig().allow_registrations
|
||||||
initial['sync_schedule'] = Services.appConfig.sync_schedule
|
initial['sync_schedule'] = Services.appConfig().sync_schedule
|
||||||
initial['auto_download'] = self.request.user.preferences['auto_download']
|
initial['auto_download'] = self.request.user.preferences['auto_download']
|
||||||
initial['download_location'] = self.request.user.preferences['download_path']
|
initial['download_location'] = self.request.user.preferences['download_path']
|
||||||
return initial
|
return initial
|
||||||
@ -158,11 +158,11 @@ class Step3ConfigureView(WizardStepMixin, FormView):
|
|||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
allow_registrations = form.cleaned_data['allow_registrations']
|
allow_registrations = form.cleaned_data['allow_registrations']
|
||||||
if allow_registrations is not None:
|
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']
|
sync_schedule = form.cleaned_data['sync_schedule']
|
||||||
if sync_schedule is not None and len(sync_schedule) > 0:
|
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']
|
auto_download = form.cleaned_data['auto_download']
|
||||||
if auto_download is not None:
|
if auto_download is not None:
|
||||||
@ -173,7 +173,7 @@ class Step3ConfigureView(WizardStepMixin, FormView):
|
|||||||
self.request.user.preferences['download_path'] = download_location
|
self.request.user.preferences['download_path'] = download_location
|
||||||
|
|
||||||
# Set initialized to true
|
# Set initialized to true
|
||||||
Services.appConfig.initialized = True
|
Services.appConfig().initialized = True
|
||||||
|
|
||||||
# Start scheduler if not started
|
# Start scheduler if not started
|
||||||
Services.scheduler.initialize()
|
Services.scheduler.initialize()
|
||||||
|
@ -7,7 +7,7 @@ from YtManagerApp.dynamic_preferences_registry import MarkDeletedAsWatched, Auto
|
|||||||
DownloadPath, DownloadFilePattern, DownloadFormat, DownloadSubtitles, DownloadAutogeneratedSubtitles, \
|
DownloadPath, DownloadFilePattern, DownloadFormat, DownloadSubtitles, DownloadAutogeneratedSubtitles, \
|
||||||
DownloadAllSubtitles, DownloadSubtitlesLangs, DownloadSubtitlesFormat
|
DownloadAllSubtitles, DownloadSubtitlesLangs, DownloadSubtitlesFormat
|
||||||
from YtManagerApp.models import VIDEO_ORDER_CHOICES
|
from YtManagerApp.models import VIDEO_ORDER_CHOICES
|
||||||
from YtManagerApp.services import Services
|
from YtManagerApp.management.services import Services
|
||||||
|
|
||||||
|
|
||||||
class SettingsForm(forms.Form):
|
class SettingsForm(forms.Form):
|
||||||
|
@ -12,7 +12,7 @@ from django.conf import settings
|
|||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from YtManagerApp.management.videos import get_videos
|
from YtManagerApp.management.videos import get_videos
|
||||||
from YtManagerApp.models import Subscription, SubscriptionFolder, VIDEO_ORDER_CHOICES, VIDEO_ORDER_MAPPING
|
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.utils import youtube, subscription_file_parser
|
||||||
from YtManagerApp.views.controls.modal import ModalMixin
|
from YtManagerApp.views.controls.modal import ModalMixin
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ from django.http import HttpResponseForbidden
|
|||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.views.generic import FormView
|
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
|
from YtManagerApp.views.forms.settings import SettingsForm, AdminSettingsForm
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user