mirror of
				https://github.com/chibicitiberiu/ytsm.git
				synced 2024-02-24 05:43:31 +00:00 
			
		
		
		
	Integrated django-dynamic-preferences library into project. Settings are now stored in the database, the ini file only contains a minimal number of settings.
This commit is contained in:
		@@ -23,17 +23,17 @@ SESSION_COOKIE_AGE = 3600 * 30      # one month
 | 
			
		||||
# Application definition
 | 
			
		||||
 | 
			
		||||
INSTALLED_APPS = [
 | 
			
		||||
    'django.contrib.auth',
 | 
			
		||||
    'dynamic_preferences',
 | 
			
		||||
    'dynamic_preferences.users.apps.UserPreferencesConfig',
 | 
			
		||||
    'YtManagerApp.apps.YtManagerAppConfig',
 | 
			
		||||
    'crispy_forms',
 | 
			
		||||
    'django.contrib.admin',
 | 
			
		||||
    'django.contrib.auth',
 | 
			
		||||
    'django.contrib.contenttypes',
 | 
			
		||||
    'django.contrib.sessions',
 | 
			
		||||
    'django.contrib.messages',
 | 
			
		||||
    'django.contrib.staticfiles',
 | 
			
		||||
    'django.contrib.humanize',
 | 
			
		||||
    'dynamic_preferences',
 | 
			
		||||
    'dynamic_preferences.users.apps.UserPreferencesConfig',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
MIDDLEWARE = [
 | 
			
		||||
@@ -125,16 +125,14 @@ LOG_FORMAT = '%(asctime)s|%(process)d|%(thread)d|%(name)s|%(filename)s|%(lineno)
 | 
			
		||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
 | 
			
		||||
PROJECT_ROOT = up(up(os.path.dirname(__file__)))            # Project root
 | 
			
		||||
BASE_DIR = up(os.path.dirname(__file__))                    # Base dir of the application
 | 
			
		||||
CONFIG_DIR = os.path.join(PROJECT_ROOT, "config")
 | 
			
		||||
DATA_DIR = os.path.join(PROJECT_ROOT, "data")
 | 
			
		||||
 | 
			
		||||
CONFIG_DIR = os.getenv("YTSM_CONFIG_DIR", os.path.join(PROJECT_ROOT, "config"))
 | 
			
		||||
DATA_DIR = os.getenv("YTSM_DATA_DIR", os.path.join(PROJECT_ROOT, "data"))
 | 
			
		||||
os.chdir(DATA_DIR)
 | 
			
		||||
 | 
			
		||||
STATIC_ROOT = os.path.join(PROJECT_ROOT, "static")
 | 
			
		||||
MEDIA_ROOT = os.path.join(DATA_DIR, 'media')
 | 
			
		||||
 | 
			
		||||
_DEFAULT_CONFIG_FILE = os.path.join(CONFIG_DIR, 'config.ini')
 | 
			
		||||
_DEFAULT_LOG_FILE = os.path.join(DATA_DIR, 'log.log')
 | 
			
		||||
_DEFAULT_MEDIA_ROOT = os.path.join(DATA_DIR, 'media')
 | 
			
		||||
 | 
			
		||||
CONFIG_FILE = os.getenv('YTSM_CONFIG_FILE', _DEFAULT_CONFIG_FILE)
 | 
			
		||||
DATA_CONFIG_FILE = os.path.join(DATA_DIR, 'config.ini')
 | 
			
		||||
 | 
			
		||||
#
 | 
			
		||||
# Defaults
 | 
			
		||||
@@ -142,7 +140,6 @@ DATA_CONFIG_FILE = os.path.join(DATA_DIR, 'config.ini')
 | 
			
		||||
_DEFAULT_DEBUG = False
 | 
			
		||||
 | 
			
		||||
_DEFAULT_SECRET_KEY = '^zv8@i2h!ko2lo=%ivq(9e#x=%q*i^^)6#4@(juzdx%&0c+9a0'
 | 
			
		||||
_DEFAULT_YOUTUBE_API_KEY = 'AIzaSyBabzE4Bup77WexdLMa9rN9z-wJidEfNX8'
 | 
			
		||||
 | 
			
		||||
_DEFAULT_DATABASE = {
 | 
			
		||||
        'ENGINE': 'django.db.backends.sqlite3',
 | 
			
		||||
@@ -153,10 +150,6 @@ _DEFAULT_DATABASE = {
 | 
			
		||||
        'PORT': None,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
_SCHEDULER_SYNC_SCHEDULE = '5 * * * *'
 | 
			
		||||
_DEFAULT_SCHEDULER_CONCURRENCY = 1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_ERRORS = []
 | 
			
		||||
CONFIG_WARNINGS = []
 | 
			
		||||
 | 
			
		||||
@@ -171,6 +164,8 @@ def get_global_opt(name, cfgparser, env_variable=None, fallback=None, boolean=Fa
 | 
			
		||||
    2. config parser
 | 
			
		||||
    3. fallback
 | 
			
		||||
 | 
			
		||||
    :param integer:
 | 
			
		||||
    :param cfgparser:
 | 
			
		||||
    :param name:
 | 
			
		||||
    :param env_variable:
 | 
			
		||||
    :param fallback:
 | 
			
		||||
@@ -216,25 +211,22 @@ def load_config_ini():
 | 
			
		||||
    import dj_database_url
 | 
			
		||||
 | 
			
		||||
    cfg = ConfigParser(allow_no_value=True, interpolation=ExtendedInterpolatorWithEnv())
 | 
			
		||||
    read_ok = cfg.read([DEFAULTS_FILE, CONFIG_FILE, DATA_CONFIG_FILE])
 | 
			
		||||
 | 
			
		||||
    if DEFAULTS_FILE not in read_ok:
 | 
			
		||||
        CONFIG_ERRORS.append(f'File {DEFAULTS_FILE} could not be read! Please make sure the file is in the right place,'
 | 
			
		||||
                             f' and it has read permissions.')
 | 
			
		||||
    cfg_file = os.path.join(CONFIG_DIR, "config.ini")
 | 
			
		||||
    read_ok = cfg.read([cfg_file])
 | 
			
		||||
 | 
			
		||||
    if cfg_file not in read_ok:
 | 
			
		||||
        CONFIG_ERRORS.append(f'Configuration file {cfg_file} could not be read! Please make sure the file is in the '
 | 
			
		||||
                             'right place, and it has read permissions.')
 | 
			
		||||
 | 
			
		||||
    # Debug
 | 
			
		||||
    global DEBUG
 | 
			
		||||
    DEBUG = get_global_opt('Debug', cfg, env_variable='YTSM_DEBUG', fallback=_DEFAULT_DEBUG, boolean=True)
 | 
			
		||||
 | 
			
		||||
    # Media root, which is where thumbnails are stored
 | 
			
		||||
    global MEDIA_ROOT
 | 
			
		||||
    MEDIA_ROOT = get_global_opt('MediaRoot', cfg, env_variable='YTSM_MEDIA_ROOT', fallback=_DEFAULT_MEDIA_ROOT)
 | 
			
		||||
 | 
			
		||||
    # Keys - secret key, youtube API key
 | 
			
		||||
    # Secret key
 | 
			
		||||
    # SECURITY WARNING: keep the secret key used in production secret!
 | 
			
		||||
    global SECRET_KEY, YOUTUBE_API_KEY
 | 
			
		||||
    global SECRET_KEY
 | 
			
		||||
    SECRET_KEY = get_global_opt('SecretKey', cfg, env_variable='YTSM_SECRET_KEY', fallback=_DEFAULT_SECRET_KEY)
 | 
			
		||||
    YOUTUBE_API_KEY = get_global_opt('YoutubeApiKey', cfg, env_variable='YTSM_YTAPI_KEY', fallback=_DEFAULT_YOUTUBE_API_KEY)
 | 
			
		||||
 | 
			
		||||
    # Database
 | 
			
		||||
    global DATABASES
 | 
			
		||||
@@ -247,16 +239,22 @@ def load_config_ini():
 | 
			
		||||
 | 
			
		||||
    else:
 | 
			
		||||
        DATABASES['default'] = {
 | 
			
		||||
            'ENGINE': get_global_opt('DatabaseEngine', cfg, env_variable='YTSM_DB_ENGINE', fallback=_DEFAULT_DATABASE['ENGINE']),
 | 
			
		||||
            'NAME': get_global_opt('DatabaseName', cfg, env_variable='YTSM_DB_NAME', fallback=_DEFAULT_DATABASE['NAME']),
 | 
			
		||||
            'HOST': get_global_opt('DatabaseHost', cfg, env_variable='YTSM_DB_HOST', fallback=_DEFAULT_DATABASE['HOST']),
 | 
			
		||||
            'USER': get_global_opt('DatabaseUser', cfg, env_variable='YTSM_DB_USER', fallback=_DEFAULT_DATABASE['USER']),
 | 
			
		||||
            'PASSWORD': get_global_opt('DatabasePassword', cfg, env_variable='YTSM_DB_PASSWORD', fallback=_DEFAULT_DATABASE['PASSWORD']),
 | 
			
		||||
            'PORT': get_global_opt('DatabasePort', cfg, env_variable='YTSM_DB_PORT', fallback=_DEFAULT_DATABASE['PORT']),
 | 
			
		||||
            'ENGINE': get_global_opt('DatabaseEngine', cfg,
 | 
			
		||||
                                     env_variable='YTSM_DB_ENGINE', fallback=_DEFAULT_DATABASE['ENGINE']),
 | 
			
		||||
            'NAME': get_global_opt('DatabaseName', cfg,
 | 
			
		||||
                                   env_variable='YTSM_DB_NAME', fallback=_DEFAULT_DATABASE['NAME']),
 | 
			
		||||
            'HOST': get_global_opt('DatabaseHost', cfg,
 | 
			
		||||
                                   env_variable='YTSM_DB_HOST', fallback=_DEFAULT_DATABASE['HOST']),
 | 
			
		||||
            'USER': get_global_opt('DatabaseUser', cfg,
 | 
			
		||||
                                   env_variable='YTSM_DB_USER', fallback=_DEFAULT_DATABASE['USER']),
 | 
			
		||||
            'PASSWORD': get_global_opt('DatabasePassword', cfg,
 | 
			
		||||
                                       env_variable='YTSM_DB_PASSWORD', fallback=_DEFAULT_DATABASE['PASSWORD']),
 | 
			
		||||
            'PORT': get_global_opt('DatabasePort', cfg,
 | 
			
		||||
                                   env_variable='YTSM_DB_PORT', fallback=_DEFAULT_DATABASE['PORT']),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    # Log settings
 | 
			
		||||
    global LOG_LEVEL, LOG_FILE
 | 
			
		||||
    global LOG_LEVEL
 | 
			
		||||
    log_level_str = get_global_opt('LogLevel', cfg, env_variable='YTSM_LOG_LEVEL', fallback='INFO')
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
@@ -267,12 +265,5 @@ def load_config_ini():
 | 
			
		||||
        print("Invalid log level " + LOG_LEVEL)
 | 
			
		||||
        LOG_LEVEL = logging.INFO
 | 
			
		||||
 | 
			
		||||
    LOG_FILE = get_global_opt('LogFile', cfg, env_variable='YTSM_LOG_FILE', fallback=_DEFAULT_LOG_FILE)
 | 
			
		||||
 | 
			
		||||
    # Scheduler settings
 | 
			
		||||
    global SCHEDULER_SYNC_SCHEDULE, SCHEDULER_CONCURRENCY
 | 
			
		||||
    SCHEDULER_SYNC_SCHEDULE = get_global_opt('SynchronizationSchedule', cfg, fallback=_SCHEDULER_SYNC_SCHEDULE)
 | 
			
		||||
    SCHEDULER_CONCURRENCY = get_global_opt('SchedulerConcurrency', cfg, fallback=_DEFAULT_SCHEDULER_CONCURRENCY, integer=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
load_config_ini()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,112 +0,0 @@
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
import os.path
 | 
			
		||||
from collections import ChainMap
 | 
			
		||||
from configparser import ConfigParser
 | 
			
		||||
from shutil import copyfile
 | 
			
		||||
from typing import Optional, Any
 | 
			
		||||
 | 
			
		||||
from django.conf import settings as dj_settings
 | 
			
		||||
from django.contrib.auth.models import User
 | 
			
		||||
 | 
			
		||||
from .models import UserSettings, Subscription
 | 
			
		||||
from .utils.extended_interpolation_with_env import ExtendedInterpolatorWithEnv
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AppSettings(ConfigParser):
 | 
			
		||||
    _DEFAULT_INTERPOLATION = ExtendedInterpolatorWithEnv()
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(allow_no_value=True, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def initialize(self):
 | 
			
		||||
        self.read([dj_settings.DEFAULTS_FILE, dj_settings.CONFIG_FILE])
 | 
			
		||||
 | 
			
		||||
    def save(self):
 | 
			
		||||
        if os.path.exists(dj_settings.CONFIG_FILE):
 | 
			
		||||
            # Create a backup
 | 
			
		||||
            copyfile(dj_settings.CONFIG_FILE, dj_settings.CONFIG_FILE + ".backup")
 | 
			
		||||
        else:
 | 
			
		||||
            # Ensure directory exists
 | 
			
		||||
            settings_dir = os.path.dirname(dj_settings.CONFIG_FILE)
 | 
			
		||||
            os.makedirs(settings_dir, exist_ok=True)
 | 
			
		||||
 | 
			
		||||
        with open(dj_settings.CONFIG_FILE, 'w') as f:
 | 
			
		||||
            self.write(f)
 | 
			
		||||
 | 
			
		||||
    def __get_combined_dict(self, vars: Optional[Any], sub: Optional[Subscription], user: Optional[User]) -> ChainMap:
 | 
			
		||||
        vars_dict = {}
 | 
			
		||||
        sub_overloads_dict = {}
 | 
			
		||||
        user_settings_dict = {}
 | 
			
		||||
 | 
			
		||||
        if vars is not None:
 | 
			
		||||
            vars_dict = vars
 | 
			
		||||
 | 
			
		||||
        if sub is not None:
 | 
			
		||||
            sub_overloads_dict = sub.get_overloads_dict()
 | 
			
		||||
 | 
			
		||||
        if user is not None:
 | 
			
		||||
            user_settings = UserSettings.find_by_user(user)
 | 
			
		||||
            if user_settings is not None:
 | 
			
		||||
                user_settings_dict = user_settings.to_dict()
 | 
			
		||||
 | 
			
		||||
        return ChainMap(vars_dict, sub_overloads_dict, user_settings_dict)
 | 
			
		||||
 | 
			
		||||
    def get_user(self, user: User, section: str, option: Any, vars=None, fallback=object()) -> str:
 | 
			
		||||
        return super().get(section, option,
 | 
			
		||||
                           fallback=fallback,
 | 
			
		||||
                           vars=self.__get_combined_dict(vars, None, user))
 | 
			
		||||
 | 
			
		||||
    def getboolean_user(self, user: User, section: str, option: Any, vars=None, fallback=object()) -> bool:
 | 
			
		||||
        return super().getboolean(section, option,
 | 
			
		||||
                                  fallback=fallback,
 | 
			
		||||
                                  vars=self.__get_combined_dict(vars, None, user))
 | 
			
		||||
 | 
			
		||||
    def getint_user(self, user: User, section: str, option: Any, vars=None, fallback=object()) -> int:
 | 
			
		||||
        return super().getint(section, option,
 | 
			
		||||
                              fallback=fallback,
 | 
			
		||||
                              vars=self.__get_combined_dict(vars, None, user))
 | 
			
		||||
 | 
			
		||||
    def getfloat_user(self, user: User, section: str, option: Any, vars=None, fallback=object()) -> float:
 | 
			
		||||
        return super().getfloat(section, option,
 | 
			
		||||
                                fallback=fallback,
 | 
			
		||||
                                vars=self.__get_combined_dict(vars, None, user))
 | 
			
		||||
 | 
			
		||||
    def get_sub(self, sub: Subscription, section: str, option: Any, vars=None, fallback=object()) -> str:
 | 
			
		||||
        return super().get(section, option,
 | 
			
		||||
                           fallback=fallback,
 | 
			
		||||
                           vars=self.__get_combined_dict(vars, sub, sub.user))
 | 
			
		||||
 | 
			
		||||
    def getboolean_sub(self, sub: Subscription, section: str, option: Any, vars=None, fallback=object()) -> bool:
 | 
			
		||||
        return super().getboolean(section, option,
 | 
			
		||||
                                  fallback=fallback,
 | 
			
		||||
                                  vars=self.__get_combined_dict(vars, sub, sub.user))
 | 
			
		||||
 | 
			
		||||
    def getint_sub(self, sub: Subscription, section: str, option: Any, vars=None, fallback=object()) -> int:
 | 
			
		||||
        return super().getint(section, option,
 | 
			
		||||
                              fallback=fallback,
 | 
			
		||||
                              vars=self.__get_combined_dict(vars, sub, sub.user))
 | 
			
		||||
 | 
			
		||||
    def getfloat_sub(self, sub: Subscription, section: str, option: Any, vars=None, fallback=object()) -> float:
 | 
			
		||||
        return super().getfloat(section, option,
 | 
			
		||||
                                fallback=fallback,
 | 
			
		||||
                                vars=self.__get_combined_dict(vars, sub, sub.user))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
settings = AppSettings()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def initialize_app_config():
 | 
			
		||||
    settings.initialize()
 | 
			
		||||
    __initialize_logger()
 | 
			
		||||
    logging.info('Application started!')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def __initialize_logger():
 | 
			
		||||
    log_dir = os.path.dirname(dj_settings.LOG_FILE)
 | 
			
		||||
    os.makedirs(log_dir, exist_ok=True)
 | 
			
		||||
 | 
			
		||||
    logging.basicConfig(
 | 
			
		||||
        filename=dj_settings.LOG_FILE,
 | 
			
		||||
        level=dj_settings.LOG_LEVEL,
 | 
			
		||||
        format=dj_settings.LOG_FORMAT)
 | 
			
		||||
@@ -1,11 +1,36 @@
 | 
			
		||||
from .appconfig import initialize_app_config
 | 
			
		||||
from .scheduler import initialize_scheduler
 | 
			
		||||
from .management.jobs.synchronize import schedule_synchronize_global
 | 
			
		||||
import logging
 | 
			
		||||
import logging.handlers
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
from django.conf import settings as dj_settings
 | 
			
		||||
 | 
			
		||||
from .management.appconfig import global_prefs
 | 
			
		||||
from .management.jobs.synchronize import schedule_synchronize_global
 | 
			
		||||
from .scheduler import initialize_scheduler
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def __initialize_logger():
 | 
			
		||||
    log_dir = os.path.join(dj_settings.DATA_DIR, 'logs')
 | 
			
		||||
    os.makedirs(log_dir, exist_ok=True)
 | 
			
		||||
 | 
			
		||||
    file_handler = logging.handlers.RotatingFileHandler(
 | 
			
		||||
        os.path.join(log_dir, "log.log"),
 | 
			
		||||
        maxBytes=1024 * 1024,
 | 
			
		||||
        backupCount=5
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    logging.basicConfig(
 | 
			
		||||
        level=dj_settings.LOG_LEVEL,
 | 
			
		||||
        format=dj_settings.LOG_FORMAT,
 | 
			
		||||
        handlers=[file_handler]
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
    initialize_app_config()
 | 
			
		||||
    initialize_scheduler()
 | 
			
		||||
    schedule_synchronize_global()
 | 
			
		||||
    __initialize_logger()
 | 
			
		||||
 | 
			
		||||
    if global_prefs['hidden__initialized']:
 | 
			
		||||
        initialize_scheduler()
 | 
			
		||||
        schedule_synchronize_global()
 | 
			
		||||
 | 
			
		||||
    logging.info('Initialization complete.')
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
from django.apps import AppConfig
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class YtManagerAppConfig(AppConfig):
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ scheduler = Section('scheduler')
 | 
			
		||||
manager = Section('manager')
 | 
			
		||||
downloader = Section('downloader')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Hidden settings
 | 
			
		||||
@global_preferences_registry.register
 | 
			
		||||
class Initialized(BooleanPreference):
 | 
			
		||||
@@ -20,6 +21,7 @@ class Initialized(BooleanPreference):
 | 
			
		||||
    name = 'initialized'
 | 
			
		||||
    default = False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# General settings
 | 
			
		||||
@global_preferences_registry.register
 | 
			
		||||
class YouTubeAPIKey(StringPreference):
 | 
			
		||||
@@ -28,13 +30,31 @@ class YouTubeAPIKey(StringPreference):
 | 
			
		||||
    default = 'AIzaSyBabzE4Bup77WexdLMa9rN9z-wJidEfNX8'
 | 
			
		||||
    required = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@global_preferences_registry.register
 | 
			
		||||
class AllowCDN(BooleanPreference):
 | 
			
		||||
    section = general
 | 
			
		||||
    name = 'allow_cdn'
 | 
			
		||||
    default = True
 | 
			
		||||
    required = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@global_preferences_registry.register
 | 
			
		||||
class AllowRegistrations(BooleanPreference):
 | 
			
		||||
    section = general
 | 
			
		||||
    name = 'allow_registrations'
 | 
			
		||||
    default = True
 | 
			
		||||
    required = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@global_preferences_registry.register
 | 
			
		||||
class SyncSchedule(StringPreference):
 | 
			
		||||
    section = scheduler
 | 
			
		||||
    name = 'synchronization_schedule'
 | 
			
		||||
    default = '5 * * * *' # hourly
 | 
			
		||||
    default = '5 * * * *'  # hourly
 | 
			
		||||
    required = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@global_preferences_registry.register
 | 
			
		||||
class SchedulerConcurrency(IntegerPreference):
 | 
			
		||||
    section = scheduler
 | 
			
		||||
@@ -42,8 +62,8 @@ class SchedulerConcurrency(IntegerPreference):
 | 
			
		||||
    default = 2
 | 
			
		||||
    required = True
 | 
			
		||||
 | 
			
		||||
# User settings
 | 
			
		||||
 | 
			
		||||
# User settings
 | 
			
		||||
@user_preferences_registry.register
 | 
			
		||||
class MarkDeletedAsWatched(BooleanPreference):
 | 
			
		||||
    section = manager
 | 
			
		||||
@@ -51,6 +71,7 @@ class MarkDeletedAsWatched(BooleanPreference):
 | 
			
		||||
    default = True
 | 
			
		||||
    required = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@user_preferences_registry.register
 | 
			
		||||
class AutoDeleteWatched(BooleanPreference):
 | 
			
		||||
    section = manager
 | 
			
		||||
@@ -58,20 +79,23 @@ class AutoDeleteWatched(BooleanPreference):
 | 
			
		||||
    default = True
 | 
			
		||||
    required = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@user_preferences_registry.register
 | 
			
		||||
class AutoDownloadEnabled(BooleanPreference):
 | 
			
		||||
    section = downloader
 | 
			
		||||
    name = 'enabled'
 | 
			
		||||
    name = 'download_enabled'
 | 
			
		||||
    default = True
 | 
			
		||||
    required = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@user_preferences_registry.register
 | 
			
		||||
class DownloadGlobalLimit(IntegerPreference):
 | 
			
		||||
    section = downloader
 | 
			
		||||
    name = 'global_limit'
 | 
			
		||||
    name = 'download_global_limit'
 | 
			
		||||
    default = None
 | 
			
		||||
    required = False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@user_preferences_registry.register
 | 
			
		||||
class DownloadGlobalSizeLimit(IntegerPreference):
 | 
			
		||||
    section = downloader
 | 
			
		||||
@@ -79,49 +103,56 @@ class DownloadGlobalSizeLimit(IntegerPreference):
 | 
			
		||||
    default = None
 | 
			
		||||
    required = False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@user_preferences_registry.register
 | 
			
		||||
class DownloadSubscriptionLimit(IntegerPreference):
 | 
			
		||||
    section = downloader
 | 
			
		||||
    name = 'limit_per_subscription'
 | 
			
		||||
    name = 'download_limit_per_subscription'
 | 
			
		||||
    default = 5
 | 
			
		||||
    required = False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@user_preferences_registry.register
 | 
			
		||||
class DownloadMaxAttempts(IntegerPreference):
 | 
			
		||||
    section = downloader
 | 
			
		||||
    name = 'max_attempts'
 | 
			
		||||
    name = 'download_max_attempts'
 | 
			
		||||
    default = 3
 | 
			
		||||
    required = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@user_preferences_registry.register
 | 
			
		||||
class DownloadOrder(ChoicePreference):
 | 
			
		||||
    section = downloader
 | 
			
		||||
    name = 'order'
 | 
			
		||||
    name = 'download_order'
 | 
			
		||||
    choices = VIDEO_ORDER_CHOICES
 | 
			
		||||
    default = 'playlist'
 | 
			
		||||
    required = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@user_preferences_registry.register
 | 
			
		||||
class DownloadPath(StringPreference):
 | 
			
		||||
    section = downloader
 | 
			
		||||
    name = 'path'
 | 
			
		||||
    name = 'download_path'
 | 
			
		||||
    default = None
 | 
			
		||||
    required = False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@user_preferences_registry.register
 | 
			
		||||
class DownloadFilePattern(StringPreference):
 | 
			
		||||
    section = downloader
 | 
			
		||||
    name = 'file_pattern'
 | 
			
		||||
    name = 'download_file_pattern'
 | 
			
		||||
    default = '${channel}/${playlist}/S01E${playlist_index} - ${title} [${id}]'
 | 
			
		||||
    required = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@user_preferences_registry.register
 | 
			
		||||
class DownloadFormat(StringPreference):
 | 
			
		||||
    section = downloader
 | 
			
		||||
    name = 'format'
 | 
			
		||||
    name = 'download_format'
 | 
			
		||||
    default = 'bestvideo+bestaudio'
 | 
			
		||||
    required = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@user_preferences_registry.register
 | 
			
		||||
class DownloadSubtitles(BooleanPreference):
 | 
			
		||||
    section = downloader
 | 
			
		||||
@@ -129,6 +160,7 @@ class DownloadSubtitles(BooleanPreference):
 | 
			
		||||
    default = True
 | 
			
		||||
    required = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@user_preferences_registry.register
 | 
			
		||||
class DownloadAutogeneratedSubtitles(BooleanPreference):
 | 
			
		||||
    section = downloader
 | 
			
		||||
@@ -136,6 +168,7 @@ class DownloadAutogeneratedSubtitles(BooleanPreference):
 | 
			
		||||
    default = False
 | 
			
		||||
    required = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@user_preferences_registry.register
 | 
			
		||||
class DownloadAllSubtitles(BooleanPreference):
 | 
			
		||||
    section = downloader
 | 
			
		||||
@@ -143,6 +176,7 @@ class DownloadAllSubtitles(BooleanPreference):
 | 
			
		||||
    default = False
 | 
			
		||||
    required = False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@user_preferences_registry.register
 | 
			
		||||
class DownloadSubtitlesLangs(StringPreference):
 | 
			
		||||
    section = downloader
 | 
			
		||||
@@ -150,9 +184,10 @@ class DownloadSubtitlesLangs(StringPreference):
 | 
			
		||||
    default = 'en,ro'
 | 
			
		||||
    required = False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@user_preferences_registry.register
 | 
			
		||||
class DownloadSubtitlesFormat(StringPreference):
 | 
			
		||||
    section = downloader
 | 
			
		||||
    name = 'subtitles_format'
 | 
			
		||||
    default = False
 | 
			
		||||
    required = False
 | 
			
		||||
    required = False
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								app/YtManagerApp/management/appconfig.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								app/YtManagerApp/management/appconfig.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
from dynamic_preferences.registries import global_preferences_registry
 | 
			
		||||
 | 
			
		||||
global_prefs = global_preferences_registry.manager()
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
from YtManagerApp.appconfig import settings
 | 
			
		||||
from YtManagerApp.management.jobs.download_video import schedule_download_video
 | 
			
		||||
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
 | 
			
		||||
@@ -12,17 +12,12 @@ log = logging.getLogger('downloader')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def __get_subscription_config(sub: Subscription):
 | 
			
		||||
    enabled = settings.getboolean_sub(sub, 'user', 'AutoDownload')
 | 
			
		||||
    user = sub.user
 | 
			
		||||
 | 
			
		||||
    global_limit = -1
 | 
			
		||||
    if len(settings.get_sub(sub, 'user', 'DownloadGlobalLimit')) > 0:
 | 
			
		||||
        global_limit = settings.getint_sub(sub, 'user', 'DownloadGlobalLimit')
 | 
			
		||||
 | 
			
		||||
    limit = -1
 | 
			
		||||
    if len(settings.get_sub(sub, 'user', 'DownloadSubscriptionLimit')) > 0:
 | 
			
		||||
        limit = settings.getint_sub(sub, 'user', 'DownloadSubscriptionLimit')
 | 
			
		||||
 | 
			
		||||
    order = settings.get_sub(sub, 'user', 'DownloadOrder')
 | 
			
		||||
    enabled = first_non_null(sub.auto_download, user.preferences['download_enabled'])
 | 
			
		||||
    global_limit = user.preferences['download_global_limit']
 | 
			
		||||
    limit = first_non_null(sub.download_limit, user.preferences['download_limit_per_subscription'])
 | 
			
		||||
    order = first_non_null(sub.download_order, user.preferences['download_order'])
 | 
			
		||||
    order = VIDEO_ORDER_MAPPING[order]
 | 
			
		||||
 | 
			
		||||
    return enabled, global_limit, limit, order
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,14 @@
 | 
			
		||||
from YtManagerApp.models import Video
 | 
			
		||||
from YtManagerApp import scheduler
 | 
			
		||||
from YtManagerApp.appconfig import settings
 | 
			
		||||
import os
 | 
			
		||||
import youtube_dl
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
from string import Template
 | 
			
		||||
from threading import Lock
 | 
			
		||||
 | 
			
		||||
import youtube_dl
 | 
			
		||||
 | 
			
		||||
from YtManagerApp import scheduler
 | 
			
		||||
from YtManagerApp.models import Video
 | 
			
		||||
 | 
			
		||||
log = logging.getLogger('video_downloader')
 | 
			
		||||
log_youtube_dl = log.getChild('youtube_dl')
 | 
			
		||||
 | 
			
		||||
@@ -25,9 +27,8 @@ def __get_valid_path(path):
 | 
			
		||||
    return value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def __build_youtube_dl_params(video: Video):
 | 
			
		||||
    # resolve path
 | 
			
		||||
    pattern_dict = {
 | 
			
		||||
def __build_template_dict(video: Video):
 | 
			
		||||
    return {
 | 
			
		||||
        'channel': video.subscription.channel_name,
 | 
			
		||||
        'channel_id': video.subscription.channel_id,
 | 
			
		||||
        'playlist': video.subscription.name,
 | 
			
		||||
@@ -37,22 +38,30 @@ def __build_youtube_dl_params(video: Video):
 | 
			
		||||
        'id': video.video_id,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    download_path = settings.get_sub(video.subscription, 'user', 'DownloadPath')
 | 
			
		||||
    output_pattern = __get_valid_path(settings.get_sub(
 | 
			
		||||
        video.subscription, 'user', 'DownloadFilePattern', vars=pattern_dict))
 | 
			
		||||
 | 
			
		||||
def __build_youtube_dl_params(video: Video):
 | 
			
		||||
 | 
			
		||||
    sub = video.subscription
 | 
			
		||||
    user = sub.user
 | 
			
		||||
 | 
			
		||||
    # resolve path
 | 
			
		||||
    download_path = user.preferences['download_path']
 | 
			
		||||
 | 
			
		||||
    template_dict = __build_template_dict(video)
 | 
			
		||||
    output_pattern = Template(user.preferences['download_file_pattern']).safe_substitute(template_dict)
 | 
			
		||||
 | 
			
		||||
    output_path = os.path.join(download_path, output_pattern)
 | 
			
		||||
    output_path = os.path.normpath(output_path)
 | 
			
		||||
 | 
			
		||||
    youtube_dl_params = {
 | 
			
		||||
        'logger': log_youtube_dl,
 | 
			
		||||
        'format': settings.get_sub(video.subscription, 'user', 'DownloadFormat'),
 | 
			
		||||
        'format': user.preferences['download_format'],
 | 
			
		||||
        'outtmpl': output_path,
 | 
			
		||||
        'writethumbnail': True,
 | 
			
		||||
        'writedescription': True,
 | 
			
		||||
        'writesubtitles': settings.getboolean_sub(video.subscription, 'user', 'DownloadSubtitles'),
 | 
			
		||||
        'writeautomaticsub': settings.getboolean_sub(video.subscription, 'user', 'DownloadAutogeneratedSubtitles'),
 | 
			
		||||
        'allsubtitles': settings.getboolean_sub(video.subscription, 'user', 'DownloadSubtitlesAll'),
 | 
			
		||||
        'writesubtitles': user.preferences['subtitles_enabled'],
 | 
			
		||||
        'writeautomaticsub': user.preferences['autogenerated_subtitles'],
 | 
			
		||||
        'allsubtitles': user.preferences['all_subtitles'],
 | 
			
		||||
        'postprocessors': [
 | 
			
		||||
            {
 | 
			
		||||
                'key': 'FFmpegMetadata'
 | 
			
		||||
@@ -60,12 +69,12 @@ def __build_youtube_dl_params(video: Video):
 | 
			
		||||
        ]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    sub_langs = settings.get_sub(video.subscription, 'user', 'DownloadSubtitlesLangs').split(',')
 | 
			
		||||
    sub_langs = user.preferences['subtitles_langs'].split(',')
 | 
			
		||||
    sub_langs = [i.strip() for i in sub_langs]
 | 
			
		||||
    if len(sub_langs) > 0:
 | 
			
		||||
        youtube_dl_params['subtitleslangs'] = sub_langs
 | 
			
		||||
 | 
			
		||||
    sub_format = settings.get_sub(video.subscription, 'user', 'DownloadSubtitlesFormat')
 | 
			
		||||
    sub_format = user.preferences['subtitles_format']
 | 
			
		||||
    if len(sub_format) > 0:
 | 
			
		||||
        youtube_dl_params['subtitlesformat'] = sub_format
 | 
			
		||||
 | 
			
		||||
@@ -74,6 +83,8 @@ def __build_youtube_dl_params(video: Video):
 | 
			
		||||
 | 
			
		||||
def download_video(video: Video, attempt: int = 1):
 | 
			
		||||
 | 
			
		||||
    user = video.subscription.user
 | 
			
		||||
 | 
			
		||||
    log.info('Downloading video %d [%s %s]', video.id, video.video_id, video.name)
 | 
			
		||||
 | 
			
		||||
    # Issue: if multiple videos are downloaded at the same time, a race condition appears in the mkdirs() call that
 | 
			
		||||
@@ -82,7 +93,7 @@ def download_video(video: Video, attempt: int = 1):
 | 
			
		||||
    _lock.acquire()
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        max_attempts = settings.getint_sub(video.subscription, 'user', 'DownloadMaxAttempts', fallback=3)
 | 
			
		||||
        max_attempts = user.preferences['download_max_attempts']
 | 
			
		||||
 | 
			
		||||
        youtube_dl_params, output_path = __build_youtube_dl_params(video)
 | 
			
		||||
        with youtube_dl.YoutubeDL(youtube_dl_params) as yt:
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ from threading import Lock
 | 
			
		||||
from apscheduler.triggers.cron import CronTrigger
 | 
			
		||||
 | 
			
		||||
from YtManagerApp import scheduler
 | 
			
		||||
from YtManagerApp.appconfig import settings
 | 
			
		||||
from YtManagerApp.management.appconfig import global_prefs
 | 
			
		||||
from YtManagerApp.management.downloader import fetch_thumbnail, downloader_process_all, downloader_process_subscription
 | 
			
		||||
from YtManagerApp.models import *
 | 
			
		||||
from YtManagerApp.utils import youtube
 | 
			
		||||
@@ -43,6 +43,8 @@ def __check_new_videos_sub(subscription: Subscription, yt_api: youtube.YoutubeAP
 | 
			
		||||
 | 
			
		||||
def __detect_deleted(subscription: Subscription):
 | 
			
		||||
 | 
			
		||||
    user = subscription.user
 | 
			
		||||
 | 
			
		||||
    for video in Video.objects.filter(subscription=subscription, downloaded_path__isnull=False):
 | 
			
		||||
        found_video = False
 | 
			
		||||
        files = []
 | 
			
		||||
@@ -71,7 +73,7 @@ def __detect_deleted(subscription: Subscription):
 | 
			
		||||
            video.downloaded_path = None
 | 
			
		||||
 | 
			
		||||
            # Mark watched?
 | 
			
		||||
            if settings.getboolean_sub(subscription, 'user', 'MarkDeletedAsWatched'):
 | 
			
		||||
            if user.preferences['MarkDeletedAsWatched']:
 | 
			
		||||
                video.watched = True
 | 
			
		||||
 | 
			
		||||
            video.save()
 | 
			
		||||
@@ -147,7 +149,7 @@ def synchronize_subscription(subscription: Subscription):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def schedule_synchronize_global():
 | 
			
		||||
    trigger = CronTrigger.from_crontab(settings.get('global', 'SynchronizationSchedule'))
 | 
			
		||||
    trigger = CronTrigger.from_crontab(global_prefs['synchronization_schedule'])
 | 
			
		||||
    job = scheduler.scheduler.add_job(synchronize, trigger, max_instances=1, coalesce=True)
 | 
			
		||||
    log.info('Scheduled synchronize job job=%s', job.id)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
 | 
			
		||||
def first_non_null(*iterable):
 | 
			
		||||
    '''
 | 
			
		||||
    Returns the first element from the iterable which is not None.
 | 
			
		||||
    If all the elements are 'None', 'None' is returned.
 | 
			
		||||
    :param iterable: Iterable containing list of elements.
 | 
			
		||||
    :return: First non-null element, or None if all elements are 'None'.
 | 
			
		||||
    '''
 | 
			
		||||
    return next((item for item in iterable if item is not None), None)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,87 +0,0 @@
 | 
			
		||||
; Use ${env:environment_variable} to use the value of an environment variable.
 | 
			
		||||
; The global section contains settings that apply to the entire server
 | 
			
		||||
[global]
 | 
			
		||||
 | 
			
		||||
; Controls whether django debug mode is enabled. Should be false in production.
 | 
			
		||||
Debug=False
 | 
			
		||||
 | 
			
		||||
; This is the folder where thumbnails will be downloaded. By default project_root/data/media is used.
 | 
			
		||||
;MediaRoot=
 | 
			
		||||
 | 
			
		||||
; Secret key - django secret key
 | 
			
		||||
SecretKey=^zv8@i2h!ko2lo=%ivq(9e#x=%q*i^^)6#4@(juzdx%&0c+9a0
 | 
			
		||||
 | 
			
		||||
; YouTube API key - get this from your user account
 | 
			
		||||
YoutubeApiKey=AIzaSyBabzE4Bup77WexdLMa9rN9z-wJidEfNX8
 | 
			
		||||
 | 
			
		||||
; Database settings
 | 
			
		||||
; You can use any database engine supported by Django, as long as you add the required dependencies.
 | 
			
		||||
; Built-in engines: https://docs.djangoproject.com/en/2.1/ref/settings/#std:setting-DATABASE-ENGINE
 | 
			
		||||
; Others databases might be supported by installing the corect pip package.
 | 
			
		||||
 | 
			
		||||
;DatabaseEngine=django.db.backends.sqlite3
 | 
			
		||||
;DatabaseName=data/ytmanager.db
 | 
			
		||||
;DatabaseHost=
 | 
			
		||||
;DatabaseUser=
 | 
			
		||||
;DatabasePassword=
 | 
			
		||||
;DatabasePort=
 | 
			
		||||
 | 
			
		||||
; Database one-liner. If set, it will override any other Database* setting.
 | 
			
		||||
; Documentation: https://github.com/kennethreitz/dj-database-url
 | 
			
		||||
;DatabaseURL=sqlite:////full/path/to/your/database/file.sqlite
 | 
			
		||||
 | 
			
		||||
; Log settings, sets the log file location and the log level
 | 
			
		||||
LogLevel=INFO
 | 
			
		||||
; LogFile=data/log.log
 | 
			
		||||
 | 
			
		||||
; Specifies the synchronization schedule, in crontab format.
 | 
			
		||||
; Format: <minute> <hour> <day-of-month> <month-of-year> <day of week>
 | 
			
		||||
SynchronizationSchedule=5 * * * *
 | 
			
		||||
 | 
			
		||||
; Number of threads running the scheduler
 | 
			
		||||
; Since most of the jobs scheduled are downloads, there is no advantage to having
 | 
			
		||||
; a higher concurrency
 | 
			
		||||
SchedulerConcurrency=3
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
; Default user settings
 | 
			
		||||
[user]
 | 
			
		||||
; When a video is deleted on the system, it will be marked as 'watched'
 | 
			
		||||
MarkDeletedAsWatched=True
 | 
			
		||||
 | 
			
		||||
; Videos marked as watched are automatically deleted
 | 
			
		||||
DeleteWatched=True
 | 
			
		||||
 | 
			
		||||
; Enable automatic downloading
 | 
			
		||||
AutoDownload=True
 | 
			
		||||
 | 
			
		||||
; Limit the total number of videos downloaded (-1 or empty = no limit)
 | 
			
		||||
DownloadGlobalLimit=
 | 
			
		||||
 | 
			
		||||
; Limit the numbers of videos per subscription (-1 or empty = no limit)
 | 
			
		||||
DownloadSubscriptionLimit=5
 | 
			
		||||
 | 
			
		||||
; Number of download attempts
 | 
			
		||||
DownloadMaxAttempts=3
 | 
			
		||||
 | 
			
		||||
; Download order
 | 
			
		||||
; Options: newest, oldest, playlist, playlist_reverse, popularity, rating
 | 
			
		||||
DownloadOrder=playlist
 | 
			
		||||
 | 
			
		||||
; Path where downloaded videos are stored
 | 
			
		||||
DownloadPath=data/videos
 | 
			
		||||
 | 
			
		||||
; A pattern which describes how downloaded files are organized. Extensions are automatically appended.
 | 
			
		||||
; Supported fields: channel, channel_id, playlist, playlist_id, playlist_index, title, id
 | 
			
		||||
; The default pattern should work pretty well with Plex
 | 
			
		||||
DownloadFilePattern=${channel}/${playlist}/S01E${playlist_index} - ${title} [${id}]
 | 
			
		||||
 | 
			
		||||
; Download format that will be passed to youtube-dl. See the youtube-dl documentation for more details.
 | 
			
		||||
DownloadFormat=bestvideo+bestaudio
 | 
			
		||||
 | 
			
		||||
; Subtitles - these options match the youtube-dl options
 | 
			
		||||
DownloadSubtitles=True
 | 
			
		||||
DownloadAutogeneratedSubtitles=False
 | 
			
		||||
DownloadSubtitlesAll=False
 | 
			
		||||
DownloadSubtitlesLangs=en,ro
 | 
			
		||||
DownloadSubtitlesFormat=
 | 
			
		||||
		Reference in New Issue
	
	Block a user