├── .dockerignore
├── .editorconfig
├── .github
├── FUNDING.yml
├── actions
│ ├── FFmpeg
│ │ └── action.yml
│ ├── get-tag
│ │ └── action.yml
│ ├── string-case
│ │ └── action.yml
│ └── yt-dlp
│ │ └── action.yml
├── sh
│ └── library
│ │ └── variables.inc.sh
└── workflows
│ ├── ci.yaml
│ └── release.yaml
├── .gitignore
├── Dockerfile
├── LICENSE
├── Makefile
├── Pipfile
├── README.md
├── config
└── root
│ ├── docs
│ └── index.html
│ └── etc
│ ├── nginx
│ ├── nginx.conf
│ └── token_server.conf
│ └── s6-overlay
│ └── s6-rc.d
│ ├── gunicorn
│ ├── dependencies
│ ├── run
│ └── type
│ ├── nginx
│ ├── dependencies
│ ├── run
│ └── type
│ ├── tubesync-db-worker
│ ├── dependencies
│ ├── down-signal
│ ├── run
│ └── type
│ ├── tubesync-fs-worker
│ ├── dependencies
│ ├── down-signal
│ ├── run
│ └── type
│ ├── tubesync-init
│ ├── dependancies
│ ├── run
│ ├── type
│ └── up
│ ├── tubesync-network-worker
│ ├── dependencies
│ ├── down-signal
│ ├── run
│ └── type
│ └── user
│ └── contents.d
│ ├── gunicorn
│ ├── nginx
│ ├── tubesync-db-worker
│ ├── tubesync-fs-worker
│ ├── tubesync-init
│ └── tubesync-network-worker
├── dev.env
├── docs
├── create-missing-metadata.md
├── custom-filters.md
├── dashboard-v0.5.png
├── import-existing-media.md
├── media-item-v0.5.png
├── media-v0.5.png
├── other-database-backends.md
├── reset-metadata.md
├── reset-tasks.md
├── source-v0.5.png
├── sources-v0.5.png
└── using-cookies.md
├── patches
├── background_task
│ ├── management
│ │ └── commands
│ │ │ └── process_tasks.py
│ ├── models.py
│ └── utils.py
└── yt_dlp
│ ├── patch
│ ├── __init__.py
│ ├── check_thumbnails.py
│ └── fatal_http_errors.py
│ └── postprocessor
│ └── modify_chapters.py
└── tubesync
├── common
├── __init__.py
├── admin.py
├── apps.py
├── context_processors.py
├── errors.py
├── json.py
├── logger.py
├── middleware.py
├── migrations
│ └── __init__.py
├── models.py
├── static
│ ├── fonts
│ │ ├── fontawesome
│ │ │ ├── fa-brands-400.eot
│ │ │ ├── fa-brands-400.svg
│ │ │ ├── fa-brands-400.ttf
│ │ │ ├── fa-brands-400.woff
│ │ │ ├── fa-brands-400.woff2
│ │ │ ├── fa-regular-400.eot
│ │ │ ├── fa-regular-400.svg
│ │ │ ├── fa-regular-400.ttf
│ │ │ ├── fa-regular-400.woff
│ │ │ ├── fa-regular-400.woff2
│ │ │ ├── fa-solid-900.eot
│ │ │ ├── fa-solid-900.svg
│ │ │ ├── fa-solid-900.ttf
│ │ │ ├── fa-solid-900.woff
│ │ │ └── fa-solid-900.woff2
│ │ └── roboto
│ │ │ ├── roboto-bold.woff
│ │ │ ├── roboto-light.woff
│ │ │ └── roboto-regular.woff
│ ├── images
│ │ ├── favicon.ico
│ │ ├── nothumb.png
│ │ ├── tubesync-transparent.png
│ │ └── tubesync.png
│ └── styles
│ │ ├── _colours.scss
│ │ ├── _fonts.scss
│ │ ├── _forms.scss
│ │ ├── _helpers.scss
│ │ ├── _template.scss
│ │ ├── _variables.scss
│ │ ├── fontawesome
│ │ ├── _animated.scss
│ │ ├── _bordered-pulled.scss
│ │ ├── _core.scss
│ │ ├── _fixed-width.scss
│ │ ├── _icons.scss
│ │ ├── _larger.scss
│ │ ├── _list.scss
│ │ ├── _mixins.scss
│ │ ├── _rotated-flipped.scss
│ │ ├── _screen-reader.scss
│ │ ├── _shims.scss
│ │ ├── _stacked.scss
│ │ ├── _variables.scss
│ │ ├── brands.scss
│ │ ├── fontawesome.scss
│ │ ├── regular.scss
│ │ ├── solid.scss
│ │ └── v4-shims.scss
│ │ ├── materializecss
│ │ ├── components
│ │ │ ├── _badges.scss
│ │ │ ├── _buttons.scss
│ │ │ ├── _cards.scss
│ │ │ ├── _carousel.scss
│ │ │ ├── _chips.scss
│ │ │ ├── _collapsible.scss
│ │ │ ├── _color-classes.scss
│ │ │ ├── _color-variables.scss
│ │ │ ├── _datepicker.scss
│ │ │ ├── _dropdown.scss
│ │ │ ├── _global.scss
│ │ │ ├── _grid.scss
│ │ │ ├── _icons-material-design.scss
│ │ │ ├── _materialbox.scss
│ │ │ ├── _modal.scss
│ │ │ ├── _navbar.scss
│ │ │ ├── _normalize.scss
│ │ │ ├── _preloader.scss
│ │ │ ├── _pulse.scss
│ │ │ ├── _sidenav.scss
│ │ │ ├── _slider.scss
│ │ │ ├── _table_of_contents.scss
│ │ │ ├── _tabs.scss
│ │ │ ├── _tapTarget.scss
│ │ │ ├── _timepicker.scss
│ │ │ ├── _toast.scss
│ │ │ ├── _tooltip.scss
│ │ │ ├── _transitions.scss
│ │ │ ├── _typography.scss
│ │ │ ├── _variables.scss
│ │ │ ├── _waves.scss
│ │ │ └── forms
│ │ │ │ ├── _checkboxes.scss
│ │ │ │ ├── _file-input.scss
│ │ │ │ ├── _forms.scss
│ │ │ │ ├── _input-fields.scss
│ │ │ │ ├── _radio-buttons.scss
│ │ │ │ ├── _range.scss
│ │ │ │ ├── _select.scss
│ │ │ │ └── _switches.scss
│ │ └── materialize.scss
│ │ └── tubesync.scss
├── templates
│ ├── base.html
│ ├── error403.html
│ ├── error404.html
│ ├── error500.html
│ ├── errorbox.html
│ ├── infobox.html
│ ├── pagination.html
│ ├── simpleform.html
│ ├── tubesync-coloured.svg
│ └── tubesync.svg
├── tests.py
├── testutils.py
├── third_party_versions.py
├── timestamp.py
├── urls.py
├── utils.py
└── views.py
├── full_playlist.sh
├── healthcheck.py
├── manage.py
├── restart_services.sh
├── sync
├── __init__.py
├── admin.py
├── apps.py
├── choices.py
├── fields.py
├── filtering.py
├── forms.py
├── hooks.py
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ ├── delete-source.py
│ │ ├── fix-mariadb.py
│ │ ├── import-existing-media.py
│ │ ├── list-sources.py
│ │ ├── reset-metadata.py
│ │ ├── reset-tasks.py
│ │ ├── sync-missing-metadata.py
│ │ └── youtube-dl-info.py
├── matching.py
├── mediaservers.py
├── migrations
│ ├── 0001_initial.py
│ ├── 0001_squashed_0030_alter_source_source_vcodec.py
│ ├── 0002_auto_20201213_0817.py
│ ├── 0003_source_copy_thumbnails.py
│ ├── 0004_source_media_format.py
│ ├── 0005_auto_20201219_0312.py
│ ├── 0006_source_write_nfo.py
│ ├── 0007_auto_20201219_0645.py
│ ├── 0008_source_download_cap.py
│ ├── 0009_auto_20210218_0442.py
│ ├── 0010_auto_20210924_0554.py
│ ├── 0011_auto_20220201_1654.py
│ ├── 0012_alter_media_downloaded_format.py
│ ├── 0013_fix_elative_media_file.py
│ ├── 0014_alter_media_media_file.py
│ ├── 0015_auto_20230213_0603.py
│ ├── 0016_auto_20230214_2052.py
│ ├── 0017_alter_source_sponsorblock_categories.py
│ ├── 0018_source_subtitles.py
│ ├── 0019_add_delete_removed_media.py
│ ├── 0020_auto_20231024_1825.py
│ ├── 0021_source_copy_channel_images.py
│ ├── 0022_add_delete_files_on_disk.py
│ ├── 0023_media_duration_filter.py
│ ├── 0024_auto_20240717_1535.py
│ ├── 0025_add_video_type_support.py
│ ├── 0026_alter_source_sub_langs.py
│ ├── 0027_alter_source_sponsorblock_categories.py
│ ├── 0028_alter_source_source_resolution.py
│ ├── 0029_alter_mediaserver_fields.py
│ ├── 0030_alter_source_source_vcodec.py
│ ├── 0031_metadata_metadataformat.py
│ ├── 0031_squashed_metadata_metadataformat.py
│ ├── 0032_alter_metadata_options_alter_metadataformat_options_and_more.py
│ ├── 0032_metadata_transfer.py
│ ├── 0033_alter_mediaserver_options_alter_source_source_acodec_and_more.py
│ ├── 0034_source_target_schedule_and_more.py
│ └── __init__.py
├── models
│ ├── __init__.py
│ ├── _migrations.py
│ ├── _private.py
│ ├── media.py
│ ├── media__tasks.py
│ ├── media_server.py
│ ├── metadata.py
│ ├── metadata_format.py
│ └── source.py
├── overrides
│ └── custom_filter.py
├── signals.py
├── tasks.py
├── templates
│ ├── sync
│ │ ├── _mediaformatvars.html
│ │ ├── dashboard.html
│ │ ├── media-enable.html
│ │ ├── media-item.html
│ │ ├── media-redownload.html
│ │ ├── media-skip.html
│ │ ├── media.html
│ │ ├── mediaserver-add.html
│ │ ├── mediaserver-delete.html
│ │ ├── mediaserver-update.html
│ │ ├── mediaserver.html
│ │ ├── mediaservers.html
│ │ ├── source-add.html
│ │ ├── source-delete.html
│ │ ├── source-update.html
│ │ ├── source-validate.html
│ │ ├── source.html
│ │ ├── sources.html
│ │ ├── task-schedule.html
│ │ ├── tasks-completed.html
│ │ ├── tasks-reset.html
│ │ └── tasks.html
│ └── widgets
│ │ ├── checkbox_option.html
│ │ └── checkbox_select.html
├── templatetags
│ ├── __init__.py
│ └── filters.py
├── testdata
│ ├── README.md
│ ├── metadata.json
│ ├── metadata_2023-06-29.json
│ ├── metadata_60fps.json
│ ├── metadata_60fps_hdr.json
│ ├── metadata_hdr.json
│ └── metadata_low_formats.json
├── tests.py
├── urls.py
├── utils.py
├── views.py
└── youtube.py
├── tubesync
├── __init__.py
├── asgi.py
├── dbutils.py
├── gunicorn.py
├── local_settings.py.container
├── local_settings.py.example
├── settings.py
├── urls.py
└── wsgi.py
└── upgrade_yt-dlp.sh
/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | .gitignore
3 | .github
4 | .gitattributes
5 | README.md
6 | tubesync/media
7 | tubesync/tubesync/downloads
8 | db.sqlite3
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # https://editorconfig.org/
2 |
3 | root = true
4 |
5 | [*]
6 | indent_style = space
7 | indent_size = 4
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 | end_of_line = lf
11 | charset = utf-8
12 |
13 | # rc.d files should be unchanged
14 | [config/root/etc/s6-overlay/s6-rc.d/**]
15 | indent_style = unset
16 | indent_size = tab
17 | insert_final_newline = false
18 | trim_trailing_whitespace = false
19 |
20 | # Docstrings and comments use max_line_length = 79
21 | [*.py]
22 | max_line_length = 88
23 |
24 | # Use 2 spaces for the YAML files
25 | [*.y{,a}ml]
26 | indent_size = 2
27 |
28 | # Use 2 spaces for the HTML files
29 | [*.html]
30 | indent_size = 2
31 |
32 | # Use 2 spaces for the SCSS files
33 | [*.scss]
34 | indent_size = 2
35 |
36 | [**/admin/js/vendor/**]
37 | indent_style = unset
38 | indent_size = unset
39 |
40 | # Minified JavaScript files shouldn't be changed
41 | [**.min.js]
42 | indent_style = unset
43 | indent_size = unset
44 | insert_final_newline = false
45 | trim_trailing_whitespace = false
46 |
47 | # Makefiles always use tabs for indentation
48 | [Makefile]
49 | indent_style = tab
50 |
51 | # Batch files use tabs for indentation
52 | [*.bat]
53 | indent_style = tab
54 |
55 | [docs/**.txt]
56 | max_line_length = 79
57 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [meeb]
2 |
--------------------------------------------------------------------------------
/.github/actions/get-tag/action.yml:
--------------------------------------------------------------------------------
1 | name: Get tag
2 | description: Get tag name from GITHUB_REF environment variable
3 | inputs:
4 | strip_v:
5 | required: false
6 | default: false
7 | description: Whether to strip "v" from the tag or not
8 | outputs:
9 | tag:
10 | value: ${{ steps.set.outputs.tag }}
11 | description: Git tag name
12 |
13 | runs:
14 | using: 'composite'
15 | steps:
16 | - name: Set outputs
17 | id: 'set'
18 | env:
19 | INPUT_STRIP_V: '${{ inputs.strip_v }}'
20 | shell: 'bash'
21 | run: |
22 | tag="${GITHUB_REF}"
23 | printf -- 'Manipulating string: %s\n' "${tag}"
24 | test -n "${tag}" || exit 1
25 |
26 | case "${tag}" in
27 | (refs/tags/*) tag="${tag#refs/tags/}" ;;
28 | (*) printf -- 'Not a tag ref\n' ; exit 2 ;;
29 | esac
30 |
31 | if [ 'true' = "${INPUT_STRIP_V,,}" ]
32 | then
33 | tag="${tag#[Vv]}"
34 | fi
35 |
36 | set_sl_var() { local f='%s=%s\n' ; printf -- "${f}" "$@" ; } ;
37 |
38 | set_sl_var tag "${tag}" >> "${GITHUB_OUTPUT}"
39 |
40 | set_sl_var 'tag ' " ${tag}"
41 |
42 |
--------------------------------------------------------------------------------
/.github/actions/string-case/action.yml:
--------------------------------------------------------------------------------
1 | name: Change String Case
2 | description: Make a string lowercase, uppercase, or capitalized
3 |
4 | inputs:
5 | string:
6 | description: The input string
7 | required: true
8 |
9 | outputs:
10 | lowercase:
11 | value: ${{ steps.set.outputs.lowercase }}
12 | description: The input string, with any uppercase characters replaced with lowercase ones
13 | uppercase:
14 | value: ${{ steps.set.outputs.uppercase }}
15 | description: The input string, with any lowercase characters replaced with uppercase ones
16 | capitalized:
17 | value: ${{ steps.set.outputs.capitalized }}
18 | description: The input string, with any alphabetical characters lowercase, except for the first character, which is uppercased
19 |
20 | runs:
21 | using: 'composite'
22 | steps:
23 | - name: Set outputs
24 | id: 'set'
25 | env:
26 | INPUT_STRING: '${{ inputs.string }}'
27 | shell: 'bash'
28 | run: |
29 | printf -- 'Manipulating string: %s\n' "${INPUT_STRING}"
30 | set_sl_var() { local f='%s=%s\n' ; printf -- "${f}" "$@" ; } ;
31 | mk_delim() { printf -- '"%s_EOF_%d_"' "$1" "${RANDOM}" ; } ;
32 | open_ml_var() { local f=''\%'s<<'\%'s\n' ; printf -- "${f}" "$2" "$1" ; } ;
33 | close_ml_var() { local f='%s\n' ; printf -- "${f}" "$1" ; } ;
34 | {
35 |
36 | var='lowercase' ;
37 | delim="$(mk_delim "${var}")" ;
38 | open_ml_var "${delim}" "${var}" ;
39 | printf -- '%s\n' "${INPUT_STRING,,}" ;
40 | close_ml_var "${delim}" "${var}" ;
41 |
42 | var='capitalized' ;
43 | delim="$(mk_delim "${var}")" ;
44 | open_ml_var "${delim}" "${var}" ;
45 | printf -- '%s\n' "${INPUT_STRING^}" ;
46 | close_ml_var "${delim}" "${var}" ;
47 |
48 | var='uppercase' ;
49 | delim="$(mk_delim "${var}")" ;
50 | open_ml_var "${delim}" "${var}" ;
51 | printf -- '%s\n' "${INPUT_STRING^^}" ;
52 | close_ml_var "${delim}" "${var}" ;
53 |
54 | } >> "${GITHUB_OUTPUT}"
55 | printf -- '%s: %s\n' 'lowercase' "${INPUT_STRING,,}"
56 | printf -- '%s: %s\n' 'uppercase' "${INPUT_STRING^^}"
57 | printf -- '%s: %s\n' 'capitalized' "${INPUT_STRING^}"
58 |
--------------------------------------------------------------------------------
/.github/sh/library/variables.inc.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | # For setting single line variables in the environment or output
4 | set_sl_var() { local f='%s=%s\n' ; printf -- "${f}" "$@" ; } ;
5 |
6 | # Used together to set multiple line variables in the environment or output
7 | mk_delim() { local f='%s_EOF_%d_' ; printf -- "${f}" "$1" "${RANDOM}" ; } ;
8 | open_ml_var() { local f=''\%'s<<'\%'s\n' ; printf -- "${f}" "$2" "$1" ; } ;
9 | close_ml_var() { local f='%s\n' ; printf -- "${f}" "$1" ; } ;
10 |
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | # Byte-compiled / optimized / DLL files
3 | __pycache__/
4 | *.py[cod]
5 | *$py.class
6 |
7 | # C extensions
8 | *.so
9 |
10 | # vim swap files
11 | .*.swp
12 |
13 | # Distribution / packaging
14 | .Python
15 | build/
16 | develop-eggs/
17 | dist/
18 | downloads/
19 | eggs/
20 | .eggs/
21 | lib/
22 | lib64/
23 | parts/
24 | sdist/
25 | var/
26 | wheels/
27 | pip-wheel-metadata/
28 | share/python-wheels/
29 | *.egg-info/
30 | .installed.cfg
31 | *.egg
32 | MANIFEST
33 |
34 | # PyInstaller
35 | # Usually these files are written by a python script from a template
36 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
37 | *.manifest
38 | *.spec
39 |
40 | # Installer logs
41 | pip-log.txt
42 | pip-delete-this-directory.txt
43 |
44 | # Unit test / coverage reports
45 | htmlcov/
46 | .tox/
47 | .nox/
48 | .coverage
49 | .coverage.*
50 | .cache
51 | nosetests.xml
52 | coverage.xml
53 | *.cover
54 | *.py,cover
55 | .hypothesis/
56 | .pytest_cache/
57 |
58 | # Translations
59 | *.mo
60 | *.pot
61 |
62 | # Django stuff:
63 | *.log
64 | local_settings.py
65 | db.sqlite3
66 | db.sqlite3-journal
67 | /tubesync/static/
68 | /tubesync/media/
69 | /tubesync/tubesync/downloads/
70 |
71 | # Flask stuff:
72 | instance/
73 | .webassets-cache
74 |
75 | # Scrapy stuff:
76 | .scrapy
77 |
78 | # Sphinx documentation
79 | docs/_build/
80 |
81 | # PyBuilder
82 | target/
83 |
84 | # Jupyter Notebook
85 | .ipynb_checkpoints
86 |
87 | # IPython
88 | profile_default/
89 | ipython_config.py
90 |
91 | # pyenv
92 | .python-version
93 |
94 | # pipenv
95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
98 | # install all needed dependencies.
99 | #Pipfile.lock
100 |
101 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
102 | __pypackages__/
103 |
104 | # Celery stuff
105 | celerybeat-schedule
106 | celerybeat.pid
107 |
108 | # SageMath parsed files
109 | *.sage.py
110 |
111 | # Environments
112 | .env
113 | .venv
114 | env/
115 | venv/
116 | ENV/
117 | env.bak/
118 | venv.bak/
119 |
120 | # Spyder project settings
121 | .spyderproject
122 | .spyproject
123 |
124 | # Rope project settings
125 | .ropeproject
126 |
127 | # mkdocs documentation
128 | /site
129 |
130 | # mypy
131 | .mypy_cache/
132 | .dmypy.json
133 | dmypy.json
134 |
135 | # Pyre type checker
136 | .pyre/
137 |
138 | Pipfile.lock
139 | .vscode/launch.json
140 |
141 | # Ignore Jetbrains IDE files
142 | .idea/
143 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | python=/usr/bin/env python
2 | docker=/usr/bin/docker
3 | name=tubesync
4 | image=$(name):latest
5 |
6 |
7 | all: clean build
8 |
9 |
10 | dev:
11 | $(python) tubesync/manage.py runserver
12 |
13 |
14 | build:
15 | mkdir -p tubesync/media
16 | mkdir -p tubesync/static
17 | $(python) tubesync/manage.py collectstatic --noinput
18 |
19 |
20 | clean:
21 | rm -rf tubesync/static
22 |
23 |
24 | container: clean
25 | $(docker) build -t $(image) .
26 |
27 |
28 | runcontainer:
29 | $(docker) run --rm --name $(name) --env-file dev.env --log-opt max-size=50m -ti -p 4848:4848 $(image)
30 |
31 |
32 | stopcontainer:
33 | $(docker) stop $(name)
34 |
35 |
36 | test: build
37 | cd tubesync && $(python) manage.py test --verbosity=2 && cd ..
38 |
39 |
40 | shell:
41 | cd tubesync && $(python) manage.py shell
42 |
--------------------------------------------------------------------------------
/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | name = "pypi"
3 | url = "https://pypi.org/simple"
4 | verify_ssl = true
5 |
6 | [dev-packages]
7 | autopep8 = "*"
8 |
9 | [packages]
10 | django = "~=5.2.1"
11 | django-sass-processor = {extras = ["management-command"], version = "*"}
12 | pillow = "*"
13 | whitenoise = "*"
14 | gunicorn = "*"
15 | httptools = "*"
16 | django-background-tasks = ">=1.2.8"
17 | django-basicauth = "*"
18 | psycopg = {extras = ["binary", "pool"], version = "*"}
19 | mysqlclient = "*"
20 | PySocks = "*"
21 | urllib3 = {extras = ["socks"], version = "*"}
22 | requests = {extras = ["socks"], version = "*"}
23 | yt-dlp = {extras = ["default", "curl-cffi"], version = "*"}
24 | emoji = "*"
25 | brotli = "*"
26 | html5lib = "*"
27 | bgutil-ytdlp-pot-provider = "*"
28 |
--------------------------------------------------------------------------------
/config/root/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Something has gone very wrong
7 |
14 |
15 |
16 |
17 | Something has gone very wrong
18 |
19 |
20 |
21 | If you can see this message then the front end web server has not forwarded the
22 | connection on to the TubeSync back end server. This probably means something has
23 | gone wrong with the container build or a process has crashed. Try restarting it.
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/config/root/etc/nginx/token_server.conf:
--------------------------------------------------------------------------------
1 | upstream token_server {
2 | server 127.0.0.2:4416 down;
3 | }
4 |
5 | server {
6 |
7 | # Ports
8 | listen 4416;
9 | listen [::]:4416;
10 |
11 | # Server domain name
12 | server_name _;
13 |
14 | set_by_lua_block $pot_url {
15 | local default = 'http://token_server'
16 | local url = os.getenv('YT_POT_BGUTIL_BASE_URL')
17 | if not url then
18 | return default
19 | end
20 | if #url and url:find('://') then
21 | return url
22 | end
23 | return default
24 | }
25 |
26 | location / {
27 | proxy_pass $pot_url;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/gunicorn/dependencies:
--------------------------------------------------------------------------------
1 | tubesync-init
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/gunicorn/run:
--------------------------------------------------------------------------------
1 | #!/command/with-contenv bash
2 |
3 | UMASK_SET=${UMASK_SET:-022}
4 | umask "$UMASK_SET"
5 |
6 | cd /app || exit
7 |
8 | PIDFILE=/run/app/gunicorn.pid
9 |
10 | if [ -f "${PIDFILE}" ]
11 | then
12 | PID=$(cat $PIDFILE)
13 | echo "Unexpected PID file exists at ${PIDFILE} with PID: ${PID}"
14 | if kill -0 $PID
15 | then
16 | echo "Killing old gunicorn process with PID: ${PID}"
17 | kill -9 $PID
18 | fi
19 | echo "Removing stale PID file: ${PIDFILE}"
20 | rm ${PIDFILE}
21 | fi
22 |
23 | exec s6-setuidgid app \
24 | /usr/local/bin/gunicorn -c /app/tubesync/gunicorn.py --capture-output tubesync.wsgi:application
25 |
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/gunicorn/type:
--------------------------------------------------------------------------------
1 | longrun
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/nginx/dependencies:
--------------------------------------------------------------------------------
1 | gunicorn
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/nginx/run:
--------------------------------------------------------------------------------
1 | #!/command/with-contenv bash
2 |
3 | cd /
4 |
5 | https="${TUBESYNC_POT_HTTPS:+https}"
6 | ip_address="${TUBESYNC_POT_IPADDR:-${POTSERVER_PORT_4416_TCP_ADDR}}"
7 | : "${TUBESYNC_POT_PORT:=${POTSERVER_PORT_4416_TCP_PORT}}"
8 | port="${TUBESYNC_POT_PORT:+:}${TUBESYNC_POT_PORT}"
9 |
10 | if [ -n "${ip_address}" ]
11 | then
12 | YT_POT_BGUTIL_BASE_URL="${https:-http}://${ip_address}${port}"
13 | export YT_POT_BGUTIL_BASE_URL
14 | fi
15 |
16 | exec /usr/bin/openresty -c /etc/nginx/nginx.conf -e stderr
17 |
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/nginx/type:
--------------------------------------------------------------------------------
1 | longrun
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/tubesync-db-worker/dependencies:
--------------------------------------------------------------------------------
1 | gunicorn
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/tubesync-db-worker/down-signal:
--------------------------------------------------------------------------------
1 | SIGINT
2 |
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/tubesync-db-worker/run:
--------------------------------------------------------------------------------
1 | #!/command/with-contenv bash
2 |
3 | exec nice -n "${TUBESYNC_NICE:-1}" s6-setuidgid app \
4 | /usr/bin/python3 /app/manage.py process_tasks \
5 | --queue database --duration 86400 \
6 | --sleep "30.${RANDOM}"
7 |
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/tubesync-db-worker/type:
--------------------------------------------------------------------------------
1 | longrun
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/tubesync-fs-worker/dependencies:
--------------------------------------------------------------------------------
1 | gunicorn
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/tubesync-fs-worker/down-signal:
--------------------------------------------------------------------------------
1 | SIGINT
2 |
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/tubesync-fs-worker/run:
--------------------------------------------------------------------------------
1 | #!/command/with-contenv bash
2 |
3 | exec nice -n "${TUBESYNC_NICE:-1}" s6-setuidgid app \
4 | /usr/bin/python3 /app/manage.py process_tasks \
5 | --queue filesystem --duration 43200 \
6 | --sleep "20.${RANDOM}"
7 |
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/tubesync-fs-worker/type:
--------------------------------------------------------------------------------
1 | longrun
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/tubesync-init/dependancies:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/config/root/etc/s6-overlay/s6-rc.d/tubesync-init/dependancies
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/tubesync-init/run:
--------------------------------------------------------------------------------
1 | #!/command/with-contenv bash
2 |
3 | # Change runtime user UID and GID
4 | groupmod -o -g "${PGID:=911}" app
5 | usermod -o -u "${PUID:=911}" app
6 |
7 | # Reset permissions
8 | chown -R app:app /run/app
9 | chmod -R 0700 /run/app
10 | chown -R app:app /config
11 | chmod -R 0755 /config
12 | chown -R root:app /app
13 | chmod -R 0750 /app
14 | chmod 0755 /app/*.py /app/*.sh
15 | find /app -mindepth 2 -type f -execdir chmod 640 '{}' +
16 | chown -R app:app /app/common/static
17 | chown -R app:app /app/static
18 |
19 | # Optionally reset the download dir permissions
20 | if [ "${TUBESYNC_RESET_DOWNLOAD_DIR:=True}" == "True" ]
21 | then
22 | export TUBESYNC_RESET_DOWNLOAD_DIR
23 | echo "TUBESYNC_RESET_DOWNLOAD_DIR=True, Resetting /downloads directory permissions"
24 | chown -R app:app /downloads
25 | chmod -R 0755 /downloads
26 | fi
27 |
28 | if [ 'True' = "${TUBESYNC_DEBUG:-False}" ]
29 | then
30 | s6-setuidgid app \
31 | /usr/bin/python3 /app/manage.py \
32 | showmigrations -v 3 --list
33 | fi
34 |
35 | # Run migrations
36 | exec s6-setuidgid app \
37 | /usr/bin/python3 /app/manage.py migrate
38 |
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/tubesync-init/type:
--------------------------------------------------------------------------------
1 | oneshot
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/tubesync-init/up:
--------------------------------------------------------------------------------
1 | #!/command/execlineb -P
2 |
3 | /etc/s6-overlay/s6-rc.d/tubesync-init/run
4 |
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/tubesync-network-worker/dependencies:
--------------------------------------------------------------------------------
1 | gunicorn
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/tubesync-network-worker/down-signal:
--------------------------------------------------------------------------------
1 | SIGINT
2 |
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/tubesync-network-worker/run:
--------------------------------------------------------------------------------
1 | #!/command/with-contenv bash
2 |
3 | exec nice -n "${TUBESYNC_NICE:-1}" s6-setuidgid app \
4 | /usr/bin/python3 /app/manage.py process_tasks \
5 | --queue network --duration 43200 \
6 | --sleep "10.${RANDOM}"
7 |
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/tubesync-network-worker/type:
--------------------------------------------------------------------------------
1 | longrun
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/user/contents.d/gunicorn:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/config/root/etc/s6-overlay/s6-rc.d/user/contents.d/gunicorn
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/user/contents.d/nginx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/config/root/etc/s6-overlay/s6-rc.d/user/contents.d/nginx
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/user/contents.d/tubesync-db-worker:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/config/root/etc/s6-overlay/s6-rc.d/user/contents.d/tubesync-db-worker
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/user/contents.d/tubesync-fs-worker:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/config/root/etc/s6-overlay/s6-rc.d/user/contents.d/tubesync-fs-worker
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/user/contents.d/tubesync-init:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/config/root/etc/s6-overlay/s6-rc.d/user/contents.d/tubesync-init
--------------------------------------------------------------------------------
/config/root/etc/s6-overlay/s6-rc.d/user/contents.d/tubesync-network-worker:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/config/root/etc/s6-overlay/s6-rc.d/user/contents.d/tubesync-network-worker
--------------------------------------------------------------------------------
/dev.env:
--------------------------------------------------------------------------------
1 | GUNICORN_WORKERS=1
2 | DJANGO_ALLOWED_HOSTS=localhost
3 | DJANGO_SECRET_KEY=not-a-secret
4 | PUID=1234
5 | PGID=1234
6 |
--------------------------------------------------------------------------------
/docs/create-missing-metadata.md:
--------------------------------------------------------------------------------
1 | # TubeSync
2 |
3 | ## Advanced usage guide - creating missing metadata
4 |
5 | This is a new feature in v0.9 of TubeSync and later. It allows you to create or
6 | re-create missing metadata in your TubeSync download directories for missing `nfo`
7 | files and thumbnails.
8 |
9 | If you add a source with "write NFO files" or "copy thumbnails" disabled, download
10 | some media and then update the source to write NFO files or copy thumbnails then
11 | TubeSync will not automatically retroactively attempt to copy or create your missing
12 | metadata files. You can use a special one-off command to manually write missing
13 | metadata files to the correct locations.
14 |
15 | ## Requirements
16 |
17 | You have added a source without metadata writing enabled, downloaded some media, then
18 | updated the source to enable metadata writing.
19 |
20 | ## Steps
21 |
22 | ### 1. Run the batch metadata sync command
23 |
24 | Execute the following Django command:
25 |
26 | `./manage.py sync-missing-metadata`
27 |
28 | When deploying TubeSync inside a container, you can execute this with:
29 |
30 | `docker exec -ti tubesync python3 /app/manage.py sync-missing-metadata`
31 |
32 | This command will log what its doing to the terminal when you run it.
33 |
34 | Internally, this command loops over all your sources which have been saved with
35 | "write NFO files" or "copy thumbnails" enabled. Then, loops over all media saved to
36 | that source and confirms that the appropriate thumbnail files have been copied over and
37 | the NFO file has been written if enabled.
38 |
--------------------------------------------------------------------------------
/docs/custom-filters.md:
--------------------------------------------------------------------------------
1 | # TubeSync
2 |
3 | ## Advanced usage guide - Writing Custom Filters
4 |
5 | Tubesync provides ways to filter media based on age, title string, and
6 | duration. This is sufficient for most use cases, but there more complicated
7 | use cases that can't easily be anticipated. Custom filters allow you to
8 | write some Python code to easily add your own logic into the filtering.
9 |
10 | Any call to an external API, or that requires access the metadata of the
11 | media item, will be much slower than the checks for title/age/duration. So
12 | this custom filter is only called if the other checks have already passed.
13 | You should also be aware that external API calls will significantly slow
14 | down the check process, and for large channels or databases this could be
15 | an issue.
16 |
17 | ### How to use
18 | 1. Copy `tubesync/sync/overrides/custom_filter.py` to your local computer
19 | 2. Make your code changes to the `filter_custom` function in that file. Simply return `True` to skip downloading the item, and `False` to allow it to download
20 | 3. Override `tubesync/sync/overrides/custom_filter.py` in your docker container.
21 |
22 | #### Docker run
23 | Include `-v /some/directory/tubesync-overrides:/app/sync/overrides` in your docker run
24 | command, pointing to the location of your override file.
25 |
26 | #### Docker Compose
27 | Include a volume line pointing to the location of your override file.
28 | e.g.
29 | ```yaml
30 | services:
31 | tubesync:
32 | image: ghcr.io/meeb/tubesync:latest
33 | container_name: tubesync
34 | restart: unless-stopped
35 | ports:
36 | - 4848:4848
37 | volumes:
38 | - /some/directory/tubesync-config:/config
39 | - /some/directory/tubesync-downloads:/downloads
40 | - /some/directory/tubesync-overrides:/app/sync/overrides
41 | ```
--------------------------------------------------------------------------------
/docs/dashboard-v0.5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/docs/dashboard-v0.5.png
--------------------------------------------------------------------------------
/docs/import-existing-media.md:
--------------------------------------------------------------------------------
1 | # TubeSync
2 |
3 | ## Advanced usage guide - importing existing media
4 |
5 | This is a new feature in v0.9 of TubeSync and later. It allows you to mark existing
6 | downloaded media as "downloaded" in TubeSync. You can use this feature if, for example,
7 | you already have an extensive catalogue of downloaded media which you want to mark
8 | as downloaded into TubeSync so TubeSync doesn't re-download media you already have.
9 |
10 | ## Requirements
11 |
12 | Your existing downloaded media MUST contain the unique ID. For YouTube videos, this is
13 | means the YouTube video ID MUST be in the filename.
14 |
15 | Supported extensions to be imported are .m4a, .ogg, .mkv, .mp3, .mp4 and .avi. Your
16 | media you want to import must end in one of these file extensions.
17 |
18 | ## Caveats
19 |
20 | As TubeSync does not probe media and your existing media may be re-encoded or in
21 | different formats to what is available in the current media metadata there is no way
22 | for TubeSync to know what codecs, resolution, bitrate etc. your imported media is in.
23 | Any manually imported existing local media will display blank boxes for this
24 | information on the TubeSync interface as it's unavailable.
25 |
26 | ## Steps
27 |
28 | ### 1. Add your source to TubeSync
29 |
30 | Add your source to TubeSync, such as a YouTube channel. **Make sure you untick the
31 | "download media" checkbox.**
32 |
33 | This will allow TubeSync to index all the available media on your source, but won't
34 | start downloading any media.
35 |
36 | ### 2. Wait
37 |
38 | Wait for all the media on your source to be indexed. This may take some time.
39 |
40 | ### 3. Move your existing media into TubeSync
41 |
42 | You now need to move your existing media into TubeSync. You need to move the media
43 | files into the correct download directories created by TubeSync. For example, if you
44 | have downloaded videos for a YouTube channel "TestChannel", you would have added this
45 | as a source called TestChannel and in a directory called test-channel in Tubesync. It
46 | would have a download directory created on disk at:
47 |
48 | `/path/to/downloads/test-channel`
49 |
50 | You would move all of your pre-existing videos you downloaded outside of TubeSync for
51 | this channel into this directory.
52 |
53 | In short, your existing media needs to be moved into the correct TubeSync source
54 | directory to be detected.
55 |
56 | This is required so TubeSync can known which Source to link the media to.
57 |
58 | ### 4. Run the batch import command
59 |
60 | Execute the following Django command:
61 |
62 | `./manage.py import-existing-media`
63 |
64 | When deploying TubeSync inside a container, you can execute this with:
65 |
66 | `docker exec -ti tubesync python3 /app/manage.py import-existing-media`
67 |
68 | This command will log what its doing to the terminal when you run it.
69 |
70 | Internally, `import-existing-media` looks for the unique media key (for YouTube, this
71 | is the YouTube video ID) in the filename and detects the source to link it to based
72 | on the directory the media file is inside.
73 |
74 |
75 | ### 5. Re-enable downloading at the source
76 |
77 | Edit your source and re-enable / tick the "download media" option. This will allow
78 | TubeSync to download any missing media you did not manually import.
79 |
80 | Note that TubeSync will still get screenshots write `nfo` files etc. for files you
81 | manually import if enabled at the source level.
82 |
--------------------------------------------------------------------------------
/docs/media-item-v0.5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/docs/media-item-v0.5.png
--------------------------------------------------------------------------------
/docs/media-v0.5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/docs/media-v0.5.png
--------------------------------------------------------------------------------
/docs/reset-metadata.md:
--------------------------------------------------------------------------------
1 | # TubeSync
2 |
3 | ## Advanced usage guide - reset media metadata from the command line
4 |
5 | This command allows you to reset all media item metadata. You might want to use
6 | this if you have a lot of media items with invalid metadata and you want to
7 | wipe it which triggers the metadata to be redownloaded.
8 |
9 |
10 | ## Requirements
11 |
12 | You have added some sources and media
13 |
14 | ## Steps
15 |
16 | ### 1. Run the reset tasks command
17 |
18 | Execute the following Django command:
19 |
20 | `./manage.py reset-metadata`
21 |
22 | When deploying TubeSync inside a container, you can execute this with:
23 |
24 | `docker exec -ti tubesync python3 /app/manage.py reset-metadata`
25 |
26 | This command will log what its doing to the terminal when you run it.
27 |
28 | When this is run, new tasks will be immediately created so all your media
29 | items will start downloading updated metadata straight away, any missing information
30 | such as thumbnails will be redownloaded, etc.
31 |
--------------------------------------------------------------------------------
/docs/reset-tasks.md:
--------------------------------------------------------------------------------
1 | # TubeSync
2 |
3 | ## Advanced usage guide - reset tasks from the command line
4 |
5 | This is a new feature in v1.0 of TubeSync and later. It allows you to reset all
6 | scheduled tasks from the command line as well as the "reset tasks" button in the
7 | "tasks" tab of the dashboard.
8 |
9 | This is useful for TubeSync installations where you may have a lot of media and
10 | sources added and the "reset tasks" button may take too long to the extent where
11 | the page times out (with a 502 error or similar issue).
12 |
13 | ## Requirements
14 |
15 | You have added some sources and media
16 |
17 | ## Steps
18 |
19 | ### 1. Run the reset tasks command
20 |
21 | Execute the following Django command:
22 |
23 | `./manage.py reset-tasks`
24 |
25 | When deploying TubeSync inside a container, you can execute this with:
26 |
27 | `docker exec -ti tubesync python3 /app/manage.py reset-tasks`
28 |
29 | This command will log what its doing to the terminal when you run it.
30 |
31 | When this is run, new tasks will be immediately created so all your sources will be
32 | indexed again straight away, any missing information such as thumbnails will be
33 | redownloaded, etc.
34 |
--------------------------------------------------------------------------------
/docs/source-v0.5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/docs/source-v0.5.png
--------------------------------------------------------------------------------
/docs/sources-v0.5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/docs/sources-v0.5.png
--------------------------------------------------------------------------------
/docs/using-cookies.md:
--------------------------------------------------------------------------------
1 | # TubeSync
2 |
3 | ## Advanced usage guide - using exported cookies
4 |
5 | This is a new feature in v0.10 of TubeSync and later. It allows you to use the cookies
6 | file exported from your browser in "Netscape" format with TubeSync to authenticate
7 | to YouTube. This can bypass some throttling, age restrictions and other blocks at
8 | YouTube.
9 |
10 | **IMPORTANT NOTE**: Using cookies exported from your browser that is authenticated
11 | to YouTube identifes your Google account as using TubeSync. This may result in
12 | potential account impacts and is entirely at your own risk. Do not use this
13 | feature unless you really know what you're doing.
14 |
15 | ## Requirements
16 |
17 | Have a browser that supports exporting your cookies and be logged into YouTube.
18 |
19 | ## Steps
20 |
21 | ### 1. Export your cookies
22 |
23 | You need to export cookies for youtube.com from your browser, you can either do
24 | this manually or there are plug-ins to automate this for you. This file must be
25 | in the "Netscape" cookie export format.
26 |
27 | Save your cookies as a `cookies.txt` file.
28 |
29 | ### 2. Import into TubeSync
30 |
31 | Drop the `cookies.txt` file into your TubeSync `config` directory.
32 |
33 | If detected correctly, you will see something like this in the worker or container
34 | logs:
35 |
36 | ```
37 | YYYY-MM-DD HH:MM:SS,mmm [tubesync/INFO] [youtube-dl] using cookies.txt from: /config/cookies.txt
38 | ```
39 |
40 | If you see that line it's working correctly.
41 |
42 | If you see errors in your logs like this:
43 |
44 | ```
45 | http.cookiejar.LoadError: '/config/cookies.txt' does not look like a Netscape format cookies file
46 | ```
47 |
48 | Then your `cookies.txt` file was not generated or created correctly as it's not
49 | in the required "Netscape" format. You can fix this by exporting your `cookies.txt`
50 | in the correct "Netscape" format.
51 |
--------------------------------------------------------------------------------
/patches/background_task/utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import signal
3 | import platform
4 |
5 | TTW_SLOW = [0.5, 1.5]
6 | TTW_FAST = [0.0, 0.1]
7 |
8 |
9 | class SignalManager():
10 | """Manages POSIX signals."""
11 |
12 | kill_now = False
13 | time_to_wait = TTW_SLOW
14 |
15 | def __init__(self):
16 | signal.signal(signal.SIGINT, self.exit_gracefully)
17 | # On Windows, signal() can only be called with:
18 | # SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM, or SIGBREAK.
19 | if platform.system() == 'Windows':
20 | signal.signal(signal.SIGBREAK, self.exit_gracefully)
21 | else:
22 | signal.signal(signal.SIGHUP, self.exit_gracefully)
23 | signal.signal(signal.SIGUSR1, self.speed_up)
24 | signal.signal(signal.SIGUSR2, self.slow_down)
25 |
26 | def exit_gracefully(self, signum, frame):
27 | self.kill_now = True
28 | # Using interrupt again should raise
29 | # a KeyboardInterrupt exception.
30 | signal.signal(signal.SIGINT, signal.SIG_DFL)
31 |
32 | def speed_up(self, signum, frame):
33 | self.time_to_wait = TTW_FAST
34 |
35 | def slow_down(self, signum, frame):
36 | self.time_to_wait = TTW_SLOW
37 |
--------------------------------------------------------------------------------
/patches/yt_dlp/patch/__init__.py:
--------------------------------------------------------------------------------
1 | from yt_dlp.compat.compat_utils import passthrough_module
2 |
3 | passthrough_module(__name__, '.patch')
4 | del passthrough_module
5 |
6 |
--------------------------------------------------------------------------------
/patches/yt_dlp/patch/check_thumbnails.py:
--------------------------------------------------------------------------------
1 | from yt_dlp import YoutubeDL
2 | from yt_dlp.utils import sanitize_url, LazyList
3 |
4 | class PatchedYoutubeDL(YoutubeDL):
5 |
6 | def _sanitize_thumbnails(self, info_dict):
7 | thumbnails = info_dict.get('thumbnails')
8 | if thumbnails is None:
9 | thumbnail = info_dict.get('thumbnail')
10 | if thumbnail:
11 | info_dict['thumbnails'] = thumbnails = [{'url': thumbnail}]
12 | if not thumbnails:
13 | return
14 |
15 |
16 | def check_thumbnails(thumbnails):
17 | for t in thumbnails:
18 | self.to_screen(f'[info] Testing thumbnail {t["id"]}: {t["url"]!r}')
19 | try:
20 | self.urlopen(HEADRequest(t['url']))
21 | except network_exceptions as err:
22 | self.to_screen(f'[info] Unable to connect to thumbnail {t["id"]} URL {t["url"]!r} - {err}. Skipping...')
23 | continue
24 | yield t
25 |
26 |
27 | self._sort_thumbnails(thumbnails)
28 | for i, t in enumerate(thumbnails):
29 | if t.get('id') is None:
30 | t['id'] = str(i)
31 | if t.get('width') and t.get('height'):
32 | t['resolution'] = '%dx%d' % (t['width'], t['height'])
33 | t['url'] = sanitize_url(t['url'])
34 |
35 |
36 | if self.params.get('check_thumbnails') is True:
37 | info_dict['thumbnails'] = LazyList(check_thumbnails(thumbnails[::-1]), reverse=True)
38 | else:
39 | info_dict['thumbnails'] = thumbnails
40 |
41 |
42 | YoutubeDL.__unpatched___sanitize_thumbnails = YoutubeDL._sanitize_thumbnails
43 | YoutubeDL._sanitize_thumbnails = PatchedYoutubeDL._sanitize_thumbnails
44 |
--------------------------------------------------------------------------------
/patches/yt_dlp/patch/fatal_http_errors.py:
--------------------------------------------------------------------------------
1 | from yt_dlp.extractor.youtube import YoutubeIE
2 |
3 |
4 | class PatchedYoutubeIE(YoutubeIE):
5 |
6 | def _download_player_responses(self, url, smuggled_data, video_id, webpage_url):
7 | webpage = None
8 | if 'webpage' not in self._configuration_arg('player_skip'):
9 | query = {'bpctr': '9999999999', 'has_verified': '1'}
10 | pp = self._configuration_arg('player_params', [None], casesense=True)[0]
11 | if pp:
12 | query['pp'] = pp
13 | webpage = self._download_webpage_with_retries(webpage_url, video_id, retry_fatal=True, query=query)
14 |
15 | master_ytcfg = self.extract_ytcfg(video_id, webpage) or self._get_default_ytcfg()
16 |
17 | player_responses, player_url = self._extract_player_responses(
18 | self._get_requested_clients(url, smuggled_data),
19 | video_id, webpage, master_ytcfg, smuggled_data)
20 |
21 | return webpage, master_ytcfg, player_responses, player_url
22 |
23 |
24 | YoutubeIE.__unpatched___download_player_responses = YoutubeIE._download_player_responses
25 | YoutubeIE._download_player_responses = PatchedYoutubeIE._download_player_responses
26 |
--------------------------------------------------------------------------------
/tubesync/common/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/common/__init__.py
--------------------------------------------------------------------------------
/tubesync/common/admin.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/common/admin.py
--------------------------------------------------------------------------------
/tubesync/common/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class CommonConfig(AppConfig):
5 |
6 | name = 'common'
7 |
--------------------------------------------------------------------------------
/tubesync/common/context_processors.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from .third_party_versions import yt_dlp_version, ffmpeg_version
3 |
4 |
5 | def app_details(request):
6 | return {
7 | 'app_version': str(settings.VERSION),
8 | 'yt_dlp_version': yt_dlp_version,
9 | 'ffmpeg_version': ffmpeg_version,
10 | }
11 |
--------------------------------------------------------------------------------
/tubesync/common/errors.py:
--------------------------------------------------------------------------------
1 | class NoMediaException(Exception):
2 | '''
3 | Raised when a source returns no media to be indexed. Could be an invalid
4 | playlist name or similar, or the upstream source returned an error.
5 | '''
6 | pass
7 |
8 |
9 | class NoFormatException(Exception):
10 | '''
11 | Raised when a media item is attempted to be downloaded but it has no valid
12 | format combination.
13 | '''
14 | pass
15 |
16 |
17 | class NoMetadataException(Exception):
18 | '''
19 | Raised when a media item is attempted to be downloaded but it has no valid
20 | metadata.
21 | '''
22 | pass
23 |
24 |
25 | class NoThumbnailException(Exception):
26 | '''
27 | Raised when a thumbnail was not found at the remote URL.
28 | '''
29 | pass
30 |
31 |
32 | class DownloadFailedException(Exception):
33 | '''
34 | Raised when a downloaded media file is expected to be present, but doesn't
35 | exist.
36 | '''
37 | pass
38 |
39 |
40 | class DatabaseConnectionError(Exception):
41 | '''
42 | Raised when parsing or initially connecting to a database.
43 | '''
44 | pass
45 |
--------------------------------------------------------------------------------
/tubesync/common/json.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from django.core.serializers.json import DjangoJSONEncoder
3 | from yt_dlp.utils import LazyList
4 |
5 |
6 | class JSONEncoder(DjangoJSONEncoder):
7 | item_separator = ','
8 | key_separator = ':'
9 |
10 | def default(self, obj):
11 | try:
12 | iterable = iter(obj)
13 | except TypeError:
14 | pass
15 | else:
16 | return list(iterable)
17 | return super().default(obj)
18 |
19 |
20 | def json_serial(obj):
21 | if isinstance(obj, datetime):
22 | return obj.isoformat()
23 | if isinstance(obj, LazyList):
24 | return list(obj)
25 | raise TypeError(f'Type {type(obj)} is not json_serial()-able')
26 |
27 |
--------------------------------------------------------------------------------
/tubesync/common/logger.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from django.conf import settings
3 | from .utils import getenv
4 |
5 |
6 | logging_level = logging.DEBUG if settings.DEBUG else logging.INFO
7 | default_formatter = logging.Formatter(
8 | '%(asctime)s [%(name)s/%(levelname)s] %(message)s'
9 | )
10 | default_sh = logging.StreamHandler()
11 | default_sh.setFormatter(default_formatter)
12 | default_sh.setLevel(logging_level)
13 |
14 |
15 | app_name = getenv('DJANGO_SETTINGS_MODULE')
16 | first_part = app_name.split('.', 1)[0]
17 | log = app_logger = logging.getLogger(first_part)
18 | app_logger.addHandler(default_sh)
19 | app_logger.setLevel(logging_level)
20 |
21 |
22 | class NoWaitingForTasksFilter(logging.Filter):
23 | def filter(self, record):
24 | return 'waiting for tasks' != record.getMessage()
25 |
26 | background_task_name = 'background_task.management.commands.process_tasks'
27 | last_part = background_task_name.rsplit('.', 1)[-1]
28 | background_task_formatter = logging.Formatter(
29 | f'%(asctime)s [{last_part}/%(levelname)s] %(message)s'
30 | )
31 | background_task_sh = logging.StreamHandler()
32 | background_task_sh.addFilter(NoWaitingForTasksFilter())
33 | background_task_sh.setFormatter(background_task_formatter)
34 | background_task_sh.setLevel(logging_level)
35 | background_task_logger = logging.getLogger(background_task_name)
36 | background_task_logger.addHandler(background_task_sh)
37 | background_task_logger.setLevel(logging_level)
38 |
--------------------------------------------------------------------------------
/tubesync/common/middleware.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.forms import BaseForm
3 | from basicauth.middleware import BasicAuthMiddleware as BaseBasicAuthMiddleware
4 |
5 |
6 | class MaterializeDefaultFieldsMiddleware:
7 | '''
8 | Adds 'browser-default' CSS attribute class to all form fields.
9 | '''
10 |
11 | def __init__(self, get_response):
12 | self.get_response = get_response
13 |
14 | def __call__(self, request):
15 | response = self.get_response(request)
16 | return response
17 |
18 | def process_template_response(self, request, response):
19 | for _, v in getattr(response, 'context_data', {}).items():
20 | if isinstance(v, BaseForm):
21 | for _, field in v.fields.items():
22 | field.widget.attrs.update({'class':'browser-default'})
23 | return response
24 |
25 |
26 | class BasicAuthMiddleware(BaseBasicAuthMiddleware):
27 |
28 | def process_request(self, request):
29 | bypass_uris = getattr(settings, 'BASICAUTH_ALWAYS_ALLOW_URIS', [])
30 | if request.path in bypass_uris:
31 | return None
32 | return super().process_request(request)
33 |
--------------------------------------------------------------------------------
/tubesync/common/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/common/migrations/__init__.py
--------------------------------------------------------------------------------
/tubesync/common/models.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/common/models.py
--------------------------------------------------------------------------------
/tubesync/common/static/fonts/fontawesome/fa-brands-400.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/common/static/fonts/fontawesome/fa-brands-400.eot
--------------------------------------------------------------------------------
/tubesync/common/static/fonts/fontawesome/fa-brands-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/common/static/fonts/fontawesome/fa-brands-400.ttf
--------------------------------------------------------------------------------
/tubesync/common/static/fonts/fontawesome/fa-brands-400.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/common/static/fonts/fontawesome/fa-brands-400.woff
--------------------------------------------------------------------------------
/tubesync/common/static/fonts/fontawesome/fa-brands-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/common/static/fonts/fontawesome/fa-brands-400.woff2
--------------------------------------------------------------------------------
/tubesync/common/static/fonts/fontawesome/fa-regular-400.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/common/static/fonts/fontawesome/fa-regular-400.eot
--------------------------------------------------------------------------------
/tubesync/common/static/fonts/fontawesome/fa-regular-400.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/common/static/fonts/fontawesome/fa-regular-400.ttf
--------------------------------------------------------------------------------
/tubesync/common/static/fonts/fontawesome/fa-regular-400.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/common/static/fonts/fontawesome/fa-regular-400.woff
--------------------------------------------------------------------------------
/tubesync/common/static/fonts/fontawesome/fa-regular-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/common/static/fonts/fontawesome/fa-regular-400.woff2
--------------------------------------------------------------------------------
/tubesync/common/static/fonts/fontawesome/fa-solid-900.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/common/static/fonts/fontawesome/fa-solid-900.eot
--------------------------------------------------------------------------------
/tubesync/common/static/fonts/fontawesome/fa-solid-900.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/common/static/fonts/fontawesome/fa-solid-900.ttf
--------------------------------------------------------------------------------
/tubesync/common/static/fonts/fontawesome/fa-solid-900.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/common/static/fonts/fontawesome/fa-solid-900.woff
--------------------------------------------------------------------------------
/tubesync/common/static/fonts/fontawesome/fa-solid-900.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/common/static/fonts/fontawesome/fa-solid-900.woff2
--------------------------------------------------------------------------------
/tubesync/common/static/fonts/roboto/roboto-bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/common/static/fonts/roboto/roboto-bold.woff
--------------------------------------------------------------------------------
/tubesync/common/static/fonts/roboto/roboto-light.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/common/static/fonts/roboto/roboto-light.woff
--------------------------------------------------------------------------------
/tubesync/common/static/fonts/roboto/roboto-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/common/static/fonts/roboto/roboto-regular.woff
--------------------------------------------------------------------------------
/tubesync/common/static/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/common/static/images/favicon.ico
--------------------------------------------------------------------------------
/tubesync/common/static/images/nothumb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/common/static/images/nothumb.png
--------------------------------------------------------------------------------
/tubesync/common/static/images/tubesync-transparent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/common/static/images/tubesync-transparent.png
--------------------------------------------------------------------------------
/tubesync/common/static/images/tubesync.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/common/static/images/tubesync.png
--------------------------------------------------------------------------------
/tubesync/common/static/styles/_colours.scss:
--------------------------------------------------------------------------------
1 | $colour-white: #ffffff;
2 | $colour-black: #000000;
3 | $colour-near-black: #011627;
4 | $colour-near-white: #fdfffc;
5 | $colour-light-blue: #1e5c83;
6 | $colour-red: #e71d36;
7 | $colour-orange: #ff9c00;
8 |
9 | $background-colour: $colour-near-white;
10 | $text-colour: $colour-near-black;
11 |
12 | $header-background-colour: $colour-red;
13 | $header-text-colour: $colour-near-white;
14 |
15 | $nav-background-colour: $colour-near-black;
16 | $nav-text-colour: $colour-near-white;
17 | $nav-link-background-hover-colour: $colour-orange;
18 |
19 | $main-button-background-colour: $colour-light-blue;
20 | $main-button-background-hover-colour: $colour-orange;
21 | $main-button-text-colour: $colour-near-white;
22 | $main-link-colour: $colour-light-blue;
23 | $main-link-hover-colour: $colour-orange;
24 |
25 | $footer-background-colour: $colour-red;
26 | $footer-text-colour: $colour-near-white;
27 | $footer-link-colour: $colour-near-black;
28 | $footer-link-hover-colour: $colour-orange;
29 |
30 | $dash-desc-border: $colour-near-black;
31 |
32 | $form-label-text-colour: $colour-near-black;
33 | $form-input-border-colour: $colour-light-blue;
34 | $form-input-border-active-colour: $colour-orange;
35 | $form-select-border-colour: $colour-light-blue;
36 | $form-error-background-colour: $colour-red;
37 | $form-error-text-colour: $colour-near-white;
38 | $form-help-text-colour: $colour-light-blue;
39 | $form-delete-button-background-colour: $colour-red;
40 |
41 | $collection-no-items-text-colour: $colour-near-black;
42 | $collection-text-colour: $colour-light-blue;
43 | $collection-background-hover-colour: $colour-orange;
44 | $collection-text-hover-colour: $colour-near-white;
45 |
46 | $mediacard-title-background-colour: $colour-near-black;
47 | $mediacard-title-text-colour: $colour-near-white;
48 | $mediacard-border-colour: $colour-near-white;
49 | $mediacard-border-hover-colour: $colour-orange;
50 |
51 | $box-error-background-colour: $colour-red;
52 | $box-error-text-colour: $colour-near-white;
53 |
54 | $infobox-background-colour: $colour-near-black;
55 | $infobox-text-colour: $colour-near-white;
56 |
57 | $errorbox-background-colour: $colour-red;
58 | $errorbox-text-colour: $colour-near-white;
59 | $error-text-colour: $colour-red;
60 |
61 | $pagination-background-colour: $colour-near-white;
62 | $pagination-text-colour: $colour-near-black;
63 | $pagination-border-colour: $colour-light-blue;
64 | $pagination-background-hover-colour: $colour-light-blue;
65 | $pagination-text-hover-colour: $colour-near-white;
66 | $pagination-border-hover-colour: $colour-light-blue;
67 | $pagination-current-background-colour: $colour-orange;
68 | $pagination-current-text-colour: $colour-near-white;
69 | $pagination-current-border-colour: $colour-orange;
70 |
71 | $error-text-colour: $colour-red;
72 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/_fonts.scss:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'roboto';
3 | src: url('../fonts/roboto/roboto-light.woff') format('woff');
4 | font-weight: lighter;
5 | font-style: normal;
6 | }
7 |
8 | @font-face {
9 | font-family: 'roboto';
10 | src: url('../fonts/roboto/roboto-regular.woff') format('woff');
11 | font-weight: normal;
12 | font-style: normal;
13 | }
14 |
15 | @font-face {
16 | font-family: 'roboto';
17 | src: url('../fonts/roboto/roboto-bold.woff') format('woff');
18 | font-weight: bold;
19 | font-style: normal;
20 | }
21 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/_forms.scss:
--------------------------------------------------------------------------------
1 | .simpleform {
2 | .row {
3 | margin-bottom: 0;
4 | }
5 | .help-text {
6 | color: $form-help-text-colour;
7 | padding-bottom: 1rem;
8 | }
9 | label {
10 | text-transform: uppercase;
11 | display: block;
12 | font-size: 0.9rem;
13 | position: relative;
14 | transition: none;
15 | top: initial;
16 | left: initial !important;
17 | transform: none;
18 | color: $form-label-text-colour;
19 | }
20 | input {
21 | width: 100%;
22 | padding: 5px 8px 5px 8px;
23 | font-size: 1.1rem;
24 | border: 2px $form-input-border-colour solid;
25 | border-radius: 2px;
26 | outline: none;
27 | &:focus {
28 | outline: none;
29 | border: 2px $form-input-border-active-colour solid;
30 | }
31 | }
32 | textarea {
33 | min-height: 150px;
34 | }
35 | }
36 |
37 | select {
38 | display: initial !important;
39 | border: 2px $form-select-border-colour solid;
40 | height: initial !important;
41 | }
42 |
43 | .delete-button {
44 | background-color: $form-delete-button-background-colour !important;
45 | &:hover {
46 | background-color: $main-button-background-hover-colour !important;
47 | }
48 | }
49 |
50 | .no-text-transform {
51 | text-transform: none;
52 | }
53 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/_helpers.scss:
--------------------------------------------------------------------------------
1 | strong {
2 | font-weight: bold;
3 | }
4 |
5 | .nowrap {
6 | white-space: nowrap;
7 | }
8 |
9 | .no-para-margin-top {
10 | margin-block-start: 0;
11 | }
12 |
13 | .no-margin-bottom {
14 | margin-bottom: 0 !important;
15 | }
16 |
17 | .margin-bottom {
18 | margin-bottom: 20px !important;
19 | }
20 |
21 | .padding-top {
22 | padding-top: 20px;
23 | }
24 |
25 | .error-text {
26 | color: $error-text-colour;
27 | }
28 |
29 | .errors {
30 | background-color: $box-error-background-colour;
31 | border-radius: 2px;
32 | padding: 10px 0 5px 0;
33 | }
34 |
35 | .errorlist {
36 | li {
37 | color: $box-error-text-colour;
38 | padding: 0 10px 5px 10px;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/_variables.scss:
--------------------------------------------------------------------------------
1 | $font-family: 'roboto', Arial, Helvetica, sans-serif;
2 | $font-size: 1.05rem;
3 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/fontawesome/_animated.scss:
--------------------------------------------------------------------------------
1 | // Animated Icons
2 | // --------------------------
3 |
4 | .#{$fa-css-prefix}-spin {
5 | animation: fa-spin 2s infinite linear;
6 | }
7 |
8 | .#{$fa-css-prefix}-pulse {
9 | animation: fa-spin 1s infinite steps(8);
10 | }
11 |
12 | @keyframes fa-spin {
13 | 0% {
14 | transform: rotate(0deg);
15 | }
16 |
17 | 100% {
18 | transform: rotate(360deg);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/fontawesome/_bordered-pulled.scss:
--------------------------------------------------------------------------------
1 | // Bordered & Pulled
2 | // -------------------------
3 |
4 | .#{$fa-css-prefix}-border {
5 | border: solid .08em $fa-border-color;
6 | border-radius: .1em;
7 | padding: .2em .25em .15em;
8 | }
9 |
10 | .#{$fa-css-prefix}-pull-left { float: left; }
11 | .#{$fa-css-prefix}-pull-right { float: right; }
12 |
13 | .#{$fa-css-prefix},
14 | .fas,
15 | .far,
16 | .fal,
17 | .fab {
18 | &.#{$fa-css-prefix}-pull-left { margin-right: .3em; }
19 | &.#{$fa-css-prefix}-pull-right { margin-left: .3em; }
20 | }
21 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/fontawesome/_core.scss:
--------------------------------------------------------------------------------
1 | // Base Class Definition
2 | // -------------------------
3 |
4 | .#{$fa-css-prefix},
5 | .fas,
6 | .far,
7 | .fal,
8 | .fad,
9 | .fab {
10 | -moz-osx-font-smoothing: grayscale;
11 | -webkit-font-smoothing: antialiased;
12 | display: inline-block;
13 | font-style: normal;
14 | font-variant: normal;
15 | text-rendering: auto;
16 | line-height: 1;
17 | }
18 |
19 | %fa-icon {
20 | @include fa-icon;
21 | }
22 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/fontawesome/_fixed-width.scss:
--------------------------------------------------------------------------------
1 | // Fixed Width Icons
2 | // -------------------------
3 | .#{$fa-css-prefix}-fw {
4 | text-align: center;
5 | width: $fa-fw-width;
6 | }
7 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/fontawesome/_larger.scss:
--------------------------------------------------------------------------------
1 | // Icon Sizes
2 | // -------------------------
3 |
4 | // makes the font 33% larger relative to the icon container
5 | .#{$fa-css-prefix}-lg {
6 | font-size: (4em / 3);
7 | line-height: (3em / 4);
8 | vertical-align: -.0667em;
9 | }
10 |
11 | .#{$fa-css-prefix}-xs {
12 | font-size: .75em;
13 | }
14 |
15 | .#{$fa-css-prefix}-sm {
16 | font-size: .875em;
17 | }
18 |
19 | @for $i from 1 through 10 {
20 | .#{$fa-css-prefix}-#{$i}x {
21 | font-size: $i * 1em;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/fontawesome/_list.scss:
--------------------------------------------------------------------------------
1 | // List Icons
2 | // -------------------------
3 |
4 | .#{$fa-css-prefix}-ul {
5 | list-style-type: none;
6 | margin-left: $fa-li-width * 5/4;
7 | padding-left: 0;
8 |
9 | > li { position: relative; }
10 | }
11 |
12 | .#{$fa-css-prefix}-li {
13 | left: -$fa-li-width;
14 | position: absolute;
15 | text-align: center;
16 | width: $fa-li-width;
17 | line-height: inherit;
18 | }
19 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/fontawesome/_mixins.scss:
--------------------------------------------------------------------------------
1 | // Mixins
2 | // --------------------------
3 |
4 | @mixin fa-icon {
5 | -webkit-font-smoothing: antialiased;
6 | -moz-osx-font-smoothing: grayscale;
7 | display: inline-block;
8 | font-style: normal;
9 | font-variant: normal;
10 | font-weight: normal;
11 | line-height: 1;
12 | }
13 |
14 | @mixin fa-icon-rotate($degrees, $rotation) {
15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})";
16 | transform: rotate($degrees);
17 | }
18 |
19 | @mixin fa-icon-flip($horiz, $vert, $rotation) {
20 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)";
21 | transform: scale($horiz, $vert);
22 | }
23 |
24 |
25 | // Only display content to screen readers. A la Bootstrap 4.
26 | //
27 | // See: http://a11yproject.com/posts/how-to-hide-content/
28 |
29 | @mixin sr-only {
30 | border: 0;
31 | clip: rect(0, 0, 0, 0);
32 | height: 1px;
33 | margin: -1px;
34 | overflow: hidden;
35 | padding: 0;
36 | position: absolute;
37 | width: 1px;
38 | }
39 |
40 | // Use in conjunction with .sr-only to only display content when it's focused.
41 | //
42 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1
43 | //
44 | // Credit: HTML5 Boilerplate
45 |
46 | @mixin sr-only-focusable {
47 | &:active,
48 | &:focus {
49 | clip: auto;
50 | height: auto;
51 | margin: 0;
52 | overflow: visible;
53 | position: static;
54 | width: auto;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/fontawesome/_rotated-flipped.scss:
--------------------------------------------------------------------------------
1 | // Rotated & Flipped Icons
2 | // -------------------------
3 |
4 | .#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); }
5 | .#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); }
6 | .#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); }
7 |
8 | .#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); }
9 | .#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); }
10 | .#{$fa-css-prefix}-flip-both, .#{$fa-css-prefix}-flip-horizontal.#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(-1, -1, 2); }
11 |
12 | // Hook for IE8-9
13 | // -------------------------
14 |
15 | :root {
16 | .#{$fa-css-prefix}-rotate-90,
17 | .#{$fa-css-prefix}-rotate-180,
18 | .#{$fa-css-prefix}-rotate-270,
19 | .#{$fa-css-prefix}-flip-horizontal,
20 | .#{$fa-css-prefix}-flip-vertical,
21 | .#{$fa-css-prefix}-flip-both {
22 | filter: none;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/fontawesome/_screen-reader.scss:
--------------------------------------------------------------------------------
1 | // Screen Readers
2 | // -------------------------
3 |
4 | .sr-only { @include sr-only; }
5 | .sr-only-focusable { @include sr-only-focusable; }
6 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/fontawesome/_stacked.scss:
--------------------------------------------------------------------------------
1 | // Stacked Icons
2 | // -------------------------
3 |
4 | .#{$fa-css-prefix}-stack {
5 | display: inline-block;
6 | height: 2em;
7 | line-height: 2em;
8 | position: relative;
9 | vertical-align: middle;
10 | width: ($fa-fw-width*2);
11 | }
12 |
13 | .#{$fa-css-prefix}-stack-1x,
14 | .#{$fa-css-prefix}-stack-2x {
15 | left: 0;
16 | position: absolute;
17 | text-align: center;
18 | width: 100%;
19 | }
20 |
21 | .#{$fa-css-prefix}-stack-1x {
22 | line-height: inherit;
23 | }
24 |
25 | .#{$fa-css-prefix}-stack-2x {
26 | font-size: 2em;
27 | }
28 |
29 | .#{$fa-css-prefix}-inverse {
30 | color: $fa-inverse;
31 | }
32 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/fontawesome/brands.scss:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com
3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
4 | */
5 | @import 'variables';
6 |
7 | @font-face {
8 | font-family: 'Font Awesome 5 Brands';
9 | font-style: normal;
10 | font-weight: 400;
11 | font-display: $fa-font-display;
12 | src: url('#{$fa-font-path}/fa-brands-400.eot');
13 | src: url('#{$fa-font-path}/fa-brands-400.eot?#iefix') format('embedded-opentype'),
14 | url('#{$fa-font-path}/fa-brands-400.woff2') format('woff2'),
15 | url('#{$fa-font-path}/fa-brands-400.woff') format('woff'),
16 | url('#{$fa-font-path}/fa-brands-400.ttf') format('truetype'),
17 | url('#{$fa-font-path}/fa-brands-400.svg#fontawesome') format('svg');
18 | }
19 |
20 | .fab {
21 | font-family: 'Font Awesome 5 Brands';
22 | font-weight: 400;
23 | }
24 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/fontawesome/fontawesome.scss:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com
3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
4 | */
5 | @import 'variables';
6 | @import 'mixins';
7 | @import 'core';
8 | @import 'larger';
9 | @import 'fixed-width';
10 | @import 'list';
11 | @import 'bordered-pulled';
12 | @import 'animated';
13 | @import 'rotated-flipped';
14 | @import 'stacked';
15 | @import 'icons';
16 | @import 'screen-reader';
17 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/fontawesome/regular.scss:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com
3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
4 | */
5 | @import 'variables';
6 |
7 | @font-face {
8 | font-family: 'Font Awesome 5 Free';
9 | font-style: normal;
10 | font-weight: 400;
11 | font-display: $fa-font-display;
12 | src: url('#{$fa-font-path}/fa-regular-400.eot');
13 | src: url('#{$fa-font-path}/fa-regular-400.eot?#iefix') format('embedded-opentype'),
14 | url('#{$fa-font-path}/fa-regular-400.woff2') format('woff2'),
15 | url('#{$fa-font-path}/fa-regular-400.woff') format('woff'),
16 | url('#{$fa-font-path}/fa-regular-400.ttf') format('truetype'),
17 | url('#{$fa-font-path}/fa-regular-400.svg#fontawesome') format('svg');
18 | }
19 |
20 | .far {
21 | font-family: 'Font Awesome 5 Free';
22 | font-weight: 400;
23 | }
24 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/fontawesome/solid.scss:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com
3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
4 | */
5 | @import 'variables';
6 |
7 | @font-face {
8 | font-family: 'Font Awesome 5 Free';
9 | font-style: normal;
10 | font-weight: 900;
11 | font-display: $fa-font-display;
12 | src: url('#{$fa-font-path}/fa-solid-900.eot');
13 | src: url('#{$fa-font-path}/fa-solid-900.eot?#iefix') format('embedded-opentype'),
14 | url('#{$fa-font-path}/fa-solid-900.woff2') format('woff2'),
15 | url('#{$fa-font-path}/fa-solid-900.woff') format('woff'),
16 | url('#{$fa-font-path}/fa-solid-900.ttf') format('truetype'),
17 | url('#{$fa-font-path}/fa-solid-900.svg#fontawesome') format('svg');
18 | }
19 |
20 | .fa,
21 | .fas {
22 | font-family: 'Font Awesome 5 Free';
23 | font-weight: 900;
24 | }
25 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/fontawesome/v4-shims.scss:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome Free 5.15.1 by @fontawesome - https://fontawesome.com
3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
4 | */
5 | @import 'variables';
6 | @import 'shims';
7 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/materializecss/components/_badges.scss:
--------------------------------------------------------------------------------
1 | // Badges
2 | span.badge {
3 | min-width: 3rem;
4 | padding: 0 6px;
5 | margin-left: 14px;
6 | text-align: center;
7 | font-size: 1rem;
8 | line-height: $badge-height;
9 | height: $badge-height;
10 | color: color('grey', 'darken-1');
11 | float: right;
12 | box-sizing: border-box;
13 |
14 | &.new {
15 | font-weight: 300;
16 | font-size: 0.8rem;
17 | color: #fff;
18 | background-color: $badge-bg-color;
19 | border-radius: 2px;
20 | }
21 | &.new:after {
22 | content: " new";
23 | }
24 |
25 | &[data-badge-caption]::after {
26 | content: " " attr(data-badge-caption);
27 | }
28 | }
29 |
30 | // Special cases
31 | nav ul a span.badge {
32 | display: inline-block;
33 | float: none;
34 | margin-left: 4px;
35 | line-height: $badge-height;
36 | height: $badge-height;
37 | -webkit-font-smoothing: auto;
38 | }
39 |
40 | // Line height centering
41 | .collection-item span.badge {
42 | margin-top: calc(#{$collection-line-height / 2} - #{$badge-height / 2});
43 | }
44 | .collapsible span.badge {
45 | margin-left: auto;
46 | }
47 | .sidenav span.badge {
48 | margin-top: calc(#{$sidenav-line-height / 2} - #{$badge-height / 2});
49 | }
50 |
51 | table span.badge {
52 | display: inline-block;
53 | float: none;
54 | margin-left: auto;
55 | }
56 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/materializecss/components/_carousel.scss:
--------------------------------------------------------------------------------
1 | .carousel {
2 | &.carousel-slider {
3 | top: 0;
4 | left: 0;
5 |
6 | .carousel-fixed-item {
7 | &.with-indicators {
8 | bottom: 68px;
9 | }
10 |
11 | position: absolute;
12 | left: 0;
13 | right: 0;
14 | bottom: 20px;
15 | z-index: 1;
16 | }
17 |
18 | .carousel-item {
19 | width: 100%;
20 | height: 100%;
21 | min-height: $carousel-height;
22 | position: absolute;
23 | top: 0;
24 | left: 0;
25 |
26 | h2 {
27 | font-size: 24px;
28 | font-weight: 500;
29 | line-height: 32px;
30 | }
31 |
32 | p {
33 | font-size: 15px;
34 | }
35 | }
36 | }
37 |
38 | overflow: hidden;
39 | position: relative;
40 | width: 100%;
41 | height: $carousel-height;
42 | perspective: 500px;
43 | transform-style: preserve-3d;
44 | transform-origin: 0% 50%;
45 |
46 | .carousel-item {
47 | visibility: hidden;
48 | width: $carousel-item-width;
49 | height: $carousel-item-height;
50 | position: absolute;
51 | top: 0;
52 | left: 0;
53 |
54 | & > img {
55 | width: 100%;
56 | }
57 | }
58 |
59 | .indicators {
60 | position: absolute;
61 | text-align: center;
62 | left: 0;
63 | right: 0;
64 | bottom: 0;
65 | margin: 0;
66 |
67 | .indicator-item {
68 | &.active {
69 | background-color: #fff;
70 | }
71 |
72 | display: inline-block;
73 | position: relative;
74 | cursor: pointer;
75 | height: 8px;
76 | width: 8px;
77 | margin: 24px 4px;
78 | background-color: rgba(255,255,255,.5);
79 |
80 | transition: background-color .3s;
81 | border-radius: 50%;
82 | }
83 | }
84 |
85 | // Materialbox compatibility
86 | &.scrolling .carousel-item .materialboxed,
87 | .carousel-item:not(.active) .materialboxed {
88 | pointer-events: none;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/materializecss/components/_chips.scss:
--------------------------------------------------------------------------------
1 | .chip {
2 | &:focus {
3 | outline: none;
4 | background-color: $chip-selected-color;
5 | color: #fff;
6 | }
7 |
8 | display: inline-block;
9 | height: 32px;
10 | font-size: 13px;
11 | font-weight: 500;
12 | color: rgba(0,0,0,.6);
13 | line-height: 32px;
14 | padding: 0 12px;
15 | border-radius: 16px;
16 | background-color: $chip-bg-color;
17 | margin-bottom: $chip-margin;
18 | margin-right: $chip-margin;
19 |
20 | > img {
21 | float: left;
22 | margin: 0 8px 0 -12px;
23 | height: 32px;
24 | width: 32px;
25 | border-radius: 50%;
26 | }
27 |
28 | .close {
29 | cursor: pointer;
30 | float: right;
31 | font-size: 16px;
32 | line-height: 32px;
33 | padding-left: 8px;
34 | }
35 | }
36 |
37 | .chips {
38 | border: none;
39 | border-bottom: 1px solid $chip-border-color;
40 | box-shadow: none;
41 | margin: $input-margin;
42 | min-height: 45px;
43 | outline: none;
44 | transition: all .3s;
45 |
46 | &.focus {
47 | border-bottom: 1px solid $chip-selected-color;
48 | box-shadow: 0 1px 0 0 $chip-selected-color;
49 | }
50 |
51 | &:hover {
52 | cursor: text;
53 | }
54 |
55 | .input {
56 | background: none;
57 | border: 0;
58 | color: rgba(0,0,0,.6);
59 | display: inline-block;
60 | font-size: $input-font-size;
61 | height: $input-height;
62 | line-height: 32px;
63 | outline: 0;
64 | margin: 0;
65 | padding: 0 !important;
66 | width: 120px !important;
67 | }
68 |
69 | .input:focus {
70 | border: 0 !important;
71 | box-shadow: none !important;
72 | }
73 |
74 | // Autocomplete
75 | .autocomplete-content {
76 | margin-top: 0;
77 | margin-bottom: 0;
78 | }
79 | }
80 |
81 | // Form prefix
82 | .prefix ~ .chips {
83 | margin-left: 3rem;
84 | width: 92%;
85 | width: calc(100% - 3rem);
86 | }
87 | .chips:empty ~ label {
88 | font-size: 0.8rem;
89 | transform: translateY(-140%);
90 | }
91 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/materializecss/components/_collapsible.scss:
--------------------------------------------------------------------------------
1 | .collapsible {
2 | border-top: 1px solid $collapsible-border-color;
3 | border-right: 1px solid $collapsible-border-color;
4 | border-left: 1px solid $collapsible-border-color;
5 | margin: $element-top-margin 0 $element-bottom-margin 0;
6 | @extend .z-depth-1;
7 | }
8 |
9 | .collapsible-header {
10 | &:focus {
11 | outline: 0
12 | }
13 |
14 | display: flex;
15 | cursor: pointer;
16 | -webkit-tap-highlight-color: transparent;
17 | line-height: 1.5;
18 | padding: 1rem;
19 | background-color: $collapsible-header-color;
20 | border-bottom: 1px solid $collapsible-border-color;
21 |
22 | i {
23 | width: 2rem;
24 | font-size: 1.6rem;
25 | display: inline-block;
26 | text-align: center;
27 | margin-right: 1rem;
28 | }
29 | }
30 | .keyboard-focused .collapsible-header:focus {
31 | background-color: #eee;
32 | }
33 |
34 | .collapsible-body {
35 | display: none;
36 | border-bottom: 1px solid $collapsible-border-color;
37 | box-sizing: border-box;
38 | padding: 2rem;
39 | }
40 |
41 | // Sidenav collapsible styling
42 | .sidenav,
43 | .sidenav.fixed {
44 |
45 | .collapsible {
46 | border: none;
47 | box-shadow: none;
48 |
49 | li { padding: 0; }
50 | }
51 |
52 | .collapsible-header {
53 | background-color: transparent;
54 | border: none;
55 | line-height: inherit;
56 | height: inherit;
57 | padding: 0 $sidenav-padding;
58 |
59 | &:hover { background-color: rgba(0,0,0,.05); }
60 | i { line-height: inherit; }
61 | }
62 |
63 | .collapsible-body {
64 | border: 0;
65 | background-color: $collapsible-header-color;
66 |
67 | li a {
68 | padding: 0 (7.5px + $sidenav-padding)
69 | 0 (15px + $sidenav-padding);
70 | }
71 | }
72 |
73 | }
74 |
75 | // Popout Collapsible
76 |
77 | .collapsible.popout {
78 | border: none;
79 | box-shadow: none;
80 | > li {
81 | box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
82 | // transform: scaleX(.92);
83 | margin: 0 24px;
84 | transition: margin .35s cubic-bezier(0.250, 0.460, 0.450, 0.940);
85 | }
86 | > li.active {
87 | box-shadow: 0 5px 11px 0 rgba(0, 0, 0, 0.18), 0 4px 15px 0 rgba(0, 0, 0, 0.15);
88 | margin: 16px 0;
89 | // transform: scaleX(1);
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/materializecss/components/_color-classes.scss:
--------------------------------------------------------------------------------
1 | // Color Classes
2 |
3 | @each $color_name, $color in $colors {
4 | @each $color_type, $color_value in $color {
5 | @if $color_type == "base" {
6 | .#{$color_name} {
7 | background-color: $color_value !important;
8 | }
9 | .#{$color_name}-text {
10 | color: $color_value !important;
11 | }
12 | }
13 | @else if $color_name != "shades" {
14 | .#{$color_name}.#{$color_type} {
15 | background-color: $color_value !important;
16 | }
17 | .#{$color_name}-text.text-#{$color_type} {
18 | color: $color_value !important;
19 | }
20 | }
21 | }
22 | }
23 |
24 | // Shade classes
25 | @each $color, $color_value in $shades {
26 | .#{$color} {
27 | background-color: $color_value !important;
28 | }
29 | .#{$color}-text {
30 | color: $color_value !important;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/materializecss/components/_dropdown.scss:
--------------------------------------------------------------------------------
1 | .dropdown-content {
2 | &:focus {
3 | outline: 0;
4 | }
5 |
6 |
7 | @extend .z-depth-1;
8 | background-color: $dropdown-bg-color;
9 | margin: 0;
10 | display: none;
11 | min-width: 100px;
12 | overflow-y: auto;
13 | opacity: 0;
14 | position: absolute;
15 | left: 0;
16 | top: 0;
17 | z-index: 9999; // TODO: Check if this doesn't break other things
18 | transform-origin: 0 0;
19 |
20 |
21 | li {
22 | &:hover, &.active {
23 | background-color: $dropdown-hover-bg-color;
24 | }
25 |
26 | &:focus {
27 | outline: none;
28 | }
29 |
30 | &.divider {
31 | min-height: 0;
32 | height: 1px;
33 | }
34 |
35 | & > a, & > span {
36 | font-size: 16px;
37 | color: $dropdown-color;
38 | display: block;
39 | line-height: 22px;
40 | padding: (($dropdown-item-height - 22) / 2) 16px;
41 | }
42 |
43 | & > span > label {
44 | top: 1px;
45 | left: 0;
46 | height: 18px;
47 | }
48 |
49 | // Icon alignment override
50 | & > a > i {
51 | height: inherit;
52 | line-height: inherit;
53 | float: left;
54 | margin: 0 24px 0 0;
55 | width: 24px;
56 | }
57 |
58 |
59 | clear: both;
60 | color: $off-black;
61 | cursor: pointer;
62 | min-height: $dropdown-item-height;
63 | line-height: 1.5rem;
64 | width: 100%;
65 | text-align: left;
66 | }
67 | }
68 |
69 | body.keyboard-focused {
70 | .dropdown-content li:focus {
71 | background-color: darken($dropdown-hover-bg-color, 8%);
72 | }
73 | }
74 |
75 | // Input field specificity bugfix
76 | .input-field.col .dropdown-content [type="checkbox"] + label {
77 | top: 1px;
78 | left: 0;
79 | height: 18px;
80 | transform: none;
81 | }
82 |
83 | .dropdown-trigger {
84 | cursor: pointer;
85 | }
--------------------------------------------------------------------------------
/tubesync/common/static/styles/materializecss/components/_icons-material-design.scss:
--------------------------------------------------------------------------------
1 | /* This is needed for some mobile phones to display the Google Icon font properly */
2 | .material-icons {
3 | text-rendering: optimizeLegibility;
4 | font-feature-settings: 'liga';
5 | }
6 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/materializecss/components/_materialbox.scss:
--------------------------------------------------------------------------------
1 | .materialboxed {
2 | &:hover {
3 | &:not(.active) {
4 | opacity: .8;
5 | }
6 | }
7 |
8 | display: block;
9 | cursor: zoom-in;
10 | position: relative;
11 | transition: opacity .4s;
12 | -webkit-backface-visibility: hidden;
13 |
14 | &.active {
15 | cursor: zoom-out;
16 | }
17 | }
18 |
19 | #materialbox-overlay {
20 | position:fixed;
21 | top: 0;
22 | right: 0;
23 | bottom: 0;
24 | left: 0;
25 | background-color: #292929;
26 | z-index: 1000;
27 | will-change: opacity;
28 | }
29 |
30 | .materialbox-caption {
31 | position: fixed;
32 | display: none;
33 | color: #fff;
34 | line-height: 50px;
35 | bottom: 0;
36 | left: 0;
37 | width: 100%;
38 | text-align: center;
39 | padding: 0% 15%;
40 | height: 50px;
41 | z-index: 1000;
42 | -webkit-font-smoothing: antialiased;
43 | }
--------------------------------------------------------------------------------
/tubesync/common/static/styles/materializecss/components/_modal.scss:
--------------------------------------------------------------------------------
1 | .modal {
2 | &:focus {
3 | outline: none;
4 | }
5 |
6 | @extend .z-depth-5;
7 |
8 | display: none;
9 | position: fixed;
10 | left: 0;
11 | right: 0;
12 | background-color: #fafafa;
13 | padding: 0;
14 | max-height: 70%;
15 | width: 55%;
16 | margin: auto;
17 | overflow-y: auto;
18 |
19 | border-radius: 2px;
20 | will-change: top, opacity;
21 |
22 | @media #{$medium-and-down} {
23 | width: 80%;
24 | }
25 |
26 | h1,h2,h3,h4 {
27 | margin-top: 0;
28 | }
29 |
30 | .modal-content {
31 | padding: 24px;
32 | }
33 | .modal-close {
34 | cursor: pointer;
35 | }
36 |
37 | .modal-footer {
38 | border-radius: 0 0 2px 2px;
39 | background-color: #fafafa;
40 | padding: 4px 6px;
41 | height: 56px;
42 | width: 100%;
43 | text-align: right;
44 |
45 | .btn, .btn-flat {
46 | margin: 6px 0;
47 | }
48 | }
49 | }
50 | .modal-overlay {
51 | position: fixed;
52 | z-index: 999;
53 | top: -25%;
54 | left: 0;
55 | bottom: 0;
56 | right: 0;
57 | height: 125%;
58 | width: 100%;
59 | background: #000;
60 | display: none;
61 |
62 | will-change: opacity;
63 | }
64 |
65 | // Modal with fixed action footer
66 | .modal.modal-fixed-footer {
67 | padding: 0;
68 | height: 70%;
69 |
70 | .modal-content {
71 | position: absolute;
72 | height: calc(100% - 56px);
73 | max-height: 100%;
74 | width: 100%;
75 | overflow-y: auto;
76 | }
77 |
78 | .modal-footer {
79 | border-top: 1px solid rgba(0,0,0,.1);
80 | position: absolute;
81 | bottom: 0;
82 | }
83 | }
84 |
85 | // Modal Bottom Sheet Style
86 | .modal.bottom-sheet {
87 | top: auto;
88 | bottom: -100%;
89 | margin: 0;
90 | width: 100%;
91 | max-height: 45%;
92 | border-radius: 0;
93 | will-change: bottom, opacity;
94 | }
95 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/materializecss/components/_pulse.scss:
--------------------------------------------------------------------------------
1 | .pulse {
2 | &::before {
3 | content: '';
4 | display: block;
5 | position: absolute;
6 | width: 100%;
7 | height: 100%;
8 | top: 0;
9 | left: 0;
10 | background-color: inherit;
11 | border-radius: inherit;
12 | transition: opacity .3s, transform .3s;
13 | animation: pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;
14 | z-index: -1;
15 | }
16 |
17 | overflow: visible;
18 | position: relative;
19 | }
20 |
21 | @keyframes pulse-animation {
22 | 0% {
23 | opacity: 1;
24 | transform: scale(1);
25 | }
26 | 50% {
27 | opacity: 0;
28 | transform: scale(1.5);
29 | }
30 | 100% {
31 | opacity: 0;
32 | transform: scale(1.5);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/materializecss/components/_slider.scss:
--------------------------------------------------------------------------------
1 | .slider {
2 | position: relative;
3 | height: 400px;
4 | width: 100%;
5 |
6 | // Fullscreen slider
7 | &.fullscreen {
8 | height: 100%;
9 | width: 100%;
10 | position: absolute;
11 | top: 0;
12 | left: 0;
13 | right: 0;
14 | bottom: 0;
15 |
16 | ul.slides {
17 | height: 100%;
18 | }
19 |
20 | ul.indicators {
21 | z-index: 2;
22 | bottom: 30px;
23 | }
24 | }
25 |
26 | .slides {
27 | background-color: $slider-bg-color;
28 | margin: 0;
29 | height: 400px;
30 |
31 | li {
32 | opacity: 0;
33 | position: absolute;
34 | top: 0;
35 | left: 0;
36 | z-index: 1;
37 | width: 100%;
38 | height: inherit;
39 | overflow: hidden;
40 |
41 | img {
42 | height: 100%;
43 | width: 100%;
44 | background-size: cover;
45 | background-position: center;
46 | }
47 |
48 | .caption {
49 | color: #fff;
50 | position: absolute;
51 | top: 15%;
52 | left: 15%;
53 | width: 70%;
54 | opacity: 0;
55 |
56 | p { color: $slider-bg-color-light; }
57 | }
58 |
59 | &.active {
60 | z-index: 2;
61 | }
62 | }
63 | }
64 |
65 |
66 | .indicators {
67 | position: absolute;
68 | text-align: center;
69 | left: 0;
70 | right: 0;
71 | bottom: 0;
72 | margin: 0;
73 |
74 | .indicator-item {
75 | display: inline-block;
76 | position: relative;
77 | cursor: pointer;
78 | height: 16px;
79 | width: 16px;
80 | margin: 0 12px;
81 | background-color: $slider-bg-color-light;
82 |
83 | transition: background-color .3s;
84 | border-radius: 50%;
85 |
86 | &.active {
87 | background-color: $slider-indicator-color;
88 | }
89 | }
90 | }
91 |
92 | }
--------------------------------------------------------------------------------
/tubesync/common/static/styles/materializecss/components/_table_of_contents.scss:
--------------------------------------------------------------------------------
1 | /***************
2 | Nav List
3 | ***************/
4 | .table-of-contents {
5 | &.fixed {
6 | position: fixed;
7 | }
8 |
9 | li {
10 | padding: 2px 0;
11 | }
12 | a {
13 | display: inline-block;
14 | font-weight: 300;
15 | color: #757575;
16 | padding-left: 16px;
17 | height: 1.5rem;
18 | line-height: 1.5rem;
19 | letter-spacing: .4;
20 | display: inline-block;
21 |
22 | &:hover {
23 | color: lighten(#757575, 20%);
24 | padding-left: 15px;
25 | border-left: 1px solid $primary-color;
26 | }
27 | &.active {
28 | font-weight: 500;
29 | padding-left: 14px;
30 | border-left: 2px solid $primary-color;
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/materializecss/components/_tabs.scss:
--------------------------------------------------------------------------------
1 | .tabs {
2 | &.tabs-transparent {
3 | background-color: transparent;
4 |
5 | .tab a,
6 | .tab.disabled a,
7 | .tab.disabled a:hover {
8 | color: rgba(255,255,255,0.7);
9 | }
10 |
11 | .tab a:hover,
12 | .tab a.active {
13 | color: #fff;
14 | }
15 |
16 | .indicator {
17 | background-color: #fff;
18 | }
19 | }
20 |
21 | &.tabs-fixed-width {
22 | display: flex;
23 |
24 | .tab {
25 | flex-grow: 1;
26 | }
27 | }
28 |
29 | position: relative;
30 | overflow-x: auto;
31 | overflow-y: hidden;
32 | height: 48px;
33 | width: 100%;
34 | background-color: $tabs-bg-color;
35 | margin: 0 auto;
36 | white-space: nowrap;
37 |
38 | .tab {
39 | display: inline-block;
40 | text-align: center;
41 | line-height: 48px;
42 | height: 48px;
43 | padding: 0;
44 | margin: 0;
45 | text-transform: uppercase;
46 |
47 | a {
48 | &:focus,
49 | &:focus.active {
50 | background-color: transparentize($tabs-underline-color, .8);
51 | outline: none;
52 | }
53 |
54 | &:hover,
55 | &.active {
56 | background-color: transparent;
57 | color: $tabs-text-color;
58 | }
59 |
60 | color: rgba($tabs-text-color, .7);
61 | display: block;
62 | width: 100%;
63 | height: 100%;
64 | padding: 0 24px;
65 | font-size: 14px;
66 | text-overflow: ellipsis;
67 | overflow: hidden;
68 | transition: color .28s ease, background-color .28s ease;
69 | }
70 |
71 | &.disabled a,
72 | &.disabled a:hover {
73 | color: rgba($tabs-text-color, .4);
74 | cursor: default;
75 | }
76 | }
77 | .indicator {
78 | position: absolute;
79 | bottom: 0;
80 | height: 2px;
81 | background-color: $tabs-underline-color;
82 | will-change: left, right;
83 | }
84 | }
85 |
86 | // Fixed Sidenav hide on smaller
87 | @media #{$medium-and-down} {
88 | .tabs {
89 | display: flex;
90 |
91 | .tab {
92 | flex-grow: 1;
93 |
94 | a {
95 | padding: 0 12px;
96 | }
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/materializecss/components/_tapTarget.scss:
--------------------------------------------------------------------------------
1 | .tap-target-wrapper {
2 | width: 800px;
3 | height: 800px;
4 | position: fixed;
5 | z-index: 1000;
6 | visibility: hidden;
7 | transition: visibility 0s .3s;
8 | }
9 |
10 | .tap-target-wrapper.open {
11 | visibility: visible;
12 | transition: visibility 0s;
13 |
14 | .tap-target {
15 | transform: scale(1);
16 | opacity: .95;
17 | transition:
18 | transform .3s cubic-bezier(.42,0,.58,1),
19 | opacity .3s cubic-bezier(.42,0,.58,1);
20 | }
21 |
22 | .tap-target-wave::before {
23 | transform: scale(1);
24 | }
25 | .tap-target-wave::after {
26 | visibility: visible;
27 | animation: pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;
28 | transition:
29 | opacity .3s,
30 | transform .3s,
31 | visibility 0s 1s;
32 | }
33 | }
34 |
35 | .tap-target {
36 | position: absolute;
37 | font-size: 1rem;
38 | border-radius: 50%;
39 | background-color: $primary-color;
40 | box-shadow: 0 20px 20px 0 rgba(0,0,0,0.14), 0 10px 50px 0 rgba(0,0,0,0.12), 0 30px 10px -20px rgba(0,0,0,0.2);
41 | width: 100%;
42 | height: 100%;
43 | opacity: 0;
44 | transform: scale(0);
45 | transition:
46 | transform .3s cubic-bezier(.42,0,.58,1),
47 | opacity .3s cubic-bezier(.42,0,.58,1);
48 | }
49 |
50 | .tap-target-content {
51 | position: relative;
52 | display: table-cell;
53 | }
54 |
55 | .tap-target-wave {
56 | &::before,
57 | &::after {
58 | content: '';
59 | display: block;
60 | position: absolute;
61 | width: 100%;
62 | height: 100%;
63 | border-radius: 50%;
64 | background-color: #ffffff;
65 | }
66 | &::before {
67 | transform: scale(0);
68 | transition: transform .3s;
69 | }
70 | &::after {
71 | visibility: hidden;
72 | transition:
73 | opacity .3s,
74 | transform .3s,
75 | visibility 0s;
76 | z-index: -1;
77 | }
78 |
79 | position: absolute;
80 | border-radius: 50%;
81 | z-index: 10001;
82 | }
83 |
84 | .tap-target-origin {
85 | &:not(.btn),
86 | &:not(.btn):hover {
87 | background: none;
88 | }
89 |
90 | top: 50%;
91 | left: 50%;
92 | transform: translate(-50%,-50%);
93 |
94 | z-index: 10002;
95 | position: absolute !important;
96 | }
97 |
98 | @media only screen and (max-width: 600px) {
99 | .tap-target, .tap-target-wrapper {
100 | width: 600px;
101 | height: 600px;
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/materializecss/components/_toast.scss:
--------------------------------------------------------------------------------
1 | #toast-container {
2 | display:block;
3 | position: fixed;
4 | z-index: 10000;
5 |
6 | @media #{$small-and-down} {
7 | min-width: 100%;
8 | bottom: 0%;
9 | }
10 | @media #{$medium-only} {
11 | left: 5%;
12 | bottom: 7%;
13 | max-width: 90%;
14 | }
15 | @media #{$large-and-up} {
16 | top: 10%;
17 | right: 7%;
18 | max-width: 86%;
19 | }
20 | }
21 |
22 | .toast {
23 | @extend .z-depth-1;
24 | border-radius: 2px;
25 | top: 35px;
26 | width: auto;
27 | margin-top: 10px;
28 | position: relative;
29 | max-width:100%;
30 | height: auto;
31 | min-height: $toast-height;
32 | line-height: 1.5em;
33 | background-color: $toast-color;
34 | padding: 10px 25px;
35 | font-size: 1.1rem;
36 | font-weight: 300;
37 | color: $toast-text-color;
38 | display: flex;
39 | align-items: center;
40 | justify-content: space-between;
41 | cursor: default;
42 |
43 | .toast-action {
44 | color: $toast-action-color;
45 | font-weight: 500;
46 | margin-right: -25px;
47 | margin-left: 3rem;
48 | }
49 |
50 | &.rounded{
51 | border-radius: 24px;
52 | }
53 |
54 | @media #{$small-and-down} {
55 | width: 100%;
56 | border-radius: 0;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/materializecss/components/_tooltip.scss:
--------------------------------------------------------------------------------
1 | .material-tooltip {
2 | padding: 10px 8px;
3 | font-size: 1rem;
4 | z-index: 2000;
5 | background-color: transparent;
6 | border-radius: 2px;
7 | color: #fff;
8 | min-height: 36px;
9 | line-height: 120%;
10 | opacity: 0;
11 | position: absolute;
12 | text-align: center;
13 | max-width: calc(100% - 4px);
14 | overflow: hidden;
15 | left: 0;
16 | top: 0;
17 | pointer-events: none;
18 | visibility: hidden;
19 | background-color: #323232;
20 | }
21 |
22 | .backdrop {
23 | position: absolute;
24 | opacity: 0;
25 | height: 7px;
26 | width: 14px;
27 | border-radius: 0 0 50% 50%;
28 | background-color: #323232;
29 | z-index: -1;
30 | transform-origin: 50% 0%;
31 | visibility: hidden;
32 | }
33 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/materializecss/components/_transitions.scss:
--------------------------------------------------------------------------------
1 | // Scale transition
2 | .scale-transition {
3 | &.scale-out {
4 | transform: scale(0);
5 | transition: transform .2s !important;
6 | }
7 |
8 | &.scale-in {
9 | transform: scale(1);
10 | }
11 |
12 | transition: transform .3s cubic-bezier(0.53, 0.01, 0.36, 1.63) !important;
13 | }
--------------------------------------------------------------------------------
/tubesync/common/static/styles/materializecss/components/_typography.scss:
--------------------------------------------------------------------------------
1 |
2 | a {
3 | text-decoration: none;
4 | }
5 |
6 | html{
7 | line-height: 1.5;
8 |
9 | @media only screen and (min-width: 0) {
10 | font-size: 14px;
11 | }
12 |
13 | @media only screen and (min-width: $medium-screen) {
14 | font-size: 14.5px;
15 | }
16 |
17 | @media only screen and (min-width: $large-screen) {
18 | font-size: 15px;
19 | }
20 |
21 | font-family: $font-stack;
22 | font-weight: normal;
23 | color: $off-black;
24 | }
25 | h1, h2, h3, h4, h5, h6 {
26 | font-weight: 400;
27 | line-height: 1.3;
28 | }
29 |
30 | // Header Styles
31 | h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { font-weight: inherit; }
32 | h1 { font-size: $h1-fontsize; line-height: 110%; margin: ($h1-fontsize / 1.5) 0 ($h1-fontsize / 2.5) 0;}
33 | h2 { font-size: $h2-fontsize; line-height: 110%; margin: ($h2-fontsize / 1.5) 0 ($h2-fontsize / 2.5) 0;}
34 | h3 { font-size: $h3-fontsize; line-height: 110%; margin: ($h3-fontsize / 1.5) 0 ($h3-fontsize / 2.5) 0;}
35 | h4 { font-size: $h4-fontsize; line-height: 110%; margin: ($h4-fontsize / 1.5) 0 ($h4-fontsize / 2.5) 0;}
36 | h5 { font-size: $h5-fontsize; line-height: 110%; margin: ($h5-fontsize / 1.5) 0 ($h5-fontsize / 2.5) 0;}
37 | h6 { font-size: $h6-fontsize; line-height: 110%; margin: ($h6-fontsize / 1.5) 0 ($h6-fontsize / 2.5) 0;}
38 |
39 | // Text Styles
40 | em { font-style: italic; }
41 | strong { font-weight: 500; }
42 | small { font-size: 75%; }
43 | .light { font-weight: 300; }
44 | .thin { font-weight: 200; }
45 |
46 |
47 | .flow-text{
48 | $i: 0;
49 | @while $i <= $intervals {
50 | @media only screen and (min-width : 360 + ($i * $interval-size)) {
51 | font-size: 1.2rem * (1 + (.02 * $i));
52 | }
53 | $i: $i + 1;
54 | }
55 |
56 | // Handle below 360px screen
57 | @media only screen and (max-width: 360px) {
58 | font-size: 1.2rem;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/materializecss/components/_waves.scss:
--------------------------------------------------------------------------------
1 |
2 | /*!
3 | * Waves v0.6.0
4 | * http://fian.my.id/Waves
5 | *
6 | * Copyright 2014 Alfiana E. Sibuea and other contributors
7 | * Released under the MIT license
8 | * https://github.com/fians/Waves/blob/master/LICENSE
9 | */
10 |
11 |
12 | .waves-effect {
13 | position: relative;
14 | cursor: pointer;
15 | display: inline-block;
16 | overflow: hidden;
17 | user-select: none;
18 | -webkit-tap-highlight-color: transparent;
19 | vertical-align: middle;
20 | z-index: 1;
21 | transition: .3s ease-out;
22 |
23 | .waves-ripple {
24 | position: absolute;
25 | border-radius: 50%;
26 | width: 20px;
27 | height: 20px;
28 | margin-top:-10px;
29 | margin-left:-10px;
30 | opacity: 0;
31 |
32 | background: rgba(0,0,0,0.2);
33 | transition: all 0.7s ease-out;
34 | transition-property: transform, opacity;
35 | transform: scale(0);
36 | pointer-events: none;
37 | }
38 |
39 | // Waves Colors
40 | &.waves-light .waves-ripple {
41 | background-color: rgba(255, 255, 255, 0.45);
42 | }
43 | &.waves-red .waves-ripple {
44 | background-color: rgba(244, 67, 54, .70);
45 | }
46 | &.waves-yellow .waves-ripple {
47 | background-color: rgba(255, 235, 59, .70);
48 | }
49 | &.waves-orange .waves-ripple {
50 | background-color: rgba(255, 152, 0, .70);
51 | }
52 | &.waves-purple .waves-ripple {
53 | background-color: rgba(156, 39, 176, 0.70);
54 | }
55 | &.waves-green .waves-ripple {
56 | background-color: rgba(76, 175, 80, 0.70);
57 | }
58 | &.waves-teal .waves-ripple {
59 | background-color: rgba(0, 150, 136, 0.70);
60 | }
61 |
62 | // Style input button bug.
63 | input[type="button"], input[type="reset"], input[type="submit"] {
64 | border: 0;
65 | font-style: normal;
66 | font-size: inherit;
67 | text-transform: inherit;
68 | background: none;
69 | }
70 |
71 | img {
72 | position: relative;
73 | z-index: -1;
74 | }
75 | }
76 |
77 | .waves-notransition {
78 | transition: none #{"!important"};
79 | }
80 |
81 | .waves-circle {
82 | transform: translateZ(0);
83 | -webkit-mask-image: -webkit-radial-gradient(circle, white 100%, black 100%);
84 | }
85 |
86 | .waves-input-wrapper {
87 | border-radius: 0.2em;
88 | vertical-align: bottom;
89 |
90 | .waves-button-input {
91 | position: relative;
92 | top: 0;
93 | left: 0;
94 | z-index: 1;
95 | }
96 | }
97 |
98 | .waves-circle {
99 | text-align: center;
100 | width: 2.5em;
101 | height: 2.5em;
102 | line-height: 2.5em;
103 | border-radius: 50%;
104 | -webkit-mask-image: none;
105 | }
106 |
107 | .waves-block {
108 | display: block;
109 | }
110 |
111 | /* Firefox Bug: link not triggered */
112 | .waves-effect .waves-ripple {
113 | z-index: -1;
114 | }
--------------------------------------------------------------------------------
/tubesync/common/static/styles/materializecss/components/forms/_file-input.scss:
--------------------------------------------------------------------------------
1 | /* File Input
2 | ========================================================================== */
3 |
4 | .file-field {
5 | position: relative;
6 |
7 | .file-path-wrapper {
8 | overflow: hidden;
9 | padding-left: 10px;
10 | }
11 |
12 | input.file-path { width: 100%; }
13 |
14 | .btn {
15 | float: left;
16 | height: $input-height;
17 | line-height: $input-height;
18 | }
19 |
20 | span {
21 | cursor: pointer;
22 | }
23 |
24 | input[type=file] {
25 |
26 | // Needed to override webkit button
27 | &::-webkit-file-upload-button {
28 | display: none;
29 | }
30 |
31 | position: absolute;
32 | top: 0;
33 | right: 0;
34 | left: 0;
35 | bottom: 0;
36 | width: 100%;
37 | margin: 0;
38 | padding: 0;
39 | font-size: 20px;
40 | cursor: pointer;
41 | opacity: 0;
42 | filter: alpha(opacity=0);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/materializecss/components/forms/_forms.scss:
--------------------------------------------------------------------------------
1 | // Remove Focus Boxes
2 | select:focus {
3 | outline: $select-focus;
4 | }
5 |
6 | button:focus {
7 | outline: none;
8 | background-color: $button-background-focus;
9 | }
10 |
11 | label {
12 | font-size: $label-font-size;
13 | color: $input-border-color;
14 | }
15 |
16 | @import 'input-fields';
17 | @import 'radio-buttons';
18 | @import 'checkboxes';
19 | @import 'switches';
20 | @import 'select';
21 | @import 'file-input';
22 | @import 'range';
23 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/materializecss/components/forms/_radio-buttons.scss:
--------------------------------------------------------------------------------
1 | /* Radio Buttons
2 | ========================================================================== */
3 |
4 | // Remove default Radio Buttons
5 | [type="radio"]:not(:checked),
6 | [type="radio"]:checked {
7 | position: absolute;
8 | opacity: 0;
9 | pointer-events: none;
10 | }
11 |
12 | [type="radio"]:not(:checked) + span,
13 | [type="radio"]:checked + span {
14 | position: relative;
15 | padding-left: 35px;
16 | cursor: pointer;
17 | display: inline-block;
18 | height: 25px;
19 | line-height: 25px;
20 | font-size: 1rem;
21 | transition: .28s ease;
22 | user-select: none;
23 | }
24 |
25 | [type="radio"] + span:before,
26 | [type="radio"] + span:after {
27 | content: '';
28 | position: absolute;
29 | left: 0;
30 | top: 0;
31 | margin: 4px;
32 | width: 16px;
33 | height: 16px;
34 | z-index: 0;
35 | transition: .28s ease;
36 | }
37 |
38 | /* Unchecked styles */
39 | [type="radio"]:not(:checked) + span:before,
40 | [type="radio"]:not(:checked) + span:after,
41 | [type="radio"]:checked + span:before,
42 | [type="radio"]:checked + span:after,
43 | [type="radio"].with-gap:checked + span:before,
44 | [type="radio"].with-gap:checked + span:after {
45 | border-radius: 50%;
46 | }
47 |
48 | [type="radio"]:not(:checked) + span:before,
49 | [type="radio"]:not(:checked) + span:after {
50 | border: 2px solid $radio-empty-color;
51 | }
52 |
53 | [type="radio"]:not(:checked) + span:after {
54 | transform: scale(0);
55 | }
56 |
57 | /* Checked styles */
58 | [type="radio"]:checked + span:before {
59 | border: 2px solid transparent;
60 | }
61 |
62 | [type="radio"]:checked + span:after,
63 | [type="radio"].with-gap:checked + span:before,
64 | [type="radio"].with-gap:checked + span:after {
65 | border: $radio-border;
66 | }
67 |
68 | [type="radio"]:checked + span:after,
69 | [type="radio"].with-gap:checked + span:after {
70 | background-color: $radio-fill-color;
71 | }
72 |
73 | [type="radio"]:checked + span:after {
74 | transform: scale(1.02);
75 | }
76 |
77 | /* Radio With gap */
78 | [type="radio"].with-gap:checked + span:after {
79 | transform: scale(.5);
80 | }
81 |
82 | /* Focused styles */
83 | [type="radio"].tabbed:focus + span:before {
84 | box-shadow: 0 0 0 10px rgba(0,0,0,.1);
85 | }
86 |
87 | /* Disabled Radio With gap */
88 | [type="radio"].with-gap:disabled:checked + span:before {
89 | border: 2px solid $input-disabled-color;
90 | }
91 |
92 | [type="radio"].with-gap:disabled:checked + span:after {
93 | border: none;
94 | background-color: $input-disabled-color;
95 | }
96 |
97 | /* Disabled style */
98 | [type="radio"]:disabled:not(:checked) + span:before,
99 | [type="radio"]:disabled:checked + span:before {
100 | background-color: transparent;
101 | border-color: $input-disabled-color;
102 | }
103 |
104 | [type="radio"]:disabled + span {
105 | color: $input-disabled-color;
106 | }
107 |
108 | [type="radio"]:disabled:not(:checked) + span:before {
109 | border-color: $input-disabled-color;
110 | }
111 |
112 | [type="radio"]:disabled:checked + span:after {
113 | background-color: $input-disabled-color;
114 | border-color: $input-disabled-solid-color;
115 | }
116 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/materializecss/components/forms/_switches.scss:
--------------------------------------------------------------------------------
1 | /* Switch
2 | ========================================================================== */
3 |
4 | .switch,
5 | .switch * {
6 | -webkit-tap-highlight-color: transparent;
7 | user-select: none;
8 | }
9 |
10 | .switch label {
11 | cursor: pointer;
12 | }
13 |
14 | .switch label input[type=checkbox] {
15 | opacity: 0;
16 | width: 0;
17 | height: 0;
18 |
19 | &:checked + .lever {
20 | background-color: $switch-checked-lever-bg;
21 |
22 | &:before, &:after {
23 | left: 18px;
24 | }
25 |
26 | &:after {
27 | background-color: $switch-bg-color;
28 | }
29 | }
30 | }
31 |
32 | .switch label .lever {
33 | content: "";
34 | display: inline-block;
35 | position: relative;
36 | width: 36px;
37 | height: 14px;
38 | background-color: $switch-unchecked-lever-bg;
39 | border-radius: $switch-radius;
40 | margin-right: 10px;
41 | transition: background 0.3s ease;
42 | vertical-align: middle;
43 | margin: 0 16px;
44 |
45 | &:before, &:after {
46 | content: "";
47 | position: absolute;
48 | display: inline-block;
49 | width: 20px;
50 | height: 20px;
51 | border-radius: 50%;
52 | left: 0;
53 | top: -3px;
54 | transition: left 0.3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease;
55 | }
56 |
57 | &:before {
58 | background-color: transparentize($switch-bg-color, .85);
59 | }
60 |
61 | &:after {
62 | background-color: $switch-unchecked-bg;
63 | box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12);
64 | }
65 | }
66 |
67 | // Switch active style
68 | input[type=checkbox]:checked:not(:disabled) ~ .lever:active::before,
69 | input[type=checkbox]:checked:not(:disabled).tabbed:focus ~ .lever::before {
70 | transform: scale(2.4);
71 | background-color: transparentize($switch-bg-color, .85);
72 | }
73 |
74 | input[type=checkbox]:not(:disabled) ~ .lever:active:before,
75 | input[type=checkbox]:not(:disabled).tabbed:focus ~ .lever::before {
76 | transform: scale(2.4);
77 | background-color: rgba(0,0,0,.08);
78 | }
79 |
80 | // Disabled Styles
81 | .switch input[type=checkbox][disabled] + .lever {
82 | cursor: default;
83 | background-color: rgba(0,0,0,.12);
84 | }
85 |
86 | .switch label input[type=checkbox][disabled] + .lever:after,
87 | .switch label input[type=checkbox][disabled]:checked + .lever:after {
88 | background-color: $input-disabled-solid-color;
89 | }
90 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/materializecss/materialize.scss:
--------------------------------------------------------------------------------
1 | @charset "UTF-8";
2 |
3 | // Color
4 | @import "components/color-variables";
5 | @import "components/color-classes";
6 |
7 | // Variables;
8 | @import "components/variables";
9 |
10 | // Reset
11 | @import "components/normalize";
12 |
13 | // components
14 | @import "components/global";
15 | @import "components/badges";
16 | @import "components/icons-material-design";
17 | @import "components/grid";
18 | @import "components/navbar";
19 | @import "components/typography";
20 | @import "components/transitions";
21 | @import "components/cards";
22 | @import "components/toast";
23 | @import "components/tabs";
24 | @import "components/tooltip";
25 | @import "components/buttons";
26 | @import "components/dropdown";
27 | @import "components/waves";
28 | @import "components/modal";
29 | @import "components/collapsible";
30 | @import "components/chips";
31 | @import "components/materialbox";
32 | @import "components/forms/forms";
33 | @import "components/table_of_contents";
34 | @import "components/sidenav";
35 | @import "components/preloader";
36 | @import "components/slider";
37 | @import "components/carousel";
38 | @import "components/tapTarget";
39 | @import "components/pulse";
40 | @import "components/datepicker";
41 | @import "components/timepicker";
42 |
--------------------------------------------------------------------------------
/tubesync/common/static/styles/tubesync.scss:
--------------------------------------------------------------------------------
1 | @charset "UTF-8";
2 |
3 | @import "materializecss/materialize";
4 | @import "fontawesome/fontawesome";
5 | @import "fontawesome/regular";
6 | @import "fontawesome/solid";
7 | @import "fontawesome/brands";
8 |
9 | @import "fonts";
10 | @import "variables";
11 | @import "colours";
12 | @import "helpers";
13 | @import "forms";
14 | @import "template";
15 |
16 | html {
17 | visibility: visible;
18 | opacity: 1;
19 | }
20 |
21 | .flex-collection-container {
22 | display: flex !important;
23 | align-items: center;
24 | }
25 |
26 | .flex-grow {
27 | flex-grow: 1;
28 | }
29 |
30 | .help-text > i {
31 | padding-right: 6px;
32 | }
33 |
34 | .issue-641 {
35 | display: block !important;
36 | overflow-wrap: anywhere;
37 | }
38 |
--------------------------------------------------------------------------------
/tubesync/common/templates/base.html:
--------------------------------------------------------------------------------
1 | {% load static %}{% load sass_tags %}
2 |
3 |
4 |
5 |
11 |
12 |
13 |
14 | TubeSync - {% block headtitle %}Synchronize YouTube to your local media server{% endblock %}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
29 |
30 |
31 |
40 |
41 |
42 |
43 |
44 | {% block content %}{% endblock %}
45 |
46 |
47 |
48 |
49 |
50 |
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/tubesync/common/templates/error403.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | 403 - Forbidden
4 |
5 |
6 | 403 - Forbidden
7 | Your request was denied. You do not have access to the requested resource.
8 |
9 |
10 |
--------------------------------------------------------------------------------
/tubesync/common/templates/error404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | 404 - Page Not Found
4 |
5 |
6 | 404 - Page Not Found
7 | The resource you have requested does not exist.
8 |
9 |
10 |
--------------------------------------------------------------------------------
/tubesync/common/templates/error500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | 500 - Internal Server Error
4 |
5 |
6 | 500 - Internal Server Error
7 | Your request caused an internal server error. This has been logged and our developers will implement a fix shortly.
8 |
9 |
10 |
--------------------------------------------------------------------------------
/tubesync/common/templates/errorbox.html:
--------------------------------------------------------------------------------
1 | {% if message %}
2 |
3 |
4 |
5 |
6 | {{ message|safe }}
7 |
8 |
9 |
10 |
11 | {% endif %}
12 |
--------------------------------------------------------------------------------
/tubesync/common/templates/infobox.html:
--------------------------------------------------------------------------------
1 | {% if message %}
2 |
3 |
4 |
5 |
6 | {{ message|safe }}
7 |
8 |
9 |
10 |
11 | {% endif %}
12 |
--------------------------------------------------------------------------------
/tubesync/common/templates/pagination.html:
--------------------------------------------------------------------------------
1 | {% if paginator.num_pages > 1 %}
2 |
11 | {% endif %}
12 |
--------------------------------------------------------------------------------
/tubesync/common/templates/simpleform.html:
--------------------------------------------------------------------------------
1 | {% if form %}
2 | {% if form.errors %}
3 |
4 | {% for _, error in form.errors.items %}
5 | {{ error }}
6 | {% endfor %}
7 |
8 | {% endif %}
9 | {% for field in form %}
10 | {% if field.field.widget.input_type == 'hidden' %}{{ field }}{% else %}
11 |
12 |
13 | {% if field.field.widget.input_type == 'checkbox' %}
14 |
15 | {{ field }}
16 | {{ field.label }}
17 |
18 | {% else %}
19 | {{ field.label_tag }}
20 | {{ field }}
21 | {% endif %}
22 | {% if field.help_text %} {{ field.help_text }} {% endif %}
23 |
24 |
25 | {% endif %}
26 | {% endfor %}
27 | {% endif %}
28 |
--------------------------------------------------------------------------------
/tubesync/common/templates/tubesync.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/tubesync/common/testutils.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 |
4 | def prevent_request_warnings(original_function):
5 | '''
6 | Suppress errors from views that raise legitimate errors, such as
7 | testing that a page does indeed 404 or a non-authenticated user
8 | cannot access page requiring authentication which raises a 403. You
9 | can wrap test methods with this to drop the error logging down a notch.
10 | '''
11 |
12 | def new_function(*args, **kwargs):
13 | logger = logging.getLogger('django.request')
14 | previous_logging_level = logger.getEffectiveLevel()
15 | logger.setLevel(logging.CRITICAL)
16 | original_function(*args, **kwargs)
17 | logger.setLevel(previous_logging_level)
18 |
19 | return new_function
20 |
--------------------------------------------------------------------------------
/tubesync/common/third_party_versions.py:
--------------------------------------------------------------------------------
1 | from yt_dlp import version as yt_dlp_version
2 |
3 |
4 | yt_dlp_version = str(yt_dlp_version.__version__)
5 | ffmpeg_version = '(shared install)'
6 |
7 |
8 | # This file may contain data dynamically written during the container build process
9 | # that replaces the above versions. Do not edit below this line
10 |
--------------------------------------------------------------------------------
/tubesync/common/timestamp.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 |
4 | utc_tz = datetime.timezone.utc
5 | posix_epoch = datetime.datetime.fromtimestamp(0, utc_tz)
6 |
7 |
8 | def add_epoch(seconds):
9 | assert seconds is not None
10 | assert seconds >= 0, 'seconds must be a positive number'
11 |
12 | return datetime.timedelta(seconds=seconds) + posix_epoch
13 |
14 | def subtract_epoch(arg_dt, /):
15 | assert isinstance(arg_dt, datetime.datetime)
16 | utc_dt = arg_dt.astimezone(utc_tz)
17 |
18 | return utc_dt - posix_epoch
19 |
20 | def datetime_to_timestamp(arg_dt, /, *, integer=True):
21 | timestamp = subtract_epoch(arg_dt).total_seconds()
22 |
23 | if not integer:
24 | return timestamp
25 | return round(timestamp)
26 |
27 | def timestamp_to_datetime(seconds, /):
28 | return add_epoch(seconds=seconds).astimezone(utc_tz)
29 |
30 |
--------------------------------------------------------------------------------
/tubesync/common/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.urls import path
3 | from django.views.generic.base import RedirectView
4 | from django.http import HttpResponse
5 | from .views import error403, error404, error500, HealthCheckView
6 |
7 |
8 | app_name = 'common'
9 | robots_view = HttpResponse(settings.ROBOTS, content_type='text/plain')
10 | favicon_uri = settings.STATIC_URL + 'images/favicon.ico'
11 | favicon_view = RedirectView.as_view(url=favicon_uri, permanent=False)
12 |
13 |
14 | urlpatterns = [
15 |
16 | path('error403',
17 | error403,
18 | name='error403'),
19 |
20 | path('error404',
21 | error404,
22 | name='error404'),
23 |
24 | path('error500',
25 | error500,
26 | name='error500'),
27 |
28 | path('robots.txt',
29 | lambda r: robots_view,
30 | name='robots'),
31 |
32 | path('favicon.ico',
33 | favicon_view,
34 | name='favicon'),
35 |
36 | path('healthcheck',
37 | HealthCheckView.as_view(),
38 | name='healthcheck'),
39 |
40 | ]
41 |
--------------------------------------------------------------------------------
/tubesync/common/views.py:
--------------------------------------------------------------------------------
1 | from random import random
2 | from django.conf import settings
3 | from django.shortcuts import render
4 | from django.views.generic import View
5 | from django.http import HttpResponse, HttpResponseServerError
6 | from django.core.exceptions import PermissionDenied
7 | from django.db import connection
8 | from .utils import get_client_ip
9 |
10 |
11 | def error403(request, *args, **kwargs):
12 | return render(request, 'error403.html', status=403)
13 |
14 |
15 | def error404(request, *args, **kwargs):
16 | return render(request, 'error404.html', status=404)
17 |
18 |
19 | def error500(request, *args, **kwargs):
20 | return render(request, 'error500.html', status=500)
21 |
22 |
23 | class HealthCheckView(View):
24 | '''
25 | A basic healthcheck view. SELECTs a random int via the database connection
26 | and verifies it matches. This checks that the application server, django and
27 | the database connection are all working correctly.
28 | '''
29 |
30 | ALLOWED_IPS = settings.HEALTHCHECK_ALLOWED_IPS
31 |
32 | def get(self, request, *args, **kwargs):
33 | if settings.HEALTHCHECK_FIREWALL:
34 | client_ip = get_client_ip(request)
35 | if client_ip not in self.ALLOWED_IPS:
36 | raise PermissionDenied
37 | randomint = int(random() * (10 ** 10))
38 | with connection.cursor() as cursor:
39 | cursor.execute('select {}'.format(randomint))
40 | row = cursor.fetchone()
41 | try:
42 | pong = row[0]
43 | except IndexError:
44 | pong = False
45 | if str(pong) != str(randomint):
46 | err = 'Failed healtcheck, expected "{}" got "{}"'
47 | return HttpResponseServerError(err.format(randomint, pong))
48 | else:
49 | return HttpResponse('ok')
50 |
--------------------------------------------------------------------------------
/tubesync/full_playlist.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | playlist_id="${1}"
4 | total_entries="${2}"
5 |
6 | # select YOUTUBE_*DIR settings
7 | # convert None to ''
8 | # convert PosixPath('VALUE') to 'VALUE'
9 | # assign a shell variable with the setting name and value
10 | _awk_prog='$2 == "=" && $1 ~ /^YOUTUBE_/ && $1 ~ /DIR$/ {
11 | sub(/^None$/, "'\'\''", $3);
12 | r = sub(/^PosixPath[(]/, "", $3);
13 | NF--;
14 | if(r) {sub(/[)]$/, "", $NF);};
15 | $3=$1 $2 $3; $1=$2=""; sub("^" OFS "+", "");
16 | print;
17 | }'
18 | . <(python3 /app/manage.py diffsettings --output hash | awk "${_awk_prog}")
19 | WHERE="${YOUTUBE_DL_CACHEDIR:-/dev/shm}"
20 |
21 | downloaded_entries="$( find /dev/shm "${WHERE}" \
22 | -path '*/infojson/playlist/postprocessor_*_temp\.info\.json' \
23 | -name "postprocessor_[[]${playlist_id}[]]_*_${total_entries}_temp\.info\.json" \
24 | -exec basename '{}' ';' | \
25 | sed -e 's/^postprocessor_[[].*[]]_//;s/_temp.*\.json$//;' | \
26 | cut -d '_' -f 1 )"
27 |
28 | find /dev/shm "${WHERE}" \
29 | -path '*/infojson/playlist/postprocessor_*_temp\.info\.json' \
30 | -name "postprocessor_[[]${playlist_id}[]]_*_temp\.info\.json" \
31 | -type f -delete
32 |
33 | if [ 'NA' != "${downloaded_entries:=${3:-NA}}" ] &&
34 | [ 'NA' != "${total_entries:-NA}" ] &&
35 | [ "${downloaded_entries}" != "${total_entries}" ]
36 | then
37 | exit 1
38 | fi
39 |
40 | exit 0
41 |
--------------------------------------------------------------------------------
/tubesync/healthcheck.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | '''
3 |
4 | Perform an HTTP request to a URL and exit with an exit code of 1 if the
5 | request did not return an HTTP/200 status code.
6 |
7 | Usage:
8 | $ ./healthcheck.py http://some.url.here/healthcheck/resource
9 |
10 | '''
11 |
12 |
13 | import os
14 | import sys
15 | import requests
16 |
17 |
18 | TIMEOUT = 5 # Seconds
19 | HTTP_USER = os.getenv('HTTP_USER')
20 | HTTP_PASS = os.getenv('HTTP_PASS')
21 | # never use proxy for healthcheck requests
22 | os.environ['no_proxy'] = '*'
23 |
24 |
25 | def do_heatlhcheck(url):
26 | headers = {'User-Agent': 'healthcheck'}
27 | auth = None
28 | if HTTP_USER and HTTP_PASS:
29 | auth = (HTTP_USER, HTTP_PASS)
30 | response = requests.get(url, headers=headers, auth=auth, timeout=TIMEOUT)
31 | return response.status_code == 200
32 |
33 |
34 | if __name__ == '__main__':
35 | # if it is marked as intentionally down, nothing else matters
36 | if os.path.exists('/run/service/gunicorn/down'):
37 | sys.exit(0)
38 | try:
39 | url = sys.argv[1]
40 | except IndexError:
41 | sys.stderr.write('URL must be supplied\n')
42 | sys.exit(1)
43 | if do_heatlhcheck(url):
44 | sys.exit(0)
45 | else:
46 | sys.exit(1)
47 |
--------------------------------------------------------------------------------
/tubesync/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 |
4 | import os
5 | import sys
6 |
7 |
8 | def main():
9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tubesync.settings')
10 | try:
11 | from django.core.management import execute_from_command_line
12 | except ImportError as exc:
13 | raise ImportError('Unable to import django, is it installed?') from exc
14 | execute_from_command_line(sys.argv)
15 |
16 |
17 | if __name__ == '__main__':
18 | main()
19 |
--------------------------------------------------------------------------------
/tubesync/restart_services.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | dir='/run/service'
4 | svc_path() (
5 | cd "${dir}"
6 | realpath -e -s "$@"
7 | )
8 |
9 | if [ 0 -eq $# ]
10 | then
11 | set -- \
12 | $( cd "${dir}" && svc_path tubesync*-worker ) \
13 | "$( svc_path gunicorn )" \
14 | "$( svc_path nginx )"
15 | fi
16 |
17 | for service in $( svc_path "$@" )
18 | do
19 | printf -- 'Restarting %-28s' "${service#${dir}/}..."
20 | _began="$( date '+%s' )"
21 | /command/s6-svc -wr -r "${service}"
22 | _ended="$( date '+%s' )"
23 | printf -- '\tcompleted (in %2.1d seconds).\n' \
24 | "$( expr "${_ended}" - "${_began}" )"
25 | done
26 | unset -v _began _ended service
27 |
--------------------------------------------------------------------------------
/tubesync/sync/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/sync/__init__.py
--------------------------------------------------------------------------------
/tubesync/sync/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from .models import (
3 | Source,
4 | Media,
5 | Metadata,
6 | MetadataFormat,
7 | MediaServer
8 | )
9 |
10 |
11 | @admin.register(Source)
12 | class SourceAdmin(admin.ModelAdmin):
13 |
14 | ordering = ('-created',)
15 | list_display = ('uuid', 'name', 'source_type', 'last_crawl',
16 | 'download_media', 'has_failed')
17 | readonly_fields = ('uuid', 'created')
18 | search_fields = ('uuid', 'key', 'name')
19 |
20 |
21 | @admin.register(Media)
22 | class MediaAdmin(admin.ModelAdmin):
23 |
24 | ordering = ('-created',)
25 | list_display = ('uuid', 'key', 'source', 'can_download', 'skip', 'downloaded')
26 | readonly_fields = ('uuid', 'created')
27 | search_fields = ('uuid', 'source__key', 'key')
28 |
29 |
30 | @admin.register(Metadata)
31 | class MetadataAdmin(admin.ModelAdmin):
32 |
33 | ordering = ('-retrieved', '-created', '-uploaded')
34 | list_display = ('uuid', 'key', 'retrieved', 'uploaded', 'created', 'site')
35 | readonly_fields = ('uuid', 'created', 'retrieved')
36 | search_fields = ('uuid', 'media__uuid', 'key')
37 |
38 |
39 | @admin.register(MetadataFormat)
40 | class MetadataFormatAdmin(admin.ModelAdmin):
41 |
42 | ordering = ('site', 'key', 'number')
43 | list_display = ('uuid', 'key', 'site', 'number', 'metadata')
44 | readonly_fields = ('uuid', 'metadata', 'site', 'key', 'number')
45 | search_fields = ('uuid', 'metadata__uuid', 'metadata__media__uuid', 'key')
46 |
47 |
48 | @admin.register(MediaServer)
49 | class MediaServerAdmin(admin.ModelAdmin):
50 |
51 | ordering = ('host', 'port')
52 | list_display = ('pk', 'server_type', 'host', 'port', 'use_https', 'verify_https')
53 | search_fields = ('host',)
54 |
--------------------------------------------------------------------------------
/tubesync/sync/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class SyncConfig(AppConfig):
5 |
6 | name = 'sync'
7 |
--------------------------------------------------------------------------------
/tubesync/sync/management/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/sync/management/__init__.py
--------------------------------------------------------------------------------
/tubesync/sync/management/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/sync/management/commands/__init__.py
--------------------------------------------------------------------------------
/tubesync/sync/management/commands/delete-source.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from django.core.management.base import BaseCommand, CommandError
3 | from django.db.transaction import atomic
4 | from django.utils.translation import gettext_lazy as _
5 | from common.logger import log
6 | from sync.models import Source
7 | from sync.tasks import schedule_media_servers_update
8 |
9 |
10 | class Command(BaseCommand):
11 |
12 | help = 'Deletes a source by UUID'
13 |
14 | def add_arguments(self, parser):
15 | parser.add_argument('--source', action='store', required=True, help=_('Source UUID'))
16 |
17 | def handle(self, *args, **options):
18 | source_uuid_str = options.get('source', '')
19 | try:
20 | source_uuid = uuid.UUID(source_uuid_str)
21 | except Exception as e:
22 | raise CommandError(f'Failed to parse source UUID: {e}')
23 | log.info(f'Deleting source with UUID: {source_uuid}')
24 | # Fetch the source by UUID
25 | try:
26 | source = Source.objects.get(uuid=source_uuid)
27 | except Source.DoesNotExist:
28 | raise CommandError(f'Source does not exist with '
29 | f'UUID: {source_uuid}')
30 | # Reconfigure the source to not update the disk or media servers
31 | with atomic(durable=True):
32 | source.deactivate()
33 | # Delete the source, triggering pre-delete signals for each media item
34 | log.info(f'Found source with UUID "{source.uuid}" with name '
35 | f'"{source.name}" and deleting it, this may take some time!')
36 | log.info(f'Source directory: {source.directory_path}')
37 | with atomic(durable=True):
38 | source.delete()
39 | # Update any media servers
40 | schedule_media_servers_update()
41 | # All done
42 | log.info('Done')
43 |
--------------------------------------------------------------------------------
/tubesync/sync/management/commands/list-sources.py:
--------------------------------------------------------------------------------
1 | from django.core.management.base import BaseCommand, CommandError # noqa
2 | from common.logger import log
3 | from sync.models import Source
4 |
5 |
6 | class Command(BaseCommand):
7 |
8 | help = ('Lists sources')
9 |
10 | def handle(self, *args, **options):
11 | log.info('Listing sources...')
12 | for source in Source.objects.all():
13 | log.info(f' - {source.uuid}: {source.name}')
14 | log.info('Done')
15 |
--------------------------------------------------------------------------------
/tubesync/sync/management/commands/reset-metadata.py:
--------------------------------------------------------------------------------
1 | from django.core.management.base import BaseCommand
2 | from common.utils import django_queryset_generator as qs_gen
3 | from sync.models import Media, Metadata
4 |
5 |
6 | from common.logger import log
7 |
8 |
9 | class Command(BaseCommand):
10 |
11 | help = 'Resets all media item metadata'
12 |
13 | def handle(self, *args, **options):
14 | log.info('Resetting all media metadata...')
15 | # Delete all metadata
16 | Metadata.objects.all().delete()
17 | # Trigger the save signal on each media item
18 | for media in qs_gen(Media.objects.filter(metadata__isnull=False)):
19 | media.metadata_clear(save=True)
20 | log.info('Done')
21 |
--------------------------------------------------------------------------------
/tubesync/sync/management/commands/reset-tasks.py:
--------------------------------------------------------------------------------
1 | from django.core.management.base import BaseCommand, CommandError # noqa
2 | from django.db.transaction import atomic
3 | from django.utils.translation import gettext_lazy as _
4 | from background_task.models import Task
5 | from common.logger import log
6 | from sync.models import Source
7 | from sync.tasks import index_source_task, check_source_directory_exists
8 |
9 |
10 | class Command(BaseCommand):
11 |
12 | help = 'Resets all tasks'
13 |
14 | def handle(self, *args, **options):
15 | log.info('Resettings all tasks...')
16 | with atomic(durable=True):
17 | # Delete all tasks
18 | Task.objects.all().delete()
19 | # Iter all sources, creating new tasks
20 | for source in Source.objects.all():
21 | verbose_name = _('Check download directory exists for source "{}"')
22 | check_source_directory_exists(
23 | str(source.pk),
24 | verbose_name=verbose_name.format(source.name),
25 | )
26 | # Recreate the initial indexing task
27 | log.info(f'Resetting tasks for source: {source}')
28 | verbose_name = _('Index media from source "{}"')
29 | index_source_task(
30 | str(source.pk),
31 | repeat=source.index_schedule,
32 | schedule=source.task_run_at_dt,
33 | verbose_name=verbose_name.format(source.name),
34 | )
35 | # This also chains down to call each Media objects .save() as well
36 | source.save()
37 |
38 | log.info('Done')
39 |
--------------------------------------------------------------------------------
/tubesync/sync/management/commands/sync-missing-metadata.py:
--------------------------------------------------------------------------------
1 | from shutil import copyfile
2 | from django.core.management.base import BaseCommand, CommandError # noqa
3 | from django.db.models import Q
4 | from common.logger import log
5 | from sync.models import Source, Media
6 | from sync.utils import write_text_file
7 |
8 |
9 | class Command(BaseCommand):
10 |
11 | help = 'Syncs missing metadata (such as nfo files) if source settings are updated'
12 |
13 | def handle(self, *args, **options):
14 | log.info('Syncing missing metadata...')
15 | sources = Source.objects.filter(Q(copy_thumbnails=True) | Q(write_nfo=True))
16 | for source in sources.order_by('name'):
17 | log.info(f'Finding media for source: {source}')
18 | for item in Media.objects.filter(source=source, downloaded=True):
19 | log.info(f'Checking media for missing metadata: {source} / {item}')
20 | thumbpath = item.thumbpath
21 | if not thumbpath.is_file():
22 | if item.thumb:
23 | log.info(f'Copying missing thumbnail from: {item.thumb.path} '
24 | f'to: {thumbpath}')
25 | copyfile(item.thumb.path, thumbpath)
26 | else:
27 | log.error(f'Tried to copy missing thumbnail for {item} but '
28 | f'the thumbnail has not been downloaded')
29 | nfopath = item.nfopath
30 | if not nfopath.is_file():
31 | log.info(f'Writing missing NFO file: {nfopath}')
32 | write_text_file(nfopath, item.nfoxml)
33 | log.info('Done')
34 |
--------------------------------------------------------------------------------
/tubesync/sync/management/commands/youtube-dl-info.py:
--------------------------------------------------------------------------------
1 | import json
2 | from django.core.management.base import BaseCommand, CommandError # noqa
3 | from sync.youtube import get_media_info
4 | from common.json import JSONEncoder
5 |
6 |
7 | class Command(BaseCommand):
8 |
9 | help = 'Displays information obtained by youtube-dl in JSON to the console'
10 |
11 | def add_arguments(self, parser):
12 | parser.add_argument('url', type=str)
13 |
14 | def handle(self, *args, **options):
15 | url = options['url']
16 | self.stdout.write(f'Showing information for URL: {url}')
17 | info = get_media_info(url)
18 | d = json.dumps(info, indent=4, sort_keys=True, cls=JSONEncoder)
19 | self.stdout.write(d)
20 | self.stdout.write('Done')
21 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0002_auto_20201213_0817.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.4 on 2020-12-13 08:17
2 |
3 | import django.core.files.storage
4 | from django.db import migrations, models
5 | import sync.models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('sync', '0001_initial'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name='media',
17 | name='media_file',
18 | field=models.FileField(blank=True, help_text='Media file', max_length=200, null=True, storage=django.core.files.storage.FileSystemStorage(location='/downloads'), upload_to=sync.models.get_media_file_path, verbose_name='media file'),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0003_source_copy_thumbnails.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.4 on 2020-12-18 01:34
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('sync', '0002_auto_20201213_0817'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='source',
15 | name='copy_thumbnails',
16 | field=models.BooleanField(default=False, help_text='Copy thumbnails with the media, these may be detected and used by some media servers', verbose_name='copy thumbnails'),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0004_source_media_format.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.4 on 2020-12-18 01:55
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('sync', '0003_source_copy_thumbnails'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='source',
15 | name='media_format',
16 | field=models.CharField(default='{yyyymmdd}_{source}_{title}_{key}_{format}.{ext}', help_text='File format to use for saving files', max_length=200, verbose_name='media format'),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0005_auto_20201219_0312.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.4 on 2020-12-19 03:12
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('sync', '0004_source_media_format'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='source',
15 | name='source_type',
16 | field=models.CharField(choices=[('c', 'YouTube channel'), ('i', 'YouTube channel by ID'), ('p', 'YouTube playlist')], db_index=True, default='c', help_text='Source type', max_length=1, verbose_name='source type'),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0006_source_write_nfo.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.4 on 2020-12-19 03:12
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('sync', '0005_auto_20201219_0312'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='source',
15 | name='write_nfo',
16 | field=models.BooleanField(default=False, help_text='Write an NFO file with the media, these may be detected and used by some media servers', verbose_name='write nfo'),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0007_auto_20201219_0645.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.4 on 2020-12-19 06:45
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('sync', '0006_source_write_nfo'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='source',
15 | name='write_nfo',
16 | field=models.BooleanField(default=False, help_text='Write an NFO file in XML with the media info, these may be detected and used by some media servers', verbose_name='write nfo'),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0008_source_download_cap.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.4 on 2020-12-19 06:59
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('sync', '0007_auto_20201219_0645'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='source',
15 | name='download_cap',
16 | field=models.IntegerField(choices=[(0, 'No cap'), (604800, '1 week (7 days)'), (2592000, '1 month (30 days)'), (7776000, '3 months (90 days)'), (15552000, '6 months (180 days)'), (31536000, '1 year (365 days)'), (63072000, '2 years (730 days)'), (94608000, '3 years (1095 days)'), (157680000, '5 years (1825 days)'), (315360000, '10 years (3650 days)')], default=0, help_text='Do not download media older than this capped date', verbose_name='download cap'),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0009_auto_20210218_0442.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.6 on 2021-02-18 04:42
2 |
3 | import django.core.files.storage
4 | from django.db import migrations, models
5 | import sync.models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('sync', '0008_source_download_cap'),
12 | ]
13 |
14 | operations = [
15 | migrations.AddField(
16 | model_name='source',
17 | name='download_media',
18 | field=models.BooleanField(default=True, help_text='Download media from this source, if not selected the source will only be indexed', verbose_name='download media'),
19 | ),
20 | migrations.AlterField(
21 | model_name='media',
22 | name='media_file',
23 | field=models.FileField(blank=True, help_text='Media file', max_length=200, null=True, storage=django.core.files.storage.FileSystemStorage(location='/home/meeb/Repos/github.com/meeb/tubesync/tubesync/downloads'), upload_to=sync.models.get_media_file_path, verbose_name='media file'),
24 | ),
25 | migrations.AlterField(
26 | model_name='source',
27 | name='media_format',
28 | field=models.CharField(default='{yyyymmdd}_{source}_{title}_{key}_{format}.{ext}', help_text='File format to use for saving files, detailed options at bottom of page.', max_length=200, verbose_name='media format'),
29 | ),
30 | ]
31 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0010_auto_20210924_0554.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.7 on 2021-09-24 05:54
2 |
3 | import django.core.files.storage
4 | from django.db import migrations, models
5 | import sync.models
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('sync', '0009_auto_20210218_0442'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name='media',
17 | name='media_file',
18 | field=models.FileField(blank=True, help_text='Media file', max_length=255, null=True, storage=django.core.files.storage.FileSystemStorage(location='/home/meeb/Repos/github.com/meeb/tubesync/tubesync/downloads'), upload_to=sync.models.get_media_file_path, verbose_name='media file'),
19 | ),
20 | migrations.AlterField(
21 | model_name='source',
22 | name='index_schedule',
23 | field=models.IntegerField(choices=[(3600, 'Every hour'), (7200, 'Every 2 hours'), (10800, 'Every 3 hours'), (14400, 'Every 4 hours'), (18000, 'Every 5 hours'), (21600, 'Every 6 hours'), (43200, 'Every 12 hours'), (86400, 'Every 24 hours'), (259200, 'Every 3 days'), (604800, 'Every 7 days'), (0, 'Never')], db_index=True, default=86400, help_text='Schedule of how often to index the source for new media', verbose_name='index schedule'),
24 | ),
25 | migrations.AlterField(
26 | model_name='source',
27 | name='media_format',
28 | field=models.CharField(default='{yyyy_mm_dd}_{source}_{title}_{key}_{format}.{ext}', help_text='File format to use for saving files, detailed options at bottom of page.', max_length=200, verbose_name='media format'),
29 | ),
30 | ]
31 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0011_auto_20220201_1654.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.11 on 2022-02-01 16:54
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('sync', '0010_auto_20210924_0554'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='source',
15 | name='write_json',
16 | field=models.BooleanField(
17 | default=False, help_text='Write a JSON file with the media info, these may be detected and used by some media servers', verbose_name='write json'),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0012_alter_media_downloaded_format.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.12 on 2022-04-06 06:19
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('sync', '0011_auto_20220201_1654'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='media',
15 | name='downloaded_format',
16 | field=models.CharField(blank=True, help_text='Video format (resolution) of the downloaded media', max_length=30, null=True, verbose_name='downloaded format'),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0013_fix_elative_media_file.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.12 on 2022-04-06 06:19
2 |
3 | from django.conf import settings
4 | from django.db import migrations
5 |
6 |
7 | def fix_media_file(apps, schema_editor):
8 | Media = apps.get_model('sync', 'Media')
9 | for media in Media.objects.filter(downloaded=True):
10 | download_dir = str(settings.DOWNLOAD_ROOT)
11 |
12 | if media.media_file.name.startswith(download_dir):
13 | media.media_file.name = media.media_file.name[len(download_dir) + 1:]
14 | media.save()
15 |
16 |
17 | class Migration(migrations.Migration):
18 |
19 | dependencies = [
20 | ('sync', '0012_alter_media_downloaded_format'),
21 | ]
22 |
23 | operations = [
24 | migrations.RunPython(fix_media_file)
25 | ]
26 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0014_alter_media_media_file.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.15 on 2022-12-28 20:33
2 |
3 | import django.core.files.storage
4 | from django.conf import settings
5 | from django.db import migrations, models
6 | import sync.models
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | dependencies = [
12 | ('sync', '0013_fix_elative_media_file'),
13 | ]
14 |
15 | operations = [
16 | migrations.AlterField(
17 | model_name='media',
18 | name='media_file',
19 | field=models.FileField(blank=True, help_text='Media file', max_length=255, null=True, storage=django.core.files.storage.FileSystemStorage(base_url='/media-data/', location=str(settings.DOWNLOAD_ROOT)), upload_to=sync.models.get_media_file_path, verbose_name='media file'),
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0015_auto_20230213_0603.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.17 on 2023-02-13 06:03
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('sync', '0014_alter_media_media_file'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='media',
15 | name='manual_skip',
16 | field=models.BooleanField(db_index=True, default=False, help_text='Media marked as "skipped", won\' be downloaded', verbose_name='manual_skip'),
17 | ),
18 | migrations.AlterField(
19 | model_name='media',
20 | name='skip',
21 | field=models.BooleanField(db_index=True, default=False, help_text='INTERNAL FLAG - Media will be skipped and not downloaded', verbose_name='skip'),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0016_auto_20230214_2052.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.18 on 2023-02-14 20:52
2 |
3 | from django.db import migrations, models
4 | import sync.fields
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('sync', '0015_auto_20230213_0603'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='source',
16 | name='embed_metadata',
17 | field=models.BooleanField(default=False, help_text='Embed metadata from source into file', verbose_name='embed metadata'),
18 | ),
19 | migrations.AddField(
20 | model_name='source',
21 | name='embed_thumbnail',
22 | field=models.BooleanField(default=False, help_text='Embed thumbnail into the file', verbose_name='embed thumbnail'),
23 | ),
24 | migrations.AddField(
25 | model_name='source',
26 | name='enable_sponsorblock',
27 | field=models.BooleanField(default=True, help_text='Use SponsorBlock?', verbose_name='enable sponsorblock'),
28 | ),
29 | migrations.AddField(
30 | model_name='source',
31 | name='sponsorblock_categories',
32 | field=sync.fields.CommaSepChoiceField(default='all', possible_choices=(('all', 'All'), ('sponsor', 'Sponsor'), ('intro', 'Intermission/Intro Animation'), ('outro', 'Endcards/Credits'), ('selfpromo', 'Unpaid/Self Promotion'), ('preview', 'Preview/Recap'), ('filler', 'Filler Tangent'), ('interaction', 'Interaction Reminder'), ('music_offtopic', 'Non-Music Section'))),
33 | ),
34 | ]
35 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0017_alter_source_sponsorblock_categories.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.18 on 2023-02-20 02:23
2 |
3 | from django.db import migrations
4 | import sync.fields
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('sync', '0016_auto_20230214_2052'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='source',
16 | name='sponsorblock_categories',
17 | field=sync.fields.CommaSepChoiceField(default='all', help_text='Select the sponsorblocks you want to enforce', separator=''),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0018_source_subtitles.py:
--------------------------------------------------------------------------------
1 | # Generated by pac
2 |
3 | from django.db import migrations, models
4 |
5 | class Migration(migrations.Migration):
6 |
7 | dependencies = [
8 | ('sync', '0017_alter_source_sponsorblock_categories'),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name='source',
14 | name='write_subtitles',
15 | field=models.BooleanField(default=False, help_text='Download video subtitles', verbose_name='write subtitles'),
16 | ),
17 | migrations.AddField(
18 | model_name='source',
19 | name='auto_subtitles',
20 | field=models.BooleanField(default=False, help_text='Accept auto-generated subtitles', verbose_name='accept auto subtitles'),
21 | ),
22 | migrations.AddField(
23 | model_name='source',
24 | name='sub_langs',
25 | field=models.CharField(default='en', help_text='List of subtitles langs to download comma-separated. Example: en,fr',max_length=30),
26 | ),
27 | ]
28 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0019_add_delete_removed_media.py:
--------------------------------------------------------------------------------
1 | # Generated by pac
2 |
3 | from django.db import migrations, models
4 |
5 | class Migration(migrations.Migration):
6 |
7 | dependencies = [
8 | ('sync', '0018_source_subtitles'),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name='source',
14 | name='delete_removed_media',
15 | field=models.BooleanField(default=False, help_text='Delete media that is no longer on this playlist', verbose_name='delete removed media'),
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0020_auto_20231024_1825.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.22 on 2023-10-24 17:25
2 |
3 | import django.core.validators
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('sync', '0019_add_delete_removed_media'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='source',
16 | name='filter_text',
17 | field=models.CharField(blank=True, default='', help_text='Regex compatible filter string for video titles', max_length=100, verbose_name='filter string'),
18 | ),
19 | migrations.AlterField(
20 | model_name='source',
21 | name='auto_subtitles',
22 | field=models.BooleanField(default=False, help_text='Accept auto-generated subtitles', verbose_name='accept auto-generated subs'),
23 | ),
24 | migrations.AlterField(
25 | model_name='source',
26 | name='sub_langs',
27 | field=models.CharField(default='en', help_text='List of subtitles langs to download, comma-separated. Example: en,fr or all,-fr,-live_chat', max_length=30, validators=[django.core.validators.RegexValidator(message='Subtitle langs must be a comma-separated list of langs. example: en,fr or all,-fr,-live_chat', regex='^(\\-?[\\_\\.a-zA-Z]+,)*(\\-?[\\_\\.a-zA-Z]+){1}$')], verbose_name='subs langs'),
28 | ),
29 | ]
30 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0021_source_copy_channel_images.py:
--------------------------------------------------------------------------------
1 | # Generated by nothing. Done manually by InterN0te on 2023-12-10 16:36
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('sync', '0020_auto_20231024_1825'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='source',
15 | name='copy_channel_images',
16 | field=models.BooleanField(default=False, help_text='Copy channel banner and avatar. These may be detected and used by some media servers', verbose_name='copy channel images'),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0022_add_delete_files_on_disk.py:
--------------------------------------------------------------------------------
1 | # Generated by pac
2 |
3 | from django.db import migrations, models
4 |
5 | class Migration(migrations.Migration):
6 |
7 | dependencies = [
8 | ('sync', '0021_source_copy_channel_images'),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name='source',
14 | name='delete_files_on_disk',
15 | field=models.BooleanField(default=False, help_text='Delete files on disk when they are removed from TubeSync', verbose_name='delete files on disk'),
16 | ),
17 | ]
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0023_media_duration_filter.py:
--------------------------------------------------------------------------------
1 | from django.db import migrations, models
2 |
3 |
4 | class Migration(migrations.Migration):
5 | dependencies = [
6 | ("sync", "0022_add_delete_files_on_disk"),
7 | ]
8 |
9 | operations = [
10 | migrations.AddField(
11 | model_name="media",
12 | name="title",
13 | field=models.CharField(
14 | verbose_name="title",
15 | max_length=100,
16 | blank=True,
17 | null=False,
18 | default="",
19 | help_text="Video title",
20 | ),
21 | ),
22 | migrations.AddField(
23 | model_name="media",
24 | name="duration",
25 | field=models.PositiveIntegerField(
26 | verbose_name="duration",
27 | blank=True,
28 | null=True,
29 | help_text="Duration of media in seconds",
30 | ),
31 | ),
32 | migrations.AddField(
33 | model_name="source",
34 | name="filter_seconds",
35 | field=models.PositiveIntegerField(
36 | verbose_name="filter seconds",
37 | blank=True,
38 | null=True,
39 | help_text="Filter Media based on Min/Max duration. Leave blank or 0 to disable filtering",
40 | ),
41 | ),
42 | migrations.AddField(
43 | model_name="source",
44 | name="filter_seconds_min",
45 | field=models.BooleanField(
46 | verbose_name="filter seconds min/max",
47 | choices=[(True, "Minimum Length"), (False, "Maximum Length")],
48 | default=True,
49 | help_text="When Filter Seconds is > 0, do we skip on minimum (video shorter than limit) or maximum ("
50 | "video greater than maximum) video duration",
51 | ),
52 | ),
53 | migrations.AddField(
54 | model_name="source",
55 | name="filter_text_invert",
56 | field=models.BooleanField(
57 | verbose_name="invert filter text matching",
58 | default=False,
59 | help_text="Invert filter string regex match, skip any matching titles when selected",
60 | ),
61 | ),
62 | ]
63 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0024_auto_20240717_1535.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.25 on 2024-07-17 15:35
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('sync', '0023_media_duration_filter'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='media',
15 | name='manual_skip',
16 | field=models.BooleanField(db_index=True, default=False, help_text='Media marked as "skipped", won\'t be downloaded', verbose_name='manual_skip'),
17 | ),
18 | migrations.AlterField(
19 | model_name='media',
20 | name='title',
21 | field=models.CharField(blank=True, default='', help_text='Video title', max_length=200, verbose_name='title'),
22 | ),
23 | migrations.AlterField(
24 | model_name='source',
25 | name='filter_text',
26 | field=models.CharField(blank=True, default='', help_text='Regex compatible filter string for video titles', max_length=200, verbose_name='filter string'),
27 | ),
28 | ]
29 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0025_add_video_type_support.py:
--------------------------------------------------------------------------------
1 | from django.db import migrations, models
2 |
3 | class Migration(migrations.Migration):
4 |
5 | dependencies = [
6 | ('sync', '0024_auto_20240717_1535'),
7 | ]
8 |
9 | operations = [
10 | migrations.AddField(
11 | model_name='source',
12 | name='index_videos',
13 | field=models.BooleanField(default=True, help_text='Index video media from this source', verbose_name='index videos'),
14 | ),
15 | migrations.AddField(
16 | model_name='source',
17 | name='index_streams',
18 | field=models.BooleanField(default=False, help_text='Index live stream media from this source', verbose_name='index streams'),
19 | ),
20 | ]
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0026_alter_source_sub_langs.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.25 on 2024-12-11 12:43
2 |
3 | import django.core.validators
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('sync', '0025_add_video_type_support'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='source',
16 | name='sub_langs',
17 | field=models.CharField(default='en', help_text='List of subtitles langs to download, comma-separated. Example: en,fr or all,-fr,-live_chat', max_length=30, validators=[django.core.validators.RegexValidator(message='Subtitle langs must be a comma-separated list of langs. example: en,fr or all,-fr,-live_chat', regex='^(\\-?[\\_\\.a-zA-Z-]+(,|$))+')], verbose_name='subs langs'),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0027_alter_source_sponsorblock_categories.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.25 on 2025-01-29 06:14
2 |
3 | from django.db import migrations
4 | import sync.fields
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('sync', '0026_alter_source_sub_langs'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='source',
16 | name='sponsorblock_categories',
17 | field=sync.fields.CommaSepChoiceField(all_choice='all', all_label='(All Categories)', allow_all=True, default='all', help_text='Select the SponsorBlock categories that you wish to be removed from downloaded videos.', max_length=128, possible_choices=[('sponsor', 'Sponsor'), ('intro', 'Intermission/Intro Animation'), ('outro', 'Endcards/Credits'), ('selfpromo', 'Unpaid/Self Promotion'), ('preview', 'Preview/Recap'), ('filler', 'Filler Tangent'), ('interaction', 'Interaction Reminder'), ('music_offtopic', 'Non-Music Section')], verbose_name=''),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0028_alter_source_source_resolution.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.25 on 2025-02-12 18:31
2 |
3 | from django.db import migrations, models
4 |
5 | class Migration(migrations.Migration):
6 | dependencies = [
7 | ('sync', '0027_alter_source_sponsorblock_categories'),
8 | ]
9 |
10 | operations = [
11 | migrations.AlterField(
12 | model_name='source',
13 | name='source_resolution',
14 | field=models.CharField(choices=[('audio', 'Audio only'), ('360p', '360p (SD)'), ('480p', '480p (SD)'), ('720p', '720p (HD)'), ('1080p', '1080p (Full HD)'), ('1440p', '1440p (2K)'), ('2160p', '2160p (4K)'), ('4320p', '4320p (8K)')], db_index=True, default='1080p', help_text='Source resolution, desired video resolution to download', max_length=8, verbose_name='source resolution'),
15 | ),
16 | ]
17 |
18 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0029_alter_mediaserver_fields.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.25 on 2025-02-22 03:52
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('sync', '0028_alter_source_source_resolution'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='mediaserver',
15 | name='options',
16 | field=models.TextField(help_text='JSON encoded options for the media server', null=True, verbose_name='options'),
17 | ),
18 | migrations.AlterField(
19 | model_name='mediaserver',
20 | name='server_type',
21 | field=models.CharField(choices=[('j', 'Jellyfin'), ('p', 'Plex')], db_index=True, default='p', help_text='Server type', max_length=1, verbose_name='server type'),
22 | ),
23 | migrations.AlterField(
24 | model_name='mediaserver',
25 | name='use_https',
26 | field=models.BooleanField(default=False, help_text='Connect to the media server over HTTPS', verbose_name='use https'),
27 | ),
28 | migrations.AlterField(
29 | model_name='mediaserver',
30 | name='verify_https',
31 | field=models.BooleanField(default=True, help_text='If connecting over HTTPS, verify the SSL certificate is valid', verbose_name='verify https'),
32 | ),
33 | ]
34 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0030_alter_source_source_vcodec.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.1.8 on 2025-04-07 18:28
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('sync', '0029_alter_mediaserver_fields'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='source',
15 | name='source_vcodec',
16 | field=models.CharField(choices=[('AVC1', 'AVC1 (H.264)'), ('VP9', 'VP9'), ('AV1', 'AV1')], db_index=True, default='VP9', help_text='Source video codec, desired video encoding format to download (ignored if "resolution" is audio only)', max_length=8, verbose_name='source video codec'),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0032_metadata_transfer.py:
--------------------------------------------------------------------------------
1 | # Hand-crafted data migration
2 |
3 | from django.db import migrations
4 | from common.utils import django_queryset_generator as qs_gen
5 | from sync.models import Media
6 |
7 |
8 | def use_tables(apps, schema_editor):
9 | #Media = apps.get_model('sync', 'Media')
10 | qs = Media.objects.filter(metadata__isnull=False)
11 | for media in qs_gen(qs):
12 | media.save_to_metadata('migrated', True)
13 |
14 | def restore_metadata_column(apps, schema_editor):
15 | #Media = apps.get_model('sync', 'Media')
16 | qs = Media.objects.filter(metadata__isnull=False)
17 | for media in qs_gen(qs):
18 | metadata = media.loaded_metadata
19 | for key in {'migrated', '_using_table'}:
20 | metadata.pop(key, None)
21 | media.metadata = media.metadata_dumps(arg_dict=metadata)
22 | media.save()
23 |
24 |
25 | class Migration(migrations.Migration):
26 |
27 | dependencies = [
28 | ('sync', '0031_squashed_metadata_metadataformat'),
29 | ]
30 |
31 | operations = [
32 | migrations.RunPython(
33 | code=use_tables,
34 | reverse_code=restore_metadata_column,
35 | ),
36 | ]
37 |
38 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0033_alter_mediaserver_options_alter_source_source_acodec_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.1.9 on 2025-05-10 06:18
2 |
3 | import common.json
4 | from django.db import migrations, models
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('sync', '0032_metadata_transfer'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='mediaserver',
16 | name='options',
17 | field=models.JSONField(encoder=common.json.JSONEncoder, help_text='Options for the media server', null=True, verbose_name='options'),
18 | ),
19 | migrations.AlterField(
20 | model_name='source',
21 | name='source_acodec',
22 | field=models.CharField(choices=[('OPUS', 'OPUS'), ('MP4A', 'MP4A')], db_index=True, default='OPUS', help_text='Source audio codec, desired audio encoding format to download', max_length=8, verbose_name='source audio codec'),
23 | ),
24 | migrations.AlterField(
25 | model_name='source',
26 | name='source_vcodec',
27 | field=models.CharField(choices=[('AV1', 'AV1'), ('VP9', 'VP9'), ('AVC1', 'AVC1 (H.264)')], db_index=True, default='VP9', help_text='Source video codec, desired video encoding format to download (ignored if "resolution" is audio only)', max_length=8, verbose_name='source video codec'),
28 | ),
29 | ]
30 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/0034_source_target_schedule_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 5.2.1 on 2025-05-26 04:43
2 |
3 | import django.utils.timezone
4 | import sync.fields
5 | from django.db import migrations, models
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('sync', '0033_alter_mediaserver_options_alter_source_source_acodec_and_more'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='source',
16 | name='target_schedule',
17 | field=models.DateTimeField(
18 | blank=True, db_index=True, default=django.utils.timezone.now,
19 | help_text='Date and time when the task to index the source should begin',
20 | verbose_name='target schedule',
21 | ),
22 | ),
23 | migrations.AlterField(
24 | model_name='source',
25 | name='sponsorblock_categories',
26 | field=sync.fields.CommaSepChoiceField(
27 | all_choice='all', all_label='(All Categories)', allow_all=True, default='all',
28 | help_text='Select the SponsorBlock categories that you wish to be removed from downloaded videos.',
29 | max_length=128, possible_choices=[
30 | ('sponsor', 'Sponsor'), ('intro', 'Intermission/Intro Animation'), ('outro', 'Endcards/Credits'),
31 | ('selfpromo', 'Unpaid/Self Promotion'), ('preview', 'Preview/Recap'), ('filler', 'Filler Tangent'),
32 | ('interaction', 'Interaction Reminder'), ('music_offtopic', 'Non-Music Section'),
33 | ],
34 | verbose_name='removed categories',
35 | ),
36 | ),
37 | ]
38 |
39 |
--------------------------------------------------------------------------------
/tubesync/sync/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/sync/migrations/__init__.py
--------------------------------------------------------------------------------
/tubesync/sync/models/__init__.py:
--------------------------------------------------------------------------------
1 | # These are referenced from the migration files
2 |
3 | from ._migrations import (
4 | get_media_file_path,
5 | get_media_thumb_path,
6 | media_file_storage,
7 | )
8 |
9 | # The actual model classes
10 | # The order starts with independent classes
11 | # then the classes that depend on them follow.
12 |
13 | from .media_server import MediaServer
14 |
15 | from .source import Source
16 | from .media import Media
17 | from .metadata import Metadata
18 | from .metadata_format import MetadataFormat
19 |
20 | __all__ = [
21 | 'get_media_file_path', 'get_media_thumb_path',
22 | 'media_file_storage', 'MediaServer', 'Source',
23 | 'Media', 'Metadata', 'MetadataFormat',
24 | ]
25 |
26 |
--------------------------------------------------------------------------------
/tubesync/sync/models/_migrations.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from django.conf import settings
3 | from django.core.files.storage import FileSystemStorage
4 |
5 |
6 | media_file_storage = FileSystemStorage(location=str(settings.DOWNLOAD_ROOT), base_url='/media-data/')
7 |
8 |
9 | def get_media_file_path(instance, filename):
10 | return instance.filepath
11 |
12 |
13 | def get_media_thumb_path(instance, filename):
14 | # we don't want to use alternate names for thumb files
15 | if instance.thumb:
16 | instance.thumb.delete(save=False)
17 | fileid = str(instance.uuid).lower()
18 | filename = f'{fileid}.jpg'
19 | prefix = fileid[:2]
20 | return Path('thumbs') / prefix / filename
21 |
22 |
--------------------------------------------------------------------------------
/tubesync/sync/models/_private.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from ..choices import Val, YouTube_SourceType # noqa
3 |
4 |
5 | _srctype_dict = lambda n: dict(zip( YouTube_SourceType.values, (n,) * len(YouTube_SourceType.values) ))
6 |
7 |
8 | def _nfo_element(nfo, label, text, /, *, attrs={}, tail='\n', char=' ', indent=2):
9 | element = nfo.makeelement(label, attrs)
10 | element.text = text
11 | element.tail = tail + (char * indent)
12 | return element
13 |
14 | def directory_and_stem(arg_path, /, all_suffixes=False):
15 | filepath = Path(arg_path)
16 | stem = Path(filepath.stem)
17 | while all_suffixes and stem.suffixes and '' != stem.suffix:
18 | stem = Path(stem.stem)
19 | stem = str(stem)
20 | return (filepath.parent, stem,)
21 |
22 |
--------------------------------------------------------------------------------
/tubesync/sync/models/media_server.py:
--------------------------------------------------------------------------------
1 | from common.json import JSONEncoder
2 | from django import db
3 | from django.utils.translation import gettext_lazy as _
4 | from ..choices import Val, MediaServerType
5 |
6 |
7 | class MediaServer(db.models.Model):
8 | '''
9 | A remote media server, such as a Plex server.
10 | '''
11 |
12 | ICONS = {
13 | Val(MediaServerType.JELLYFIN): ' ',
14 | Val(MediaServerType.PLEX): ' ',
15 | }
16 | HANDLERS = MediaServerType.handlers_dict()
17 |
18 | server_type = db.models.CharField(
19 | _('server type'),
20 | max_length=1,
21 | db_index=True,
22 | choices=MediaServerType.choices,
23 | default=MediaServerType.PLEX,
24 | help_text=_('Server type'),
25 | )
26 | host = db.models.CharField(
27 | _('host'),
28 | db_index=True,
29 | max_length=200,
30 | help_text=_('Hostname or IP address of the media server'),
31 | )
32 | port = db.models.PositiveIntegerField(
33 | _('port'),
34 | db_index=True,
35 | help_text=_('Port number of the media server'),
36 | )
37 | use_https = db.models.BooleanField(
38 | _('use https'),
39 | default=False,
40 | help_text=_('Connect to the media server over HTTPS'),
41 | )
42 | verify_https = db.models.BooleanField(
43 | _('verify https'),
44 | default=True,
45 | help_text=_('If connecting over HTTPS, verify the SSL certificate is valid'),
46 | )
47 | options = db.models.JSONField(
48 | _('options'),
49 | encoder=JSONEncoder,
50 | blank=False,
51 | null=True,
52 | help_text=_('Options for the media server'),
53 | )
54 |
55 | def __str__(self):
56 | return f'{self.get_server_type_display()} server at {self.url}'
57 |
58 | class Meta:
59 | verbose_name = _('Media Server')
60 | verbose_name_plural = _('Media Servers')
61 | unique_together = (
62 | ('host', 'port'),
63 | )
64 |
65 | @property
66 | def url(self):
67 | scheme = 'https' if self.use_https else 'http'
68 | return f'{scheme}://{self.host.strip()}:{self.port}'
69 |
70 | @property
71 | def icon(self):
72 | return self.ICONS.get(self.server_type)
73 |
74 | @property
75 | def handler(self):
76 | handler_class = self.HANDLERS.get(self.server_type)
77 | return handler_class(self)
78 |
79 | def validate(self):
80 | return self.handler.validate()
81 |
82 | def update(self):
83 | return self.handler.update()
84 |
85 | def get_help_html(self):
86 | return self.handler.HELP
87 |
--------------------------------------------------------------------------------
/tubesync/sync/models/metadata_format.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from common.json import JSONEncoder
3 | from django import db
4 | from django.utils.translation import gettext_lazy as _
5 | from .metadata import Metadata
6 |
7 | class MetadataFormat(db.models.Model):
8 | '''
9 | A format from the Metadata for an indexed `Media` item.
10 | '''
11 | class Meta:
12 | db_table = f'{Metadata._meta.db_table}_format'
13 | verbose_name = _('Format from Media Metadata')
14 | verbose_name_plural = _('Formats from Media Metadata')
15 | unique_together = (
16 | ('metadata', 'site', 'key', 'number'),
17 | )
18 | ordering = ['site', 'key', 'number']
19 |
20 | uuid = db.models.UUIDField(
21 | _('uuid'),
22 | primary_key=True,
23 | editable=False,
24 | default=uuid.uuid4,
25 | help_text=_('UUID of the format'),
26 | )
27 | metadata = db.models.ForeignKey(
28 | Metadata,
29 | # on_delete=models.DO_NOTHING,
30 | on_delete=db.models.CASCADE,
31 | related_name='format',
32 | help_text=_('Metadata the format belongs to'),
33 | null=False,
34 | )
35 | site = db.models.CharField(
36 | _('site'),
37 | max_length=256,
38 | blank=True,
39 | db_index=True,
40 | null=False,
41 | default='Youtube',
42 | help_text=_('Site from which the format is available'),
43 | )
44 | key = db.models.CharField(
45 | _('key'),
46 | max_length=256,
47 | blank=True,
48 | db_index=True,
49 | null=False,
50 | default='',
51 | help_text=_('Media identifier at the site from which this format is available'),
52 | )
53 | number = db.models.PositiveIntegerField(
54 | _('number'),
55 | blank=False,
56 | null=False,
57 | help_text=_('Ordering number for this format'),
58 | )
59 | value = db.models.JSONField(
60 | _('value'),
61 | encoder=JSONEncoder,
62 | null=False,
63 | default=dict,
64 | help_text=_('JSON metadata format object'),
65 | )
66 |
67 |
68 | def __str__(self):
69 | template = '#{:n} "{}" from {}: {}'
70 | return template.format(
71 | self.number,
72 | self.key,
73 | self.site,
74 | self.value.get('format') or self.value.get('format_id'),
75 | )
76 |
--------------------------------------------------------------------------------
/tubesync/sync/overrides/custom_filter.py:
--------------------------------------------------------------------------------
1 | """
2 | This file can be overridden with a docker volume to allow specifying a custom filter function to call. This allows
3 | for higher order filtering for those that really want advanced controls, without exposing the web interface to
4 | potential RCE issues.
5 |
6 | You are simply provided with an instance of Media, and need to return True to skip it, or False to allow it to be
7 | downloaded.
8 |
9 | To use this custom file, download this file and modify the function to do your check for skipping a media item.
10 | Then use docker volumes to override /app/sync/overrides/ with your custom file (it must be called
11 | `custom_filter.py`)
12 | e.g. your `docker run` could have `-v /some/directory/tubesync-overrides:/app/sync/overrides`
13 | or docker-compose could have
14 | volumes:
15 | - /some/directory/tubesync-overrides:/app/sync/overrides
16 |
17 |
18 | The logic is that if any condition marks an item to be skipped, it will be skipped. To save resources, this
19 | custom filter won't be called if any other filter as already marked it to be skipped
20 | """
21 |
22 | from ..models import Media
23 | from common.logger import log
24 |
25 |
26 | def filter_custom(instance: Media) -> bool:
27 | # Return True to skip, or False to allow the media item to be downloaded
28 |
29 | # Put your conditional logic here
30 | if False:
31 | # It's in your best interest to log when skipping, so you can look at the logs and see why your media isn't
32 | # downloading
33 | log.info(
34 | f"Media: {instance.source} / {instance} has met some custom condition. Marking to be skipped"
35 | )
36 | return True
37 |
38 | # Return False if we aren't skipping the media
39 | return False
40 |
--------------------------------------------------------------------------------
/tubesync/sync/templates/sync/_mediaformatvars.html:
--------------------------------------------------------------------------------
1 | Available media name variables
2 |
3 |
4 |
5 | Name
6 | Description
7 | Output example
8 |
9 |
10 |
11 |
12 | {yyyymmdd}
13 | Media publish date in YYYYMMDD
14 | 20210131
15 |
16 |
17 | {yyyy_mm_dd}
18 | Media publish date in YYYY-MM-DD
19 | 2021-01-31
20 |
21 |
22 | {yyyy}
23 | Media publish year in YYYY
24 | 2021
25 |
26 |
27 | {mm}
28 | Media publish month in MM
29 | 01
30 |
31 |
32 | {dd}
33 | Media publish day in DD
34 | 31
35 |
36 |
37 | {source}
38 | Lower case source name, max 80 chars
39 | my-source
40 |
41 |
42 | {source_full}
43 | Full source name
44 | My Source
45 |
46 |
47 | {uploader}
48 | Uploader name
49 | Some Channel Name
50 |
51 |
52 | {title}
53 | Lower case media title, max 80 chars
54 | my-video
55 |
56 |
57 | {title_full}
58 | Full media title
59 | My Video
60 |
61 |
62 | {key}
63 | Media unique key or video ID
64 | SoMeUnIqUeId
65 |
66 |
67 | {format}
68 | Media format string
69 | 720p-avc1-mp4a
70 |
71 |
72 | {playlist_title}
73 | Playlist title of media, if it's in a playlist
74 | Some Playlist
75 |
76 |
77 | {video_order}
78 | Episode order in playlist, if in playlist (can cause issues if playlist is changed after adding)
79 | 01
80 |
81 |
82 | {ext}
83 | File extension
84 | mkv
85 |
86 |
87 | {resolution}
88 | Resolution
89 | 720p
90 |
91 |
92 | {height}
93 | Media height in pixels
94 | 720
95 |
96 |
97 | {width}
98 | Media width in pixels
99 | 1280
100 |
101 |
102 | {vcodec}
103 | Media video codec
104 | avc1
105 |
106 |
107 | {acodec}
108 | Media audio codec
109 | opus
110 |
111 |
112 | {fps}
113 | Media fps
114 | 60fps
115 |
116 |
117 | {flag_hdr}
118 | Media has HDR flag
119 | hdr
120 |
121 |
122 |
--------------------------------------------------------------------------------
/tubesync/sync/templates/sync/media-enable.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block headtitle %}Enable (unskip) media - {{ media }}{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
Enable (unskip) {{ media }}
9 |
10 | You can enable your previously skipped media {{ media }} . This
11 | will re-enable the media to be downloaded.
12 |
13 |
14 |
15 |
26 | {% endblock %}
27 |
--------------------------------------------------------------------------------
/tubesync/sync/templates/sync/media-redownload.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block headtitle %}Redownload media - {{ media }}{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
Redownload media {{ media }}
9 |
10 | You can delete the downloaded file for your media {{ media }} and
11 | schedule it to be redownloaded. You might want to use this if you moved the original
12 | file on disk and want to download it again, or, if you changed your source settings
13 | such as changed the desired resolution and want to redownload the media in a different
14 | format.
15 |
16 |
17 |
18 |
29 | {% endblock %}
30 |
--------------------------------------------------------------------------------
/tubesync/sync/templates/sync/media-skip.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block headtitle %}Skip media - {{ media }}{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
Delete and skip media {{ media }}
9 |
10 | You can delete the downloaded file for your media {{ media }} and
11 | mark it to never be downloaded. You might want to do this if you don't want a local
12 | copy of some media or want to skip a single video from a source.
13 |
14 |
15 |
16 |
27 | {% endblock %}
28 |
--------------------------------------------------------------------------------
/tubesync/sync/templates/sync/media.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}{% load static %}
2 |
3 | {% block headtitle %}Media{% if source %} - {{ source }}{% endif %}{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
Media
9 |
10 |
17 |
24 |
25 | {% include 'infobox.html' with message=message %}
26 |
27 | {% for m in media %}
28 |
59 | {% empty %}
60 |
61 |
62 | No media has been indexed{% if source %} that matches the specified source filter{% endif %}.
63 |
64 |
65 | {% endfor %}
66 |
67 | {% include 'pagination.html' with pagination=sources.paginator filter=source.pk show_skipped=show_skipped only_skipped=only_skipped%}
68 | {% endblock %}
69 |
--------------------------------------------------------------------------------
/tubesync/sync/templates/sync/mediaserver-add.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block headtitle %}Add a new {{ server_type_name }} media server{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
Add a {{ server_type_name }} media server
9 |
10 | You can use this form to add a new {{ server_type_name }} media server. All media
11 | servers added will be updated or refreshed every time some media is downloaded.
12 |
13 | {% if server_help %}{{ server_help|safe }}{% endif %}
14 |
15 |
16 |
27 | {% endblock %}
28 |
--------------------------------------------------------------------------------
/tubesync/sync/templates/sync/mediaserver-delete.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block headtitle %}Delete media server - {{ mediaserver }}{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
Delete {{ mediaserver }}
9 |
10 | Deleting a media server will stop it from being updated by TubeSync. This action
11 | is permanent. You will have to manually re-add the media server details again if
12 | you want to update it automatically in future again.
13 |
14 |
15 |
16 |
27 | {% endblock %}
28 |
--------------------------------------------------------------------------------
/tubesync/sync/templates/sync/mediaserver-update.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block headtitle %}Update media server - {{ mediaserver }}{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
Update {{ mediaserver }}
9 |
10 | You can use this form to update your media server details. The details will be
11 | validated when you save the form.
12 |
13 | {% if server_help %}{{ server_help|safe }}{% endif %}
14 |
15 |
16 |
27 | {% endblock %}
28 |
--------------------------------------------------------------------------------
/tubesync/sync/templates/sync/mediaserver.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block headtitle %}Media server - {{ mediaserver }}{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
{{ mediaserver.get_server_type_display }} server at {{ mediaserver.url }}
9 |
10 |
11 | {% include 'infobox.html' with message=message %}
12 |
13 |
14 |
15 |
16 | Type
17 | Type {{ mediaserver.get_server_type_display }}
18 |
19 |
20 | Location
21 | Location {{ mediaserver.url }}
22 |
23 |
24 | Use HTTPS
25 | Use HTTPS {% if mediaserver.use_https %} {% else %} {% endif %}
26 |
27 |
28 | Verify HTTPS
29 | Verify HTTPS {% if mediaserver.verify_https %} {% else %} {% endif %}
30 |
31 | {% for name, value in mediaserver.options.items %}
32 |
33 | {{ name|title }}
34 | {{ name|title }} {% if name in private_options %}{{ value|truncatechars:6 }} (hidden){% else %}{{ value }}{% endif %}
35 |
36 | {% endfor %}
37 |
38 |
39 |
40 |
48 | {% endblock %}
49 |
--------------------------------------------------------------------------------
/tubesync/sync/templates/sync/mediaservers.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block headtitle %}Media servers{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
Media servers
9 |
10 | Media servers are services like Plex which you may be running on your network. If
11 | you add your media server TubeSync will notify your media server to rescan or
12 | refresh its libraries every time media is successfully downloaded. Currently,
13 | TubeSync only supports Jellyfin and Plex.
14 |
15 |
16 |
17 | {% include 'infobox.html' with message=message %}
18 | {% for mst in media_server_types %}
19 |
24 | {% endfor %}
25 |
38 | {% endblock %}
39 |
--------------------------------------------------------------------------------
/tubesync/sync/templates/sync/source-add.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block headtitle %}Add a new source{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
Add a source
9 |
10 | You can use this form to add a new source. A source is what's polled on regular
11 | basis to find new media to download, such as a channel or playlist.
12 |
13 |
14 |
15 |
26 |
27 |
28 | {% include 'sync/_mediaformatvars.html' %}
29 |
30 |
31 | {% endblock %}
32 |
--------------------------------------------------------------------------------
/tubesync/sync/templates/sync/source-delete.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block headtitle %}Delete source - {{ source.name }}{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
Delete source {{ source.name }}
9 |
10 | Are you sure you want to delete this source? Deleting a source is permanent.
11 | By default, deleting a source does not delete any saved media files. You can
12 | tick the "also delete downloaded media" checkbox to also remove directory {{ source.directory_path }}
13 | when you delete the source. Deleting a source cannot be undone.
14 |
15 |
16 |
17 |
28 | {% endblock %}
29 |
--------------------------------------------------------------------------------
/tubesync/sync/templates/sync/source-update.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block headtitle %}Update source - {{ source.name }}{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
Update source {{ source.name }}
9 |
10 | You can use this form to update your source. A source is what's polled on regular
11 | basis to find new media to download, such as a channel or playlist. Any changes
12 | to a source will only apply to new media and will not update media previously
13 | downloaded.
14 |
15 |
16 |
17 |
28 |
29 |
30 | {% include 'sync/_mediaformatvars.html' %}
31 |
32 |
33 | {% endblock %}
34 |
--------------------------------------------------------------------------------
/tubesync/sync/templates/sync/source-validate.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block headtitle %}Validate a {{ help_item }}{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
Validate a {{ help_item }}
9 |
{{ help_text|safe }}
10 |
Example: {{ help_example }}
11 |
12 |
13 |
24 | {% endblock %}
25 |
--------------------------------------------------------------------------------
/tubesync/sync/templates/sync/sources.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block headtitle %}Sources{% endblock %}
4 |
5 | {% block content %}
6 |
11 | {% include 'infobox.html' with message=message %}
12 |
23 |
45 | {% include 'pagination.html' with pagination=sources.paginator %}
46 | {% endblock %}
47 |
--------------------------------------------------------------------------------
/tubesync/sync/templates/sync/task-schedule.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block headtitle %}Schedule task{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
Schedule task
9 |
10 | If you don't want to wait for the existing schedule to be triggered,
11 | you can use this to change when the task will be scheduled to run.
12 | It is not guaranteed to run at any exact time, because when a task
13 | requests to run and when a slot to execute it, in the appropriate
14 | queue and with the priority level assigned, is dependent on how long
15 | other tasks are taking to complete the assigned work.
16 |
17 |
18 | This will change the time that the task is requesting to be the
19 | current time, or a chosen future time.
20 |
21 |
22 |
23 |
34 | {% endblock %}
35 |
--------------------------------------------------------------------------------
/tubesync/sync/templates/sync/tasks-completed.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block headtitle %}Tasks - Completed{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
Completed tasks
9 |
10 |
11 | {% include 'infobox.html' with message=message %}
12 |
13 |
14 |
15 | {% for task in tasks %}
16 |
17 | {% if task.has_error %}
18 |
19 | {{ task.verbose_name }}
20 | Queue: "{{ task.queue }}"
21 | Error: "{{ task.error_message }}"
22 | Task ran at {{ task.run_at|date:'Y-m-d H:i:s' }}
23 |
24 | {% else %}
25 |
26 | {{ task.verbose_name }}
27 | Queue: "{{ task.queue }}"
28 | Task ran at {{ task.run_at|date:'Y-m-d H:i:s' }}
29 |
30 | {% endif %}
31 |
32 | {% empty %}
33 | There have been no completed tasks{% if source %} that match the specified source filter{% endif %}.
34 | {% endfor %}
35 |
36 |
37 |
38 | {% include 'pagination.html' with pagination=sources.paginator filter=source.pk %}
39 | {% endblock %}
40 |
--------------------------------------------------------------------------------
/tubesync/sync/templates/sync/tasks-reset.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block headtitle %}Reset tasks{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
Reset tasks
9 |
10 | If your TubeSync installation has gotten itself into any form of synchronisation
11 | state issues (such you moved then restored files on disk) and the state in
12 | TubeSync isn't up to date you can use this button to force a state reset.
13 |
14 |
15 | This will delete all current tasks then all souces will be checked for their
16 | states and new tasks to be created where required.
17 |
18 |
19 |
20 |
31 | {% endblock %}
32 |
--------------------------------------------------------------------------------
/tubesync/sync/templates/widgets/checkbox_option.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 | {{option.label}}
7 |
8 |
--------------------------------------------------------------------------------
/tubesync/sync/templates/widgets/checkbox_select.html:
--------------------------------------------------------------------------------
1 |
2 | {% for option in widget.multipleChoiceProperties %}
3 | {% include option.template_name with option=option %}
4 | {% endfor %}
5 |
--------------------------------------------------------------------------------
/tubesync/sync/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/tubesync/sync/templatetags/filters.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from django.template.defaultfilters import filesizeformat
3 |
4 |
5 | register = template.Library()
6 |
7 |
8 | @register.filter(is_safe=True)
9 | def bytesformat(input):
10 | output = filesizeformat(input)
11 | if not (output and output.endswith('B', -1)):
12 | return output
13 | return output[: -1 ] + 'iB'
14 |
15 | @register.filter(is_safe=False)
16 | def sub(value, arg):
17 | """Subtract the arg from the value."""
18 | try:
19 | return int(value) - int(arg)
20 | except (ValueError, TypeError):
21 | try:
22 | return value - arg
23 | except Exception:
24 | return ""
25 |
26 |
--------------------------------------------------------------------------------
/tubesync/sync/testdata/README.md:
--------------------------------------------------------------------------------
1 | # metadata
2 |
3 | This directory contains metadata extracted from some test YouTube videos with
4 | youtube-dl.
5 |
6 | They are used to test (with `sync/tests.py`) the format matchers in `sync/matching.py`
7 | and are not otherwise used in TubeSync. Removing this directory will not break TubeSync
8 | but will break test running.
9 |
--------------------------------------------------------------------------------
/tubesync/tubesync/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meeb/tubesync/4dc7243972e9b999053eae1d29c4e07a575f0960/tubesync/tubesync/__init__.py
--------------------------------------------------------------------------------
/tubesync/tubesync/asgi.py:
--------------------------------------------------------------------------------
1 | import os
2 | from django.core.asgi import get_asgi_application
3 |
4 |
5 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tubesync.settings')
6 | application = get_asgi_application()
7 |
--------------------------------------------------------------------------------
/tubesync/tubesync/dbutils.py:
--------------------------------------------------------------------------------
1 | import importlib
2 | from django.conf import settings
3 | from django.db.backends.utils import CursorWrapper
4 |
5 |
6 | def patch_ensure_connection():
7 | for name, config in settings.DATABASES.items():
8 |
9 | # Don't patch for PostgreSQL, it doesn't need it and can cause issues
10 | if config['ENGINE'] == 'django.db.backends.postgresql':
11 | continue
12 |
13 | module = importlib.import_module(config['ENGINE'] + '.base')
14 |
15 | def ensure_connection(self):
16 | if self.connection is not None:
17 | try:
18 | with CursorWrapper(self.create_cursor(), self) as cursor:
19 | cursor.execute('SELECT 1;')
20 | return
21 | except Exception:
22 | pass
23 |
24 | with self.wrap_database_errors:
25 | self.connect()
26 |
27 | module.DatabaseWrapper.ensure_connection = ensure_connection
28 |
--------------------------------------------------------------------------------
/tubesync/tubesync/gunicorn.py:
--------------------------------------------------------------------------------
1 | import os
2 | import multiprocessing
3 |
4 |
5 | def get_num_workers():
6 | # Sane max workers to allow to be spawned
7 | cpu_workers = multiprocessing.cpu_count() * 2 + 1
8 | # But default to 3
9 | try:
10 | num_workers = int(os.getenv('GUNICORN_WORKERS', 3))
11 | except ValueError:
12 | num_workers = cpu_workers
13 | if 0 < num_workers < cpu_workers:
14 | return num_workers
15 | else:
16 | return cpu_workers
17 |
18 |
19 | def get_bind():
20 | host = os.getenv('LISTEN_HOST', '127.0.0.1')
21 | port = os.getenv('LISTEN_PORT', '8080')
22 | return '{}:{}'.format(host, port)
23 |
24 |
25 | workers = get_num_workers()
26 | timeout = 90
27 | chdir = '/app'
28 | daemon = False
29 | pidfile = '/run/app/gunicorn.pid'
30 | user = 'app'
31 | group = 'app'
32 | loglevel = 'info'
33 | errorlog = '-'
34 | accesslog = '/dev/null' # Access logs are printed to stdout from nginx
35 | django_settings = 'django.settings'
36 | bind = get_bind()
37 |
--------------------------------------------------------------------------------
/tubesync/tubesync/local_settings.py.example:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 |
4 | BASE_DIR = Path(__file__).resolve().parent.parent
5 | CONFIG_BASE_DIR = BASE_DIR
6 | DOWNLOADS_BASE_DIR = BASE_DIR
7 |
8 |
9 | SECRET_KEY = 'example-secret-key'
10 | DEBUG = False
11 |
12 |
13 | DATABASES = {
14 | 'default': {
15 | 'ENGINE': 'django.db.backends.sqlite3',
16 | 'NAME': CONFIG_BASE_DIR / 'db.sqlite3',
17 | }
18 | }
19 | DATABASE_CONNECTION_STR = f'sqlite at "{DATABASES["default"]["NAME"]}"'
20 |
21 |
22 | DOWNLOAD_ROOT = DOWNLOADS_BASE_DIR / 'downloads'
23 |
--------------------------------------------------------------------------------
/tubesync/tubesync/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path, include
2 | from django.contrib import admin
3 |
4 |
5 | admin.site.site_title = 'TubeSync dashboard admin'
6 | admin.site.site_header = 'TubeSync dashboard admin'
7 | handler404 = 'common.views.error404'
8 | handler500 = 'common.views.error500'
9 |
10 |
11 | urlpatterns = [
12 |
13 | path('admin/',
14 | admin.site.urls),
15 |
16 | path('',
17 | include('common.urls', namespace='common')),
18 |
19 | path('',
20 | include('sync.urls', namespace='sync')),
21 |
22 | ]
23 |
--------------------------------------------------------------------------------
/tubesync/tubesync/wsgi.py:
--------------------------------------------------------------------------------
1 | import os
2 | from django.core.wsgi import get_wsgi_application
3 |
4 |
5 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tubesync.settings')
6 | DJANGO_URL_PREFIX = os.getenv('DJANGO_URL_PREFIX', None)
7 | _application = get_wsgi_application()
8 |
9 |
10 | def application(environ, start_response):
11 | script_name = None
12 | if DJANGO_URL_PREFIX:
13 | if DJANGO_URL_PREFIX.endswith('/'):
14 | script_name = DJANGO_URL_PREFIX
15 | else:
16 | raise Exception(f'DJANGO_URL_PREFIX must end with a /, '
17 | f'got: {DJANGO_URL_PREFIX}')
18 | if script_name is not None:
19 | environ['SCRIPT_NAME'] = script_name
20 | path_info = environ['PATH_INFO']
21 | if path_info.startswith(script_name):
22 | environ['PATH_INFO'] = path_info[len(script_name) - 1:]
23 | return _application(environ, start_response)
24 |
--------------------------------------------------------------------------------
/tubesync/upgrade_yt-dlp.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | warning_message() {
4 | cat <&2
11 |
12 | pip3() {
13 | local pip_runner pip_whl run_whl
14 |
15 | # pipenv
16 | pip_runner='/usr/lib/python3/dist-packages/pipenv/patched/pip/__pip-runner__.py'
17 | test -s "${pip_runner}" || pip_runner=''
18 |
19 | # python3-pip-whl
20 | pip_whl="$(ls -1r /usr/share/python-wheels/pip-*-py3-none-any.whl | head -n 1)"
21 | run_whl="${pip_whl}/pip"
22 |
23 | python3 "${pip_runner:-"${run_whl}"}" "$@"
24 | }
25 |
26 | warning_message
27 | test -n "${TUBESYNC_DEBUG}" || exit 1
28 |
29 | # Use the flag added in 23.0.1, if possible.
30 | # https://github.com/pypa/pip/pull/11780
31 | break_system_packages='--break-system-packages'
32 | pip_version="$(pip3 --version | awk '$1 = "pip" { print $2; exit; }')"
33 | if [[ "${pip_version}" < "23.0.1" ]]; then
34 | break_system_packages=''
35 | fi
36 |
37 | pip3 install --upgrade ${break_system_packages} yt-dlp
38 |
39 |
--------------------------------------------------------------------------------