├── .gitignore ├── Docker_README.md ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── YtManager │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── YtManagerApp │ ├── __init__.py │ ├── admin.py │ ├── appmain.py │ ├── apps.py │ ├── dynamic_preferences_registry.py │ ├── management │ │ ├── __init__.py │ │ ├── appconfig.py │ │ ├── downloader.py │ │ ├── jobs │ │ │ ├── __init__.py │ │ │ ├── delete_video.py │ │ │ ├── download_video.py │ │ │ └── synchronize.py │ │ └── videos.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_subscriptionfolder_user.py │ │ ├── 0003_auto_20181013_2018.py │ │ ├── 0004_auto_20181014_1702.py │ │ ├── 0005_auto_20181026_2013.py │ │ ├── 0006_auto_20181027_0256.py │ │ ├── 0007_auto_20181029_1638.py │ │ ├── 0008_auto_20181229_2035.py │ │ ├── 0009_jobexecution_jobmessage.py │ │ ├── 0010_auto_20190819_1317.py │ │ ├── 0011_auto_20190819_1613.py │ │ ├── 0012_auto_20190819_1615.py │ │ ├── __init__.py │ │ ├── subscription_last_synchronised.py │ │ └── video_duration.py │ ├── models.py │ ├── scheduler.py │ ├── static │ │ └── YtManagerApp │ │ │ ├── css │ │ │ ├── login.css │ │ │ ├── login.css.map │ │ │ ├── login.scss │ │ │ ├── style.css │ │ │ ├── style.css.map │ │ │ └── style.scss │ │ │ ├── favicon.ico │ │ │ ├── img │ │ │ ├── baseline-folder-24px.svg │ │ │ ├── baseline-person-24px.svg │ │ │ └── first_time │ │ │ │ ├── ytapi_create_credential.png │ │ │ │ ├── ytapi_create_credential_options.png │ │ │ │ ├── ytapi_create_project.png │ │ │ │ ├── ytapi_done.png │ │ │ │ ├── ytapi_enable_ytapi.png │ │ │ │ ├── ytapi_goto_apis.png │ │ │ │ ├── ytapi_goto_credentials.png │ │ │ │ ├── ytapi_project_name.png │ │ │ │ ├── ytapi_select_project.png │ │ │ │ └── ytapi_select_ytapi.png │ │ │ └── import │ │ │ ├── bootstrap │ │ │ ├── css │ │ │ │ ├── bootstrap-grid.css │ │ │ │ ├── bootstrap-grid.css.map │ │ │ │ ├── bootstrap-grid.min.css │ │ │ │ ├── bootstrap-grid.min.css.map │ │ │ │ ├── bootstrap-reboot.css │ │ │ │ ├── bootstrap-reboot.css.map │ │ │ │ ├── bootstrap-reboot.min.css │ │ │ │ ├── bootstrap-reboot.min.css.map │ │ │ │ ├── bootstrap.css │ │ │ │ ├── bootstrap.css.map │ │ │ │ ├── bootstrap.min.css │ │ │ │ └── bootstrap.min.css.map │ │ │ └── js │ │ │ │ ├── bootstrap.bundle.js │ │ │ │ ├── bootstrap.bundle.js.map │ │ │ │ ├── bootstrap.bundle.min.js │ │ │ │ ├── bootstrap.bundle.min.js.map │ │ │ │ ├── bootstrap.js │ │ │ │ ├── bootstrap.js.map │ │ │ │ ├── bootstrap.min.js │ │ │ │ └── bootstrap.min.js.map │ │ │ ├── jquery │ │ │ ├── jquery-3.3.1.js │ │ │ └── jquery-3.3.1.min.js │ │ │ ├── jstree │ │ │ ├── .gitignore │ │ │ ├── LICENSE-MIT │ │ │ ├── README.md │ │ │ ├── bower.json │ │ │ ├── component.json │ │ │ ├── composer.json │ │ │ ├── demo │ │ │ │ ├── README.md │ │ │ │ └── basic │ │ │ │ │ ├── index.html │ │ │ │ │ └── root.json │ │ │ ├── dist │ │ │ │ ├── jstree.js │ │ │ │ ├── jstree.min.js │ │ │ │ └── themes │ │ │ │ │ ├── default-dark │ │ │ │ │ ├── 32px.png │ │ │ │ │ ├── 40px.png │ │ │ │ │ ├── style.css │ │ │ │ │ ├── style.min.css │ │ │ │ │ └── throbber.gif │ │ │ │ │ └── default │ │ │ │ │ ├── 32px.png │ │ │ │ │ ├── 40px.png │ │ │ │ │ ├── style.css │ │ │ │ │ ├── style.min.css │ │ │ │ │ └── throbber.gif │ │ │ ├── gruntfile.js │ │ │ ├── jstree.jquery.json │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── intro.js │ │ │ │ ├── jstree.changed.js │ │ │ │ ├── jstree.checkbox.js │ │ │ │ ├── jstree.conditionalselect.js │ │ │ │ ├── jstree.contextmenu.js │ │ │ │ ├── jstree.dnd.js │ │ │ │ ├── jstree.js │ │ │ │ ├── jstree.massload.js │ │ │ │ ├── jstree.search.js │ │ │ │ ├── jstree.sort.js │ │ │ │ ├── jstree.state.js │ │ │ │ ├── jstree.types.js │ │ │ │ ├── jstree.unique.js │ │ │ │ ├── jstree.wholerow.js │ │ │ │ ├── misc.js │ │ │ │ ├── outro.js │ │ │ │ ├── sample.js │ │ │ │ ├── themes │ │ │ │ │ ├── base.less │ │ │ │ │ ├── default-dark │ │ │ │ │ │ ├── 32px.png │ │ │ │ │ │ ├── 40px.png │ │ │ │ │ │ ├── style.css │ │ │ │ │ │ ├── style.less │ │ │ │ │ │ └── throbber.gif │ │ │ │ │ ├── default │ │ │ │ │ │ ├── 32px.png │ │ │ │ │ │ ├── 40px.png │ │ │ │ │ │ ├── style.css │ │ │ │ │ │ ├── style.less │ │ │ │ │ │ └── throbber.gif │ │ │ │ │ ├── main.less │ │ │ │ │ ├── mixins.less │ │ │ │ │ └── responsive.less │ │ │ │ └── vakata-jstree.js │ │ │ └── test │ │ │ │ ├── unit │ │ │ │ ├── index.html │ │ │ │ ├── libs │ │ │ │ │ ├── qunit.css │ │ │ │ │ └── qunit.js │ │ │ │ └── test.js │ │ │ │ └── visual │ │ │ │ ├── desktop │ │ │ │ └── index.html │ │ │ │ ├── mobile │ │ │ │ └── index.html │ │ │ │ └── screenshots │ │ │ │ ├── desktop │ │ │ │ ├── .png │ │ │ │ ├── desktop.png │ │ │ │ └── home.png │ │ │ │ └── mobile │ │ │ │ ├── .png │ │ │ │ ├── home.png │ │ │ │ └── mobile.png │ │ │ ├── popper │ │ │ ├── popper.js │ │ │ └── popper.min.js │ │ │ └── typicons │ │ │ ├── LICENCE.md │ │ │ ├── demo.html │ │ │ ├── typicons.css │ │ │ ├── typicons.eot │ │ │ ├── typicons.min.css │ │ │ ├── typicons.svg │ │ │ ├── typicons.ttf │ │ │ └── typicons.woff │ ├── templates │ │ ├── YtManagerApp │ │ │ ├── controls │ │ │ │ ├── folder_create_modal.html │ │ │ │ ├── folder_delete_modal.html │ │ │ │ ├── folder_update_modal.html │ │ │ │ ├── modal.html │ │ │ │ ├── setup_errors_banner.html │ │ │ │ ├── subscription_create_modal.html │ │ │ │ ├── subscription_delete_modal.html │ │ │ │ ├── subscription_update_modal.html │ │ │ │ └── subscriptions_import_modal.html │ │ │ ├── first_time_setup │ │ │ │ ├── done.html │ │ │ │ ├── step0_welcome.html │ │ │ │ ├── step1_apikey.html │ │ │ │ ├── step2_admin.html │ │ │ │ └── step3_configure.html │ │ │ ├── index.html │ │ │ ├── index_unauthenticated.html │ │ │ ├── index_videos.html │ │ │ ├── js │ │ │ │ ├── common.js │ │ │ │ └── index.js │ │ │ ├── master_default.html │ │ │ ├── settings.html │ │ │ ├── settings_admin.html │ │ │ └── video.html │ │ └── registration │ │ │ ├── logged_out.html │ │ │ ├── login.html │ │ │ ├── password_reset_complete.html │ │ │ ├── password_reset_confirm.html │ │ │ ├── password_reset_done.html │ │ │ ├── password_reset_email.html │ │ │ ├── password_reset_form.html │ │ │ ├── register.html │ │ │ └── register_done.html │ ├── templatetags │ │ ├── __init__.py │ │ ├── common.py │ │ └── ratings.py │ ├── tests.py │ ├── urls.py │ ├── utils │ │ ├── __init__.py │ │ ├── algorithms.py │ │ ├── extended_interpolation_with_env.py │ │ ├── progress_tracker.py │ │ ├── subscription_file_parser.py │ │ └── youtube.py │ └── views │ │ ├── __init__.py │ │ ├── actions.py │ │ ├── auth.py │ │ ├── controls │ │ ├── __init__.py │ │ └── modal.py │ │ ├── first_time.py │ │ ├── forms │ │ ├── auth.py │ │ ├── first_time.py │ │ └── settings.py │ │ ├── index.py │ │ ├── notifications.py │ │ ├── settings.py │ │ └── video.py ├── external │ ├── __init__.py │ └── pytaw │ │ ├── .gitignore │ │ ├── .pytaw.conf │ │ ├── README.md │ │ ├── __init__.py │ │ ├── docs │ │ ├── Makefile │ │ ├── conf.py │ │ ├── index.rst │ │ └── make.bat │ │ ├── main_test.py │ │ ├── pytaw │ │ ├── __init__.py │ │ ├── utils.py │ │ └── youtube.py │ │ ├── setup.py │ │ └── tests │ │ ├── __init__.py │ │ └── test_pytaw.py └── manage.py ├── assets ├── favicon.png ├── logo.svg ├── ytsm_promo-tibich.jpg ├── ytsm_promo-tibich.png ├── ytsm_promo-tibich.xcf └── ytsm_promo.xcf ├── config └── config.ini ├── docker-compose.yml ├── docker ├── init.sh └── nginx │ └── nginx.conf ├── examples └── import_subscriptions │ ├── opml_list.opml │ └── subscription_list.txt └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | .vscode 3 | temp/ 4 | env.env 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | #dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # celery beat schedule file 89 | celerybeat-schedule 90 | 91 | # SageMath parsed files 92 | *.sage.py 93 | 94 | # Environments 95 | .env 96 | .venv 97 | env/ 98 | venv/ 99 | ENV/ 100 | env.bak/ 101 | venv.bak/ 102 | 103 | # Spyder project settings 104 | .spyderproject 105 | .spyproject 106 | 107 | # Rope project settings 108 | .ropeproject 109 | 110 | # mkdocs documentation 111 | /site 112 | 113 | # mypy 114 | .mypy_cache/ 115 | .dmypy.json 116 | dmypy.json 117 | 118 | data/ 119 | 120 | .vscode/* 121 | !.vscode/settings.json 122 | !.vscode/tasks.json 123 | !.vscode/launch.json 124 | !.vscode/extensions.json 125 | 126 | .idea 127 | 128 | # Dolphin generated file 129 | .directory 130 | 131 | -------------------------------------------------------------------------------- /Docker_README.md: -------------------------------------------------------------------------------- 1 | Running with Docker 2 | === 3 | 4 | Sample Run command 5 | ----- 6 | ```bash 7 | docker run -d --name ytsm -p 80:8000 --volume /media/ytsm/data:/usr/src/ytsm/data --volume /media/ytsm/config:/usr/src/ytsm/config chibicitiberiu/ytsm:latest 8 | ``` 9 | ### Quick Rundown: 10 | - `--expose 80:8000` maps the Host OS port 80 to the container port 80 11 | - `--volume /media/ytsm/data:/usr/src/app/data` maps the data folder on the host to the container folder `data` 12 | - `--volume /media/ytsm/coinfig:/usr/src/app/config` maps the config folder on the host to the container folder `config` 13 | - `chibicitiberiu/ytsm:latest` tells Docker which image to run the container with (in this case, the latest version) 14 | 15 | 16 | Environment variables 17 | ----- 18 | - YTSM_DATABASE_ENGINE 19 | - YTSM_DATABASE_NAME 20 | - YTSM_YOUTUBE_API_KEY 21 | 22 | 23 | Volumes 24 | ----- 25 | - /usr/src/app/data 26 | - /usr/src/app/config 27 | 28 | 29 | Notes 30 | ---- 31 | If you experience any issues with the app running, make sure to run the following command to apply Django migrations to the database 32 | 33 | ### When using just the Dockerfile/Image 34 | - `docker exec ytsm python manage.py migrate` 35 | 36 | ### When using the docker-compose file 37 | - `docker exec ytsm_web_1 python manage.py migrate` 38 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | 3 | WORKDIR /usr/src/ytsm/app 4 | 5 | # ffmpeg is needed for youtube-dl 6 | RUN apt-get update 7 | RUN apt-get install ffmpeg -y 8 | 9 | COPY ./requirements.txt ./ 10 | RUN pip install --no-cache-dir -r requirements.txt 11 | 12 | ENV YTSM_DEBUG='False' 13 | ENV YTSM_DATA_DIR='/usr/src/ytsm/data' 14 | 15 | VOLUME /usr/src/ytsm/data 16 | VOLUME /usr/src/ytsm/download 17 | 18 | COPY ./app/ ./ 19 | COPY ./config/ ./ 20 | COPY ./docker/init.sh ./ 21 | 22 | EXPOSE 8000 23 | 24 | CMD ["/bin/bash", "init.sh"] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Tiberiu Chibici 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/YtManager/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManager/__init__.py -------------------------------------------------------------------------------- /app/YtManager/urls.py: -------------------------------------------------------------------------------- 1 | """YtManager URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import include 17 | from django.contrib import admin 18 | from django.urls import path 19 | 20 | urlpatterns = [ 21 | path('admin/', admin.site.urls), 22 | path('', include('YtManagerApp.urls')), 23 | ] 24 | -------------------------------------------------------------------------------- /app/YtManager/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for YtManager project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "YtManager.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /app/YtManagerApp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/__init__.py -------------------------------------------------------------------------------- /app/YtManagerApp/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import SubscriptionFolder, Subscription, Video 3 | 4 | admin.site.register(SubscriptionFolder) 5 | admin.site.register(Subscription) 6 | admin.site.register(Video) 7 | -------------------------------------------------------------------------------- /app/YtManagerApp/appmain.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import logging.handlers 3 | import os 4 | import sys 5 | 6 | from django.conf import settings as dj_settings 7 | 8 | from .management.appconfig import appconfig 9 | from .management.jobs.synchronize import SynchronizeJob 10 | from .scheduler import scheduler 11 | from django.db.utils import OperationalError 12 | 13 | 14 | def __initialize_logger(): 15 | log_dir = os.path.join(dj_settings.DATA_DIR, 'logs') 16 | os.makedirs(log_dir, exist_ok=True) 17 | 18 | file_handler = logging.handlers.RotatingFileHandler( 19 | os.path.join(log_dir, "log.log"), 20 | maxBytes=1024 * 1024, 21 | backupCount=5 22 | ) 23 | file_handler.setLevel(dj_settings.LOG_LEVEL) 24 | file_handler.setFormatter(logging.Formatter(dj_settings.LOG_FORMAT)) 25 | logging.root.addHandler(file_handler) 26 | logging.root.setLevel(dj_settings.LOG_LEVEL) 27 | 28 | if dj_settings.DEBUG: 29 | console_handler = logging.StreamHandler(stream=sys.stdout) 30 | console_handler.setLevel(logging.DEBUG) 31 | console_handler.setFormatter(logging.Formatter(dj_settings.CONSOLE_LOG_FORMAT)) 32 | logging.root.addHandler(console_handler) 33 | 34 | 35 | def main(): 36 | __initialize_logger() 37 | 38 | try: 39 | if appconfig.initialized: 40 | scheduler.initialize() 41 | SynchronizeJob.schedule_global_job() 42 | except OperationalError: 43 | # Settings table is not created when running migrate or makemigrations; 44 | # Just don't do anything in this case. 45 | pass 46 | 47 | logging.info('Initialization complete.') 48 | -------------------------------------------------------------------------------- /app/YtManagerApp/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class YtManagerAppConfig(AppConfig): 5 | name = 'YtManagerApp' 6 | 7 | def ready(self): 8 | # Run server using --noreload to avoid having the scheduler run on 2 different processes 9 | from .appmain import main 10 | main() 11 | -------------------------------------------------------------------------------- /app/YtManagerApp/dynamic_preferences_registry.py: -------------------------------------------------------------------------------- 1 | from dynamic_preferences.types import BooleanPreference, StringPreference, IntegerPreference, ChoicePreference 2 | from dynamic_preferences.preferences import Section 3 | from dynamic_preferences.registries import global_preferences_registry 4 | from dynamic_preferences.users.registries import user_preferences_registry 5 | 6 | from YtManagerApp.models import VIDEO_ORDER_CHOICES 7 | from django.conf import settings 8 | import os 9 | 10 | # we create some section objects to link related preferences together 11 | 12 | hidden = Section('hidden') 13 | general = Section('general') 14 | scheduler = Section('scheduler') 15 | 16 | 17 | # Hidden settings 18 | @global_preferences_registry.register 19 | class Initialized(BooleanPreference): 20 | section = hidden 21 | name = 'initialized' 22 | default = False 23 | 24 | 25 | # General settings 26 | @global_preferences_registry.register 27 | class YouTubeAPIKey(StringPreference): 28 | section = general 29 | name = 'youtube_api_key' 30 | default = 'AIzaSyBabzE4Bup77WexdLMa9rN9z-wJidEfNX8' 31 | required = True 32 | 33 | 34 | @global_preferences_registry.register 35 | class AllowRegistrations(BooleanPreference): 36 | section = general 37 | name = 'allow_registrations' 38 | default = True 39 | required = True 40 | 41 | 42 | @global_preferences_registry.register 43 | class SyncSchedule(StringPreference): 44 | section = scheduler 45 | name = 'synchronization_schedule' 46 | default = '5 * * * *' # hourly 47 | required = True 48 | 49 | 50 | @global_preferences_registry.register 51 | class SchedulerConcurrency(IntegerPreference): 52 | section = scheduler 53 | name = 'concurrency' 54 | default = 2 55 | required = True 56 | 57 | 58 | # User settings 59 | @user_preferences_registry.register 60 | class MarkDeletedAsWatched(BooleanPreference): 61 | name = 'mark_deleted_as_watched' 62 | default = True 63 | required = True 64 | 65 | 66 | @user_preferences_registry.register 67 | class AutoDeleteWatched(BooleanPreference): 68 | name = 'automatically_delete_watched' 69 | default = True 70 | required = True 71 | 72 | 73 | @user_preferences_registry.register 74 | class AutoDownloadEnabled(BooleanPreference): 75 | name = 'auto_download' 76 | default = True 77 | required = True 78 | 79 | 80 | @user_preferences_registry.register 81 | class DownloadGlobalLimit(IntegerPreference): 82 | name = 'download_global_limit' 83 | default = -1 84 | required = False 85 | 86 | 87 | @user_preferences_registry.register 88 | class DownloadGlobalSizeLimit(IntegerPreference): 89 | name = 'download_global_size_limit' 90 | default = -1 91 | required = False 92 | 93 | 94 | @user_preferences_registry.register 95 | class DownloadSubscriptionLimit(IntegerPreference): 96 | name = 'download_subscription_limit' 97 | default = 5 98 | required = False 99 | 100 | 101 | @user_preferences_registry.register 102 | class DownloadMaxAttempts(IntegerPreference): 103 | name = 'max_download_attempts' 104 | default = 3 105 | required = True 106 | 107 | 108 | @user_preferences_registry.register 109 | class DownloadOrder(ChoicePreference): 110 | name = 'download_order' 111 | choices = VIDEO_ORDER_CHOICES 112 | default = 'playlist' 113 | required = True 114 | 115 | 116 | @user_preferences_registry.register 117 | class DownloadPath(StringPreference): 118 | name = 'download_path' 119 | default = os.path.join(settings.DATA_DIR, 'downloads') 120 | required = False 121 | 122 | 123 | @user_preferences_registry.register 124 | class DownloadFilePattern(StringPreference): 125 | name = 'download_file_pattern' 126 | default = '${channel}/${playlist}/S01E${playlist_index} - ${title} [${id}]' 127 | required = True 128 | 129 | 130 | @user_preferences_registry.register 131 | class DownloadFormat(StringPreference): 132 | name = 'download_format' 133 | default = 'bestvideo+bestaudio' 134 | required = True 135 | 136 | 137 | @user_preferences_registry.register 138 | class DownloadSubtitles(BooleanPreference): 139 | name = 'download_subtitles' 140 | default = True 141 | required = True 142 | 143 | 144 | @user_preferences_registry.register 145 | class DownloadAutogeneratedSubtitles(BooleanPreference): 146 | name = 'download_autogenerated_subtitles' 147 | default = False 148 | required = True 149 | 150 | 151 | @user_preferences_registry.register 152 | class DownloadAllSubtitles(BooleanPreference): 153 | name = 'download_subtitles_all' 154 | default = False 155 | required = False 156 | 157 | 158 | @user_preferences_registry.register 159 | class DownloadSubtitlesLangs(StringPreference): 160 | name = 'download_subtitles_langs' 161 | default = 'en,ro' 162 | required = False 163 | 164 | 165 | @user_preferences_registry.register 166 | class DownloadSubtitlesFormat(StringPreference): 167 | name = 'download_subtitles_format' 168 | default = '' 169 | required = False 170 | -------------------------------------------------------------------------------- /app/YtManagerApp/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/management/__init__.py -------------------------------------------------------------------------------- /app/YtManagerApp/management/appconfig.py: -------------------------------------------------------------------------------- 1 | from dynamic_preferences.registries import global_preferences_registry 2 | from YtManagerApp.dynamic_preferences_registry import Initialized, YouTubeAPIKey, AllowRegistrations, SyncSchedule, SchedulerConcurrency 3 | 4 | 5 | class AppConfig(object): 6 | # Properties 7 | props = { 8 | 'initialized': Initialized, 9 | 'youtube_api_key': YouTubeAPIKey, 10 | 'allow_registrations': AllowRegistrations, 11 | 'sync_schedule': SyncSchedule, 12 | 'concurrency': SchedulerConcurrency 13 | } 14 | 15 | # Init 16 | def __init__(self, pref_manager): 17 | self.__pref_manager = pref_manager 18 | 19 | def __getattr__(self, item): 20 | prop_class = AppConfig.props[item] 21 | prop_full_name = prop_class.section.name + "__" + prop_class.name 22 | return self.__pref_manager[prop_full_name] 23 | 24 | def __setattr__(self, key, value): 25 | if key in AppConfig.props: 26 | prop_class = AppConfig.props[key] 27 | prop_full_name = prop_class.section.name + "__" + prop_class.name 28 | self.__pref_manager[prop_full_name] = value 29 | else: 30 | super().__setattr__(key, value) 31 | 32 | def for_sub(self, subscription, pref: str): 33 | value = getattr(subscription, pref) 34 | if value is None: 35 | value = subscription.user.preferences[pref] 36 | 37 | return value 38 | 39 | 40 | global_prefs = global_preferences_registry.manager() 41 | appconfig = AppConfig(global_prefs) 42 | -------------------------------------------------------------------------------- /app/YtManagerApp/management/downloader.py: -------------------------------------------------------------------------------- 1 | from YtManagerApp.management.jobs.download_video import DownloadVideoJob 2 | from YtManagerApp.models import Video, Subscription, VIDEO_ORDER_MAPPING 3 | from YtManagerApp.utils import first_non_null 4 | from django.conf import settings as srv_settings 5 | import logging 6 | import requests 7 | import mimetypes 8 | import os 9 | import PIL.Image 10 | import PIL.ImageOps 11 | from urllib.parse import urljoin 12 | 13 | log = logging.getLogger('downloader') 14 | 15 | 16 | def __get_subscription_config(sub: Subscription): 17 | user = sub.user 18 | 19 | enabled = first_non_null(sub.auto_download, user.preferences['auto_download']) 20 | global_limit = user.preferences['download_global_limit'] 21 | limit = first_non_null(sub.download_limit, user.preferences['download_subscription_limit']) 22 | order = first_non_null(sub.download_order, user.preferences['download_order']) 23 | order = VIDEO_ORDER_MAPPING[order] 24 | 25 | return enabled, global_limit, limit, order 26 | 27 | 28 | def downloader_process_subscription(sub: Subscription): 29 | log.info('Processing subscription %d [%s %s]', sub.id, sub.playlist_id, sub.id) 30 | 31 | enabled, global_limit, limit, order = __get_subscription_config(sub) 32 | log.info('Determined settings enabled=%s global_limit=%d limit=%d order="%s"', enabled, global_limit, limit, order) 33 | 34 | if enabled: 35 | videos_to_download = Video.objects\ 36 | .filter(subscription=sub, downloaded_path__isnull=True, watched=False)\ 37 | .order_by(order) 38 | 39 | log.info('%d download candidates.', len(videos_to_download)) 40 | 41 | if global_limit > 0: 42 | global_downloaded = Video.objects.filter(subscription__user=sub.user, downloaded_path__isnull=False).count() 43 | allowed_count = max(global_limit - global_downloaded, 0) 44 | videos_to_download = videos_to_download[0:allowed_count] 45 | log.info('Global limit is set, can only download up to %d videos.', allowed_count) 46 | 47 | if limit > 0: 48 | sub_downloaded = Video.objects.filter(subscription=sub, downloaded_path__isnull=False).count() 49 | allowed_count = max(limit - sub_downloaded, 0) 50 | videos_to_download = videos_to_download[0:allowed_count] 51 | log.info('Limit is set, can only download up to %d videos.', allowed_count) 52 | 53 | # enqueue download 54 | for video in videos_to_download: 55 | log.info('Enqueuing video %d [%s %s] index=%d', video.id, video.video_id, video.name, video.playlist_index) 56 | DownloadVideoJob.schedule(video) 57 | 58 | log.info('Finished processing subscription %d [%s %s]', sub.id, sub.playlist_id, sub.id) 59 | 60 | 61 | def downloader_process_all(): 62 | for subscription in Subscription.objects.all(): 63 | downloader_process_subscription(subscription) 64 | 65 | 66 | def fetch_thumbnail(url, object_type, identifier, thumb_size): 67 | 68 | log.info('Fetching thumbnail url=%s object_type=%s identifier=%s', url, object_type, identifier) 69 | 70 | # Make request to obtain mime type 71 | try: 72 | response = requests.get(url, stream=True) 73 | except requests.exceptions.RequestException as e: 74 | log.error('Failed to fetch thumbnail %s. Error: %s', url, e) 75 | return url 76 | 77 | ext = mimetypes.guess_extension(response.headers['Content-Type']) 78 | 79 | # Build file path 80 | file_name = f"{identifier}{ext}" 81 | abs_path_dir = os.path.join(srv_settings.MEDIA_ROOT, "thumbs", object_type) 82 | abs_path = os.path.join(abs_path_dir, file_name) 83 | abs_path_tmp = file_name + '.tmp' 84 | 85 | # Store image 86 | try: 87 | os.makedirs(abs_path_dir, exist_ok=True) 88 | with open(abs_path_tmp, "wb") as f: 89 | for chunk in response.iter_content(chunk_size=1024): 90 | if chunk: 91 | f.write(chunk) 92 | 93 | # Resize and crop to thumbnail size 94 | image = PIL.Image.open(abs_path_tmp) 95 | image = PIL.ImageOps.fit(image, thumb_size) 96 | image.save(abs_path) 97 | image.close() 98 | 99 | # Delete temp file 100 | os.unlink(abs_path_tmp) 101 | 102 | except requests.exceptions.RequestException as e: 103 | log.error('Error while downloading stream for thumbnail %s. Error: %s', url, e) 104 | return url 105 | except OSError as e: 106 | log.error('Error while writing to file %s for thumbnail %s. Error: %s', abs_path, url, e) 107 | return url 108 | 109 | # Return 110 | media_url = urljoin(srv_settings.MEDIA_URL, f"thumbs/{object_type}/{file_name}") 111 | return media_url 112 | -------------------------------------------------------------------------------- /app/YtManagerApp/management/jobs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/management/jobs/__init__.py -------------------------------------------------------------------------------- /app/YtManagerApp/management/jobs/delete_video.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from YtManagerApp.models import Video 4 | from YtManagerApp.scheduler import Job, scheduler 5 | 6 | 7 | class DeleteVideoJob(Job): 8 | name = "DeleteVideoJob" 9 | 10 | def __init__(self, job_execution, video: Video): 11 | super().__init__(job_execution) 12 | self._video = video 13 | 14 | def get_description(self): 15 | return f"Deleting video {self._video}" 16 | 17 | def run(self): 18 | count = 0 19 | 20 | try: 21 | for file in self._video.get_files(): 22 | self.log.info("Deleting file %s", file) 23 | count += 1 24 | try: 25 | os.unlink(file) 26 | except OSError as e: 27 | self.log.error("Failed to delete file %s: Error: %s", file, e) 28 | 29 | except OSError as e: 30 | self.log.error("Failed to delete video %d [%s %s]. Error: %s", self._video.id, 31 | self._video.video_id, self._video.name, e) 32 | 33 | self._video.downloaded_path = None 34 | self._video.save() 35 | 36 | self.log.info('Deleted video %d successfully! (%d files) [%s %s]', self._video.id, count, 37 | self._video.video_id, self._video.name) 38 | 39 | @staticmethod 40 | def schedule(video: Video): 41 | """ 42 | Schedules a delete video job to run immediately. 43 | :param video: 44 | :return: 45 | """ 46 | scheduler.add_job(DeleteVideoJob, args=[video]) 47 | -------------------------------------------------------------------------------- /app/YtManagerApp/management/videos.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import Optional 3 | 4 | from django.contrib.auth.models import User 5 | from django.db.models import Q 6 | 7 | from YtManagerApp.models import Subscription, Video, SubscriptionFolder 8 | 9 | 10 | def get_videos(user: User, 11 | sort_order: Optional[str], 12 | query: Optional[str] = None, 13 | subscription_id: Optional[int] = None, 14 | folder_id: Optional[int] = None, 15 | only_watched: Optional[bool] = None, 16 | only_downloaded: Optional[bool] = None, 17 | ): 18 | 19 | filter_args = [] 20 | filter_kwargs = { 21 | 'subscription__user': user 22 | } 23 | 24 | # Process query string - basically, we break it down into words, 25 | # and then search for the given text in the name, description, uploader name and subscription name 26 | if query is not None: 27 | for match in re.finditer(r'\w+', query): 28 | word = match[0] 29 | filter_args.append(Q(name__icontains=word) 30 | | Q(description__icontains=word) 31 | | Q(uploader_name__icontains=word) 32 | | Q(subscription__name__icontains=word)) 33 | 34 | # Subscription id 35 | if subscription_id is not None: 36 | filter_kwargs['subscription_id'] = subscription_id 37 | 38 | # Folder id 39 | if folder_id is not None: 40 | # Visit function - returns only the subscription IDs 41 | def visit(node): 42 | if isinstance(node, Subscription): 43 | return node.id 44 | return None 45 | filter_kwargs['subscription_id__in'] = SubscriptionFolder.traverse(folder_id, user, visit) 46 | 47 | # Only watched 48 | if only_watched is not None: 49 | filter_kwargs['watched'] = only_watched 50 | 51 | # Only downloaded 52 | # - not downloaded (False) -> is null (True) 53 | # - downloaded (True) -> is not null (False) 54 | if only_downloaded is not None: 55 | filter_kwargs['downloaded_path__isnull'] = not only_downloaded 56 | 57 | return Video.objects.filter(*filter_args, **filter_kwargs).order_by(sort_order) 58 | -------------------------------------------------------------------------------- /app/YtManagerApp/migrations/0002_subscriptionfolder_user.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2018-10-11 18:16 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('YtManagerApp', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AddField( 17 | model_name='subscriptionfolder', 18 | name='user', 19 | field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), 20 | preserve_default=False, 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /app/YtManagerApp/migrations/0003_auto_20181013_2018.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2018-10-13 17:18 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('YtManagerApp', '0002_subscriptionfolder_user'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='video', 15 | name='rating', 16 | field=models.FloatField(default=0.5), 17 | ), 18 | migrations.AddField( 19 | model_name='video', 20 | name='uploader_name', 21 | field=models.CharField(default=None, max_length=255), 22 | preserve_default=False, 23 | ), 24 | migrations.AddField( 25 | model_name='video', 26 | name='views', 27 | field=models.IntegerField(default=0), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /app/YtManagerApp/migrations/0004_auto_20181014_1702.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2018-10-14 14:02 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('YtManagerApp', '0003_auto_20181013_2018'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='subscriptionfolder', 15 | name='name', 16 | field=models.CharField(max_length=250), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /app/YtManagerApp/migrations/0006_auto_20181027_0256.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2018-10-26 23:56 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('YtManagerApp', '0005_auto_20181026_2013'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='subscription', 15 | old_name='manager_delete_after_watched', 16 | new_name='delete_after_watched', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /app/YtManagerApp/migrations/0007_auto_20181029_1638.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2018-10-29 16:38 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('YtManagerApp', '0006_auto_20181027_0256'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='subscription', 15 | name='channel', 16 | ), 17 | migrations.AddField( 18 | model_name='subscription', 19 | name='channel_id', 20 | field=models.CharField(default='test', max_length=128), 21 | preserve_default=False, 22 | ), 23 | migrations.AddField( 24 | model_name='subscription', 25 | name='channel_name', 26 | field=models.CharField(default='Unknown', max_length=1024), 27 | preserve_default=False, 28 | ), 29 | migrations.DeleteModel( 30 | name='Channel', 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /app/YtManagerApp/migrations/0008_auto_20181229_2035.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.2 on 2018-12-29 20:35 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('YtManagerApp', '0007_auto_20181029_1638'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='usersettings', 15 | name='user', 16 | ), 17 | migrations.RenameField( 18 | model_name='subscription', 19 | old_name='delete_after_watched', 20 | new_name='automatically_delete_watched', 21 | ), 22 | migrations.DeleteModel( 23 | name='UserSettings', 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /app/YtManagerApp/migrations/0009_jobexecution_jobmessage.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-04-08 15:26 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 12 | ('YtManagerApp', '0008_auto_20181229_2035'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='JobExecution', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('start_date', models.DateTimeField(auto_now=True)), 21 | ('end_date', models.DateTimeField(null=True)), 22 | ('description', models.CharField(default='', max_length=250)), 23 | ('status', models.IntegerField(choices=[('running', 0), ('finished', 1), ('failed', 2), ('interrupted', 3)], default=0)), 24 | ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 25 | ], 26 | ), 27 | migrations.CreateModel( 28 | name='JobMessage', 29 | fields=[ 30 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 31 | ('timestamp', models.DateTimeField(auto_now=True)), 32 | ('progress', models.FloatField(null=True)), 33 | ('message', models.CharField(default='', max_length=1024)), 34 | ('level', models.IntegerField(choices=[('normal', 0), ('warning', 1), ('error', 2)], default=0)), 35 | ('suppress_notification', models.BooleanField(default=False)), 36 | ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='YtManagerApp.JobExecution')), 37 | ], 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /app/YtManagerApp/migrations/0010_auto_20190819_1317.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2019-08-19 13:17 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('YtManagerApp', '0009_jobexecution_jobmessage'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='subscription', 15 | name='rewrite_playlist_indices', 16 | field=models.BooleanField(default=False), 17 | ), 18 | migrations.AddField( 19 | model_name='video', 20 | name='new', 21 | field=models.BooleanField(default=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /app/YtManagerApp/migrations/0011_auto_20190819_1613.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2019-08-19 16:13 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('YtManagerApp', '0010_auto_20190819_1317'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='subscription', 15 | name='icon_default', 16 | ), 17 | migrations.RemoveField( 18 | model_name='video', 19 | name='icon_default', 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /app/YtManagerApp/migrations/0012_auto_20190819_1615.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.4 on 2019-08-19 16:15 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('YtManagerApp', '0011_auto_20190819_1613'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='subscription', 15 | old_name='icon_best', 16 | new_name='thumbnail', 17 | ), 18 | migrations.RenameField( 19 | model_name='video', 20 | old_name='icon_best', 21 | new_name='thumbnail', 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /app/YtManagerApp/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/migrations/__init__.py -------------------------------------------------------------------------------- /app/YtManagerApp/migrations/subscription_last_synchronised.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-11-14 20:54 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('YtManagerApp', '0012_auto_20190819_1615'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='subscription', 15 | name='last_synchronised', 16 | field=models.DateTimeField(blank=True, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /app/YtManagerApp/migrations/video_duration.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-10-18 21:10 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('YtManagerApp', '0012_auto_20190819_1615'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='video', 15 | name='duration', 16 | field=models.IntegerField(default=0), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/css/login.css: -------------------------------------------------------------------------------- 1 | .login-card { 2 | width: 26rem; 3 | margin: 2rem 0; } 4 | 5 | .register-card { 6 | max-width: 35rem; 7 | margin: 2rem 0; } 8 | 9 | /*# sourceMappingURL=login.css.map */ 10 | -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/css/login.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": "AAAA,WAAY;EACR,KAAK,EAAE,KAAK;EACZ,MAAM,EAAE,MAAM;;AAGlB,cAAe;EACX,SAAS,EAAE,KAAK;EAChB,MAAM,EAAE,MAAM", 4 | "sources": ["login.scss"], 5 | "names": [], 6 | "file": "login.css" 7 | } -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/css/login.scss: -------------------------------------------------------------------------------- 1 | .login-card { 2 | max-width: 26rem; 3 | margin: 2rem 0; 4 | } 5 | 6 | .register-card { 7 | max-width: 35rem; 8 | margin: 2rem 0; 9 | } 10 | -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/css/style.css: -------------------------------------------------------------------------------- 1 | #main_body { 2 | margin-bottom: 4rem; 3 | margin-top: 0; } 4 | 5 | #main_footer { 6 | position: fixed; 7 | left: 0; 8 | right: 0; 9 | bottom: 0; 10 | height: 2rem; 11 | line-height: 2rem; 12 | padding: 0 1rem; 13 | display: flex; 14 | align-content: center; 15 | font-size: 10pt; } 16 | 17 | /* Loading animation */ 18 | .loading-dual-ring { 19 | display: inline-block; 20 | width: 64px; 21 | height: 64px; } 22 | .loading-dual-ring:after { 23 | content: " "; 24 | display: block; 25 | width: 46px; 26 | height: 46px; 27 | margin: 1px; 28 | border-radius: 50%; 29 | border: 5px solid #007bff; 30 | border-color: #007bff transparent #007bff transparent; 31 | animation: loading-dual-ring 1.2s linear infinite; } 32 | 33 | .loading-dual-ring-small { 34 | display: inline-block; 35 | width: 32px; 36 | height: 32px; } 37 | .loading-dual-ring-small:after { 38 | content: " "; 39 | display: block; 40 | width: 23px; 41 | height: 23px; 42 | margin: 1px; 43 | border-radius: 50%; 44 | border: 2.5px solid #007bff; 45 | border-color: #007bff transparent #007bff transparent; 46 | animation: loading-dual-ring 1.2s linear infinite; } 47 | 48 | @keyframes loading-dual-ring { 49 | 0% { 50 | transform: rotate(0deg); } 51 | 100% { 52 | transform: rotate(360deg); } } 53 | .loading-dual-ring-center-screen { 54 | position: fixed; 55 | top: 50%; 56 | left: 50%; 57 | margin-top: -32px; 58 | margin-left: -32px; } 59 | 60 | .black-overlay { 61 | position: fixed; 62 | /* Sit on top of the page content */ 63 | display: none; 64 | /* Hidden by default */ 65 | width: 100%; 66 | /* Full width (cover the whole page) */ 67 | height: 100%; 68 | /* Full height (cover the whole page) */ 69 | top: 0; 70 | left: 0; 71 | right: 0; 72 | bottom: 0; 73 | background-color: rgba(0, 0, 0, 0.5); 74 | /* Black background with opacity */ 75 | z-index: 2; 76 | /* Specify a stack order in case you're using a different order for other elements */ 77 | cursor: pointer; 78 | /* Add a pointer on hover */ } 79 | 80 | .video-gallery .card-wrapper { 81 | padding: 1rem; 82 | margin-bottom: .5rem; } 83 | .video-gallery .card-wrapper:hover { 84 | background-color: #fafafa; } 85 | .video-gallery .card { 86 | border: none; 87 | background: none; } 88 | .video-gallery .card .card-body { 89 | margin-top: .5em; 90 | padding: 0; } 91 | .video-gallery .card .card-text { 92 | font-size: 10pt; 93 | margin-bottom: .5rem; } 94 | .video-gallery .card .card-title { 95 | font-size: 11pt; 96 | margin-bottom: .5rem; 97 | line-height: 1.2rem; } 98 | .video-gallery .card .card-title .badge { 99 | font-size: 8pt; } 100 | .video-gallery .card .card-footer { 101 | padding: .5rem .75rem; } 102 | .video-gallery .card .card-more { 103 | margin-right: -0.25rem; 104 | margin-top: -0.27rem; } 105 | .video-gallery .card .card-more:hover { 106 | text-decoration: none; } 107 | .video-gallery .card .progress { 108 | width: 100px; } 109 | 110 | .video-badges { 111 | position: absolute; 112 | top: .6em; 113 | left: 0; 114 | width: 6.2em; } 115 | .video-badges .video-badge { 116 | width: 100%; 117 | margin: 0 0 .6em 0; 118 | padding: 0 0 0 .5em; 119 | line-height: 1.65em; 120 | background-color: #9af; 121 | box-shadow: 0.2em 0 0.6em 0 rgba(0, 0, 0, 0.4); 122 | border-radius: 0 3px 3px 0; 123 | color: white; 124 | text-align: center; 125 | font-family: Noto Sans, Helvetica, Arial, sans-serif; 126 | font-weight: 500; 127 | font-size: 8pt; 128 | text-transform: uppercase; } 129 | .video-badges .video-badge.video-badge-new { 130 | background-color: #007bff; } 131 | .video-badges .video-badge.video-badge-downloaded { 132 | background-color: #59b352; } 133 | .video-badges .video-badge.video-badge-watched { 134 | background-color: #444; } 135 | 136 | .alert-card { 137 | max-width: 35rem; 138 | margin: 2rem 0; } 139 | 140 | .no-asterisk .asteriskField { 141 | display: none; } 142 | 143 | .modal-field-error { 144 | margin: 0.5rem 0; 145 | padding: 0.5rem 0; } 146 | .modal-field-error ul { 147 | margin: 0; } 148 | 149 | .star-rating { 150 | display: inline-block; 151 | margin-bottom: 0.5rem; } 152 | 153 | .btn-toolbar { 154 | margin: .5rem 0; } 155 | .btn-toolbar .btn { 156 | padding: 0.15rem 0.4rem; 157 | font-size: 14pt; } 158 | 159 | .status-timestamp { 160 | margin-right: 0.25rem; } 161 | 162 | .dropdown-jobs { 163 | min-width: 25rem; } 164 | .dropdown-jobs .dropdown-item p { 165 | margin: 0; 166 | line-height: normal; } 167 | 168 | img.muted { 169 | opacity: .5; } 170 | 171 | /*# sourceMappingURL=style.css.map */ 172 | -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/css/style.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": "AAEA,UAAW;EACP,aAAa,EAAE,IAAI;EACnB,UAAU,EAAE,CAAC;;AAGjB,YAAa;EACT,QAAQ,EAAE,KAAK;EACf,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,MAAM,EAAE,CAAC;EACT,MAAM,EAAE,IAAI;EACZ,WAAW,EAAE,IAAI;EACjB,OAAO,EAAE,MAAM;EACf,OAAO,EAAE,IAAI;EACb,aAAa,EAAE,MAAM;EACrB,SAAS,EAAE,IAAI;;AAqBnB,uBAAuB;AACvB,kBAAmB;EAlBf,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,IAAa;EACpB,MAAM,EAAE,IAAa;EAErB,wBAAQ;IACJ,OAAO,EAAE,GAAG;IACZ,OAAO,EAAE,KAAK;IACd,KAAK,EAAE,IAAa;IACpB,MAAM,EAAE,IAAa;IACrB,MAAM,EAAE,GAAG;IACX,aAAa,EAAE,GAAG;IAClB,MAAM,EAAE,iBAAkC;IAC1C,YAAY,EAAE,uCAAmD;IACjE,SAAS,EAAE,sCAAsC;;AASzD,wBAAyB;EAtBrB,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,IAAa;EACpB,MAAM,EAAE,IAAa;EAErB,8BAAQ;IACJ,OAAO,EAAE,GAAG;IACZ,OAAO,EAAE,KAAK;IACd,KAAK,EAAE,IAAa;IACpB,MAAM,EAAE,IAAa;IACrB,MAAM,EAAE,GAAG;IACX,aAAa,EAAE,GAAG;IAClB,MAAM,EAAE,mBAAkC;IAC1C,YAAY,EAAE,uCAAmD;IACjE,SAAS,EAAE,sCAAsC;;AAazD,4BAOC;EANG,EAAG;IACC,SAAS,EAAE,YAAY;EAE3B,IAAK;IACD,SAAS,EAAE,cAAc;AAIjC,gCAAiC;EAC7B,QAAQ,EAAE,KAAK;EACf,GAAG,EAAE,GAAG;EACR,IAAI,EAAE,GAAG;EACT,UAAU,EAAE,KAAK;EACjB,WAAW,EAAE,KAAK;;AAGtB,cAAe;EACX,QAAQ,EAAE,KAAK;EAAE,oCAAoC;EACrD,OAAO,EAAE,IAAI;EAAE,uBAAuB;EACtC,KAAK,EAAE,IAAI;EAAE,uCAAuC;EACpD,MAAM,EAAE,IAAI;EAAE,wCAAwC;EACtD,GAAG,EAAE,CAAC;EACN,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,CAAC;EACR,MAAM,EAAE,CAAC;EACT,gBAAgB,EAAE,kBAAe;EAAE,mCAAmC;EACtE,OAAO,EAAE,CAAC;EAAE,qFAAqF;EACjG,MAAM,EAAE,OAAO;EAAE,4BAA4B;;AAI7C,4BAAc;EACV,OAAO,EAAE,IAAI;EACb,aAAa,EAAE,KAAK;EAEpB,kCAAQ;IACJ,gBAAgB,EAAE,OAAO;AAGjC,oBAAM;EACF,MAAM,EAAE,IAAI;EACZ,UAAU,EAAE,IAAI;EAEhB,+BAAW;IACP,UAAU,EAAE,IAAI;IAChB,OAAO,EAAE,CAAC;EAEd,+BAAW;IACP,SAAS,EAAE,IAAI;IACf,aAAa,EAAE,KAAK;EAExB,gCAAY;IACR,SAAS,EAAE,IAAI;IACf,aAAa,EAAE,KAAK;IACpB,WAAW,EAAE,MAAM;IAEnB,uCAAO;MACH,SAAS,EAAE,GAAG;EAGtB,iCAAa;IACT,OAAO,EAAE,YAAY;EAGzB,+BAAW;IACP,YAAY,EAAE,QAAQ;IACtB,UAAU,EAAE,QAAQ;IACpB,qCAAQ;MACJ,eAAe,EAAE,IAAI;EAO7B,8BAAU;IACN,KAAK,EAAE,KAAK;;AAMxB,aAAc;EACV,QAAQ,EAAE,QAAQ;EAClB,GAAG,EAAE,IAAI;EACT,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,KAAK;EAEZ,0BAAa;IACT,KAAK,EAAE,IAAI;IACX,MAAM,EAAE,UAAU;IAClB,OAAO,EAAE,UAAU;IACnB,WAAW,EAAE,MAAM;IAEnB,gBAAgB,EAAE,IAAI;IACtB,UAAU,EAAE,kCAA6B;IACzC,aAAa,EAAE,WAAW;IAE1B,KAAK,EAAE,KAAK;IACZ,UAAU,EAAE,MAAM;IAClB,WAAW,EAAE,uCAAuC;IACpD,WAAW,EAAE,GAAG;IAChB,SAAS,EAAE,GAAG;IACd,cAAc,EAAE,SAAS;IAEzB,0CAAkB;MACd,gBAAgB,EA1Jb,OAAO;IA4Jd,iDAAyB;MACrB,gBAAgB,EAAE,OAAO;IAE7B,8CAAsB;MAClB,gBAAgB,EAAE,IAAI;;AAMlC,WAAY;EACR,SAAS,EAAE,KAAK;EAChB,MAAM,EAAE,MAAM;;AAId,2BAAe;EACX,OAAO,EAAE,IAAI;;AAIrB,kBAAmB;EACf,MAAM,EAAE,QAAQ;EAChB,OAAO,EAAE,QAAQ;EAEjB,qBAAG;IACC,MAAM,EAAE,CAAC;;AAIjB,YAAa;EACT,OAAO,EAAE,YAAY;EACrB,aAAa,EAAE,MAAM;;AAGzB,YAAa;EACT,MAAM,EAAE,OAAO;EACf,iBAAK;IACD,OAAO,EAAE,cAAc;IACvB,SAAS,EAAE,IAAI;;AAIvB,iBAAkB;EACd,YAAY,EAAE,OAAO;;AAGzB,cAAe;EACX,SAAS,EAAE,KAAK;EAEhB,+BAAiB;IACb,MAAM,EAAE,CAAC;IACT,WAAW,EAAE,MAAM;;AAI3B,SAAU;EACN,OAAO,EAAE,EAAE", 4 | "sources": ["style.scss"], 5 | "names": [], 6 | "file": "style.css" 7 | } -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/css/style.scss: -------------------------------------------------------------------------------- 1 | $accent-color: #007bff; 2 | 3 | #main_body { 4 | margin-bottom: 4rem; 5 | margin-top: 0; 6 | } 7 | 8 | #main_footer { 9 | position: fixed; 10 | left: 0; 11 | right: 0; 12 | bottom: 0; 13 | height: 2rem; 14 | line-height: 2rem; 15 | padding: 0 1rem; 16 | display: flex; 17 | align-content: center; 18 | font-size: 10pt; 19 | } 20 | 21 | @mixin loading-dual-ring($scale : 1) { 22 | display: inline-block; 23 | width: $scale * 64px; 24 | height: $scale * 64px; 25 | 26 | &:after { 27 | content: " "; 28 | display: block; 29 | width: $scale * 46px; 30 | height: $scale * 46px; 31 | margin: 1px; 32 | border-radius: 50%; 33 | border: ($scale * 5px) solid $accent-color; 34 | border-color: $accent-color transparent $accent-color transparent; 35 | animation: loading-dual-ring 1.2s linear infinite; 36 | } 37 | } 38 | 39 | /* Loading animation */ 40 | .loading-dual-ring { 41 | @include loading-dual-ring(1.0); 42 | } 43 | 44 | .loading-dual-ring-small { 45 | @include loading-dual-ring(0.5); 46 | } 47 | 48 | @keyframes loading-dual-ring { 49 | 0% { 50 | transform: rotate(0deg); 51 | } 52 | 100% { 53 | transform: rotate(360deg); 54 | } 55 | } 56 | 57 | .loading-dual-ring-center-screen { 58 | position: fixed; 59 | top: 50%; 60 | left: 50%; 61 | margin-top: -32px; 62 | margin-left: -32px; 63 | } 64 | 65 | .black-overlay { 66 | position: fixed; /* Sit on top of the page content */ 67 | display: none; /* Hidden by default */ 68 | width: 100%; /* Full width (cover the whole page) */ 69 | height: 100%; /* Full height (cover the whole page) */ 70 | top: 0; 71 | left: 0; 72 | right: 0; 73 | bottom: 0; 74 | background-color: rgba(0,0,0,0.5); /* Black background with opacity */ 75 | z-index: 2; /* Specify a stack order in case you're using a different order for other elements */ 76 | cursor: pointer; /* Add a pointer on hover */ 77 | } 78 | 79 | .video-gallery { 80 | .card-wrapper { 81 | padding: 1rem; 82 | margin-bottom: .5rem; 83 | 84 | &:hover { 85 | background-color: #fafafa; 86 | } 87 | } 88 | .card { 89 | border: none; 90 | background: none; 91 | 92 | .card-body { 93 | margin-top: .5em; 94 | padding: 0; 95 | } 96 | .card-text { 97 | font-size: 10pt; 98 | margin-bottom: .5rem; 99 | } 100 | .card-title { 101 | font-size: 11pt; 102 | margin-bottom: .5rem; 103 | line-height: 1.2rem; 104 | 105 | .badge { 106 | font-size: 8pt; 107 | } 108 | } 109 | .card-footer { 110 | padding: .5rem .75rem; 111 | } 112 | 113 | .card-more { 114 | margin-right: -0.25rem; 115 | margin-top: -0.27rem; 116 | &:hover { 117 | text-decoration: none; 118 | } 119 | } 120 | 121 | .card-img-top { 122 | } 123 | 124 | .progress { 125 | width: 100px; 126 | } 127 | 128 | } 129 | } 130 | 131 | .video-badges { 132 | position: absolute; 133 | top: .6em; 134 | left: 0; 135 | width: 6.2em; 136 | 137 | .video-badge { 138 | width: 100%; 139 | margin: 0 0 .6em 0; 140 | padding: 0 0 0 .5em; 141 | line-height: 1.65em; 142 | 143 | background-color: #9af; 144 | box-shadow: .2em 0 .6em 0 rgba(0,0,0,0.4); 145 | border-radius: 0 3px 3px 0; 146 | 147 | color: white; 148 | text-align: center; 149 | font-family: Noto Sans, Helvetica, Arial, sans-serif; 150 | font-weight: 500; 151 | font-size: 8pt; 152 | text-transform: uppercase; 153 | 154 | &.video-badge-new { 155 | background-color: $accent-color; 156 | } 157 | &.video-badge-downloaded { 158 | background-color: #59b352; 159 | } 160 | &.video-badge-watched { 161 | background-color: #444; 162 | } 163 | } 164 | } 165 | 166 | 167 | .alert-card { 168 | max-width: 35rem; 169 | margin: 2rem 0; 170 | } 171 | 172 | .no-asterisk { 173 | .asteriskField { 174 | display: none; 175 | } 176 | } 177 | 178 | .modal-field-error { 179 | margin: 0.5rem 0; 180 | padding: 0.5rem 0; 181 | 182 | ul { 183 | margin: 0; 184 | } 185 | } 186 | 187 | .star-rating { 188 | display: inline-block; 189 | margin-bottom: 0.5rem; 190 | } 191 | 192 | .btn-toolbar { 193 | margin: .5rem 0; 194 | .btn { 195 | padding: 0.15rem 0.4rem; 196 | font-size: 14pt; 197 | } 198 | } 199 | 200 | .status-timestamp { 201 | margin-right: 0.25rem; 202 | } 203 | 204 | .dropdown-jobs { 205 | min-width: 25rem; 206 | 207 | .dropdown-item p { 208 | margin: 0; 209 | line-height: normal; 210 | } 211 | } 212 | 213 | img.muted { 214 | opacity: .5; 215 | } -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/favicon.ico -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/img/baseline-folder-24px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/img/baseline-person-24px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_create_credential.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_create_credential.png -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_create_credential_options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_create_credential_options.png -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_create_project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_create_project.png -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_done.png -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_enable_ytapi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_enable_ytapi.png -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_goto_apis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_goto_apis.png -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_goto_credentials.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_goto_credentials.png -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_project_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_project_name.png -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_select_project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_select_project.png -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_select_ytapi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/img/first_time/ytapi_select_ytapi.png -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/bootstrap/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.2.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2018 The Bootstrap Authors 4 | * Copyright 2011-2018 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/.gitignore: -------------------------------------------------------------------------------- 1 | /debug 2 | /jstree.sublime-project 3 | /jstree.sublime-workspace 4 | /bower_components 5 | /node_modules 6 | /site 7 | /nuget 8 | /demo/filebrowser/data/root 9 | /npm.txt 10 | /libs 11 | /docs 12 | /dist/libs 13 | /.vscode 14 | /.idea -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Ivan Bozhanov 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jstree", 3 | "license": "MIT", 4 | "version": "3.3.7", 5 | "main" : [ 6 | "./dist/jstree.js", 7 | "./dist/themes/default/style.css" 8 | ], 9 | "ignore": [ 10 | "**/.*", 11 | "docs", 12 | "demo", 13 | "libs", 14 | "node_modules", 15 | "test", 16 | "libs", 17 | "jstree.jquery.json", 18 | "gruntfile.js", 19 | "package.json", 20 | "bower.json", 21 | "component.json", 22 | "LICENCE-MIT", 23 | "README.md" 24 | ], 25 | "dependencies": { 26 | "jquery": ">=1.9.1" 27 | }, 28 | "keywords": [ 29 | "ui", 30 | "tree", 31 | "jstree" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jstree", 3 | "repo": "vakata/jstree", 4 | "description": "jsTree is jquery plugin, that provides interactive trees.", 5 | "version": "3.3.7", 6 | "license": "MIT", 7 | "keywords": [ 8 | "ui", 9 | "tree", 10 | "jstree" 11 | ], 12 | "scripts": [ 13 | "dist/jstree.js", 14 | "dist/jstree.min.js" 15 | ], 16 | "images": [ 17 | "dist/themes/default/32px.png", 18 | "dist/themes/default/40px.png", 19 | "dist/themes/default/throbber.gif" 20 | ], 21 | "styles": [ 22 | "dist/themes/default/style.css", 23 | "dist/themes/default/style.min.css" 24 | ], 25 | "dependencies": { 26 | "components/jquery": ">=1.9.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vakata/jstree", 3 | "description": "jsTree is jquery plugin, that provides interactive trees.", 4 | "type": "component", 5 | "homepage": "http://jstree.com", 6 | "license": "MIT", 7 | "support": { 8 | "issues": "https://github.com/vakata/jstree/issues", 9 | "forum": "https://groups.google.com/forum/#!forum/jstree", 10 | "source": "https://github.com/vakata/jstree" 11 | }, 12 | "authors": [ 13 | { 14 | "name": "Ivan Bozhanov", 15 | "email": "jstree@jstree.com" 16 | } 17 | ], 18 | "require": { 19 | "components/jquery": ">=1.9.1" 20 | }, 21 | "suggest": { 22 | "robloach/component-installer": "Allows installation of Components via Composer" 23 | }, 24 | "extra": { 25 | "component": { 26 | "scripts": [ 27 | "dist/jstree.js" 28 | ], 29 | "styles": [ 30 | "dist/themes/default/style.css" 31 | ], 32 | "images": [ 33 | "dist/themes/default/32px.png", 34 | "dist/themes/default/40px.png", 35 | "dist/themes/default/throbber.gif" 36 | ], 37 | "files": [ 38 | "dist/jstree.min.js", 39 | "dist/themes/default/style.min.css", 40 | "dist/themes/default/32px.png", 41 | "dist/themes/default/40px.png", 42 | "dist/themes/default/throbber.gif" 43 | ] 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/demo/README.md: -------------------------------------------------------------------------------- 1 | ## PHP demos moved to new repository 2 | https://github.com/vakata/jstree-php-demos -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/demo/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | jstree basic demos 6 | 12 | 13 | 14 | 15 |

HTML demo

16 |
17 | 25 |
26 | 27 |

Inline data demo

28 |
29 | 30 |

Data format demo

31 |
32 | 33 |

AJAX demo

34 |
35 | 36 |

Lazy loading demo

37 |
38 | 39 |

Callback function data demo

40 |
41 | 42 |

Interaction and events demo

43 | either click the button or a node in the tree 44 |
45 | 46 | 47 | 48 | 49 | 145 | 146 | -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/demo/basic/root.json: -------------------------------------------------------------------------------- 1 | [{"id":1,"text":"Root node","children":[{"id":2,"text":"Child node 1"},{"id":3,"text":"Child node 2"}]}] -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/dist/themes/default-dark/32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/import/jstree/dist/themes/default-dark/32px.png -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/dist/themes/default-dark/40px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/import/jstree/dist/themes/default-dark/40px.png -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/dist/themes/default-dark/throbber.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/import/jstree/dist/themes/default-dark/throbber.gif -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/dist/themes/default/32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/import/jstree/dist/themes/default/32px.png -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/dist/themes/default/40px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/import/jstree/dist/themes/default/40px.png -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/dist/themes/default/throbber.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/import/jstree/dist/themes/default/throbber.gif -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/jstree.jquery.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jstree", 3 | "title": "jsTree", 4 | "description": "Tree view for jQuery", 5 | "version": "3.3.7", 6 | "homepage": "http://jstree.com", 7 | "keywords": [ 8 | "ui", 9 | "tree", 10 | "jstree" 11 | ], 12 | "author": { 13 | "name": "Ivan Bozhanov", 14 | "email": "jstree@jstree.com", 15 | "url": "http://vakata.com" 16 | }, 17 | "licenses": [ 18 | { 19 | "type": "MIT", 20 | "url": "https://github.com/vakata/jstree/blob/master/LICENSE-MIT" 21 | } 22 | ], 23 | "bugs": "https://github.com/vakata/jstree/issues", 24 | "demo": "http://jstree.com/demo", 25 | "dependencies": { 26 | "jquery": ">=1.9.1" 27 | } 28 | } -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jstree", 3 | "title": "jsTree", 4 | "description": "jQuery tree plugin", 5 | "version": "3.3.7", 6 | "homepage": "http://jstree.com", 7 | "main": "./dist/jstree.js", 8 | "author": { 9 | "name": "Ivan Bozhanov", 10 | "email": "jstree@jstree.com", 11 | "url": "http://vakata.com" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/vakata/jstree.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/vakata/jstree/issues" 19 | }, 20 | "license": "MIT", 21 | "licenses": [ 22 | { 23 | "type": "MIT", 24 | "url": "https://github.com/vakata/jstree/blob/master/LICENSE-MIT" 25 | } 26 | ], 27 | "keywords": [], 28 | "devDependencies": { 29 | "dox": "~0.9.0", 30 | "grunt": "~1.0.0", 31 | "grunt-contrib-concat": "*", 32 | "grunt-contrib-copy": "*", 33 | "grunt-contrib-imagemin": "~2.0.1", 34 | "grunt-contrib-jshint": "*", 35 | "grunt-contrib-less": "~1.4.1", 36 | "grunt-contrib-qunit": "~2.0.0", 37 | "grunt-contrib-uglify": "*", 38 | "grunt-contrib-watch": "~1.1.0", 39 | "grunt-resemble-cli": "0.0.8", 40 | "grunt-text-replace": "~0.4.0", 41 | "lodash": "^4.17.10" 42 | }, 43 | "dependencies": { 44 | "jquery": ">=1.9.1" 45 | }, 46 | "npmName": "jstree", 47 | "npmFileMap": [ 48 | { 49 | "basePath": "/dist/", 50 | "files": [ 51 | "jstree.min.js", 52 | "themes/**/*.png", 53 | "themes/**/*.gif", 54 | "themes/**/*.min.css" 55 | ] 56 | } 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/src/intro.js: -------------------------------------------------------------------------------- 1 | /*globals jQuery, define, module, exports, require, window, document, postMessage */ 2 | (function (factory) { 3 | "use strict"; 4 | if (typeof define === 'function' && define.amd) { 5 | define(['jquery'], factory); 6 | } 7 | else if(typeof module !== 'undefined' && module.exports) { 8 | module.exports = factory(require('jquery')); 9 | } 10 | else { 11 | factory(jQuery); 12 | } 13 | }(function ($, undefined) { 14 | "use strict"; 15 | -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/src/jstree.changed.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ### Changed plugin 3 | * 4 | * This plugin adds more information to the `changed.jstree` event. The new data is contained in the `changed` event data property, and contains a lists of `selected` and `deselected` nodes. 5 | */ 6 | /*globals jQuery, define, exports, require, document */ 7 | (function (factory) { 8 | "use strict"; 9 | if (typeof define === 'function' && define.amd) { 10 | define('jstree.changed', ['jquery','jstree'], factory); 11 | } 12 | else if(typeof exports === 'object') { 13 | factory(require('jquery'), require('jstree')); 14 | } 15 | else { 16 | factory(jQuery, jQuery.jstree); 17 | } 18 | }(function ($, jstree, undefined) { 19 | "use strict"; 20 | 21 | if($.jstree.plugins.changed) { return; } 22 | 23 | $.jstree.plugins.changed = function (options, parent) { 24 | var last = []; 25 | this.trigger = function (ev, data) { 26 | var i, j; 27 | if(!data) { 28 | data = {}; 29 | } 30 | if(ev.replace('.jstree','') === 'changed') { 31 | data.changed = { selected : [], deselected : [] }; 32 | var tmp = {}; 33 | for(i = 0, j = last.length; i < j; i++) { 34 | tmp[last[i]] = 1; 35 | } 36 | for(i = 0, j = data.selected.length; i < j; i++) { 37 | if(!tmp[data.selected[i]]) { 38 | data.changed.selected.push(data.selected[i]); 39 | } 40 | else { 41 | tmp[data.selected[i]] = 2; 42 | } 43 | } 44 | for(i = 0, j = last.length; i < j; i++) { 45 | if(tmp[last[i]] === 1) { 46 | data.changed.deselected.push(last[i]); 47 | } 48 | } 49 | last = data.selected.slice(); 50 | } 51 | /** 52 | * triggered when selection changes (the "changed" plugin enhances the original event with more data) 53 | * @event 54 | * @name changed.jstree 55 | * @param {Object} node 56 | * @param {Object} action the action that caused the selection to change 57 | * @param {Array} selected the current selection 58 | * @param {Object} changed an object containing two properties `selected` and `deselected` - both arrays of node IDs, which were selected or deselected since the last changed event 59 | * @param {Object} event the event (if any) that triggered this changed event 60 | * @plugin changed 61 | */ 62 | parent.trigger.call(this, ev, data); 63 | }; 64 | this.refresh = function (skip_loading, forget_state) { 65 | last = []; 66 | return parent.refresh.apply(this, arguments); 67 | }; 68 | }; 69 | })); -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/src/jstree.conditionalselect.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ### Conditionalselect plugin 3 | * 4 | * This plugin allows defining a callback to allow or deny node selection by user input (activate node method). 5 | */ 6 | /*globals jQuery, define, exports, require, document */ 7 | (function (factory) { 8 | "use strict"; 9 | if (typeof define === 'function' && define.amd) { 10 | define('jstree.conditionalselect', ['jquery','jstree'], factory); 11 | } 12 | else if(typeof exports === 'object') { 13 | factory(require('jquery'), require('jstree')); 14 | } 15 | else { 16 | factory(jQuery, jQuery.jstree); 17 | } 18 | }(function ($, jstree, undefined) { 19 | "use strict"; 20 | 21 | if($.jstree.plugins.conditionalselect) { return; } 22 | 23 | /** 24 | * a callback (function) which is invoked in the instance's scope and receives two arguments - the node and the event that triggered the `activate_node` call. Returning false prevents working with the node, returning true allows invoking activate_node. Defaults to returning `true`. 25 | * @name $.jstree.defaults.checkbox.visible 26 | * @plugin checkbox 27 | */ 28 | $.jstree.defaults.conditionalselect = function () { return true; }; 29 | $.jstree.plugins.conditionalselect = function (options, parent) { 30 | // own function 31 | this.activate_node = function (obj, e) { 32 | if(this.settings.conditionalselect.call(this, this.get_node(obj), e)) { 33 | return parent.activate_node.call(this, obj, e); 34 | } 35 | }; 36 | }; 37 | 38 | })); -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/src/jstree.massload.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ### Massload plugin 3 | * 4 | * Adds massload functionality to jsTree, so that multiple nodes can be loaded in a single request (only useful with lazy loading). 5 | */ 6 | /*globals jQuery, define, exports, require, document */ 7 | (function (factory) { 8 | "use strict"; 9 | if (typeof define === 'function' && define.amd) { 10 | define('jstree.massload', ['jquery','jstree'], factory); 11 | } 12 | else if(typeof exports === 'object') { 13 | factory(require('jquery'), require('jstree')); 14 | } 15 | else { 16 | factory(jQuery, jQuery.jstree); 17 | } 18 | }(function ($, jstree, undefined) { 19 | "use strict"; 20 | 21 | if($.jstree.plugins.massload) { return; } 22 | 23 | /** 24 | * massload configuration 25 | * 26 | * It is possible to set this to a standard jQuery-like AJAX config. 27 | * In addition to the standard jQuery ajax options here you can supply functions for `data` and `url`, the functions will be run in the current instance's scope and a param will be passed indicating which node IDs need to be loaded, the return value of those functions will be used. 28 | * 29 | * You can also set this to a function, that function will receive the node IDs being loaded as argument and a second param which is a function (callback) which should be called with the result. 30 | * 31 | * Both the AJAX and the function approach rely on the same return value - an object where the keys are the node IDs, and the value is the children of that node as an array. 32 | * 33 | * { 34 | * "id1" : [{ "text" : "Child of ID1", "id" : "c1" }, { "text" : "Another child of ID1", "id" : "c2" }], 35 | * "id2" : [{ "text" : "Child of ID2", "id" : "c3" }] 36 | * } 37 | * 38 | * @name $.jstree.defaults.massload 39 | * @plugin massload 40 | */ 41 | $.jstree.defaults.massload = null; 42 | $.jstree.plugins.massload = function (options, parent) { 43 | this.init = function (el, options) { 44 | this._data.massload = {}; 45 | parent.init.call(this, el, options); 46 | }; 47 | this._load_nodes = function (nodes, callback, is_callback, force_reload) { 48 | var s = this.settings.massload, 49 | nodesString = JSON.stringify(nodes), 50 | toLoad = [], 51 | m = this._model.data, 52 | i, j, dom; 53 | if (!is_callback) { 54 | for(i = 0, j = nodes.length; i < j; i++) { 55 | if(!m[nodes[i]] || ( (!m[nodes[i]].state.loaded && !m[nodes[i]].state.failed) || force_reload) ) { 56 | toLoad.push(nodes[i]); 57 | dom = this.get_node(nodes[i], true); 58 | if (dom && dom.length) { 59 | dom.addClass("jstree-loading").attr('aria-busy',true); 60 | } 61 | } 62 | } 63 | this._data.massload = {}; 64 | if (toLoad.length) { 65 | if($.isFunction(s)) { 66 | return s.call(this, toLoad, $.proxy(function (data) { 67 | var i, j; 68 | if(data) { 69 | for(i in data) { 70 | if(data.hasOwnProperty(i)) { 71 | this._data.massload[i] = data[i]; 72 | } 73 | } 74 | } 75 | for(i = 0, j = nodes.length; i < j; i++) { 76 | dom = this.get_node(nodes[i], true); 77 | if (dom && dom.length) { 78 | dom.removeClass("jstree-loading").attr('aria-busy',false); 79 | } 80 | } 81 | parent._load_nodes.call(this, nodes, callback, is_callback, force_reload); 82 | }, this)); 83 | } 84 | if(typeof s === 'object' && s && s.url) { 85 | s = $.extend(true, {}, s); 86 | if($.isFunction(s.url)) { 87 | s.url = s.url.call(this, toLoad); 88 | } 89 | if($.isFunction(s.data)) { 90 | s.data = s.data.call(this, toLoad); 91 | } 92 | return $.ajax(s) 93 | .done($.proxy(function (data,t,x) { 94 | var i, j; 95 | if(data) { 96 | for(i in data) { 97 | if(data.hasOwnProperty(i)) { 98 | this._data.massload[i] = data[i]; 99 | } 100 | } 101 | } 102 | for(i = 0, j = nodes.length; i < j; i++) { 103 | dom = this.get_node(nodes[i], true); 104 | if (dom && dom.length) { 105 | dom.removeClass("jstree-loading").attr('aria-busy',false); 106 | } 107 | } 108 | parent._load_nodes.call(this, nodes, callback, is_callback, force_reload); 109 | }, this)) 110 | .fail($.proxy(function (f) { 111 | parent._load_nodes.call(this, nodes, callback, is_callback, force_reload); 112 | }, this)); 113 | } 114 | } 115 | } 116 | return parent._load_nodes.call(this, nodes, callback, is_callback, force_reload); 117 | }; 118 | this._load_node = function (obj, callback) { 119 | var data = this._data.massload[obj.id], 120 | rslt = null, dom; 121 | if(data) { 122 | rslt = this[typeof data === 'string' ? '_append_html_data' : '_append_json_data']( 123 | obj, 124 | typeof data === 'string' ? $($.parseHTML(data)).filter(function () { return this.nodeType !== 3; }) : data, 125 | function (status) { callback.call(this, status); } 126 | ); 127 | dom = this.get_node(obj.id, true); 128 | if (dom && dom.length) { 129 | dom.removeClass("jstree-loading").attr('aria-busy',false); 130 | } 131 | delete this._data.massload[obj.id]; 132 | return rslt; 133 | } 134 | return parent._load_node.call(this, obj, callback); 135 | }; 136 | }; 137 | })); -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/src/jstree.sort.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ### Sort plugin 3 | * 4 | * Automatically sorts all siblings in the tree according to a sorting function. 5 | */ 6 | /*globals jQuery, define, exports, require */ 7 | (function (factory) { 8 | "use strict"; 9 | if (typeof define === 'function' && define.amd) { 10 | define('jstree.sort', ['jquery','jstree'], factory); 11 | } 12 | else if(typeof exports === 'object') { 13 | factory(require('jquery'), require('jstree')); 14 | } 15 | else { 16 | factory(jQuery, jQuery.jstree); 17 | } 18 | }(function ($, jstree, undefined) { 19 | "use strict"; 20 | 21 | if($.jstree.plugins.sort) { return; } 22 | 23 | /** 24 | * the settings function used to sort the nodes. 25 | * It is executed in the tree's context, accepts two nodes as arguments and should return `1` or `-1`. 26 | * @name $.jstree.defaults.sort 27 | * @plugin sort 28 | */ 29 | $.jstree.defaults.sort = function (a, b) { 30 | //return this.get_type(a) === this.get_type(b) ? (this.get_text(a) > this.get_text(b) ? 1 : -1) : this.get_type(a) >= this.get_type(b); 31 | return this.get_text(a) > this.get_text(b) ? 1 : -1; 32 | }; 33 | $.jstree.plugins.sort = function (options, parent) { 34 | this.bind = function () { 35 | parent.bind.call(this); 36 | this.element 37 | .on("model.jstree", $.proxy(function (e, data) { 38 | this.sort(data.parent, true); 39 | }, this)) 40 | .on("rename_node.jstree create_node.jstree", $.proxy(function (e, data) { 41 | this.sort(data.parent || data.node.parent, false); 42 | this.redraw_node(data.parent || data.node.parent, true); 43 | }, this)) 44 | .on("move_node.jstree copy_node.jstree", $.proxy(function (e, data) { 45 | this.sort(data.parent, false); 46 | this.redraw_node(data.parent, true); 47 | }, this)); 48 | }; 49 | /** 50 | * used to sort a node's children 51 | * @private 52 | * @name sort(obj [, deep]) 53 | * @param {mixed} obj the node 54 | * @param {Boolean} deep if set to `true` nodes are sorted recursively. 55 | * @plugin sort 56 | * @trigger search.jstree 57 | */ 58 | this.sort = function (obj, deep) { 59 | var i, j; 60 | obj = this.get_node(obj); 61 | if(obj && obj.children && obj.children.length) { 62 | obj.children.sort($.proxy(this.settings.sort, this)); 63 | if(deep) { 64 | for(i = 0, j = obj.children_d.length; i < j; i++) { 65 | this.sort(obj.children_d[i], false); 66 | } 67 | } 68 | } 69 | }; 70 | }; 71 | 72 | // include the sort plugin by default 73 | // $.jstree.defaults.plugins.push("sort"); 74 | })); -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/src/jstree.state.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ### State plugin 3 | * 4 | * Saves the state of the tree (selected nodes, opened nodes) on the user's computer using available options (localStorage, cookies, etc) 5 | */ 6 | /*globals jQuery, define, exports, require */ 7 | (function (factory) { 8 | "use strict"; 9 | if (typeof define === 'function' && define.amd) { 10 | define('jstree.state', ['jquery','jstree'], factory); 11 | } 12 | else if(typeof exports === 'object') { 13 | factory(require('jquery'), require('jstree')); 14 | } 15 | else { 16 | factory(jQuery, jQuery.jstree); 17 | } 18 | }(function ($, jstree, undefined) { 19 | "use strict"; 20 | 21 | if($.jstree.plugins.state) { return; } 22 | 23 | var to = false; 24 | /** 25 | * stores all defaults for the state plugin 26 | * @name $.jstree.defaults.state 27 | * @plugin state 28 | */ 29 | $.jstree.defaults.state = { 30 | /** 31 | * A string for the key to use when saving the current tree (change if using multiple trees in your project). Defaults to `jstree`. 32 | * @name $.jstree.defaults.state.key 33 | * @plugin state 34 | */ 35 | key : 'jstree', 36 | /** 37 | * A space separated list of events that trigger a state save. Defaults to `changed.jstree open_node.jstree close_node.jstree`. 38 | * @name $.jstree.defaults.state.events 39 | * @plugin state 40 | */ 41 | events : 'changed.jstree open_node.jstree close_node.jstree check_node.jstree uncheck_node.jstree', 42 | /** 43 | * Time in milliseconds after which the state will expire. Defaults to 'false' meaning - no expire. 44 | * @name $.jstree.defaults.state.ttl 45 | * @plugin state 46 | */ 47 | ttl : false, 48 | /** 49 | * A function that will be executed prior to restoring state with one argument - the state object. Can be used to clear unwanted parts of the state. 50 | * @name $.jstree.defaults.state.filter 51 | * @plugin state 52 | */ 53 | filter : false, 54 | /** 55 | * Should loaded nodes be restored (setting this to true means that it is possible that the whole tree will be loaded for some users - use with caution). Defaults to `false` 56 | * @name $.jstree.defaults.state.preserve_loaded 57 | * @plugin state 58 | */ 59 | preserve_loaded : false 60 | }; 61 | $.jstree.plugins.state = function (options, parent) { 62 | this.bind = function () { 63 | parent.bind.call(this); 64 | var bind = $.proxy(function () { 65 | this.element.on(this.settings.state.events, $.proxy(function () { 66 | if(to) { clearTimeout(to); } 67 | to = setTimeout($.proxy(function () { this.save_state(); }, this), 100); 68 | }, this)); 69 | /** 70 | * triggered when the state plugin is finished restoring the state (and immediately after ready if there is no state to restore). 71 | * @event 72 | * @name state_ready.jstree 73 | * @plugin state 74 | */ 75 | this.trigger('state_ready'); 76 | }, this); 77 | this.element 78 | .on("ready.jstree", $.proxy(function (e, data) { 79 | this.element.one("restore_state.jstree", bind); 80 | if(!this.restore_state()) { bind(); } 81 | }, this)); 82 | }; 83 | /** 84 | * save the state 85 | * @name save_state() 86 | * @plugin state 87 | */ 88 | this.save_state = function () { 89 | var tm = this.get_state(); 90 | if (!this.settings.state.preserve_loaded) { 91 | delete tm.core.loaded; 92 | } 93 | var st = { 'state' : tm, 'ttl' : this.settings.state.ttl, 'sec' : +(new Date()) }; 94 | $.vakata.storage.set(this.settings.state.key, JSON.stringify(st)); 95 | }; 96 | /** 97 | * restore the state from the user's computer 98 | * @name restore_state() 99 | * @plugin state 100 | */ 101 | this.restore_state = function () { 102 | var k = $.vakata.storage.get(this.settings.state.key); 103 | if(!!k) { try { k = JSON.parse(k); } catch(ex) { return false; } } 104 | if(!!k && k.ttl && k.sec && +(new Date()) - k.sec > k.ttl) { return false; } 105 | if(!!k && k.state) { k = k.state; } 106 | if(!!k && $.isFunction(this.settings.state.filter)) { k = this.settings.state.filter.call(this, k); } 107 | if(!!k) { 108 | if (!this.settings.state.preserve_loaded) { 109 | delete k.core.loaded; 110 | } 111 | this.element.one("set_state.jstree", function (e, data) { data.instance.trigger('restore_state', { 'state' : $.extend(true, {}, k) }); }); 112 | this.set_state(k); 113 | return true; 114 | } 115 | return false; 116 | }; 117 | /** 118 | * clear the state on the user's computer 119 | * @name clear_state() 120 | * @plugin state 121 | */ 122 | this.clear_state = function () { 123 | return $.vakata.storage.del(this.settings.state.key); 124 | }; 125 | }; 126 | 127 | (function ($, undefined) { 128 | $.vakata.storage = { 129 | // simply specifying the functions in FF throws an error 130 | set : function (key, val) { return window.localStorage.setItem(key, val); }, 131 | get : function (key) { return window.localStorage.getItem(key); }, 132 | del : function (key) { return window.localStorage.removeItem(key); } 133 | }; 134 | }($)); 135 | 136 | // include the state plugin by default 137 | // $.jstree.defaults.plugins.push("state"); 138 | })); -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/src/outro.js: -------------------------------------------------------------------------------- 1 | })); -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/src/sample.js: -------------------------------------------------------------------------------- 1 | /*global jQuery */ 2 | // wrap in IIFE and pass jQuery as $ 3 | (function ($, undefined) { 4 | "use strict"; 5 | 6 | // some private plugin stuff if needed 7 | var private_var = null; 8 | 9 | // extending the defaults 10 | $.jstree.defaults.sample = { 11 | sample_option : 'sample_val' 12 | }; 13 | 14 | // the actual plugin code 15 | $.jstree.plugins.sample = function (options, parent) { 16 | // own function 17 | this.sample_function = function (arg) { 18 | // you can chain this method if needed and available 19 | if(parent.sample_function) { parent.sample_function.call(this, arg); } 20 | }; 21 | 22 | // *SPECIAL* FUNCTIONS 23 | this.init = function (el, options) { 24 | // do not forget parent 25 | parent.init.call(this, el, options); 26 | }; 27 | // bind events if needed 28 | this.bind = function () { 29 | // call parent function first 30 | parent.bind.call(this); 31 | // do(stuff); 32 | }; 33 | // unbind events if needed (all in jquery namespace are taken care of by the core) 34 | this.unbind = function () { 35 | // do(stuff); 36 | // call parent function last 37 | parent.unbind.call(this); 38 | }; 39 | this.teardown = function () { 40 | // do not forget parent 41 | parent.teardown.call(this); 42 | }; 43 | // state management - get and restore 44 | this.get_state = function () { 45 | // always get state from parent first 46 | var state = parent.get_state.call(this); 47 | // add own stuff to state 48 | state.sample = { 'var' : 'val' }; 49 | return state; 50 | }; 51 | this.set_state = function (state, callback) { 52 | // only process your part if parent returns true 53 | // there will be multiple times with false 54 | if(parent.set_state.call(this, state, callback)) { 55 | // check the key you set above 56 | if(state.sample) { 57 | // do(stuff); // like calling this.sample_function(state.sample.var); 58 | // remove your part of the state, call again and RETURN FALSE, the next cycle will be TRUE 59 | delete state.sample; 60 | this.set_state(state, callback); 61 | return false; 62 | } 63 | // return true if your state is gone (cleared in the previous step) 64 | return true; 65 | } 66 | // parent was false - return false too 67 | return false; 68 | }; 69 | // node transportation 70 | this.get_json = function (obj, options, flat) { 71 | // get the node from the parent 72 | var tmp = parent.get_json.call(this, obj, options, flat), i, j; 73 | if($.isArray(tmp)) { 74 | for(i = 0, j = tmp.length; i < j; i++) { 75 | tmp[i].sample = 'value'; 76 | } 77 | } 78 | else { 79 | tmp.sample = 'value'; 80 | } 81 | // return the original / modified node 82 | return tmp; 83 | }; 84 | }; 85 | 86 | // attach to document ready if needed 87 | $(function () { 88 | // do(stuff); 89 | }); 90 | 91 | // you can include the sample plugin in all instances by default 92 | $.jstree.defaults.plugins.push("sample"); 93 | })(jQuery); -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/src/themes/base.less: -------------------------------------------------------------------------------- 1 | // base jstree 2 | .jstree-node, .jstree-children, .jstree-container-ul { display:block; margin:0; padding:0; list-style-type:none; list-style-image:none; } 3 | .jstree-node { white-space:nowrap; } 4 | .jstree-anchor { display:inline-block; color:black; white-space:nowrap; padding:0 4px 0 1px; margin:0; vertical-align:top; } 5 | .jstree-anchor:focus { outline:0; } 6 | .jstree-anchor, .jstree-anchor:link, .jstree-anchor:visited, .jstree-anchor:hover, .jstree-anchor:active { text-decoration:none; color:inherit; } 7 | .jstree-icon { display:inline-block; text-decoration:none; margin:0; padding:0; vertical-align:top; text-align:center; } 8 | .jstree-icon:empty { display:inline-block; text-decoration:none; margin:0; padding:0; vertical-align:top; text-align:center; } 9 | .jstree-ocl { cursor:pointer; } 10 | .jstree-leaf > .jstree-ocl { cursor:default; } 11 | .jstree .jstree-open > .jstree-children { display:block; } 12 | .jstree .jstree-closed > .jstree-children, 13 | .jstree .jstree-leaf > .jstree-children { display:none; } 14 | .jstree-anchor > .jstree-themeicon { margin-right:2px; } 15 | .jstree-no-icons .jstree-themeicon, 16 | .jstree-anchor > .jstree-themeicon-hidden { display:none; } 17 | .jstree-hidden, .jstree-node.jstree-hidden { display:none; } 18 | 19 | // base jstree rtl 20 | .jstree-rtl { 21 | .jstree-anchor { padding:0 1px 0 4px; } 22 | .jstree-anchor > .jstree-themeicon { margin-left:2px; margin-right:0; } 23 | .jstree-node { margin-left:0; } 24 | .jstree-container-ul > .jstree-node { margin-right:0; } 25 | } 26 | 27 | // base jstree wholerow 28 | .jstree-wholerow-ul { 29 | position:relative; 30 | display:inline-block; 31 | min-width:100%; 32 | .jstree-leaf > .jstree-ocl { cursor:pointer; } 33 | .jstree-anchor, .jstree-icon { position:relative; } 34 | .jstree-wholerow { width:100%; cursor:pointer; position:absolute; left:0; -webkit-user-select:none; -moz-user-select:none; -ms-user-select:none; user-select:none; } 35 | } 36 | 37 | // base contextmenu 38 | .jstree-contextmenu .jstree-anchor { 39 | -webkit-user-select: none; /* disable selection/Copy of UIWebView */ 40 | -webkit-touch-callout: none; /* disable the IOS popup when long-press on a link */ 41 | } 42 | .vakata-context { 43 | display:none; 44 | &, ul { margin:0; padding:2px; position:absolute; background:#f5f5f5; border:1px solid #979797; box-shadow:2px 2px 2px #999999; } 45 | ul { list-style:none; left:100%; margin-top:-2.7em; margin-left:-4px; } 46 | .vakata-context-right ul { left:auto; right:100%; margin-left:auto; margin-right:-4px; } 47 | li { 48 | list-style:none; 49 | > a { 50 | display:block; padding:0 2em 0 2em; text-decoration:none; width:auto; color:black; white-space:nowrap; line-height:2.4em; text-shadow:1px 1px 0 white; border-radius:1px; 51 | &:hover { position:relative; background-color:#e8eff7; box-shadow:0 0 2px #0a6aa1; } 52 | &.vakata-context-parent { background-image:url("data:image/gif;base64,R0lGODlhCwAHAIAAACgoKP///yH5BAEAAAEALAAAAAALAAcAAAIORI4JlrqN1oMSnmmZDQUAOw=="); background-position:right center; background-repeat:no-repeat; } 53 | } 54 | > a:focus { outline:0; } 55 | } 56 | .vakata-context-hover > a { position:relative; background-color:#e8eff7; box-shadow:0 0 2px #0a6aa1; } 57 | .vakata-context-separator { 58 | > a, > a:hover { background:white; border:0; border-top:1px solid #e2e3e3; height:1px; min-height:1px; max-height:1px; padding:0; margin:0 0 0 2.4em; border-left:1px solid #e0e0e0; text-shadow:0 0 0 transparent; box-shadow:0 0 0 transparent; border-radius:0; } 59 | } 60 | .vakata-contextmenu-disabled { 61 | a, a:hover { color:silver; background-color:transparent; border:0; box-shadow:0 0 0; } 62 | > a > i { filter: grayscale(100%); } 63 | } 64 | li > a { 65 | > i { text-decoration:none; display:inline-block; width:2.4em; height:2.4em; background:transparent; margin:0 0 0 -2em; vertical-align:top; text-align:center; line-height:2.4em; } 66 | > i:empty { width:2.4em; line-height:2.4em; } 67 | .vakata-contextmenu-sep { display:inline-block; width:1px; height:2.4em; background:white; margin:0 0.5em 0 0; border-left:1px solid #e2e3e3; } 68 | } 69 | .vakata-contextmenu-shortcut { font-size:0.8em; color:silver; opacity:0.5; display:none; } 70 | } 71 | .vakata-context-rtl { 72 | ul { left:auto; right:100%; margin-left:auto; margin-right:-4px; } 73 | li > a.vakata-context-parent { background-image:url("data:image/gif;base64,R0lGODlhCwAHAIAAACgoKP///yH5BAEAAAEALAAAAAALAAcAAAINjI+AC7rWHIsPtmoxLAA7"); background-position:left center; background-repeat:no-repeat; } 74 | .vakata-context-separator > a { margin:0 2.4em 0 0; border-left:0; border-right:1px solid #e2e3e3;} 75 | .vakata-context-left ul { right:auto; left:100%; margin-left:-4px; margin-right:auto; } 76 | li > a { 77 | > i { margin:0 -2em 0 0; } 78 | .vakata-contextmenu-sep { margin:0 0 0 0.5em; border-left-color:white; background:#e2e3e3; } 79 | } 80 | } 81 | 82 | // base drag'n'drop 83 | #jstree-marker { position: absolute; top:0; left:0; margin:-5px 0 0 0; padding:0; border-right:0; border-top:5px solid transparent; border-bottom:5px solid transparent; border-left:5px solid; width:0; height:0; font-size:0; line-height:0; } 84 | #jstree-dnd { 85 | line-height:16px; 86 | margin:0; 87 | padding:4px; 88 | .jstree-icon, 89 | .jstree-copy { display:inline-block; text-decoration:none; margin:0 2px 0 0; padding:0; width:16px; height:16px; } 90 | .jstree-ok { background:green; } 91 | .jstree-er { background:red; } 92 | .jstree-copy { margin:0 2px 0 2px; } 93 | } 94 | -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/src/themes/default-dark/32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/import/jstree/src/themes/default-dark/32px.png -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/src/themes/default-dark/40px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/import/jstree/src/themes/default-dark/40px.png -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/src/themes/default-dark/style.less: -------------------------------------------------------------------------------- 1 | /* jsTree default dark theme */ 2 | @theme-name: default-dark; 3 | @hovered-bg-color: #555; 4 | @hovered-shadow-color: #555; 5 | @disabled-color: #666666; 6 | @disabled-bg-color: #333333; 7 | @clicked-bg-color: #5fa2db; 8 | @clicked-shadow-color: #666666; 9 | @clicked-gradient-color-1: #5fa2db; 10 | @clicked-gradient-color-2: #5fa2db; 11 | @search-result-color: #ffffff; 12 | @mobile-wholerow-bg-color: #333333; 13 | @mobile-wholerow-shadow: #111111; 14 | @mobile-wholerow-bordert: #666; 15 | @mobile-wholerow-borderb: #000; 16 | @responsive: true; 17 | @image-path: ""; 18 | @base-height: 40px; 19 | 20 | @import "../mixins.less"; 21 | @import "../base.less"; 22 | @import "../main.less"; 23 | 24 | .jstree-@{theme-name} { 25 | background:#333; 26 | .jstree-anchor { color:#999; text-shadow:1px 1px 0 rgba(0,0,0,0.5); } 27 | .jstree-clicked, .jstree-checked { color:white; } 28 | .jstree-hovered { color:white; } 29 | #jstree-marker& { 30 | border-left-color:#999; 31 | background:transparent; 32 | } 33 | .jstree-anchor > .jstree-icon { opacity:0.75; } 34 | .jstree-clicked > .jstree-icon, 35 | .jstree-hovered > .jstree-icon, 36 | .jstree-checked > .jstree-icon { opacity:1; } 37 | } 38 | // theme variants 39 | .jstree-@{theme-name} { 40 | &.jstree-rtl .jstree-node { background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAACAQMAAAB49I5GAAAABlBMVEUAAACZmZl+9SADAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjAAMOBgAAGAAJMwQHdQAAAABJRU5ErkJggg=="); } 41 | &.jstree-rtl .jstree-last { background:transparent; } 42 | } 43 | .jstree-@{theme-name}-small { 44 | &.jstree-rtl .jstree-node { background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAACAQMAAABv1h6PAAAABlBMVEUAAACZmZl+9SADAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjAAMHBgAAiABBI4gz9AAAAABJRU5ErkJggg=="); } 45 | &.jstree-rtl .jstree-last { background:transparent; } 46 | } 47 | .jstree-@{theme-name}-large { 48 | &.jstree-rtl .jstree-node { background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAACAQMAAAAD0EyKAAAABlBMVEUAAACZmZl+9SADAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjgIIGBgABCgCBvVLXcAAAAABJRU5ErkJggg=="); } 49 | &.jstree-rtl .jstree-last { background:transparent; } 50 | } -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/src/themes/default-dark/throbber.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/import/jstree/src/themes/default-dark/throbber.gif -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/src/themes/default/32px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/import/jstree/src/themes/default/32px.png -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/src/themes/default/40px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/import/jstree/src/themes/default/40px.png -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/src/themes/default/style.less: -------------------------------------------------------------------------------- 1 | /* jsTree default theme */ 2 | @theme-name: default; 3 | @hovered-bg-color: #e7f4f9; 4 | @hovered-shadow-color: #cccccc; 5 | @disabled-color: #666666; 6 | @disabled-bg-color: #efefef; 7 | @clicked-bg-color: #beebff; 8 | @clicked-shadow-color: #999999; 9 | @clicked-gradient-color-1: #beebff; 10 | @clicked-gradient-color-2: #a8e4ff; 11 | @search-result-color: #8b0000; 12 | @mobile-wholerow-bg-color: #ebebeb; 13 | @mobile-wholerow-shadow: #666666; 14 | @mobile-wholerow-bordert: rgba(255,255,255,0.7); 15 | @mobile-wholerow-borderb: rgba(64,64,64,0.2); 16 | @responsive: true; 17 | @image-path: ""; 18 | @base-height: 40px; 19 | 20 | @import "../mixins.less"; 21 | @import "../base.less"; 22 | @import "../main.less"; -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/src/themes/default/throbber.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/import/jstree/src/themes/default/throbber.gif -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/src/themes/main.less: -------------------------------------------------------------------------------- 1 | .jstree-@{theme-name} { 2 | .jstree-node, 3 | .jstree-icon { background-repeat:no-repeat; background-color:transparent; } 4 | .jstree-anchor, 5 | .jstree-animated, 6 | .jstree-wholerow { transition:background-color 0.15s, box-shadow 0.15s; } 7 | .jstree-hovered { background:@hovered-bg-color; border-radius:2px; box-shadow:inset 0 0 1px @hovered-shadow-color; } 8 | .jstree-context { background:@hovered-bg-color; border-radius:2px; box-shadow:inset 0 0 1px @hovered-shadow-color; } 9 | .jstree-clicked { background:@clicked-bg-color; border-radius:2px; box-shadow:inset 0 0 1px @clicked-shadow-color; } 10 | .jstree-no-icons .jstree-anchor > .jstree-themeicon { display:none; } 11 | .jstree-disabled { 12 | background:transparent; color:@disabled-color; 13 | &.jstree-hovered { background:transparent; box-shadow:none; } 14 | &.jstree-clicked { background:@disabled-bg-color; } 15 | > .jstree-icon { opacity:0.8; filter: url("data:image/svg+xml;utf8,#jstree-grayscale"); /* Firefox 10+ */ filter: gray; /* IE6-9 */ -webkit-filter: grayscale(100%); /* Chrome 19+ & Safari 6+ */ } 16 | } 17 | // search 18 | .jstree-search { font-style:italic; color:@search-result-color; font-weight:bold; } 19 | // checkboxes 20 | .jstree-no-checkboxes .jstree-checkbox { display:none !important; } 21 | &.jstree-checkbox-no-clicked { 22 | .jstree-clicked { 23 | background:transparent; 24 | box-shadow:none; 25 | &.jstree-hovered { background:@hovered-bg-color; } 26 | } 27 | > .jstree-wholerow-ul .jstree-wholerow-clicked { 28 | background:transparent; 29 | &.jstree-wholerow-hovered { background:@hovered-bg-color; } 30 | } 31 | } 32 | // stripes 33 | > .jstree-striped { min-width:100%; display:inline-block; background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAkCAMAAAB/qqA+AAAABlBMVEUAAAAAAAClZ7nPAAAAAnRSTlMNAMM9s3UAAAAXSURBVHjajcEBAQAAAIKg/H/aCQZ70AUBjAATb6YPDgAAAABJRU5ErkJggg==") left top repeat; } 34 | // wholerow 35 | > .jstree-wholerow-ul .jstree-hovered, 36 | > .jstree-wholerow-ul .jstree-clicked { background:transparent; box-shadow:none; border-radius:0; } 37 | .jstree-wholerow { -moz-box-sizing:border-box; -webkit-box-sizing:border-box; box-sizing:border-box; } 38 | .jstree-wholerow-hovered { background:@hovered-bg-color; } 39 | .jstree-wholerow-clicked { .gradient(@clicked-gradient-color-1, @clicked-gradient-color-2); } 40 | } 41 | 42 | // theme variants 43 | .jstree-@{theme-name} { 44 | .jstree-theme(24px, "@{image-path}32px.png", 32px); 45 | &.jstree-rtl .jstree-node { background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAACAQMAAAB49I5GAAAABlBMVEUAAAAdHRvEkCwcAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjAAMOBgAAGAAJMwQHdQAAAABJRU5ErkJggg=="); } 46 | &.jstree-rtl .jstree-last { background:transparent; } 47 | } 48 | .jstree-@{theme-name}-small { 49 | .jstree-theme(18px, "@{image-path}32px.png", 32px); 50 | &.jstree-rtl .jstree-node { background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAACAQMAAABv1h6PAAAABlBMVEUAAAAdHRvEkCwcAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjAAMHBgAAiABBI4gz9AAAAABJRU5ErkJggg=="); } 51 | &.jstree-rtl .jstree-last { background:transparent; } 52 | } 53 | .jstree-@{theme-name}-large { 54 | .jstree-theme(32px, "@{image-path}32px.png", 32px); 55 | &.jstree-rtl .jstree-node { background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAACAQMAAAAD0EyKAAAABlBMVEUAAAAdHRvEkCwcAAAAAXRSTlMAQObYZgAAAAxJREFUCNdjgIIGBgABCgCBvVLXcAAAAABJRU5ErkJggg=="); } 56 | &.jstree-rtl .jstree-last { background:transparent; } 57 | } 58 | 59 | // mobile theme attempt 60 | @media (max-width: 768px) { 61 | #jstree-dnd.jstree-dnd-responsive when (@responsive = true) { 62 | line-height:@base-height; font-weight:bold; font-size:1.1em; text-shadow:1px 1px white; 63 | > i { background:transparent; width:@base-height; height:@base-height; } 64 | > .jstree-ok { background-image:url("@{image-path}@{base-height}.png"); background-position:0 -(@base-height * 5); background-size:(@base-height * 3) (@base-height * 6); } 65 | > .jstree-er { background-image:url("@{image-path}@{base-height}.png"); background-position:-(@base-height * 1) -(@base-height * 5); background-size:(@base-height * 3) (@base-height * 6); } 66 | } 67 | #jstree-marker.jstree-dnd-responsive when (@responsive = true) { 68 | border-left-width:10px; 69 | border-top-width:10px; 70 | border-bottom-width:10px; 71 | margin-top:-10px; 72 | } 73 | } 74 | 75 | .jstree-@{theme-name}-responsive when (@responsive = true) { 76 | @import "responsive.less"; 77 | } 78 | -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/src/themes/responsive.less: -------------------------------------------------------------------------------- 1 | @media (max-width: 768px) { 2 | // background image 3 | .jstree-icon { background-image:url("@{image-path}@{base-height}.png"); } 4 | 5 | .jstree-node, 6 | .jstree-leaf > .jstree-ocl { background:transparent; } 7 | 8 | .jstree-node { min-height:@base-height; line-height:@base-height; margin-left:@base-height; min-width:@base-height; white-space:nowrap; } 9 | .jstree-anchor { line-height:@base-height; height:@base-height; } 10 | .jstree-icon, .jstree-icon:empty { width:@base-height; height:@base-height; line-height:@base-height; } 11 | 12 | > .jstree-container-ul > .jstree-node { margin-left:0; } 13 | &.jstree-rtl .jstree-node { margin-left:0; margin-right:@base-height; background:transparent; } 14 | &.jstree-rtl .jstree-container-ul > .jstree-node { margin-right:0; } 15 | 16 | .jstree-ocl, 17 | .jstree-themeicon, 18 | .jstree-checkbox { background-size:(@base-height * 3) (@base-height * 6); } 19 | .jstree-leaf > .jstree-ocl, 20 | &.jstree-rtl .jstree-leaf > .jstree-ocl { background:transparent; } 21 | .jstree-open > .jstree-ocl { background-position:0 0 !important; } 22 | .jstree-closed > .jstree-ocl { background-position:0 -(@base-height * 1) !important; } 23 | &.jstree-rtl .jstree-closed > .jstree-ocl { background-position:-(@base-height * 1) 0 !important; } 24 | 25 | .jstree-themeicon { background-position:-(@base-height * 1) -(@base-height * 1); } 26 | 27 | .jstree-checkbox, .jstree-checkbox:hover { background-position:-(@base-height * 1) -(@base-height * 2); } 28 | &.jstree-checkbox-selection .jstree-clicked > .jstree-checkbox, 29 | &.jstree-checkbox-selection .jstree-clicked > .jstree-checkbox:hover, 30 | .jstree-checked > .jstree-checkbox, 31 | .jstree-checked > .jstree-checkbox:hover { background-position:0 -(@base-height * 2); } 32 | .jstree-anchor > .jstree-undetermined, .jstree-anchor > .jstree-undetermined:hover { background-position:0 -(@base-height * 3); } 33 | 34 | .jstree-anchor { font-weight:bold; font-size:1.1em; text-shadow:1px 1px white; } 35 | 36 | > .jstree-striped { background:transparent; } 37 | .jstree-wholerow { border-top:1px solid @mobile-wholerow-bordert; border-bottom:1px solid @mobile-wholerow-borderb; background:@mobile-wholerow-bg-color; height:@base-height; } 38 | .jstree-wholerow-hovered { background:@hovered-bg-color; } 39 | .jstree-wholerow-clicked { background:@clicked-bg-color; } 40 | 41 | // thanks to PHOTONUI 42 | .jstree-children .jstree-last > .jstree-wholerow { box-shadow: inset 0 -6px 3px -5px @mobile-wholerow-shadow; } 43 | .jstree-children .jstree-open > .jstree-wholerow { box-shadow: inset 0 6px 3px -5px @mobile-wholerow-shadow; border-top:0; } 44 | .jstree-children .jstree-open + .jstree-open { box-shadow:none; } 45 | 46 | // experiment 47 | .jstree-node, 48 | .jstree-icon, 49 | .jstree-node > .jstree-ocl, 50 | .jstree-themeicon, 51 | .jstree-checkbox { background-image:url("@{image-path}@{base-height}.png"); background-size:(@base-height * 3) (@base-height * 6); } 52 | 53 | .jstree-node { background-position:-(@base-height * 2) 0; background-repeat:repeat-y; } 54 | .jstree-last { background:transparent; } 55 | .jstree-leaf > .jstree-ocl { background-position:-(@base-height * 1) -(@base-height * 3); } 56 | .jstree-last > .jstree-ocl { background-position:-(@base-height * 1) -(@base-height * 4); } 57 | /* 58 | .jstree-open > .jstree-ocl, 59 | .jstree-closed > .jstree-ocl { border-radius:20px; background-color:white; } 60 | */ 61 | 62 | .jstree-themeicon-custom { background-color:transparent; background-image:none; background-position:0 0; } 63 | .jstree-file { background:url("@{image-path}@{base-height}.png") 0 -(@base-height * 4) no-repeat; background-size:(@base-height * 3) (@base-height * 6); } 64 | .jstree-folder { background:url("@{image-path}@{base-height}.png") -(@base-height * 1) -(@base-height * 1) no-repeat; background-size:(@base-height * 3) (@base-height * 6); } 65 | 66 | > .jstree-container-ul > .jstree-node { margin-left:0; margin-right:0; } 67 | } 68 | -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/src/vakata-jstree.js: -------------------------------------------------------------------------------- 1 | (function (factory) { 2 | "use strict"; 3 | if (typeof define === 'function' && define.amd) { 4 | define('jstree.checkbox', ['jquery','jstree'], factory); 5 | } 6 | else if(typeof exports === 'object') { 7 | factory(require('jquery'), require('jstree')); 8 | } 9 | else { 10 | factory(jQuery); 11 | } 12 | }(function ($, undefined) { 13 | "use strict"; 14 | if(window.customElements && Object && Object.create) { 15 | var proto = Object.create(HTMLElement.prototype); 16 | proto.createdCallback = function () { 17 | var c = { core : {}, plugins : [] }, i; 18 | for(i in $.jstree.plugins) { 19 | if($.jstree.plugins.hasOwnProperty(i) && this.attributes[i]) { 20 | c.plugins.push(i); 21 | if(this.getAttribute(i) && JSON.parse(this.getAttribute(i))) { 22 | c[i] = JSON.parse(this.getAttribute(i)); 23 | } 24 | } 25 | } 26 | for(i in $.jstree.defaults.core) { 27 | if($.jstree.defaults.core.hasOwnProperty(i) && this.attributes[i]) { 28 | c.core[i] = JSON.parse(this.getAttribute(i)) || this.getAttribute(i); 29 | } 30 | } 31 | $(this).jstree(c); 32 | }; 33 | // proto.attributeChangedCallback = function (name, previous, value) { }; 34 | try { 35 | window.customElements.define("vakata-jstree", function() {}, { prototype: proto }); 36 | } catch (ignore) { } 37 | } 38 | })); 39 | -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/test/unit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Basic Test Suite 6 | 7 | 8 | 9 | 10 | 11 |
12 |
this had better work.
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/test/unit/test.js: -------------------------------------------------------------------------------- 1 | test('basic test', function() { 2 | expect(1); 3 | ok(true, 'this had better work.'); 4 | }); 5 | 6 | 7 | test('can access the DOM', function() { 8 | expect(1); 9 | var fixture = document.getElementById('qunit-fixture'); 10 | equal(fixture.innerText || fixture.textContent, 'this had better work.', 'should be able to access the DOM.'); 11 | }); -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/test/visual/desktop/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Light theme visual tests 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 | 31 |
32 |
33 |
34 | 35 | 36 | 37 | 43 | 44 | -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/test/visual/mobile/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mobile theme visual tests 6 | 7 | 8 | 9 | 10 | 11 |
12 | 29 |
30 |
31 |
32 | 33 | 34 | 35 | 41 | 42 | -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/test/visual/screenshots/desktop/.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/import/jstree/test/visual/screenshots/desktop/.png -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/test/visual/screenshots/desktop/desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/import/jstree/test/visual/screenshots/desktop/desktop.png -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/test/visual/screenshots/desktop/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/import/jstree/test/visual/screenshots/desktop/home.png -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/test/visual/screenshots/mobile/.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/import/jstree/test/visual/screenshots/mobile/.png -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/test/visual/screenshots/mobile/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/import/jstree/test/visual/screenshots/mobile/home.png -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/jstree/test/visual/screenshots/mobile/mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/import/jstree/test/visual/screenshots/mobile/mobile.png -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/typicons/LICENCE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Stephen Hutchings (http://www.s-ings.com/). 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | 5 | This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL 6 | 7 | SIL OPEN FONT LICENSE 8 | 9 | Version 1.1 - 26 February 2007 10 | 11 | PREAMBLE 12 | The goals of the Open Font License (OFL) are to stimulate worldwide 13 | development of collaborative font projects, to support the font creation 14 | efforts of academic and linguistic communities, and to provide a free and 15 | open framework in which fonts may be shared and improved in partnership 16 | with others. 17 | 18 | The OFL allows the licensed fonts to be used, studied, modified and 19 | redistributed freely as long as they are not sold by themselves. The 20 | fonts, including any derivative works, can be bundled, embedded, 21 | redistributed and/or sold with any software provided that any reserved 22 | names are not used by derivative works. The fonts and derivatives, 23 | however, cannot be released under any other type of license. The 24 | requirement for fonts to remain under this license does not apply 25 | to any document created using the fonts or their derivatives. 26 | 27 | DEFINITIONS 28 | "Font Software" refers to the set of files released by the Copyright 29 | Holder(s) under this license and clearly marked as such. This may 30 | include source files, build scripts and documentation. 31 | 32 | "Reserved Font Name" refers to any names specified as such after the 33 | copyright statement(s). 34 | 35 | "Original Version" refers to the collection of Font Software components as 36 | distributed by the Copyright Holder(s). 37 | 38 | "Modified Version" refers to any derivative made by adding to, deleting, 39 | or substituting — in part or in whole — any of the components of the 40 | Original Version, by changing formats or by porting the Font Software to a 41 | new environment. 42 | 43 | "Author" refers to any designer, engineer, programmer, technical 44 | writer or other person who contributed to the Font Software. 45 | 46 | PERMISSION & CONDITIONS 47 | Permission is hereby granted, free of charge, to any person obtaining 48 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 49 | redistribute, and sell modified and unmodified copies of the Font 50 | Software, subject to the following conditions: 51 | 52 | 1) Neither the Font Software nor any of its individual components, 53 | in Original or Modified Versions, may be sold by itself. 54 | 55 | 2) Original or Modified Versions of the Font Software may be bundled, 56 | redistributed and/or sold with any software, provided that each copy 57 | contains the above copyright notice and this license. These can be 58 | included either as stand-alone text files, human-readable headers or 59 | in the appropriate machine-readable metadata fields within text or 60 | binary files as long as those fields can be easily viewed by the user. 61 | 62 | 3) No Modified Version of the Font Software may use the Reserved Font 63 | Name(s) unless explicit written permission is granted by the corresponding 64 | Copyright Holder. This restriction only applies to the primary font name as 65 | presented to the users. 66 | 67 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 68 | Software shall not be used to promote, endorse or advertise any 69 | Modified Version, except to acknowledge the contribution(s) of the 70 | Copyright Holder(s) and the Author(s) or with their explicit written 71 | permission. 72 | 73 | 5) The Font Software, modified or unmodified, in part or in whole, 74 | must be distributed entirely under this license, and must not be 75 | distributed under any other license. The requirement for fonts to 76 | remain under this license does not apply to any document created 77 | using the Font Software. 78 | 79 | TERMINATION 80 | This license becomes null and void if any of the above conditions are 81 | not met. 82 | 83 | DISCLAIMER 84 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 85 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 86 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 87 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 88 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 89 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 90 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 91 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 92 | OTHER DEALINGS IN THE FONT SOFTWARE. 93 | -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/typicons/typicons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/import/typicons/typicons.eot -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/typicons/typicons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/import/typicons/typicons.ttf -------------------------------------------------------------------------------- /app/YtManagerApp/static/YtManagerApp/import/typicons/typicons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/static/YtManagerApp/import/typicons/typicons.woff -------------------------------------------------------------------------------- /app/YtManagerApp/templates/YtManagerApp/controls/folder_create_modal.html: -------------------------------------------------------------------------------- 1 | {% extends 'YtManagerApp/controls/modal.html' %} 2 | {% load crispy_forms_tags %} 3 | 4 | {% block modal_title %} 5 | New folder 6 | {% endblock modal_title %} 7 | 8 | {% block modal_content %} 9 |
10 | {{ block.super }} 11 |
12 | {% endblock %} 13 | 14 | {% block modal_body %} 15 | {% crispy form %} 16 | {% endblock modal_body %} 17 | 18 | {% block modal_footer %} 19 | 20 | 21 | {% endblock modal_footer %} -------------------------------------------------------------------------------- /app/YtManagerApp/templates/YtManagerApp/controls/folder_delete_modal.html: -------------------------------------------------------------------------------- 1 | {% extends 'YtManagerApp/controls/modal.html' %} 2 | {% load crispy_forms_tags %} 3 | 4 | {% block modal_title %} 5 | Delete folder 6 | {% endblock modal_title %} 7 | 8 | {% block modal_content %} 9 |
10 | {% csrf_token %} 11 | {{ block.super }} 12 |
13 | {% endblock %} 14 | 15 | {% block modal_body %} 16 |

Are you sure you want to delete folder "{{ object }}" and all its subfolders?

17 | {{ form | crispy }} 18 | {% endblock modal_body %} 19 | 20 | {% block modal_footer %} 21 | 22 | 23 | {% endblock modal_footer %} -------------------------------------------------------------------------------- /app/YtManagerApp/templates/YtManagerApp/controls/folder_update_modal.html: -------------------------------------------------------------------------------- 1 | {% extends 'YtManagerApp/controls/modal.html' %} 2 | {% load crispy_forms_tags %} 3 | 4 | {% block modal_title %} 5 | Edit folder 6 | {% endblock modal_title %} 7 | 8 | {% block modal_content %} 9 |
10 | {{ block.super }} 11 |
12 | {% endblock %} 13 | 14 | {% block modal_body %} 15 | {% crispy form %} 16 | {% endblock modal_body %} 17 | 18 | {% block modal_footer %} 19 | 20 | 21 | {% endblock modal_footer %} -------------------------------------------------------------------------------- /app/YtManagerApp/templates/YtManagerApp/controls/modal.html: -------------------------------------------------------------------------------- 1 | {% block modal_stylesheets %} 2 | {% endblock %} 3 | 4 | 41 | 42 | {% block modal_scripts %} 43 | {% endblock %} -------------------------------------------------------------------------------- /app/YtManagerApp/templates/YtManagerApp/controls/setup_errors_banner.html: -------------------------------------------------------------------------------- 1 | {% if config_errors %} 2 |
3 |

Attention! Some critical configuration errors have been found!

4 | 9 |

Until these problems are fixed, the server may have encounter serious problems while running. 10 | Please correct these errors, and then restart the server.

11 |
12 | {% endif %} 13 | 14 | {% if config_warnings %} 15 |
16 |

Warning: some configuration problems have been found!

17 | 22 |

We recommend that you fix these issues before continuing.

23 |
24 | {% endif %} -------------------------------------------------------------------------------- /app/YtManagerApp/templates/YtManagerApp/controls/subscription_create_modal.html: -------------------------------------------------------------------------------- 1 | {% extends 'YtManagerApp/controls/modal.html' %} 2 | {% load crispy_forms_tags %} 3 | 4 | {% block modal_title %} 5 | New subscription 6 | {% endblock modal_title %} 7 | 8 | {% block modal_content %} 9 |
10 | {{ block.super }} 11 |
12 | {% endblock %} 13 | 14 | {% block modal_body %} 15 | {% crispy form %} 16 | {% endblock modal_body %} 17 | 18 | {% block modal_footer %} 19 | 20 | 21 | {% endblock modal_footer %} -------------------------------------------------------------------------------- /app/YtManagerApp/templates/YtManagerApp/controls/subscription_delete_modal.html: -------------------------------------------------------------------------------- 1 | {% extends 'YtManagerApp/controls/modal.html' %} 2 | {% load crispy_forms_tags %} 3 | 4 | {% block modal_title %} 5 | Delete subscription 6 | {% endblock modal_title %} 7 | 8 | {% block modal_content %} 9 |
10 | {% csrf_token %} 11 | {{ block.super }} 12 |
13 | {% endblock %} 14 | 15 | {% block modal_body %} 16 |

Are you sure you want to delete subscription "{{ object }}"?

17 | {{ form | crispy }} 18 | {% endblock modal_body %} 19 | 20 | {% block modal_footer %} 21 | 22 | 23 | {% endblock modal_footer %} -------------------------------------------------------------------------------- /app/YtManagerApp/templates/YtManagerApp/controls/subscription_update_modal.html: -------------------------------------------------------------------------------- 1 | {% extends 'YtManagerApp/controls/modal.html' %} 2 | {% load crispy_forms_tags %} 3 | 4 | {% block modal_title %} 5 | Edit subscription 6 | {% endblock modal_title %} 7 | 8 | {% block modal_content %} 9 |
10 | {{ block.super }} 11 |
12 | {% endblock %} 13 | 14 | {% block modal_body %} 15 | {% crispy form %} 16 | {% endblock modal_body %} 17 | 18 | {% block modal_footer %} 19 | 20 | 21 | 22 | {% endblock modal_footer %} 23 | 24 | {% block modal_scripts %} 25 | 33 | {% endblock %} 34 | 35 | -------------------------------------------------------------------------------- /app/YtManagerApp/templates/YtManagerApp/controls/subscriptions_import_modal.html: -------------------------------------------------------------------------------- 1 | {% extends 'YtManagerApp/controls/modal.html' %} 2 | {% load crispy_forms_tags %} 3 | 4 | {% block modal_title %} 5 | Import subscriptions 6 | {% endblock modal_title %} 7 | 8 | {% block modal_content %} 9 |
11 | {{ block.super }} 12 |
13 | {% endblock %} 14 | 15 | {% block modal_body %} 16 | {% crispy form %} 17 | {% endblock modal_body %} 18 | 19 | {% block modal_footer %} 20 | 21 | 22 | {% endblock modal_footer %} -------------------------------------------------------------------------------- /app/YtManagerApp/templates/YtManagerApp/first_time_setup/done.html: -------------------------------------------------------------------------------- 1 | {% extends "YtManagerApp/master_default.html" %} 2 | {% load crispy_forms_tags %} 3 | 4 | {% block body %} 5 | 6 |
7 |

Done!

8 |

The setup is finished, and the application is now ready to use!

9 | {% crispy form %} 10 |
11 | 12 | {% endblock body %} -------------------------------------------------------------------------------- /app/YtManagerApp/templates/YtManagerApp/first_time_setup/step0_welcome.html: -------------------------------------------------------------------------------- 1 | {% extends "YtManagerApp/master_default.html" %} 2 | {% load crispy_forms_tags %} 3 | 4 | {% block body %} 5 | 6 |
7 | 8 | {% include "YtManagerApp/controls/setup_errors_banner.html" %} 9 | 10 |

Welcome

11 |

This wizard will guide you through setting up the application.

12 | {% crispy form %} 13 |
14 | 15 | {% endblock body %} -------------------------------------------------------------------------------- /app/YtManagerApp/templates/YtManagerApp/first_time_setup/step1_apikey.html: -------------------------------------------------------------------------------- 1 | {% extends "YtManagerApp/master_default.html" %} 2 | {% load crispy_forms_tags %} 3 | {% load static %} 4 | 5 | {% block body %} 6 | 7 |
8 | 9 |

Step 1: Set up YouTube API key (optional)

10 |

This program uses the YouTube API in order to obtain information about videos, channels and playlists. 11 | To access this API, YouTube requires users to register on their site and request an API key. YouTube 12 | uses this key to control access and limit the number of requests made to their platform, in order to prevent abuses.

13 | 14 |

To use this program, it is recommended, but not required, that you create an API key for your own account. While a key 15 | is already provided the developer, there is a chance that the quota limits will be reached, which will prevent 16 | this program from reaching YouTube. Follow the steps below, or press Skip to use the provided key.

17 | 18 |

19 | 22 |

23 | 24 |
25 |
    26 |
  1. Visit https://developers.google.com/, log in or create an account, if necessary.
  2. 27 |
  3. Go to , and click on the Create project button. 28 | 29 |
  4. 30 |
  5. Give a name to the project, and then click on Create. Wait for a few seconds, until Google finishes creating the project. 31 | 32 |
  6. 33 |
  7. Make sure the newly created project is selected in the top bar and then, in the left sidebar, 34 | go to APIs & Services - Library. 35 | 36 |
  8. 37 |
  9. Find and click on the YouTube Data API v3 from the list. 38 | 39 |
  10. 40 |
  11. Click Enable to enable the YouTube API for your account. 41 | 42 |
  12. 43 |
  13. In the navigation sidebar, go to the Credentials page. 44 | 45 |
  14. 46 |
  15. Click on Create credentials. 47 | 48 |
  16. 49 |
  17. Fill the requested information; we will need access to the YouTube Data v3, 50 | we will be calling the API from a Web server, and we will access the Public data. 51 | After filling the information, click on the What credentials do I need? button. 52 | 53 |
  18. 54 |
  19. Copy the created key in the box below, and then hit Done. 55 | 56 |
  20. 57 |
58 |
59 | 60 | {% crispy form %} 61 | 62 |
63 | 64 | {% endblock body %} -------------------------------------------------------------------------------- /app/YtManagerApp/templates/YtManagerApp/first_time_setup/step2_admin.html: -------------------------------------------------------------------------------- 1 | {% extends "YtManagerApp/master_default.html" %} 2 | {% load crispy_forms_tags %} 3 | {% load static %} 4 | 5 | {% block body %} 6 | 7 |
8 | 9 |

Step 2: Create an administrator account

10 | 11 | {% crispy form %} 12 | 13 |
14 | 15 | {% endblock body %} -------------------------------------------------------------------------------- /app/YtManagerApp/templates/YtManagerApp/first_time_setup/step3_configure.html: -------------------------------------------------------------------------------- 1 | {% extends "YtManagerApp/master_default.html" %} 2 | {% load crispy_forms_tags %} 3 | {% load static %} 4 | 5 | {% block body %} 6 | 7 |
8 | 9 |

Step 3: Configure the server

10 | 11 |

Here you can customize some basic options for the application. There are many more options which can be changed in the settings page.

12 | 13 | {% crispy form %} 14 | 15 |
16 | 17 | {% endblock body %} -------------------------------------------------------------------------------- /app/YtManagerApp/templates/YtManagerApp/index.html: -------------------------------------------------------------------------------- 1 | {% extends "YtManagerApp/master_default.html" %} 2 | {% load static %} 3 | {% load crispy_forms_tags %} 4 | 5 | {% block stylesheets %} 6 | 7 | {% endblock %} 8 | 9 | {% block scripts %} 10 | 11 | 14 | {% endblock %} 15 | 16 | {% block body %} 17 | 18 | 26 | 27 | {% include 'YtManagerApp/controls/setup_errors_banner.html' %} 28 | 29 |
30 | 31 |
32 | {# Tree toolbar #} 33 | 59 | 60 |
61 |
62 |
63 |
64 |
65 |
66 | 67 |
68 | {# Video toolbar #} 69 | 72 | 73 |
74 |
75 | 76 | 81 |
82 |
83 | 84 | {% endblock %} 85 | -------------------------------------------------------------------------------- /app/YtManagerApp/templates/YtManagerApp/index_unauthenticated.html: -------------------------------------------------------------------------------- 1 | {% extends "YtManagerApp/master_default.html" %} 2 | {% load static %} 3 | 4 | {% block body %} 5 | 6 | {% include 'YtManagerApp/controls/setup_errors_banner.html' %} 7 | 8 |

Hello

9 |

Please log in to continue

10 | 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /app/YtManagerApp/templates/YtManagerApp/settings.html: -------------------------------------------------------------------------------- 1 | {% extends "YtManagerApp/master_default.html" %} 2 | {% load crispy_forms_tags %} 3 | 4 | {% block body %} 5 | 6 |
7 |

Settings

8 |

If no value is set, the server's defaults will be used.

9 | {% crispy form %} 10 |
11 | 12 | {% endblock body %} -------------------------------------------------------------------------------- /app/YtManagerApp/templates/YtManagerApp/settings_admin.html: -------------------------------------------------------------------------------- 1 | {% extends "YtManagerApp/master_default.html" %} 2 | {% load crispy_forms_tags %} 3 | 4 | {% block body %} 5 | 6 |
7 | 8 |

Admin settings

9 | 10 | {% if not request.user.is_authenticated or not request.user.is_superuser %} 11 | 14 | {% else %} 15 | {% crispy form %} 16 | {% endif %} 17 |
18 | 19 | {% endblock body %} -------------------------------------------------------------------------------- /app/YtManagerApp/templates/registration/logged_out.html: -------------------------------------------------------------------------------- 1 | {% extends 'YtManagerApp/master_default.html' %} 2 | 3 | {% load static %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block scripts %} 7 | 14 | 15 | {% endblock %} 16 | 17 | {% block body %} 18 | 19 |
20 | 21 | 24 | 25 |
26 | 27 | {% endblock %} -------------------------------------------------------------------------------- /app/YtManagerApp/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'YtManagerApp/master_default.html' %} 2 | 3 | {% load static %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block stylesheets %} 7 | 8 | {% endblock %} 9 | 10 | {% block body %} 11 | 12 |
13 | 14 | {% if next %} 15 | {% if user.is_authenticated %} 16 | 20 | {% else %} 21 | 24 | {% endif %} 25 | {% endif %} 26 | 27 |
Login
28 | 29 |
30 | {% csrf_token %} 31 | 32 | 33 | {{ form | crispy }} 34 | 35 |
36 | 37 | 38 | Recover password 39 |
40 | 41 |
42 |
43 | 44 | {% endblock %} -------------------------------------------------------------------------------- /app/YtManagerApp/templates/registration/password_reset_complete.html: -------------------------------------------------------------------------------- 1 | {% extends 'YtManagerApp/master_default.html' %} 2 | 3 | {% load static %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block scripts %} 7 | 14 | {% endblock %} 15 | 16 | {% block body %} 17 | 18 |
19 | 20 | 23 | 24 |
25 | 26 | {% endblock %} -------------------------------------------------------------------------------- /app/YtManagerApp/templates/registration/password_reset_confirm.html: -------------------------------------------------------------------------------- 1 | {% extends 'YtManagerApp/master_default.html' %} 2 | 3 | {% load static %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block stylesheets %} 7 | 8 | {% endblock %} 9 | 10 | {% block body %} 11 | 12 | {% if validlink %} 13 |
14 |
Confirm password reset
15 | 16 |
17 | {% csrf_token %} 18 | {{ form | crispy }} 19 |
20 | 21 |
22 | 23 |
24 |
25 | {% else %} 26 |
27 | 30 |
31 | {% endif %} 32 | 33 | {% endblock %} -------------------------------------------------------------------------------- /app/YtManagerApp/templates/registration/password_reset_done.html: -------------------------------------------------------------------------------- 1 | {% extends 'YtManagerApp/master_default.html' %} 2 | 3 | {% load static %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block scripts %} 7 | 14 | 15 | {% endblock %} 16 | 17 | {% block body %} 18 | 19 |
20 | 21 | 25 | 26 |
27 | 28 | {% endblock %} -------------------------------------------------------------------------------- /app/YtManagerApp/templates/registration/password_reset_email.html: -------------------------------------------------------------------------------- 1 | Someone asked for password reset for email {{ email }}. Follow the link below: 2 | {{ protocol}}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %} -------------------------------------------------------------------------------- /app/YtManagerApp/templates/registration/password_reset_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'YtManagerApp/master_default.html' %} 2 | 3 | {% load static %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block stylesheets %} 7 | 8 | {% endblock %} 9 | 10 | {% block body %} 11 | 12 |
13 | 14 | {% if next %} 15 | {% if user.is_authenticated %} 16 | 20 | {% else %} 21 | 24 | {% endif %} 25 | {% endif %} 26 | 27 |
Password reset
28 | 29 |
30 | {% csrf_token %} 31 | 32 | 33 | {{ form | crispy }} 34 | 35 |

If there is any account associated with the given e-mail address, 36 | an e-mail will be sent containing the password reset link.

37 | 38 |
39 | 40 |
41 | 42 |
43 |
44 | 45 | {% endblock %} -------------------------------------------------------------------------------- /app/YtManagerApp/templates/registration/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'YtManagerApp/master_default.html' %} 2 | 3 | {% load static %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block stylesheets %} 7 | 8 | {% endblock %} 9 | 10 | {% block body %} 11 | 12 |
13 | 14 | {% if next %} 15 | {% if user.is_authenticated %} 16 | 20 | {% else %} 21 | 24 | {% endif %} 25 | {% endif %} 26 | 27 | {% if not global_preferences.general__allow_registrations %} 28 | 31 | 32 | {% else %} 33 | 34 |
Register
35 | {% crispy form %} 36 | 37 | {% endif %} 38 |
39 | 40 | {% endblock %} -------------------------------------------------------------------------------- /app/YtManagerApp/templates/registration/register_done.html: -------------------------------------------------------------------------------- 1 | {% extends 'YtManagerApp/master_default.html' %} 2 | 3 | {% load static %} 4 | {% load crispy_forms_tags %} 5 | 6 | {% block scripts %} 7 | 14 | 15 | {% endblock %} 16 | 17 | {% block body %} 18 | 19 |
20 | 21 | 24 | 25 |
26 | 27 | {% endblock %} -------------------------------------------------------------------------------- /app/YtManagerApp/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/templatetags/__init__.py -------------------------------------------------------------------------------- /app/YtManagerApp/templatetags/common.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | register = template.Library() 3 | 4 | 5 | class SetVarNode(template.Node): 6 | 7 | def __init__(self, var_name, var_value): 8 | self.var_name = var_name 9 | self.var_value = var_value 10 | 11 | def render(self, context): 12 | try: 13 | value = template.Variable(self.var_value).resolve(context) 14 | except template.VariableDoesNotExist: 15 | value = "" 16 | context[self.var_name] = value 17 | 18 | return u"" 19 | 20 | 21 | @register.tag(name='set') 22 | def set_var(parser, token): 23 | """ 24 | {% set some_var = '123' %} 25 | """ 26 | parts = token.split_contents() 27 | if len(parts) < 4: 28 | raise template.TemplateSyntaxError("'set' tag must be of the form: {% set = %}") 29 | 30 | return SetVarNode(parts[1], parts[3]) 31 | -------------------------------------------------------------------------------- /app/YtManagerApp/templatetags/ratings.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | register = template.Library() 3 | 4 | FULL_STAR_CLASS = "typcn-star-full-outline" 5 | HALF_STAR_CLASS = "typcn-star-half-outline" 6 | EMPTY_STAR_CLASS = "typcn-star-outline" 7 | 8 | 9 | class StarRatingNode(template.Node): 10 | 11 | def __init__(self, rating_percent, max_stars="5"): 12 | self.rating = rating_percent 13 | self.max_stars = max_stars 14 | 15 | def render(self, context): 16 | try: 17 | rating = template.Variable(self.rating).resolve(context) 18 | except template.VariableDoesNotExist: 19 | rating = 0 20 | 21 | try: 22 | max_stars = template.Variable(self.max_stars).resolve(context) 23 | except template.VariableDoesNotExist: 24 | max_stars = 0 25 | 26 | total_halves = (max_stars - 1) * rating * 2 27 | 28 | html = [ 29 | f'
' 30 | f'' 31 | ] 32 | 33 | for i in range(max_stars - 1): 34 | if total_halves >= 2 * i + 2: 35 | cls = FULL_STAR_CLASS 36 | elif total_halves >= 2 * i + 1: 37 | cls = HALF_STAR_CLASS 38 | else: 39 | cls = EMPTY_STAR_CLASS 40 | 41 | html.append(f'') 42 | 43 | html.append("
") 44 | 45 | return u"".join(html) 46 | 47 | 48 | @register.tag(name='starrating') 49 | def star_rating_tag(parser, token): 50 | """ 51 | {% rating percent [max_stars=5]%} 52 | """ 53 | parts = token.split_contents() 54 | if len(parts) <= 1: 55 | raise template.TemplateSyntaxError("'set' tag must be of the form: {% rating [=5] %}") 56 | 57 | if len(parts) <= 2: 58 | return StarRatingNode(parts[1]) 59 | 60 | return StarRatingNode(parts[1], parts[2]) 61 | 62 | -------------------------------------------------------------------------------- /app/YtManagerApp/tests.py: -------------------------------------------------------------------------------- 1 | # Create your tests here. 2 | -------------------------------------------------------------------------------- /app/YtManagerApp/urls.py: -------------------------------------------------------------------------------- 1 | """YtManager URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.11/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.conf.urls import url, include 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 15 | """ 16 | from django.conf import settings 17 | from django.conf.urls import include 18 | from django.conf.urls.static import static 19 | from django.urls import path 20 | 21 | from .views import first_time 22 | from .views.actions import SyncNowView, DeleteVideoFilesView, DownloadVideoFilesView, MarkVideoWatchedView, \ 23 | MarkVideoUnwatchedView 24 | from .views.auth import ExtendedLoginView, RegisterView, RegisterDoneView 25 | from .views.index import index, ajax_get_tree, ajax_get_videos, CreateFolderModal, UpdateFolderModal, DeleteFolderModal, \ 26 | CreateSubscriptionModal, UpdateSubscriptionModal, DeleteSubscriptionModal, ImportSubscriptionsModal 27 | from .views.notifications import ajax_get_running_jobs 28 | from .views.settings import SettingsView, AdminSettingsView 29 | from .views.video import VideoDetailView, video_detail_view 30 | 31 | urlpatterns = [ 32 | # Authentication URLs 33 | path('login/', ExtendedLoginView.as_view(), name='login'), 34 | path('register/', RegisterView.as_view(), name='register'), 35 | path('register_done/', RegisterDoneView.as_view(), name='register_done'), 36 | path('', include('django.contrib.auth.urls')), 37 | 38 | # Ajax 39 | path('ajax/action/sync_now/', SyncNowView.as_view(), name='ajax_action_sync_now'), 40 | path('ajax/action/sync_now/', SyncNowView.as_view(), name='ajax_action_sync_now'), 41 | path('ajax/action/delete_video_files/', DeleteVideoFilesView.as_view(), name='ajax_action_delete_video_files'), 42 | path('ajax/action/download_video_files/', DownloadVideoFilesView.as_view(), name='ajax_action_download_video_files'), 43 | path('ajax/action/mark_video_watched/', MarkVideoWatchedView.as_view(), name='ajax_action_mark_video_watched'), 44 | path('ajax/action/mark_video_unwatched/', MarkVideoUnwatchedView.as_view(), name='ajax_action_mark_video_unwatched'), 45 | 46 | path('ajax/get_tree/', ajax_get_tree, name='ajax_get_tree'), 47 | path('ajax/get_videos/', ajax_get_videos, name='ajax_get_videos'), 48 | 49 | path('ajax/get_running_jobs/', ajax_get_running_jobs, name='ajax_get_running_jobs'), 50 | 51 | # Modals 52 | path('modal/create_folder/', CreateFolderModal.as_view(), name='modal_create_folder'), 53 | path('modal/create_folder//', CreateFolderModal.as_view(), name='modal_create_folder'), 54 | path('modal/update_folder//', UpdateFolderModal.as_view(), name='modal_update_folder'), 55 | path('modal/delete_folder//', DeleteFolderModal.as_view(), name='modal_delete_folder'), 56 | 57 | path('modal/create_subscription/', CreateSubscriptionModal.as_view(), name='modal_create_subscription'), 58 | path('modal/create_subscription//', CreateSubscriptionModal.as_view(), name='modal_create_subscription'), 59 | path('modal/import_subscriptions/', ImportSubscriptionsModal.as_view(), name='modal_import_subscriptions'), 60 | path('modal/import_subscriptions//', ImportSubscriptionsModal.as_view(), name='modal_import_subscriptions'), 61 | path('modal/update_subscription//', UpdateSubscriptionModal.as_view(), name='modal_update_subscription'), 62 | path('modal/delete_subscription//', DeleteSubscriptionModal.as_view(), name='modal_delete_subscription'), 63 | 64 | # Pages 65 | path('', index, name='home'), 66 | path('settings/', SettingsView.as_view(), name='settings'), 67 | path('admin_settings/', AdminSettingsView.as_view(), name='admin_settings'), 68 | path('video//', VideoDetailView.as_view(), name='video'), 69 | path('video-src//', video_detail_view, name='video-src'), 70 | 71 | # First time setup 72 | path('first_time/step0_welcome', first_time.Step0WelcomeView.as_view(), name='first_time_0'), 73 | path('first_time/step1_apikey', first_time.Step1ApiKeyView.as_view(), name='first_time_1'), 74 | path('first_time/step2_admin', first_time.Step2SetupAdminUserView.as_view(), name='first_time_2'), 75 | path('first_time/step3_config', first_time.Step3ConfigureView.as_view(), name='first_time_3'), 76 | path('first_time/done', first_time.DoneView.as_view(), name='first_time_done'), 77 | 78 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 79 | -------------------------------------------------------------------------------- /app/YtManagerApp/utils/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | def first_non_null(*iterable): 3 | """ 4 | Returns the first element from the iterable which is not None. 5 | If all the elements are 'None', 'None' is returned. 6 | :param iterable: Iterable containing list of elements. 7 | :return: First non-null element, or None if all elements are 'None'. 8 | """ 9 | return next((item for item in iterable if item is not None), None) 10 | -------------------------------------------------------------------------------- /app/YtManagerApp/utils/algorithms.py: -------------------------------------------------------------------------------- 1 | """Bisection algorithms. 2 | 3 | These algorithms are taken from Python's standard library, and modified so they take a 'key' parameter (similar to how 4 | `sorted` works). 5 | """ 6 | 7 | 8 | def bisect_right(a, x, lo=0, hi=None, key=None): 9 | """Return the index where to insert item x in list a, assuming a is sorted. 10 | 11 | The return value i is such that all e in a[:i] have e <= x, and all e in 12 | a[i:] have e > x. So if x already appears in the list, a.insert(x) will 13 | insert just after the rightmost x already there. 14 | 15 | Optional args lo (default 0) and hi (default len(a)) bound the 16 | slice of a to be searched. 17 | """ 18 | if key is None: 19 | key = lambda x: x 20 | 21 | if lo < 0: 22 | raise ValueError('lo must be non-negative') 23 | if hi is None: 24 | hi = len(a) 25 | while lo < hi: 26 | mid = (lo+hi)//2 27 | if key(x) < key(a[mid]): hi = mid 28 | else: lo = mid+1 29 | return lo 30 | 31 | 32 | def bisect_left(a, x, lo=0, hi=None, key=None): 33 | """Return the index where to insert item x in list a, assuming a is sorted. 34 | 35 | The return value i is such that all e in a[:i] have e < x, and all e in 36 | a[i:] have e >= x. So if x already appears in the list, a.insert(x) will 37 | insert just before the leftmost x already there. 38 | 39 | Optional args lo (default 0) and hi (default len(a)) bound the 40 | slice of a to be searched. 41 | """ 42 | if key is None: 43 | key = lambda x: x 44 | 45 | if lo < 0: 46 | raise ValueError('lo must be non-negative') 47 | if hi is None: 48 | hi = len(a) 49 | while lo < hi: 50 | mid = (lo+hi)//2 51 | if key(a[mid]) < key(x): lo = mid+1 52 | else: hi = mid 53 | return lo 54 | 55 | 56 | # Create aliases 57 | bisect = bisect_right 58 | -------------------------------------------------------------------------------- /app/YtManagerApp/utils/extended_interpolation_with_env.py: -------------------------------------------------------------------------------- 1 | import os 2 | import os.path 3 | import re 4 | from configparser import Interpolation, NoSectionError, NoOptionError, InterpolationMissingOptionError, \ 5 | InterpolationDepthError, InterpolationSyntaxError 6 | 7 | MAX_INTERPOLATION_DEPTH = 10 8 | 9 | 10 | class ExtendedInterpolatorWithEnv(Interpolation): 11 | """Advanced variant of interpolation, supports the syntax used by 12 | `zc.buildout'. Enables interpolation between sections. 13 | 14 | This modified version also allows specifying environment variables 15 | using ${env:...}, and allows adding additional options using 'set_additional_options'. """ 16 | 17 | _KEYCRE = re.compile(r"\$\{([^}]+)\}") 18 | 19 | def before_get(self, parser, section, option, value, defaults): 20 | L = [] 21 | self._interpolate_some(parser, option, L, value, section, defaults, 1) 22 | return ''.join(L) 23 | 24 | def before_set(self, parser, section, option, value): 25 | tmp_value = value.replace('$$', '') # escaped dollar signs 26 | tmp_value = self._KEYCRE.sub('', tmp_value) # valid syntax 27 | if '$' in tmp_value: 28 | raise ValueError("invalid interpolation syntax in %r at " 29 | "position %d" % (value, tmp_value.find('$'))) 30 | return value 31 | 32 | def _resolve_option(self, option, defaults): 33 | return defaults[option] 34 | 35 | def _resolve_section_option(self, section, option, parser): 36 | if section == 'env': 37 | return os.getenv(option, '') 38 | return parser.get(section, parser.optionxform(option), raw=True) 39 | 40 | def _interpolate_some(self, parser, option, accum, rest, section, map, 41 | depth): 42 | rawval = parser.get(section, option, raw=True, fallback=rest) 43 | if depth > MAX_INTERPOLATION_DEPTH: 44 | raise InterpolationDepthError(option, section, rawval) 45 | while rest: 46 | p = rest.find("$") 47 | if p < 0: 48 | accum.append(rest) 49 | return 50 | if p > 0: 51 | accum.append(rest[:p]) 52 | rest = rest[p:] 53 | # p is no longer used 54 | c = rest[1:2] 55 | if c == "$": 56 | accum.append("$") 57 | rest = rest[2:] 58 | elif c == "{": 59 | m = self._KEYCRE.match(rest) 60 | if m is None: 61 | raise InterpolationSyntaxError(option, section, 62 | "bad interpolation variable reference %r" % rest) 63 | path = m.group(1).split(':') 64 | rest = rest[m.end():] 65 | sect = section 66 | opt = option 67 | try: 68 | if len(path) == 1: 69 | opt = parser.optionxform(path[0]) 70 | v = self._resolve_option(opt, map) 71 | elif len(path) == 2: 72 | sect = path[0] 73 | opt = path[1] 74 | v = self._resolve_section_option(sect, opt, parser) 75 | else: 76 | raise InterpolationSyntaxError( 77 | option, section, 78 | "More than one ':' found: %r" % (rest,)) 79 | except (KeyError, NoSectionError, NoOptionError): 80 | raise InterpolationMissingOptionError( 81 | option, section, rawval, ":".join(path)) from None 82 | if "$" in v: 83 | self._interpolate_some(parser, opt, accum, v, sect, 84 | dict(parser.items(sect, raw=True)), 85 | depth + 1) 86 | else: 87 | accum.append(v) 88 | else: 89 | raise InterpolationSyntaxError( 90 | option, section, 91 | "'$' must be followed by '$' or '{', " 92 | "found: %r" % (rest,)) 93 | -------------------------------------------------------------------------------- /app/YtManagerApp/utils/progress_tracker.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Callable 2 | 3 | 4 | class ProgressTracker(object): 5 | """ 6 | Class which helps keep track of complex operation progress. 7 | """ 8 | 9 | def __init__(self, total_steps: float = 100, initial_steps: float = 0, 10 | listener: Callable[[float, str], None] = None, 11 | completed_listener: Callable[[], None] = None, 12 | parent: Optional["ProgressTracker"] = None): 13 | """ 14 | Constructor 15 | :param total_steps: Total number of steps required by this operation 16 | :param initial_steps: Starting steps 17 | :param parent: Parent progress tracker 18 | :param listener: Callable which is called when any progress happens 19 | """ 20 | 21 | self.total_steps = total_steps 22 | self.steps = initial_steps 23 | 24 | self.__subtask: ProgressTracker = None 25 | self.__subtask_steps = 0 26 | 27 | self.__parent = parent 28 | self.__listener = listener 29 | self.__completed_listener = completed_listener 30 | 31 | def __on_progress(self, progress_msg): 32 | if self.__listener is not None: 33 | self.__listener(self.compute_progress(), progress_msg) 34 | 35 | if self.__parent is not None: 36 | self.__parent.__on_progress(progress_msg) 37 | 38 | if self.steps >= self.total_steps and self.__completed_listener is not None: 39 | self.__completed_listener() 40 | 41 | def advance(self, steps: float = 1, progress_msg: str = ''): 42 | """ 43 | Advances a number of steps. 44 | :param steps: Number of steps to advance 45 | :param progress_msg: A message which will be passed to a listener 46 | :return: 47 | """ 48 | 49 | # We can assume previous subtask is now completed 50 | if self.__subtask is not None: 51 | self.steps += self.__subtask_steps 52 | self.__subtask = None 53 | 54 | self.steps += steps 55 | self.__on_progress(progress_msg) 56 | 57 | def subtask(self, steps: float = 1, subtask_total_steps: float = 100, subtask_initial_steps: float = 0): 58 | """ 59 | Creates a 'subtask' which has its own progress, which will be used in the calculation of the final progress. 60 | :param steps: Number of steps the subtask is 'worth' 61 | :param subtask_total_steps: Total number of steps for subtask 62 | :param subtask_initial_steps: Initial steps for subtask 63 | :return: ProgressTracker for subtask 64 | """ 65 | 66 | # We can assume previous subtask is now completed 67 | if self.__subtask is not None: 68 | self.steps += self.__subtask_steps 69 | 70 | self.__subtask = ProgressTracker(total_steps=subtask_total_steps, 71 | initial_steps=subtask_initial_steps, 72 | parent=self) 73 | self.__subtask_steps = steps 74 | 75 | return self.__subtask 76 | 77 | def compute_progress(self): 78 | """ 79 | Calculates final progress value in percent. 80 | :return: value in [0,1] interval representing progress 81 | """ 82 | base = float(self.steps) / self.total_steps 83 | if self.__subtask is not None: 84 | base += self.__subtask.compute_progress() * self.__subtask_steps / self.total_steps 85 | 86 | return min(base, 1.0) 87 | 88 | 89 | # Test 90 | if __name__ == '__main__': 91 | 92 | def on_progress(progress, message): 93 | print(f'{progress * 100}%: {message}') 94 | 95 | def on_completed(): 96 | print("Complete!") 97 | 98 | main_task = ProgressTracker(total_steps=20, listener=on_progress, completed_listener=on_completed) 99 | 100 | for i in range(10): 101 | main_task.advance(progress_msg='First 10 steps') 102 | 103 | subtask = main_task.subtask(5, subtask_total_steps=10) 104 | 105 | for i in range(10): 106 | subtask.advance(progress_msg='Subtask') 107 | 108 | for i in range(5): 109 | main_task.advance(progress_msg='Main task again') 110 | -------------------------------------------------------------------------------- /app/YtManagerApp/utils/subscription_file_parser.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Optional 2 | from xml.etree import ElementTree 3 | import re 4 | 5 | 6 | class FormatNotSupportedError(Exception): 7 | pass 8 | 9 | 10 | class SubFileParser(object): 11 | 12 | def probe(self, file_handle) -> bool: 13 | """ 14 | Tests if file matches file format. 15 | :param file_handle: File handle 16 | :return: True if file matches, false otherwise 17 | """ 18 | return False 19 | 20 | def parse(self, file_handle) -> Iterable[str]: 21 | """ 22 | Parses file and returns a list of subscription URLs. 23 | :param file_handle: 24 | :return: 25 | """ 26 | return [] 27 | 28 | 29 | class SubscriptionListFileParser(SubFileParser): 30 | """ 31 | A subscription list file is file which contains just a bunch of URLs. 32 | Comments are supported using # character. 33 | """ 34 | 35 | def __is_url(self, text: str) -> bool: 36 | return text.startswith('http://') or text.startswith('https://') 37 | 38 | def probe(self, file_handle): 39 | file_handle.seek(0) 40 | for line in file_handle: 41 | if isinstance(line, bytes) or isinstance(line, bytearray): 42 | line = line.decode() 43 | # Trim comments and spaces 44 | line = re.sub('(^|\s)#.*', '', line).strip() 45 | if len(line) > 0: 46 | return self.__is_url(line) 47 | return False 48 | 49 | def parse(self, file_handle): 50 | file_handle.seek(0) 51 | for line in file_handle: 52 | if isinstance(line, bytes) or isinstance(line, bytearray): 53 | line = line.decode() 54 | # Trim comments and spaces 55 | line = re.sub('(^|\s)#.*', '', line).strip() 56 | if len(line) > 0: 57 | yield line 58 | 59 | 60 | class OPMLParser(SubFileParser): 61 | """ 62 | Parses OPML files (emitted by YouTube) 63 | """ 64 | def __init__(self): 65 | self.__cached_file = None 66 | self.__cached_tree: Optional[ElementTree.ElementTree] = None 67 | 68 | def __parse(self, file_handle): 69 | if file_handle == self.__cached_file: 70 | return self.__cached_tree 71 | 72 | file_handle.seek(0) 73 | tree = ElementTree.parse(file_handle) 74 | 75 | self.__cached_file = file_handle 76 | self.__cached_tree = tree 77 | return self.__cached_tree 78 | 79 | def probe(self, file_handle): 80 | try: 81 | tree = self.__parse(file_handle) 82 | except ElementTree.ParseError: 83 | # Malformed XML 84 | return False 85 | 86 | return tree.getroot().tag.lower() == 'opml' 87 | 88 | def parse(self, file_handle): 89 | tree = self.__parse(file_handle) 90 | root = tree.getroot() 91 | 92 | for node in root.iter('outline'): 93 | if 'xmlUrl' in node.keys(): 94 | yield node.get('xmlUrl') 95 | 96 | 97 | PARSERS = ( 98 | OPMLParser(), 99 | SubscriptionListFileParser() 100 | ) 101 | 102 | 103 | def parse(file_handle) -> Iterable[str]: 104 | for parser in PARSERS: 105 | if parser.probe(file_handle): 106 | return parser.parse(file_handle) 107 | 108 | raise FormatNotSupportedError('This file cannot be parsed!') 109 | -------------------------------------------------------------------------------- /app/YtManagerApp/utils/youtube.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from external.pytaw.pytaw.youtube import YouTube, Channel, Playlist, PlaylistItem, Thumbnail, InvalidURL, Resource, Video 3 | from typing import Optional 4 | 5 | 6 | class YoutubeAPI(YouTube): 7 | 8 | @staticmethod 9 | def build_public() -> 'YoutubeAPI': 10 | from YtManagerApp.management.appconfig import appconfig 11 | return YoutubeAPI(key=appconfig.youtube_api_key) 12 | 13 | # @staticmethod 14 | # def build_oauth() -> 'YoutubeAPI': 15 | # flow = 16 | # credentials = 17 | # service = build(API_SERVICE_NAME, API_VERSION, credentials) 18 | 19 | 20 | def default_thumbnail(resource: Resource) -> Optional[Thumbnail]: 21 | """ 22 | Gets the default thumbnail for a resource. 23 | Searches in the list of thumbnails for one with the label 'default', or takes the first one. 24 | :param resource: 25 | :return: 26 | """ 27 | thumbs = getattr(resource, 'thumbnails', None) 28 | 29 | if thumbs is None or len(thumbs) <= 0: 30 | return None 31 | 32 | return next( 33 | (i for i in thumbs if i.id == 'default'), 34 | thumbs[0] 35 | ) 36 | 37 | 38 | def best_thumbnail(resource: Resource) -> Optional[Thumbnail]: 39 | """ 40 | Gets the best thumbnail available for a resource. 41 | :param resource: 42 | :return: 43 | """ 44 | thumbs = getattr(resource, 'thumbnails', None) 45 | 46 | if thumbs is None or len(thumbs) <= 0: 47 | return None 48 | 49 | return max(thumbs, key=lambda t: t.width * t.height) -------------------------------------------------------------------------------- /app/YtManagerApp/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/views/__init__.py -------------------------------------------------------------------------------- /app/YtManagerApp/views/actions.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.mixins import LoginRequiredMixin 2 | from django.http import JsonResponse 3 | from django.views.generic import View 4 | 5 | from YtManagerApp.management.jobs.synchronize import SynchronizeJob 6 | from YtManagerApp.models import Video, Subscription 7 | 8 | 9 | class SyncNowView(LoginRequiredMixin, View): 10 | def post(self, *args, **kwargs): 11 | if 'pk' in kwargs: 12 | SynchronizeJob.schedule_now_for_subscription(Subscription.objects.get(id=kwargs['pk'])) 13 | else: 14 | SynchronizeJob.schedule_now() 15 | return JsonResponse({ 16 | 'success': True 17 | }) 18 | 19 | 20 | class DeleteVideoFilesView(LoginRequiredMixin, View): 21 | def post(self, *args, **kwargs): 22 | video = Video.objects.get(id=kwargs['pk']) 23 | video.delete_files() 24 | return JsonResponse({ 25 | 'success': True 26 | }) 27 | 28 | 29 | class DownloadVideoFilesView(LoginRequiredMixin, View): 30 | def post(self, *args, **kwargs): 31 | video = Video.objects.get(id=kwargs['pk']) 32 | video.download() 33 | return JsonResponse({ 34 | 'success': True 35 | }) 36 | 37 | 38 | class MarkVideoWatchedView(LoginRequiredMixin, View): 39 | def post(self, *args, **kwargs): 40 | video = Video.objects.get(id=kwargs['pk']) 41 | video.mark_watched() 42 | return JsonResponse({ 43 | 'success': True 44 | }) 45 | 46 | 47 | class MarkVideoUnwatchedView(LoginRequiredMixin, View): 48 | def post(self, *args, **kwargs): 49 | video = Video.objects.get(id=kwargs['pk']) 50 | video.mark_unwatched() 51 | video.save() 52 | return JsonResponse({ 53 | 'success': True 54 | }) 55 | -------------------------------------------------------------------------------- /app/YtManagerApp/views/auth.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import login, authenticate 2 | from django.contrib.auth.mixins import LoginRequiredMixin 3 | from django.contrib.auth.models import User 4 | from django.contrib.auth.views import LoginView 5 | from django.http import HttpResponseForbidden 6 | from django.urls import reverse_lazy 7 | from django.views.generic import FormView, TemplateView 8 | 9 | from YtManagerApp.management.appconfig import appconfig 10 | from YtManagerApp.views.forms.auth import ExtendedAuthenticationForm, ExtendedUserCreationForm 11 | 12 | 13 | class ExtendedLoginView(LoginView): 14 | form_class = ExtendedAuthenticationForm 15 | 16 | 17 | class RegisterView(FormView): 18 | template_name = 'registration/register.html' 19 | form_class = ExtendedUserCreationForm 20 | success_url = reverse_lazy('register_done') 21 | 22 | def form_valid(self, form): 23 | form.apply_session_expiry(self.request) 24 | form.save() 25 | 26 | username = form.cleaned_data.get('username') 27 | password = form.cleaned_data.get('password1') 28 | user = authenticate(username=username, password=password) 29 | login(self.request, user) 30 | 31 | return super().form_valid(form) 32 | 33 | def get_context_data(self, **kwargs): 34 | context = super().get_context_data(**kwargs) 35 | context['is_first_user'] = (User.objects.count() == 0) 36 | return context 37 | 38 | def post(self, request, *args, **kwargs): 39 | if not appconfig.allow_registrations: 40 | return HttpResponseForbidden("Registrations are disabled!") 41 | 42 | return super().post(request, *args, **kwargs) 43 | 44 | 45 | class RegisterDoneView(LoginRequiredMixin, TemplateView): 46 | template_name = 'registration/register_done.html' 47 | -------------------------------------------------------------------------------- /app/YtManagerApp/views/controls/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/YtManagerApp/views/controls/__init__.py -------------------------------------------------------------------------------- /app/YtManagerApp/views/controls/modal.py: -------------------------------------------------------------------------------- 1 | from django.views.generic.base import ContextMixin 2 | from django.http import JsonResponse 3 | 4 | 5 | class ModalMixin(ContextMixin): 6 | template_name = 'YtManagerApp/controls/modal.html' 7 | success_url = '/' 8 | 9 | def __init__(self, modal_id='dialog', title='', fade=True, centered=True, small=False, large=False, *args, **kwargs): 10 | super().__init__(*args, **kwargs) 11 | self.id = modal_id 12 | self.title = title 13 | self.fade = fade 14 | self.centered = centered 15 | self.small = small 16 | self.large = large 17 | 18 | def get_context_data(self, **kwargs): 19 | data = super().get_context_data(**kwargs) 20 | data['modal_id'] = self.id 21 | 22 | data['modal_classes'] = '' 23 | if self.fade: 24 | data['modal_classes'] += 'fade ' 25 | 26 | data['modal_dialog_classes'] = '' 27 | if self.centered: 28 | data['modal_dialog_classes'] += 'modal-dialog-centered ' 29 | if self.small: 30 | data['modal_dialog_classes'] += 'modal-sm ' 31 | elif self.large: 32 | data['modal_dialog_classes'] += 'modal-lg ' 33 | 34 | data['modal_title'] = self.title 35 | 36 | return data 37 | 38 | def modal_response(self, form, success=True, error_msg=None): 39 | result = {'success': success} 40 | if not success: 41 | result['errors'] = form.errors.get_json_data(escape_html=True) 42 | if error_msg is not None: 43 | result['errors']['__all__'] = [{'message': error_msg}] 44 | 45 | return JsonResponse(result) 46 | 47 | def form_valid(self, form): 48 | super().form_valid(form) 49 | return self.modal_response(form, success=True) 50 | 51 | def form_invalid(self, form): 52 | super().form_invalid(form) 53 | return self.modal_response(form, success=False) 54 | -------------------------------------------------------------------------------- /app/YtManagerApp/views/forms/auth.py: -------------------------------------------------------------------------------- 1 | from crispy_forms.helper import FormHelper 2 | from crispy_forms.layout import Submit 3 | from django import forms 4 | from django.contrib.auth.forms import AuthenticationForm, UserCreationForm 5 | from django.urls import reverse_lazy 6 | 7 | 8 | class ExtendedAuthenticationForm(AuthenticationForm): 9 | remember_me = forms.BooleanField(label='Remember me', required=False, initial=False) 10 | 11 | def apply_session_expiry(self, request): 12 | remember_me = self.cleaned_data.get('remember_me') 13 | if remember_me: 14 | expiry = 3600 * 24 * 30 15 | else: 16 | expiry = 0 17 | 18 | request.session.set_expiry(expiry) 19 | 20 | 21 | class ExtendedUserCreationForm(UserCreationForm): 22 | email = forms.EmailField(required=False, 23 | label='E-mail address', 24 | help_text='The e-mail address is optional, but it is the only way to recover a lost ' 25 | 'password.') 26 | first_name = forms.CharField(max_length=30, required=False, 27 | label='First name') 28 | last_name = forms.CharField(max_length=150, required=False, 29 | label='Last name') 30 | 31 | form_action = reverse_lazy('register') 32 | 33 | def __init__(self, *args, **kwargs): 34 | super().__init__(*args, **kwargs) 35 | self.helper = FormHelper() 36 | self.helper.label_class = 'col-3' 37 | self.helper.field_class = 'col-9' 38 | self.helper.form_class = 'form-horizontal' 39 | self.helper.form_method = 'post' 40 | self.helper.form_action = self.form_action 41 | self.helper.add_input(Submit('submit', 'register')) 42 | 43 | class Meta(UserCreationForm.Meta): 44 | fields = ['username', 'email', 'first_name', 'last_name'] 45 | -------------------------------------------------------------------------------- /app/YtManagerApp/views/forms/first_time.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from crispy_forms.helper import FormHelper 4 | from crispy_forms.layout import Layout, HTML, Submit, Column 5 | from django import forms 6 | from django.contrib.auth.models import User 7 | from django.urls import reverse_lazy 8 | 9 | from YtManagerApp.views.forms.auth import ExtendedUserCreationForm, ExtendedAuthenticationForm 10 | 11 | logger = logging.getLogger("FirstTimeWizard") 12 | 13 | 14 | class WelcomeForm(forms.Form): 15 | def __init__(self, *args, **kwargs): 16 | super().__init__(*args, **kwargs) 17 | self.helper = FormHelper() 18 | self.helper.layout = Layout( 19 | Submit('submit', value='Continue') 20 | ) 21 | 22 | 23 | class ApiKeyForm(forms.Form): 24 | api_key = forms.CharField(label="YouTube API Key:") 25 | 26 | def __init__(self, *args, **kwargs): 27 | super().__init__(*args, **kwargs) 28 | self.helper = FormHelper() 29 | self.helper.layout = Layout( 30 | 'api_key', 31 | Column( 32 | Submit('submit', value='Continue'), 33 | HTML('Skip') 34 | ) 35 | ) 36 | 37 | 38 | class UserCreationForm(ExtendedUserCreationForm): 39 | form_action = reverse_lazy('first_time_2') 40 | 41 | 42 | class LoginForm(ExtendedAuthenticationForm): 43 | 44 | def __init__(self, *args, **kwargs): 45 | super().__init__(*args, **kwargs) 46 | self.helper = FormHelper() 47 | self.helper.layout = Layout( 48 | 'username', 49 | 'password', 50 | 'remember_me', 51 | Column( 52 | Submit('submit', value='Continue'), 53 | HTML('Register new admin account') 54 | ) 55 | ) 56 | 57 | 58 | class PickAdminUserForm(forms.Form): 59 | admin_user = forms.ModelChoiceField( 60 | User.objects.order_by('username'), 61 | label='User to promote to admin', 62 | required=True) 63 | 64 | def __init__(self, *args, **kwargs): 65 | super().__init__(*args, **kwargs) 66 | self.helper = FormHelper() 67 | self.helper.layout = Layout( 68 | 'admin_user', 69 | Column( 70 | Submit('submit', value='Continue'), 71 | HTML('Register a new admin user') 72 | ) 73 | ) 74 | 75 | 76 | class ServerConfigForm(forms.Form): 77 | 78 | allow_registrations = forms.BooleanField( 79 | label="Allow user registrations", 80 | help_text="Disabling this option will prevent anyone from registering to the site.", 81 | initial=True, 82 | required=False 83 | ) 84 | 85 | sync_schedule = forms.CharField( 86 | label="Synchronization schedule", 87 | help_text="How often should the application look for new videos.", 88 | initial="5 * * * *", 89 | required=True 90 | ) 91 | 92 | auto_download = forms.BooleanField( 93 | label="Download videos automatically", 94 | required=False 95 | ) 96 | 97 | download_location = forms.CharField( 98 | label="Download location", 99 | help_text="Location on the server where videos are downloaded.", 100 | required=True 101 | ) 102 | 103 | def __init__(self, *args, **kwargs): 104 | super().__init__(*args, **kwargs) 105 | self.helper = FormHelper() 106 | self.helper.layout = Layout( 107 | HTML('

Server settings

'), 108 | 'sync_schedule', 109 | 'allow_registrations', 110 | HTML('

User settings

'), 111 | 'auto_download', 112 | 'download_location', 113 | Submit('submit', value='Continue'), 114 | ) 115 | 116 | 117 | class DoneForm(forms.Form): 118 | def __init__(self, *args, **kwargs): 119 | super().__init__(*args, **kwargs) 120 | self.helper = FormHelper() 121 | self.helper.layout = Layout( 122 | Submit('submit', value='Finish') 123 | ) 124 | -------------------------------------------------------------------------------- /app/YtManagerApp/views/notifications.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required 2 | from django.db.models import Q 3 | from django.http import HttpRequest, JsonResponse 4 | 5 | from YtManagerApp.models import JobExecution, JobMessage, JOB_STATES_MAP 6 | 7 | 8 | @login_required 9 | def ajax_get_running_jobs(request: HttpRequest): 10 | jobs = JobExecution.objects\ 11 | .filter(status=JOB_STATES_MAP['running'])\ 12 | .filter(Q(user__isnull=True) | Q(user=request.user))\ 13 | .order_by('start_date') 14 | 15 | response = [] 16 | 17 | for job in jobs: 18 | last_progress_message = JobMessage.objects\ 19 | .filter(job=job, progress__isnull=False, suppress_notification=False)\ 20 | .order_by('-timestamp').first() 21 | 22 | last_message = JobMessage.objects\ 23 | .filter(job=job, suppress_notification=False)\ 24 | .order_by('-timestamp').first() 25 | 26 | message = '' 27 | progress = 0 28 | 29 | if last_message is not None: 30 | message = last_message.message 31 | if last_progress_message is not None: 32 | progress = last_progress_message.progress 33 | 34 | response.append({ 35 | 'id': job.id, 36 | 'description': job.description, 37 | 'progress': progress, 38 | 'message': message 39 | }) 40 | 41 | return JsonResponse(response, safe=False) 42 | 43 | -------------------------------------------------------------------------------- /app/YtManagerApp/views/settings.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.mixins import LoginRequiredMixin 2 | from django.http import HttpResponseForbidden 3 | from django.urls import reverse_lazy 4 | from django.views.generic import FormView 5 | 6 | from YtManagerApp.management.jobs.synchronize import SynchronizeJob 7 | from YtManagerApp.views.forms.settings import SettingsForm, AdminSettingsForm 8 | 9 | 10 | class SettingsView(LoginRequiredMixin, FormView): 11 | form_class = SettingsForm 12 | template_name = 'YtManagerApp/settings.html' 13 | success_url = reverse_lazy('home') 14 | 15 | def get_initial(self): 16 | initial = super().get_initial() 17 | initial.update(SettingsForm.get_initials(self.request.user)) 18 | return initial 19 | 20 | def form_valid(self, form): 21 | form.save(self.request.user) 22 | return super().form_valid(form) 23 | 24 | 25 | class AdminSettingsView(LoginRequiredMixin, FormView): 26 | form_class = AdminSettingsForm 27 | template_name = 'YtManagerApp/settings_admin.html' 28 | success_url = reverse_lazy('home') 29 | 30 | def post(self, request, *args, **kwargs): 31 | if not request.user.is_authenticated or not request.user.is_superuser: 32 | return HttpResponseForbidden() 33 | 34 | return super().post(request, *args, **kwargs) 35 | 36 | def get_context_data(self, **kwargs): 37 | context = super().get_context_data(**kwargs) 38 | # TODO: present stats 39 | return context 40 | 41 | def get_initial(self): 42 | initial = super().get_initial() 43 | initial.update(AdminSettingsForm.get_initials()) 44 | return initial 45 | 46 | def form_valid(self, form): 47 | form.save() 48 | SynchronizeJob.schedule_global_job() 49 | return super().form_valid(form) 50 | -------------------------------------------------------------------------------- /app/YtManagerApp/views/video.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import login_required 2 | from django.contrib.auth.mixins import LoginRequiredMixin 3 | from django.http import HttpRequest, StreamingHttpResponse, FileResponse 4 | from django.urls import reverse, reverse_lazy 5 | from django.views import View 6 | from django.views.generic import DetailView 7 | from django.db.models import Sum 8 | 9 | from YtManagerApp.models import Video 10 | 11 | import datetime 12 | 13 | class VideoDetailView(LoginRequiredMixin, DetailView): 14 | template_name = 'YtManagerApp/video.html' 15 | model = Video 16 | 17 | def get_context_data(self, **kwargs): 18 | context = super().get_context_data(**kwargs) 19 | video, mime = self.object.find_video() 20 | if video is not None: 21 | context['video_mime'] = mime 22 | 23 | if self.request.GET.get('next'): 24 | up_next_videos = self.request.GET.get('next').split(',') 25 | context['up_next_count'] = len(up_next_videos) 26 | context['up_next_duration'] = str(datetime.timedelta(seconds=Video.objects.filter(id__in=up_next_videos).aggregate(Sum('duration'))['duration__sum'])) 27 | 28 | return context 29 | 30 | 31 | @login_required 32 | def video_detail_view(request: HttpRequest, pk): 33 | video = Video.objects.get(id = pk) 34 | video_file, _ = video.find_video() 35 | 36 | f = open(video_file, 'rb') 37 | return FileResponse(f) 38 | -------------------------------------------------------------------------------- /app/external/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/external/__init__.py -------------------------------------------------------------------------------- /app/external/pytaw/.gitignore: -------------------------------------------------------------------------------- 1 | *.bak 2 | *.egg 3 | *.egg-info/ 4 | *.eggs/ 5 | *.pyproj 6 | *.sln 7 | *.vs/ 8 | *~ 9 | .DS_Store 10 | .cache/ 11 | .coverage 12 | .idea/ 13 | .tox/ 14 | _build/ 15 | build/ 16 | dist/ 17 | 18 | __pycache__/ 19 | *.ini 20 | -------------------------------------------------------------------------------- /app/external/pytaw/.pytaw.conf: -------------------------------------------------------------------------------- 1 | ; by default pytaw will look for this file (".pytaw.conf") in the user's home directory 2 | [youtube] 3 | developer_key = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 4 | -------------------------------------------------------------------------------- /app/external/pytaw/README.md: -------------------------------------------------------------------------------- 1 | # PYTAW: Python YouTube API Wrapper 2 | 3 | ###Note 4 | This library is copied from [https://github.com/chibicitiberiu/pytaw/tree/improvements](https://github.com/chibicitiberiu/pytaw/tree/improvements). 5 | 6 | 7 | ```python 8 | >>> from pytaw import YouTube 9 | >>> youtube = YouTube(key='your_api_key') 10 | >>> video = youtube.video('4vuW6tQ0218') 11 | >>> video.title 12 | 'Monty Python - Dead Parrot' 13 | >>> video.published_at 14 | datetime.datetime(2007, 2, 14, 13, 55, 51, tzinfo=tzutc()) 15 | >>> channel = video.channel 16 | >>> channel.title 17 | 'Chadner' 18 | >>> search = youtube.search(q='monty python') 19 | >>> search[0] 20 | 21 | >>> for r in search[:5]: 22 | ... print(r) 23 | ... 24 | Monty Python 25 | Chemist Sketch - Monty Python's Flying Circus 26 | A Selection of Sketches from "Monty Python's Flying Circus" - #4 27 | Monty Python - Dead Parrot 28 | Monty Python And the holy grail 29 | ``` -------------------------------------------------------------------------------- /app/external/pytaw/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/external/pytaw/__init__.py -------------------------------------------------------------------------------- /app/external/pytaw/docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = pytaw 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /app/external/pytaw/docs/index.rst: -------------------------------------------------------------------------------- 1 | PYTAW: Python YouTube API Wrapper 2 | ================================= 3 | 4 | It's a wrapper for the YouTube python API. Written in python. 5 | 6 | .. automodule:: pytaw.youtube 7 | :members: 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | Indices and tables 14 | ================== 15 | 16 | * :ref:`genindex` 17 | * :ref:`modindex` 18 | * :ref:`search` 19 | -------------------------------------------------------------------------------- /app/external/pytaw/docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=pytaw 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /app/external/pytaw/main_test.py: -------------------------------------------------------------------------------- 1 | import pytaw 2 | 3 | yt = pytaw.YouTube(key='AIzaSyBabzE4Bup77WexdLMa9rN9z-wJidEfNX8') 4 | c = yt.channel('UCmmPgObSUPw1HL2lq6H4ffA') 5 | 6 | uploads_playlist = c.uploads_playlist 7 | print(repr(uploads_playlist)) 8 | 9 | uploads_list = list(uploads_playlist.items) 10 | for item in uploads_list: 11 | print(item.position, '...', repr(item), ' .... ', repr(item.video)) 12 | print(item.thumbnails) 13 | break 14 | -------------------------------------------------------------------------------- /app/external/pytaw/pytaw/__init__.py: -------------------------------------------------------------------------------- 1 | from .youtube import YouTube -------------------------------------------------------------------------------- /app/external/pytaw/pytaw/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | import urllib.parse 3 | import typing 4 | from datetime import datetime, timezone 5 | 6 | import dateutil.parser 7 | import itertools 8 | 9 | def string_to_datetime(string): 10 | if string is None: 11 | return None 12 | else: 13 | return dateutil.parser.parse(string) 14 | 15 | 16 | def datetime_to_string(dt): 17 | if dt is None: 18 | return None 19 | if dt.tzinfo is None: 20 | dt = dt.astimezone(timezone.utc) 21 | return dt.isoformat() 22 | 23 | 24 | def youtube_url_to_id(url): 25 | """Extract video id from a youtube url. 26 | 27 | If parsing fails, try regex. If that fails, return None. 28 | 29 | The regex is from somewhere in this thread, I think: 30 | https://stackoverflow.com/questions/3452546/how-do-i-get-the-youtube-video-id-from-a-url 31 | 32 | """ 33 | url = urllib.parse.unquote(url) 34 | url_data = urllib.parse.urlparse(url) 35 | query = urllib.parse.parse_qs(url_data.query) 36 | try: 37 | # parse the url for a video query 38 | return query["v"][0] 39 | except KeyError: 40 | # use regex to try and extract id 41 | match = re.search( 42 | r"((?<=(v|V)/)|(?<=be/)|(?<=(\?|\&)v=)|(?<=embed/))([\w-]+)", 43 | url, 44 | ) 45 | if match: 46 | return match.group() 47 | else: 48 | return None 49 | 50 | 51 | def youtube_duration_to_seconds(value): 52 | """Convert youtube (ISO 8601) duration to seconds. 53 | 54 | https://en.wikipedia.org/wiki/ISO_8601#Durations 55 | https://regex101.com/r/ALmmSS/1 56 | 57 | """ 58 | iso8601 = r"P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?T?(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?" 59 | match = re.match(iso8601, value) 60 | if match is None: 61 | return None 62 | 63 | group_names = ['years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds'] 64 | d = dict() 65 | for name, group in zip(group_names, match.groups(default=0)): 66 | d[name] = int(group) 67 | 68 | return int( 69 | d['years']*365*24*60*60 + 70 | d['months']*30*24*60*60 + 71 | d['weeks']*7*24*60*60 + 72 | d['days']*24*60*60 + 73 | d['hours']*60*60 + 74 | d['minutes']*60 + 75 | d['seconds'] 76 | ) 77 | 78 | 79 | def iterate_chunks(iterable: typing.Iterable, chunk_size: int): 80 | """ 81 | Iterates an iterable in chunks of chunk_size elements. 82 | :param iterable: An iterable containing items to iterate. 83 | :param chunk_size: Chunk size 84 | :return: Returns a generator which will yield chunks of size chunk_size 85 | """ 86 | 87 | it = iter(iterable) 88 | while True: 89 | chunk = tuple(itertools.islice(it, chunk_size)) 90 | if not chunk: 91 | return 92 | yield chunk 93 | -------------------------------------------------------------------------------- /app/external/pytaw/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='pytaw', 5 | version='0.0.1', 6 | packages=['pytaw'], 7 | url='https://github.com/6000hulls/pytaw', 8 | license='', 9 | author='6000hulls', 10 | author_email='6000hulls@gmail.com', 11 | description='PYTAW: Python YouTube API Wrapper' 12 | ) 13 | -------------------------------------------------------------------------------- /app/external/pytaw/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/app/external/pytaw/tests/__init__.py -------------------------------------------------------------------------------- /app/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "YtManager.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/assets/favicon.png -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 41 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 59 | 68 | 84 | 91 | 98 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /assets/ytsm_promo-tibich.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/assets/ytsm_promo-tibich.jpg -------------------------------------------------------------------------------- /assets/ytsm_promo-tibich.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/assets/ytsm_promo-tibich.png -------------------------------------------------------------------------------- /assets/ytsm_promo-tibich.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/assets/ytsm_promo-tibich.xcf -------------------------------------------------------------------------------- /assets/ytsm_promo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chibicitiberiu/ytsm/89499c75addc198be44be5ab9462ab58f3b983eb/assets/ytsm_promo.xcf -------------------------------------------------------------------------------- /config/config.ini: -------------------------------------------------------------------------------- 1 | ; Use ${env:environment_variable} to use the value of an environment variable. 2 | ; If a variable is not set here, it will be loaded from defaults.ini. 3 | 4 | ; The global section contains settings that apply to the entire server 5 | [global] 6 | 7 | Debug=True 8 | 9 | ; Secret key - django secret key 10 | SecretKey=^zv8@i2h!ko2lo=%ivq(9e#x=%q*i^^)6#4@(juzdx%&0c+9a0 11 | 12 | ; Database settings 13 | ; You can use any database engine supported by Django, as long as you add the required dependencies. 14 | ; Built-in engines: https://docs.djangoproject.com/en/2.1/ref/settings/#std:setting-DATABASE-ENGINE 15 | ; Others databases might be supported by installing the corect pip package. 16 | 17 | DatabaseEngine=django.db.backends.sqlite3 18 | DatabaseName=${DATA_DIR}/ytmanager.db 19 | ;DatabaseHost= 20 | ;DatabaseUser= 21 | ;DatabasePassword= 22 | ;DatabasePort= 23 | 24 | ; Database one-liner. If set, it will override any other Database* setting. 25 | ; Documentation: https://github.com/kennethreitz/dj-database-url 26 | ;DatabaseURL=sqlite:////full/path/to/your/database/file.sqlite 27 | 28 | ; Log settings, sets the log file location and the log level 29 | LogLevel=INFO 30 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | nginx: 5 | image: nginx:latest 6 | volumes: 7 | - ./docker/nginx:/etc/nginx/conf.d/ 8 | - ./app/YtManagerApp/static:/www/static 9 | - ./data/media:/www/media 10 | ports: 11 | - "80:80" 12 | depends_on: 13 | - web 14 | 15 | web: 16 | build: . 17 | tty: true 18 | ports: 19 | - "8000:8000" 20 | volumes: 21 | - ./data:/usr/src/ytsm/data 22 | - ./downloads:/usr/src/ytsm/downloads -------------------------------------------------------------------------------- /docker/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ./manage.py migrate 4 | gunicorn -b 0.0.0.0:8000 -w 4 YtManager.wsgi 5 | -------------------------------------------------------------------------------- /docker/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | # nginx.conf 2 | upstream djangoA { 3 | server web:8000 max_fails=3 fail_timeout=0; 4 | } 5 | server { 6 | include mime.types; 7 | # The port your site will be served on 8 | listen 80; 9 | # the domain name it will serve for 10 | server_name 0.0.0.0;# substitute your machine's IP address or FQDN 11 | charset utf-8; 12 | #Max upload size 13 | client_max_body_size 512M; # adjust to taste 14 | location /static { 15 | alias /www/static; 16 | expires 30d; 17 | } 18 | location /media { 19 | alias /www/media; 20 | expires 30d; 21 | } 22 | 23 | location / { 24 | try_files $uri @proxy_to_app; 25 | } 26 | 27 | # Finally, send all non-media requests to the Django server. 28 | location @proxy_to_app { 29 | proxy_set_header X-Real-IP $remote_addr; 30 | proxy_redirect off; 31 | proxy_set_header Host $host; 32 | proxy_pass http://djangoA; 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /examples/import_subscriptions/opml_list.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 9 | 11 | 13 | 15 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /examples/import_subscriptions/subscription_list.txt: -------------------------------------------------------------------------------- 1 | 2 | # This is a comment, it shold be ignored 3 | 4 | # ##Blank lines as well 5 | 6 | https://www.youtube.com/channel/UCMtFAi84ehTSYSE9XoHefig 7 | https://www.youtube.com/user/adric22 8 | 9 | 10 | https://www.youtube.com/watch?v=IuLxX07isNg&list=PLfABUWdDse7antKQRPnYLNJ6tv_hMYwIs #Comment after URL -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytz 2 | requests 3 | apscheduler 4 | gunicorn 5 | django 6 | django-crispy-forms 7 | django-dynamic-preferences 8 | dj_database_url 9 | youtube-dl 10 | google-api-python-client 11 | google_auth_oauthlib 12 | oauth2client 13 | psycopg2-binary 14 | python-dateutil 15 | Pillow --------------------------------------------------------------------------------