├── .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 |
18 | Root node
19 |
20 | Child node 1
21 | Child node 2
22 |
23 |
24 |
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 | select node with id 1 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 |
15 | Node 01
16 |
20 |
21 | Node 02
22 | Node 03
23 |
27 |
28 | Node 04
29 | Node 05
30 |
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 |
13 | Node 01
14 |
18 |
19 | Node 02
20 | Node 03
21 |
25 |
26 | Node 04
27 | Node 05
28 |
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 |
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 |
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 |
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 |
5 |
6 |
7 | {% block modal_content %}
8 |
9 | {% block modal_header_wrapper %}
10 |
20 | {% endblock modal_header_wrapper %}
21 |
22 | {% block modal_body_wrapper %}
23 |
24 | {% block modal_body %}
25 | {% endblock %}
26 |
27 | {% endblock modal_body_wrapper %}
28 |
29 | {% block modal_footer_wrapper %}
30 |
35 | {% endblock modal_footer_wrapper %}
36 |
37 | {% endblock modal_content %}
38 |
39 |
40 |
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 |
5 | {% for err in config_errors %}
6 | {{ err }}
7 | {% endfor %}
8 |
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 |
18 | {% for err in config_warnings %}
19 | {{ err }}
20 | {% endfor %}
21 |
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 |
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 |
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 |
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 |
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 |
23 |
24 |
25 |
26 | Visit https://developers.google.com/ , log in or create an account, if necessary.
27 | Go to , and click on the Create project button.
28 |
29 |
30 | Give a name to the project, and then click on Create . Wait for a few seconds, until Google finishes creating the project.
31 |
32 |
33 | 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 |
37 | Find and click on the YouTube Data API v3 from the list.
38 |
39 |
40 | Click Enable to enable the YouTube API for your account.
41 |
42 |
43 | In the navigation sidebar, go to the Credentials page.
44 |
45 |
46 | Click on Create credentials .
47 |
48 |
49 | 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 |
54 | Copy the created key in the box below, and then hit Done .
55 |
56 |
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 |
65 |
66 |
67 |
68 | {# Video toolbar #}
69 |
70 | {% crispy filter_form %}
71 |
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 |
12 | You must be an administrator to access this page!
13 |
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 |
22 | You have been logged out!
23 |
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 |
17 | Your account doesn't have access to this page. To proceed,
18 | please login with an account that has access.
19 |
20 | {% else %}
21 |
22 | Please login or register to see this page.
23 |
24 | {% endif %}
25 | {% endif %}
26 |
27 |
Login
28 |
29 |
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 |
21 | The password has been changed!
22 |
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 |
24 |
25 | {% else %}
26 |
27 |
28 | The password reset link was invalid, possibly because it has already been used. Please request a new password reset.
29 |
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 |
22 |
We've emailed you instructions for resetting your password.
23 |
If they haven't arrived in a few minutes, check your spam folder.
24 |
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 |
17 | Your account doesn't have access to this page. To proceed,
18 | please login with an account that has access.
19 |
20 | {% else %}
21 |
22 | Please login or register to see this page.
23 |
24 | {% endif %}
25 | {% endif %}
26 |
27 |
Password reset
28 |
29 |
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 |
17 | Your account doesn't have access to this page. To proceed,
18 | please login with an account that has access.
19 |
20 | {% else %}
21 |
22 | Please login or register to see this page.
23 |
24 | {% endif %}
25 | {% endif %}
26 |
27 | {% if not global_preferences.general__allow_registrations %}
28 |
29 | Registrations are disabled by the administrator!
30 |
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 |
22 | You have registered successfully!
23 |
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
--------------------------------------------------------------------------------