├── .coveragerc ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGES.rst ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── .gitignore ├── Makefile ├── api │ ├── embed_video.admin.rst │ ├── embed_video.backends.rst │ ├── embed_video.fields.rst │ ├── embed_video.settings.rst │ ├── embed_video.template_tags.rst │ ├── embed_video.utils.rst │ └── index.rst ├── changes.rst ├── conf.py ├── development │ ├── changelog.rst │ ├── contributing.rst │ ├── index.rst │ └── testing.rst ├── example-project.rst ├── examples.rst ├── ext │ ├── __init__.py │ └── djangodocs.py ├── index.rst ├── installation.rst ├── make.bat ├── requirements.txt └── websites.rst ├── embed_video ├── __init__.py ├── admin.py ├── backends.py ├── fields.py ├── locale │ └── pl │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── settings.py ├── templates │ └── embed_video │ │ └── embed_code.html ├── templatetags │ ├── __init__.py │ └── embed_video_tags.py └── tests │ ├── __init__.py │ ├── backends │ ├── __init__.py │ ├── tests_custom_backend.py │ ├── tests_soundcloud.py │ ├── tests_videobackend.py │ ├── tests_vimeo.py │ └── tests_youtube.py │ ├── django_settings.py │ ├── templatetags │ ├── __init__.py │ └── tests_embed_video_tags.py │ ├── tests_admin.py │ └── tests_fields.py ├── example_project ├── .gitignore ├── README.rst ├── embed_video ├── example_project │ ├── __init__.py │ ├── fixtures │ │ └── initial_data.yaml │ ├── models.py │ ├── settings.py │ ├── templates │ │ └── base.html │ ├── urls.py │ └── wsgi.py ├── manage.py ├── posts │ ├── __init__.py │ ├── admin.py │ ├── fixtures │ │ └── initial_data.yaml │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── templates │ │ └── posts │ │ │ ├── post_detail.html │ │ │ └── post_list.html │ ├── tests.py │ ├── urls.py │ └── views.py └── requirements.txt ├── setup.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = embed_video 4 | 5 | [report] 6 | include = embed_video/* 7 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | if: github.repository == 'jazzband/django-embed-video' 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: 3.8 22 | 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install -U pip 26 | python -m pip install -U setuptools twine wheel 27 | 28 | - name: Build package 29 | run: | 30 | python setup.py --version 31 | python setup.py sdist --format=gztar bdist_wheel 32 | twine check dist/* 33 | 34 | - name: Upload packages to Jazzband 35 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 36 | uses: pypa/gh-action-pypi-publish@master 37 | with: 38 | user: jazzband 39 | password: ${{ secrets.JAZZBAND_RELEASE_KEY }} 40 | repository_url: https://jazzband.co/projects/django-embed-video/upload 41 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: build (Python ${{ matrix.python-version }}, Django ${{ matrix.django-version }}) 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | max-parallel: 5 12 | matrix: 13 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] 14 | django-version: ['3.2', '4.1', '4.2', '5.0', '5.1'] 15 | include: 16 | # Tox configuration for QA environment 17 | - python-version: '3.11' 18 | django-version: 'qa' 19 | # Patchwork for GitHub "required" test environments 20 | - python-version: '3.7' 21 | django-version: '3.2' 22 | - python-version: '3.8' 23 | django-version: '4.0' 24 | - python-version: '3.9' 25 | django-version: '4.0' 26 | - python-version: '3.10' 27 | django-version: '4.0' 28 | exclude: 29 | # Exclude Django 3.2 for Python 3.11 30 | - python-version: '3.11' 31 | django-version: '3.2' 32 | # Django 5.0/5.1 don't support < Python 3.10 33 | - python-version: '3.8' 34 | django-version: '5.0' 35 | - python-version: '3.9' 36 | django-version: '5.0' 37 | - python-version: '3.8' 38 | django-version: '5.1' 39 | - python-version: '3.9' 40 | django-version: '5.1' 41 | 42 | steps: 43 | - uses: actions/checkout@v2 44 | 45 | - name: Set up Python ${{ matrix.python-version }} 46 | uses: actions/setup-python@v2 47 | with: 48 | python-version: ${{ matrix.python-version }} 49 | 50 | - name: Get pip cache dir 51 | id: pip-cache 52 | run: | 53 | echo "::set-output name=dir::$(pip cache dir)" 54 | 55 | - name: Cache 56 | uses: actions/cache@v2 57 | with: 58 | path: ${{ steps.pip-cache.outputs.dir }} 59 | key: 60 | ${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }} 61 | restore-keys: | 62 | ${{ matrix.python-version }}-v1- 63 | 64 | - name: Install dependencies 65 | run: | 66 | python -m pip install --upgrade pip 67 | python -m pip install --upgrade tox tox-gh-actions 68 | 69 | - name: Tox tests 70 | run: | 71 | tox -v 72 | env: 73 | DJANGO: ${{ matrix.django-version }} 74 | 75 | - name: Upload coverage 76 | uses: codecov/codecov-action@v1 77 | with: 78 | name: Python ${{ matrix.python-version }} 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | build 3 | dist 4 | *.pyc 5 | *.egg-info 6 | *.egg 7 | .coverage 8 | coverage.xml 9 | htmlcov 10 | .idea 11 | .eggs/ 12 | .tox/ 13 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pygrep-hooks 3 | rev: v1.10.0 4 | hooks: 5 | - id: python-check-blanket-noqa 6 | 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: v4.6.0 9 | hooks: 10 | - id: check-merge-conflict 11 | - id: check-yaml 12 | 13 | ci: 14 | autoupdate_schedule: quarterly 15 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | 2 | Changes 3 | ======= 4 | 5 | 6 | Release 1.4.10 (May 7, 2024) 7 | ---------------------------- 8 | 9 | - Update Python and Django versions. 10 | - Use importlib.metadata instead of deprecated pkg_resources. 11 | 12 | 13 | Release 1.4.9 (July 1, 2023) 14 | ---------------------------- 15 | 16 | - Update Python and Django versions. 17 | - Fix Soundcloud tests. 18 | - Add example sites. 19 | 20 | 21 | Release 1.4.8 (November 19, 2022) 22 | --------------------------------- 23 | 24 | - Fixes in migrations and documentation. 25 | 26 | 27 | Release 1.4.7 (September 28, 2022) 28 | ---------------------------------- 29 | 30 | - Fix rST file formatting for release automation approval. 31 | 32 | 33 | Release 1.4.6 (September 28, 2022) 34 | ---------------------------------- 35 | 36 | - Use Django 3.2 LTS for the example project. 37 | - Add URLvalidator to validate input in the admin widget. 38 | 39 | 40 | Release 1.4.5 (September 1, 2022) 41 | --------------------------------- 42 | 43 | - Add support for Django 4.1 44 | 45 | 46 | Release 1.4.4 (April 18, 2022) 47 | ------------------------------ 48 | 49 | - Add ``EMBED_VIDEO_YOUTUBE_CHECK_THUMBNAIL`` to settings. 50 | - Drop support for EOL Django 2.2. 51 | 52 | 53 | Release 1.4.3 (April 8, 2022) 54 | ------------------------------ 55 | 56 | - Add support for YouTube short URLs. 57 | 58 | 59 | Release 1.4.2 (March 17, 2022) 60 | ------------------------------ 61 | 62 | - Add Polish translation 63 | 64 | 65 | Release 1.4.1 (January 6, 2022) 66 | --------------------------------- 67 | 68 | - Add Code of Conduct to repository 69 | - Add support for Python 3.10 70 | - Add support for Django 3.2 and Django 4.0 71 | - Drop support for EOL Python 3.6 72 | - Drop support for EOL Django 3.0 and EOL Django 3.1 73 | - Remove Nose from dependencies due to version incompatibility 74 | 75 | 76 | Release 1.4.0 (December 18, 2020) 77 | --------------------------------- 78 | 79 | - Add lazy loading for video template tags. 80 | - Handle Vimeo admin "manage" URLs 81 | - Migrate from Travis CI to GitHub Actions. 82 | - Drop unsupported Django versions prior to 2.2. 83 | - Add support for Python up to 3.9. 84 | - Add support for Django up to 3.1. 85 | - Improve code formatting. 86 | 87 | 88 | Release 1.3.3 (June 10, 2020) 89 | ----------------------------- 90 | 91 | - Fix admin UI exception with form validation. 92 | 93 | 94 | Release 1.3.2 (March 29, 2020) 95 | ------------------------------ 96 | 97 | - Add support for Soundcloud mobile URLs. 98 | 99 | 100 | Release 1.3.1 (January 6, 2020) 101 | ------------------------------- 102 | 103 | - Add support for Vimeo review video URLs. 104 | - Update example project code. 105 | 106 | 107 | Release 1.3 (August 30, 2019) 108 | ----------------------------- 109 | 110 | - Drop unsupported Python version prior to 3.6. 111 | - Drop unsupported Django versions prior to 1.11. 112 | - Add support for Python up to 3.8 including PyPy. 113 | - Add support for Django up to 2.2. 114 | - Improve project structure, docs and language. 115 | - Improve CI and CD infrastructure and automation. 116 | - Move project to Jazzband organization. 117 | 118 | 119 | Release 1.2.0 (October 04, 2018) 120 | -------------------------------- 121 | 122 | - hotfix docs directive 123 | (`#94 `_) 124 | 125 | - update docs 126 | (`#92 `_) 127 | 128 | - use tests_require and setup_requires for nose testing requirements 129 | (`#91 `_) 130 | 131 | - add renderer kwarg to Widget render method to support Python 2.1 and later 132 | (`#88 `_) 133 | 134 | - enable default HTTPS support for YouTube, VimeoBackend, SoundCloudBackend 135 | (`#86 `_) 136 | 137 | - added syntax highlight in README.rst 138 | (`#81 `_) 139 | 140 | - updating requests >=2.19 141 | 142 | 143 | Release 1.1.2 (April 27, 2017) 144 | ------------------------------ 145 | 146 | - fix pypi 147 | 148 | 149 | Release 1.1.1 (March 24, 2017) 150 | ------------------------------ 151 | 152 | - updates for Django 1.10 and 1.11 153 | (`#73 `_) 154 | 155 | - update requirements for installation of the example project 156 | (`#72 `_) 157 | 158 | - use secure connection to query soundcloud endpoint 159 | (`#68 `_) 160 | 161 | 162 | 163 | Release 1.1.0 (Jan 19, 2016) 164 | ---------------------------- 165 | 166 | - added support fort Django 1.9 167 | (`#52 `_) 168 | 169 | - if possible YouTube thumbnails are returned in better resolution 170 | (`#43 `_) 171 | 172 | 173 | Release 1.0.0 (May 01, 2015) 174 | ---------------------------- 175 | 176 | **Backward incompatible changes:** 177 | 178 | - filter `embed_video_tags.embed` has been removed 179 | 180 | - changed behaviour of extra params in video tag 181 | (`#34 `_, `#36 `_) 182 | 183 | 184 | Backward compatible changes: 185 | 186 | - added support for Django 1.7 and Django 1.8 187 | 188 | - added support for Vimeo channels 189 | (`#47 `_) 190 | 191 | - fix resizing of SoundCloud iframe 192 | (`#41 `_) 193 | 194 | 195 | Release 0.11 (July 26, 2014) 196 | ---------------------------- 197 | 198 | - add support for YouTube mobile urls 199 | (`#27 `_) 200 | 201 | - fix passing parameters in calling request library 202 | (`#28 `_) 203 | 204 | - fix validation of urls 205 | (`#31 `_) 206 | 207 | 208 | Release 0.10 (May 24, 2014) 209 | --------------------------- 210 | 211 | - ``video`` tag accepts kwargs 212 | (`#20 `_) 213 | 214 | - ``video`` tag will not crash anymore with ``None`` passed as url 215 | (`#24 `_) 216 | 217 | 218 | Release 0.9 (Apr. 04, 2014) 219 | --------------------------- 220 | 221 | - Add ``VideoBackend.template_name`` and rendering embed code from file. 222 | 223 | - Allow relative sizes in template tag 224 | (`#19 `_). 225 | 226 | - Fix handling invalid urls of SoundCloud. 227 | (`#21 `_). 228 | 229 | - Catch ``VideoDoesntExistException`` and ``UnknownBackendException`` in 230 | template tags and admin widget. 231 | 232 | - Add base exception ``EmbedVideoException``. 233 | 234 | 235 | Release 0.8 (Feb. 22, 2014) 236 | --------------------------- 237 | 238 | - Add ``EMBED_VIDEO_TIMEOUT`` to settings. 239 | 240 | - Fix renderering template tag if no url is provided 241 | (`#18 `_) 242 | 243 | - If ``EMBED_VIDEO_TIMEOUT`` timeout is reached in templates, no exception is 244 | raised, error is just logged. 245 | 246 | - Fix default size in template tag. 247 | (`See more... `_) 248 | 249 | 250 | Release 0.7 (Dec. 21, 2013) 251 | --------------------------- 252 | 253 | - Support for sites running on HTTPS 254 | 255 | - ``embed`` filter is deprecated and replaced by ``video`` filter. 256 | 257 | - caching for whole backends was removed and replaced by caching properties 258 | 259 | - minor improvements on example project (fixtures, urls) 260 | 261 | 262 | Release 0.6 (Oct. 04, 2013) 263 | --------------------------- 264 | 265 | - Ability to overwrite embed code of backend 266 | 267 | - Caching backends properties 268 | 269 | - PyPy compatibility 270 | 271 | - Admin video mixin and video widget 272 | 273 | 274 | Release 0.5 (Sep. 03, 2013) 275 | --------------------------- 276 | 277 | - Added Vimeo thumbnails support 278 | 279 | - Added caching of results 280 | 281 | - Added example project 282 | 283 | - Fixed template tag embed 284 | 285 | - Fixed raising UnknownIdException in YouTube detecting. 286 | 287 | 288 | 289 | Release 0.4 (Aug. 22, 2013) 290 | --------------------------- 291 | 292 | - Documentation was rewrited and moved to http://django-embed-video.rtfd.org/ . 293 | 294 | - Custom backends 295 | (http://django-embed-video.rtfd.org/en/latest/examples.html#custom-backends). 296 | 297 | - Improved YouTube and Vimeo regex. 298 | 299 | - Support for Python 3. 300 | 301 | - Renamed ``base`` to ``backends``. 302 | 303 | 304 | 305 | Release 0.3 (Aug. 20, 2013) 306 | --------------------------- 307 | 308 | - Security fix: faked urls are treated as invalid. See `this page 309 | `_ 310 | for more details. 311 | 312 | - Fixes: 313 | 314 | - allow of empty video field. 315 | 316 | - requirements in setup.py 317 | 318 | - Added simplier way to embed video in one-line template tag:: 319 | 320 | {{ 'http://www.youtube.com/watch?v=guXyvo2FfLs'|embed:'large' }} 321 | 322 | - ``backend`` variable in ``video`` template tag. 323 | 324 | Usage:: 325 | 326 | {% video item.video as my_video %} 327 | Backend: {{ my_video.backend }} 328 | {% endvideo %} 329 | 330 | 331 | Release 0.2 (June 25, 2013) 332 | --------------------------- 333 | 334 | - Support of SoundCloud 335 | 336 | Release 0.1 (June 1, 2013) 337 | -------------------------- 338 | 339 | - Initial release 340 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | As contributors and maintainers of the Jazzband projects, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating documentation, 6 | submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in the Jazzband a harassment-free experience 9 | for everyone, regardless of the level of experience, gender, gender identity and 10 | expression, sexual orientation, disability, personal appearance, body size, race, 11 | ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | - The use of sexualized language or imagery 16 | - Personal attacks 17 | - Trolling or insulting/derogatory comments 18 | - Public or private harassment 19 | - Publishing other's private information, such as physical or electronic addresses, 20 | without explicit permission 21 | - Other unethical or unprofessional conduct 22 | 23 | The Jazzband roadies have the right and responsibility to remove, edit, or reject 24 | comments, commits, code, wiki edits, issues, and other contributions that are not 25 | aligned to this Code of Conduct, or to ban temporarily or permanently any contributor 26 | for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 27 | 28 | By adopting this Code of Conduct, the roadies commit themselves to fairly and 29 | consistently applying these principles to every aspect of managing the jazzband 30 | projects. Roadies who do not follow or enforce the Code of Conduct may be permanently 31 | removed from the Jazzband roadies. 32 | 33 | This code of conduct applies both within project spaces and in public spaces when an 34 | individual is representing the project or its community. 35 | 36 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 37 | contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and 38 | investigated and will result in a response that is deemed necessary and appropriate to 39 | the circumstances. Roadies are obligated to maintain confidentiality with regard to the 40 | reporter of an incident. 41 | 42 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 43 | 1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version] 44 | 45 | [homepage]: https://contributor-covenant.org 46 | [version]: https://contributor-covenant.org/version/1/3/0/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | [![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co/) 2 | 3 | This is a [Jazzband](https://jazzband.co/) project. 4 | By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct) 5 | and follow the [guidelines](https://jazzband.co/about/guidelines). 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | ----------- 3 | 4 | Copyright (c) 2012 Juda Kaleta (http://www.judakaleta.cz) 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include CHANGES.rst 3 | include LICENSE 4 | 5 | recursive-include embed_video/templates *.html 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-embed-video 2 | ================== 3 | 4 | Django app for easy embedding YouTube and Vimeo videos and music from SoundCloud. 5 | 6 | .. image:: https://jazzband.co/static/img/badge.svg 7 | :target: https://jazzband.co/ 8 | :alt: Jazzband 9 | .. image:: https://github.com/jazzband/django-embed-video/workflows/Test/badge.svg 10 | :target: https://github.com/jazzband/django-embed-video/actions 11 | :alt: GitHub Actions 12 | .. image:: https://coveralls.io/repos/yetty/django-embed-video/badge.svg?branch=master 13 | :target: https://coveralls.io/r/yetty/django-embed-video?branch=master 14 | :alt: Coveralls coverage percentage 15 | .. image:: https://img.shields.io/pypi/pyversions/django-embed-video.svg 16 | :target: https://pypi.org/project/django-embed-video/ 17 | :alt: Supported Python versions 18 | .. image:: https://img.shields.io/pypi/djversions/django-embed-video.svg 19 | :target: https://pypi.org/project/django-embed-video/ 20 | :alt: Supported Django versions 21 | 22 | 23 | Documentation 24 | ------------- 25 | 26 | Documentation is at: http://django-embed-video.rtfd.org/ 27 | 28 | 29 | Quick start 30 | ----------- 31 | 32 | #. Install ``django-embed-video``: 33 | 34 | :: 35 | 36 | pip install django-embed-video 37 | 38 | 39 | or from sources 40 | 41 | :: 42 | 43 | pip install git+https://github.com/jazzband/django-embed-video 44 | 45 | 46 | #. Add ``embed_video`` to ``INSTALLED_APPS`` in your Django settings. 47 | 48 | #. If you want to detect HTTP/S in template tags, you have to set ``request`` 49 | context processor in ``settings.TEMPLATES``: 50 | 51 | .. code-block:: python 52 | 53 | TEMPLATES = [ 54 | { 55 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 56 | # ... 57 | 'OPTIONS': { 58 | 'context_processors': [ 59 | # ... 60 | 'django.template.context_processors.request', 61 | ], 62 | }, 63 | }, 64 | ] 65 | 66 | #. Usage of template tags: 67 | 68 | .. code-block:: html+django 69 | 70 | {% load embed_video_tags %} 71 | 72 | 73 | {% video item.video as my_video %} 74 | URL: {{ my_video.url }} 75 | Thumbnail: {{ my_video.thumbnail }} 76 | Backend: {{ my_video.backend }} 77 | 78 | {% video my_video "large" %} 79 | {% endvideo %} 80 | 81 | 82 | {% video my_video '800x600' %} 83 | 84 | #. Usage of model fields 85 | 86 | .. code-block:: python 87 | 88 | from django.db import models 89 | from embed_video.fields import EmbedVideoField 90 | 91 | class Item(models.Model): 92 | video = EmbedVideoField() # same like models.URLField() 93 | 94 | 95 | Contributing 96 | ------------ 97 | 98 | This is a `Jazzband `_ project. 99 | By contributing you agree to abide by the `Contributor Code of Conduct `_ 100 | and follow the `guidelines `_. 101 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _* 2 | !__init__.py 3 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-embed-video.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-embed-video.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-embed-video" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-embed-video" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/api/embed_video.admin.rst: -------------------------------------------------------------------------------- 1 | Admin 2 | ===== 3 | 4 | .. automodule:: embed_video.admin 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/embed_video.backends.rst: -------------------------------------------------------------------------------- 1 | Backends 2 | ======== 3 | 4 | .. automodule:: embed_video.backends 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/embed_video.fields.rst: -------------------------------------------------------------------------------- 1 | Fields 2 | ====== 3 | 4 | .. automodule:: embed_video.fields 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/embed_video.settings.rst: -------------------------------------------------------------------------------- 1 | Settings 2 | ======== 3 | 4 | .. setting:: EMBED_VIDEO_BACKENDS 5 | 6 | 7 | EMBED_VIDEO_BACKENDS 8 | -------------------- 9 | 10 | List of backends to use. 11 | 12 | Default:: 13 | 14 | EMBED_VIDEO_BACKENDS = ( 15 | 'embed_video.backends.YoutubeBackend', 16 | 'embed_video.backends.VimeoBackend', 17 | 'embed_video.backends.SoundCloudBackend', 18 | ) 19 | 20 | 21 | .. setting:: EMBED_VIDEO_TIMEOUT 22 | 23 | 24 | EMBED_VIDEO_TIMEOUT 25 | ------------------- 26 | 27 | Sets timeout for ``GET`` requests to remote servers. 28 | 29 | Default: ``10`` 30 | 31 | 32 | .. setting:: EMBED_VIDEO_YOUTUBE_DEFAULT_QUERY 33 | 34 | 35 | EMBED_VIDEO_YOUTUBE_DEFAULT_QUERY 36 | --------------------------------- 37 | 38 | Sets default :py:attr:`~embed_video.backends.VideoBackend.query` appended 39 | to YouTube url. Can be string or :py:class:`~django.http.QueryDict` instance. 40 | 41 | Default: ``"wmode=opaque"`` 42 | 43 | 44 | .. setting:: EMBED_VIDEO_YOUTUBE_CHECK_THUMBNAIL 45 | 46 | 47 | EMBED_VIDEO_YOUTUBE_CHECK_THUMBNAIL 48 | ----------------------------------- 49 | 50 | Sets whether to check thumbnail for YouTube. If ``False``, it uses ``high`` 51 | resulution as it's guaranteed to exist. 52 | 53 | Default: ``True`` 54 | -------------------------------------------------------------------------------- /docs/api/embed_video.template_tags.rst: -------------------------------------------------------------------------------- 1 | Template tags 2 | ============= 3 | 4 | You have to load template tag library first. 5 | 6 | .. code-block:: html+django 7 | 8 | {% load embed_video_tags %} 9 | 10 | 11 | 12 | .. automodule:: embed_video.templatetags.embed_video_tags 13 | :members: 14 | -------------------------------------------------------------------------------- /docs/api/embed_video.utils.rst: -------------------------------------------------------------------------------- 1 | Utils 2 | ===== 3 | 4 | .. automodule:: embed_video.utils 5 | :members: 6 | -------------------------------------------------------------------------------- /docs/api/index.rst: -------------------------------------------------------------------------------- 1 | API reference 2 | ============= 3 | 4 | 5 | .. toctree:: 6 | :glob: 7 | 8 | embed_video.* 9 | -------------------------------------------------------------------------------- /docs/changes.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGES.rst 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # django-embed-video documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Aug 22 08:38:40 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | from pkg_resources import get_distribution 15 | import os 16 | import sys 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | # sys.path.insert(0, os.path.abspath('.')) 22 | sys.path.insert(0, os.path.abspath(".")) 23 | sys.path.insert(0, os.path.abspath("..")) 24 | 25 | os.environ["DJANGO_SETTINGS_MODULE"] = "embed_video.tests.django_settings" 26 | 27 | on_rtd = os.environ.get("READTHEDOCS", None) == "True" 28 | 29 | # -- General configuration ----------------------------------------------------- 30 | 31 | # If your documentation needs a minimal Sphinx version, state it here. 32 | # needs_sphinx = '1.0' 33 | 34 | # Add any Sphinx extension module names here, as strings. They can be extensions 35 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 36 | extensions = [ 37 | "sphinx.ext.autodoc", 38 | "sphinx.ext.intersphinx", 39 | "sphinx.ext.autosummary", 40 | "sphinx.ext.todo", 41 | "sphinx.ext.viewcode", 42 | "ext.djangodocs", 43 | ] 44 | 45 | # Add any paths that contain templates here, relative to this directory. 46 | templates_path = ["_templates"] 47 | 48 | # The suffix of source filenames. 49 | source_suffix = ".rst" 50 | 51 | # The encoding of source files. 52 | # source_encoding = 'utf-8-sig' 53 | 54 | # The master toctree document. 55 | master_doc = "index" 56 | 57 | # General information about the project. 58 | project = "django-embed-video" 59 | copyright = "2018, Jazzband" 60 | 61 | # The version info for the project you're documenting, acts as replacement for 62 | # |version| and |release|, also used in various other places throughout the 63 | # built documents. 64 | 65 | # The full version, including alpha/beta/rc tags. 66 | release = get_distribution("django-embed-video").version 67 | 68 | # The short X.Y version. 69 | version = ".".join(release.split(".")[:2]) 70 | 71 | # The language for content autogenerated by Sphinx. Refer to documentation 72 | # for a list of supported languages. 73 | # language = None 74 | 75 | todo_include_todos = True 76 | 77 | # There are two options for replacing |today|: either, you set today to some 78 | # non-false value, then it is used: 79 | # today = '' 80 | # Else, today_fmt is used as the format for a strftime call. 81 | # today_fmt = '%B %d, %Y' 82 | 83 | # List of patterns, relative to source directory, that match files and 84 | # directories to ignore when looking for source files. 85 | exclude_patterns = ["_build"] 86 | 87 | # The reST default role (used for this markup: `text`) to use for all documents. 88 | # default_role = None 89 | 90 | # If true, '()' will be appended to :func: etc. cross-reference text. 91 | # add_function_parentheses = True 92 | 93 | # If true, the current module name will be prepended to all description 94 | # unit titles (such as .. function::). 95 | # add_module_names = True 96 | 97 | # If true, sectionauthor and moduleauthor directives will be shown in the 98 | # output. They are ignored by default. 99 | # show_authors = False 100 | 101 | # The name of the Pygments (syntax highlighting) style to use. 102 | pygments_style = "sphinx" 103 | 104 | # A list of ignored prefixes for module index sorting. 105 | # modindex_common_prefix = [] 106 | 107 | # If true, keep warnings as "system message" paragraphs in the built documents. 108 | # keep_warnings = False 109 | 110 | 111 | # -- Options for HTML output --------------------------------------------------- 112 | 113 | # The theme to use for HTML and HTML Help pages. See the documentation for 114 | # a list of builtin themes. 115 | if not on_rtd: # only import and set the theme if we're building docs locally 116 | try: 117 | import sphinx_rtd_theme 118 | 119 | html_theme = "sphinx_rtd_theme" 120 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 121 | except ImportError: 122 | html_theme = "default" 123 | 124 | # Theme options are theme-specific and customize the look and feel of a theme 125 | # further. For a list of options available for each theme, see the 126 | # documentation. 127 | # html_theme_options = {} 128 | 129 | # Add any paths that contain custom themes here, relative to this directory. 130 | # html_theme_path = [] 131 | 132 | # The name for this set of Sphinx documents. If None, it defaults to 133 | # " v documentation". 134 | # html_title = None 135 | 136 | # A shorter title for the navigation bar. Default is the same as html_title. 137 | # html_short_title = None 138 | 139 | # The name of an image file (relative to this directory) to place at the top 140 | # of the sidebar. 141 | # html_logo = None 142 | 143 | # The name of an image file (within the static path) to use as favicon of the 144 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 145 | # pixels large. 146 | # html_favicon = None 147 | 148 | # Add any paths that contain custom static files (such as style sheets) here, 149 | # relative to this directory. They are copied after the builtin static files, 150 | # so a file named "default.css" will overwrite the builtin "default.css". 151 | html_static_path = ["_static"] 152 | 153 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 154 | # using the given strftime format. 155 | # html_last_updated_fmt = '%b %d, %Y' 156 | 157 | # If true, SmartyPants will be used to convert quotes and dashes to 158 | # typographically correct entities. 159 | # html_use_smartypants = True 160 | 161 | # Custom sidebar templates, maps document names to template names. 162 | # html_sidebars = {} 163 | 164 | # Additional templates that should be rendered to pages, maps page names to 165 | # template names. 166 | # html_additional_pages = {} 167 | 168 | # If false, no module index is generated. 169 | # html_domain_indices = True 170 | 171 | # If false, no index is generated. 172 | # html_use_index = True 173 | 174 | # If true, the index is split into individual pages for each letter. 175 | # html_split_index = False 176 | 177 | # If true, links to the reST sources are added to the pages. 178 | # html_show_sourcelink = True 179 | 180 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 181 | # html_show_sphinx = True 182 | 183 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 184 | # html_show_copyright = True 185 | 186 | # If true, an OpenSearch description file will be output, and all pages will 187 | # contain a tag referring to it. The value of this option must be the 188 | # base URL from which the finished HTML is served. 189 | # html_use_opensearch = '' 190 | 191 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 192 | # html_file_suffix = None 193 | 194 | # Output file base name for HTML help builder. 195 | htmlhelp_basename = "django-embed-videodoc" 196 | 197 | 198 | # -- Options for LaTeX output -------------------------------------------------- 199 | 200 | latex_elements = { 201 | # The paper size ('letterpaper' or 'a4paper'). 202 | #'papersize': 'letterpaper', 203 | # The font size ('10pt', '11pt' or '12pt'). 204 | #'pointsize': '10pt', 205 | # Additional stuff for the LaTeX preamble. 206 | #'preamble': '', 207 | } 208 | 209 | # Grouping the document tree into LaTeX files. List of tuples 210 | # (source start file, target name, title, author, documentclass [howto/manual]). 211 | latex_documents = [ 212 | ( 213 | "index", 214 | "django-embed-video.tex", 215 | "django-embed-video Documentation", 216 | "Juda Kaleta", 217 | "manual", 218 | ) 219 | ] 220 | 221 | # The name of an image file (relative to this directory) to place at the top of 222 | # the title page. 223 | # latex_logo = None 224 | 225 | # For "manual" documents, if this is true, then toplevel headings are parts, 226 | # not chapters. 227 | # latex_use_parts = False 228 | 229 | # If true, show page references after internal links. 230 | # latex_show_pagerefs = False 231 | 232 | # If true, show URL addresses after external links. 233 | # latex_show_urls = False 234 | 235 | # Documents to append as an appendix to all manuals. 236 | # latex_appendices = [] 237 | 238 | # If false, no module index is generated. 239 | # latex_domain_indices = True 240 | 241 | 242 | # -- Options for manual page output -------------------------------------------- 243 | 244 | # One entry per manual page. List of tuples 245 | # (source start file, name, description, authors, manual section). 246 | man_pages = [ 247 | ( 248 | "index", 249 | "django-embed-video", 250 | "django-embed-video Documentation", 251 | ["Juda Kaleta"], 252 | 1, 253 | ) 254 | ] 255 | 256 | # If true, show URL addresses after external links. 257 | # man_show_urls = False 258 | 259 | 260 | # -- Options for Texinfo output ------------------------------------------------ 261 | 262 | # Grouping the document tree into Texinfo files. List of tuples 263 | # (source start file, target name, title, author, 264 | # dir menu entry, description, category) 265 | texinfo_documents = [ 266 | ( 267 | "index", 268 | "django-embed-video", 269 | "django-embed-video Documentation", 270 | "Juda Kaleta", 271 | "django-embed-video", 272 | "One line description of project.", 273 | "Miscellaneous", 274 | ) 275 | ] 276 | 277 | # Documents to append as an appendix to all manuals. 278 | # texinfo_appendices = [] 279 | 280 | # If false, no module index is generated. 281 | # texinfo_domain_indices = True 282 | 283 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 284 | # texinfo_show_urls = 'footnote' 285 | 286 | # If true, do not generate a @detailmenu in the "Top" node's menu. 287 | # texinfo_no_detailmenu = False 288 | 289 | 290 | # Example configuration for intersphinx: refer to the Python standard library. 291 | intersphinx_mapping = { 292 | "python": ("http://docs.python.org/2.7", None), 293 | "django": ( 294 | "https://docs.djangoproject.com/en/1.6/", 295 | "http://docs.djangoproject.com/en/1.6/_objects/", 296 | ), 297 | } 298 | -------------------------------------------------------------------------------- /docs/development/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | .. include:: ../../CHANGES.rst 5 | -------------------------------------------------------------------------------- /docs/development/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | I will be really pleased if you will provide patch to this Django app. Feel free 5 | to change whatever, but keep `PEP8 `_ 6 | rules and `Zen `_. 7 | 8 | It is a good habit to cover your patches with :doc:`tests `. 9 | 10 | Repository is hosted on Github: https://github.com/jazzband/django-embed-video 11 | -------------------------------------------------------------------------------- /docs/development/index.rst: -------------------------------------------------------------------------------- 1 | Development 2 | =========== 3 | 4 | 5 | .. toctree:: 6 | :glob: 7 | 8 | contributing 9 | testing 10 | changelog 11 | todos 12 | -------------------------------------------------------------------------------- /docs/development/testing.rst: -------------------------------------------------------------------------------- 1 | Testing 2 | ======= 3 | 4 | Requirements 5 | ------------ 6 | 7 | The library needs ``Django`` and ``requests`` and ``nose``, ``mock``, 8 | ``south`` and ``testfixtures`` libraries to run tests. They will be installed 9 | automatically when running tests via setup.py 10 | 11 | :: 12 | 13 | 14 | Running tests 15 | ------------- 16 | 17 | Run tests with this command: 18 | 19 | :: 20 | 21 | python setup.py nosetests 22 | 23 | 24 | Be sure to run it before each commit and fix broken tests. 25 | 26 | 27 | Run tests with coverage: 28 | 29 | :: 30 | 31 | python setup.py nosetests --with-coverage --cover-package=embed_video 32 | 33 | 34 | -------------------------------------------------------------------------------- /docs/example-project.rst: -------------------------------------------------------------------------------- 1 | Example project 2 | =============== 3 | 4 | For easy start with using django-embed-video, you can take a look at example 5 | project. It is located in `example_project directory `_ in root of repository. 6 | 7 | .. include:: ../example_project/README.rst 8 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | Template examples 5 | ################# 6 | 7 | .. highlight:: html+django 8 | 9 | First you have to load the ``embed_video_tags`` template tags in your template: 10 | 11 | :: 12 | 13 | {% load embed_video_tags %} 14 | 15 | Embedding of video: 16 | 17 | :: 18 | 19 | {# you can just embed #} 20 | {% video item.video 'small' %} 21 | 22 | {# or use variables (with embedding, too) #} 23 | {% video item.video as my_video %} 24 | URL: {{ my_video.url }} 25 | Thumbnail: {{ my_video.thumbnail }} 26 | Backend: {{ my_video.backend }} 27 | {% video my_video 'small' %} 28 | {% endvideo %} 29 | 30 | 31 | Default sizes are ``tiny`` (420x315), ``small`` (480x360), ``medium`` (640x480), 32 | ``large`` (960x720) and ``huge`` (1280x960). You can set your own size: 33 | 34 | :: 35 | 36 | {% video my_video '800x600' %} 37 | 38 | .. versionadded:: 0.7 39 | 40 | This usage has been added in version 0.7. 41 | 42 | .. versionadded:: 0.9 43 | 44 | And use relative percentual size: 45 | 46 | :: 47 | 48 | {% video my_video '100% x 50%' %} 49 | 50 | 51 | It is possible to set backend options via parameters in template tag. It is 52 | useful for example to enforce HTTPS protocol or set different query appended 53 | to url. 54 | 55 | :: 56 | 57 | {% video my_video query="rel=0&wmode=transparent" is_secure=True as my_video %} 58 | {{ my_video.url }} {# always with https #} 59 | {% endvideo %} 60 | 61 | 62 | .. tip:: 63 | 64 | We recommend to use `sorl-thumbnail 65 | `_ to `change 66 | `_ 67 | thumbnail size. 68 | 69 | .. tip:: 70 | 71 | To speed up your pages, consider `template fragment caching 72 | `_. 73 | 74 | .. tip:: 75 | 76 | You can overwrite default template of embed code located in 77 | ``templates/embed_video/embed_code.html`` or set own file for custom 78 | backend (:py:data:`~embed_video.backends.VideoBackend.template_name`). 79 | 80 | .. versionadded:: 0.9 81 | 82 | ``template_name`` has been added in version 0.9. 83 | 84 | 85 | Model examples 86 | ############## 87 | 88 | .. highlight:: python 89 | 90 | Using the ``EmbedVideoField`` provides you validation of URLs. 91 | 92 | :: 93 | 94 | from django.db import models 95 | from embed_video.fields import EmbedVideoField 96 | 97 | class Item(models.Model): 98 | video = EmbedVideoField() # same like models.URLField() 99 | 100 | 101 | 102 | Admin mixin examples 103 | #################### 104 | 105 | Use ``AdminVideoMixin`` in ``admin.py``. 106 | 107 | :: 108 | 109 | from django.contrib import admin 110 | from embed_video.admin import AdminVideoMixin 111 | from .models import MyModel 112 | 113 | class MyModelAdmin(AdminVideoMixin, admin.ModelAdmin): 114 | pass 115 | 116 | admin.site.register(MyModel, MyModelAdmin) 117 | 118 | 119 | 120 | 121 | Custom backends 122 | ############### 123 | 124 | If you have specific needs and default backends don't suits you, you can write 125 | your custom backend. 126 | 127 | ``my_project/my_app/backends.py``:: 128 | 129 | from embed_video.backends import VideoBackend 130 | 131 | class CustomBackend(VideoBackend): 132 | re_detect = re.compile(r'http://myvideo\.com/[0-9]+') 133 | re_code = re.compile(r'http://myvideo\.com/(?P[0-9]+)') 134 | 135 | allow_https = False 136 | pattern_url = '{protocol}://play.myvideo.com/c/{code}/' 137 | pattern_thumbnail_url = '{protocol}://thumb.myvideo.com/c/{code}/' 138 | 139 | template_name = 'embed_video/custombackend_embed_code.html' # added in v0.9 140 | 141 | You can also overwrite :py:class:`~embed_video.backends.VideoBackend` methods, 142 | if using regular expressions isn't well enough. 143 | 144 | ``my_project/my_project/settings.py``:: 145 | 146 | EMBED_VIDEO_BACKENDS = ( 147 | 'embed_video.backends.YoutubeBackend', 148 | 'embed_video.backends.VimeoBackend', 149 | 'embed_video.backends.SoundCloudBackend', 150 | 'my_app.backends.CustomBackend', 151 | ) 152 | 153 | 154 | 155 | Low level API examples 156 | ###################### 157 | 158 | You can get instance of :py:class:`~embed_video.backends.VideoBackend` in your 159 | python code thanks to :py:func:`~embed_video.backends.detect_backend`: 160 | 161 | :: 162 | 163 | from embed_video.backends import detect_backend 164 | 165 | my_video = detect_backend('http://www.youtube.com/watch?v=H4tAOexHdR4') 166 | 167 | -------------------------------------------------------------------------------- /docs/ext/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-embed-video/fb64ba0ef06b4a6518ddcb8f1f29679285c38f91/docs/ext/__init__.py -------------------------------------------------------------------------------- /docs/ext/djangodocs.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sphinx plugins for Django documentation. 3 | """ 4 | import json 5 | import os 6 | import re 7 | 8 | from sphinx import addnodes, __version__ as sphinx_ver 9 | from sphinx.builders.html import StandaloneHTMLBuilder 10 | from sphinx.writers.html import HTMLTranslator 11 | from sphinx.util.console import bold 12 | from docutils.parsers.rst import Directive 13 | 14 | # RE for option descriptions without a '--' prefix 15 | simple_option_desc_re = re.compile(r"([-_a-zA-Z0-9]+)(\s*.*?)(?=,\s+(?:/|-|--)|$)") 16 | 17 | 18 | def setup(app): 19 | app.add_crossref_type( 20 | directivename="setting", rolename="setting", indextemplate="pair: %s; setting" 21 | ) 22 | app.add_crossref_type( 23 | directivename="templatetag", 24 | rolename="ttag", 25 | indextemplate="pair: %s; template tag", 26 | ) 27 | app.add_crossref_type( 28 | directivename="templatefilter", 29 | rolename="tfilter", 30 | indextemplate="pair: %s; template filter", 31 | ) 32 | app.add_crossref_type( 33 | directivename="fieldlookup", 34 | rolename="lookup", 35 | indextemplate="pair: %s; field lookup type", 36 | ) 37 | app.add_description_unit( 38 | directivename="django-admin", 39 | rolename="djadmin", 40 | indextemplate="pair: %s; django-admin command", 41 | parse_node=parse_django_admin_node, 42 | ) 43 | app.add_description_unit( 44 | directivename="django-admin-option", 45 | rolename="djadminopt", 46 | indextemplate="pair: %s; django-admin command-line option", 47 | parse_node=parse_django_adminopt_node, 48 | ) 49 | app.add_config_value("django_next_version", "0.0", True) 50 | app.add_directive("versionadded", VersionDirective) 51 | app.add_directive("versionchanged", VersionDirective) 52 | app.add_builder(DjangoStandaloneHTMLBuilder) 53 | 54 | 55 | class VersionDirective(Directive): 56 | has_content = True 57 | required_arguments = 1 58 | optional_arguments = 1 59 | final_argument_whitespace = True 60 | option_spec = {} 61 | 62 | def run(self): 63 | if len(self.arguments) > 1: 64 | msg = """Only one argument accepted for directive '{directive_name}::'. 65 | Comments should be provided as content, 66 | not as an extra argument.""".format( 67 | directive_name=self.name 68 | ) 69 | raise self.error(msg) 70 | 71 | env = self.state.document.settings.env 72 | ret = [] 73 | node = addnodes.versionmodified() 74 | ret.append(node) 75 | 76 | if self.arguments[0] == env.config.django_next_version: 77 | node["version"] = "Development version" 78 | else: 79 | node["version"] = self.arguments[0] 80 | 81 | node["type"] = self.name 82 | if self.content: 83 | self.state.nested_parse(self.content, self.content_offset, node) 84 | env.note_versionchange(node["type"], node["version"], node, self.lineno) 85 | return ret 86 | 87 | 88 | class DjangoHTMLTranslator(HTMLTranslator): 89 | """ 90 | Django-specific reST to HTML tweaks. 91 | """ 92 | 93 | # Don't use border=1, which docutils does by default. 94 | def visit_table(self, node): 95 | self.context.append(self.compact_p) 96 | self.compact_p = True 97 | self._table_row_index = 0 # Needed by Sphinx 98 | self.body.append(self.starttag(node, "table", CLASS="docutils")) 99 | 100 | def depart_table(self, node): 101 | self.compact_p = self.context.pop() 102 | self.body.append("\n") 103 | 104 | # ? Really? 105 | def visit_desc_parameterlist(self, node): 106 | self.body.append("(") 107 | self.first_param = 1 108 | self.param_separator = node.child_text_separator 109 | 110 | def depart_desc_parameterlist(self, node): 111 | self.body.append(")") 112 | 113 | if sphinx_ver < "1.0.8": 114 | # 115 | # Don't apply smartypants to literal blocks 116 | # 117 | def visit_literal_block(self, node): 118 | self.no_smarty += 1 119 | HTMLTranslator.visit_literal_block(self, node) 120 | 121 | def depart_literal_block(self, node): 122 | HTMLTranslator.depart_literal_block(self, node) 123 | self.no_smarty -= 1 124 | 125 | # 126 | # Turn the "new in version" stuff (versionadded/versionchanged) into a 127 | # better callout -- the Sphinx default is just a little span, 128 | # which is a bit less obvious that I'd like. 129 | # 130 | # FIXME: these messages are all hardcoded in English. We need to change 131 | # that to accommodate other language docs, but I can't work out how to make 132 | # that work. 133 | # 134 | version_text = { 135 | "deprecated": "Deprecated in Django %s", 136 | "versionchanged": "Changed in Django %s", 137 | "versionadded": "New in Django %s", 138 | } 139 | 140 | def visit_versionmodified(self, node): 141 | self.body.append(self.starttag(node, "div", CLASS=node["type"])) 142 | title = "%s%s" % ( 143 | self.version_text[node["type"]] % node["version"], 144 | ":" if len(node) else ".", 145 | ) 146 | self.body.append('%s ' % title) 147 | 148 | def depart_versionmodified(self, node): 149 | self.body.append("\n") 150 | 151 | # Give each section a unique ID -- nice for custom CSS hooks 152 | def visit_section(self, node): 153 | old_ids = node.get("ids", []) 154 | node["ids"] = ["s-" + i for i in old_ids] 155 | node["ids"].extend(old_ids) 156 | HTMLTranslator.visit_section(self, node) 157 | node["ids"] = old_ids 158 | 159 | 160 | def parse_django_admin_node(env, sig, signode): 161 | command = sig.split(" ")[0] 162 | env._django_curr_admin_command = command 163 | title = "django-admin.py %s" % sig 164 | signode += addnodes.desc_name(title, title) 165 | return sig 166 | 167 | 168 | def parse_django_adminopt_node(env, sig, signode): 169 | """A copy of sphinx.directives.CmdoptionDesc.parse_signature()""" 170 | from sphinx.domains.std import option_desc_re 171 | 172 | count = 0 173 | firstname = "" 174 | for m in option_desc_re.finditer(sig): 175 | optname, args = m.groups() 176 | if count: 177 | signode += addnodes.desc_addname(", ", ", ") 178 | signode += addnodes.desc_name(optname, optname) 179 | signode += addnodes.desc_addname(args, args) 180 | if not count: 181 | firstname = optname 182 | count += 1 183 | if not count: 184 | for m in simple_option_desc_re.finditer(sig): 185 | optname, args = m.groups() 186 | if count: 187 | signode += addnodes.desc_addname(", ", ", ") 188 | signode += addnodes.desc_name(optname, optname) 189 | signode += addnodes.desc_addname(args, args) 190 | if not count: 191 | firstname = optname 192 | count += 1 193 | if not firstname: 194 | raise ValueError 195 | return firstname 196 | 197 | 198 | class DjangoStandaloneHTMLBuilder(StandaloneHTMLBuilder): 199 | """ 200 | Subclass to add some extra things we need. 201 | """ 202 | 203 | name = "djangohtml" 204 | 205 | def finish(self): 206 | super(DjangoStandaloneHTMLBuilder, self).finish() 207 | self.info(bold("writing templatebuiltins.js...")) 208 | xrefs = self.env.domaindata["std"]["objects"] 209 | templatebuiltins = { 210 | "ttags": [ 211 | n 212 | for ((t, n), (l, a)) in xrefs.items() 213 | if t == "templatetag" and l == "ref/templates/builtins" 214 | ], 215 | "tfilters": [ 216 | n 217 | for ((t, n), (l, a)) in xrefs.items() 218 | if t == "templatefilter" and l == "ref/templates/builtins" 219 | ], 220 | } 221 | outfilename = os.path.join(self.outdir, "templatebuiltins.js") 222 | with open(outfilename, "w") as fp: 223 | fp.write("var django_template_builtins = ") 224 | json.dump(templatebuiltins, fp) 225 | fp.write(";\n") 226 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | django-embed-video documentation 2 | ================================ 3 | 4 | Django app for easy embedding YouTube and Vimeo videos and music from SoundCloud. 5 | 6 | Repository is located on GitHub: 7 | 8 | https://github.com/jazzband/django-embed-video 9 | 10 | 11 | Contents 12 | ======== 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | :glob: 17 | 18 | installation 19 | examples 20 | example-project 21 | development/index 22 | websites 23 | changes 24 | 25 | 26 | Library API 27 | =========== 28 | 29 | .. toctree:: 30 | :maxdepth: 2 31 | 32 | api/index 33 | 34 | 35 | Indices and tables 36 | ================== 37 | 38 | * :ref:`genindex` 39 | * :ref:`modindex` 40 | * :ref:`search` 41 | 42 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation & Setup 2 | ==================== 3 | 4 | Installation 5 | ############ 6 | 7 | The simpliest way is to use pip to install package: 8 | 9 | .. code-block:: bash 10 | 11 | pip install django-embed-video 12 | 13 | 14 | If you want latest version, you may use Git. It is fresh, but unstable. 15 | 16 | .. code-block:: bash 17 | 18 | pip install git+https://github.com/jazzband/django-embed-video 19 | 20 | 21 | Setup 22 | ##### 23 | 24 | Add ``embed_video`` to :py:data:`~django.settings.INSTALLED_APPS` in your Django 25 | settings. 26 | 27 | .. code-block:: python 28 | 29 | INSTALLED_APPS = ( 30 | ... 31 | 32 | 'embed_video', 33 | ) 34 | 35 | To detect HTTP/S you must use :py:class:`~django.template.context_processors.request` 36 | context processor: 37 | 38 | .. code-block:: python 39 | 40 | TEMPLATE_CONTEXT_PROCESSORS = ( 41 | ... 42 | 'django.template.context_processors.request', 43 | ) 44 | 45 | 46 | WSGI + Nginx 47 | ############ 48 | 49 | 1. Add ``SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')`` to your settings.py 50 | 2. Add ``proxy_set_header X-Forwarded-Proto $scheme;`` to your Nginx site config file. 51 | 52 | This will set ``request.is_secure()`` equal to true when it is checked by ``embed_video_tags.py``, for more information reffer `here `_. 53 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-embed-video.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-embed-video.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Django>=2.2 2 | 3 | -------------------------------------------------------------------------------- /docs/websites.rst: -------------------------------------------------------------------------------- 1 | Websites using django-embed-video 2 | ================================= 3 | 4 | - `Al.ta Cucina `_ 5 | - `Bici.news `_ 6 | - `Outdoor Passion `_ 7 | - `Snow Passion `_ 8 | - `Running Passion `_ 9 | - `Vida Imobiliária `_ 10 | 11 | *Are you using django-embed-video? Send pull request!* 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /embed_video/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if sys.version_info >= (3, 8): 4 | import importlib.metadata as metadata 5 | else: 6 | import importlib_metadata as metadata 7 | 8 | __version__ = metadata.version("django-embed-video") 9 | -------------------------------------------------------------------------------- /embed_video/admin.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.core.exceptions import ValidationError 3 | from django.core.validators import URLValidator 4 | from django.utils.safestring import mark_safe 5 | 6 | from embed_video.backends import ( 7 | UnknownBackendException, 8 | VideoDoesntExistException, 9 | detect_backend, 10 | ) 11 | from embed_video.fields import EmbedVideoField 12 | 13 | 14 | class AdminVideoWidget(forms.TextInput): 15 | """ 16 | Widget for video input in administration. If empty it works just like 17 | :py:class:`django.forms.TextInput`. Otherwise it renders embedded video 18 | together with input field. 19 | 20 | .. todo:: 21 | 22 | Django 1.6 provides better parent for this widget - 23 | :py:class:`django.forms.URLInput`. 24 | 25 | """ 26 | 27 | output_format = ( 28 | '
' 29 | "{video}
{input}
" 30 | '
' 31 | ) 32 | 33 | def __init__(self, attrs=None): 34 | """ 35 | :type attrs: dict 36 | """ 37 | default_attrs = {"size": "40"} 38 | self.validator = URLValidator() 39 | 40 | if attrs: 41 | default_attrs.update(attrs) 42 | 43 | super().__init__(default_attrs) 44 | 45 | def render(self, name, value="", attrs=None, size=(420, 315), renderer=None): 46 | """ 47 | :type name: str 48 | :type attrs: dict 49 | """ 50 | 51 | output = super().render(name, value, attrs, renderer) 52 | 53 | if not value: 54 | return output 55 | 56 | try: 57 | self.validator(value) 58 | backend = detect_backend(value) 59 | return mark_safe( 60 | self.output_format.format( 61 | video=backend.get_embed_code(*size), input=output 62 | ) 63 | ) 64 | except (UnknownBackendException, ValidationError, VideoDoesntExistException): 65 | return output 66 | 67 | 68 | class AdminVideoMixin: 69 | """ 70 | Mixin using :py:class:`AdminVideoWidget` for fields with 71 | :py:class:`~embed_video.fields.EmbedVideoField`. 72 | 73 | Usage:: 74 | 75 | from django.contrib import admin 76 | from embed_video.admin import AdminVideoMixin 77 | from .models import MyModel 78 | 79 | class MyModelAdmin(AdminVideoMixin, admin.ModelAdmin): 80 | pass 81 | 82 | admin.site.register(MyModel, MyModelAdmin) 83 | 84 | """ 85 | 86 | def formfield_for_dbfield(self, db_field, **kwargs): 87 | """ 88 | :type db_field: str 89 | """ 90 | if isinstance(db_field, EmbedVideoField): 91 | return db_field.formfield(widget=AdminVideoWidget) 92 | 93 | return super().formfield_for_dbfield(db_field, **kwargs) 94 | -------------------------------------------------------------------------------- /embed_video/backends.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | import urllib.parse as urlparse 4 | 5 | import requests 6 | from django.http import QueryDict 7 | from django.template.loader import render_to_string 8 | from django.utils.functional import cached_property 9 | from django.utils.module_loading import import_string 10 | from django.utils.safestring import mark_safe 11 | 12 | from embed_video.settings import ( 13 | EMBED_VIDEO_BACKENDS, 14 | EMBED_VIDEO_TIMEOUT, 15 | EMBED_VIDEO_YOUTUBE_CHECK_THUMBNAIL, 16 | EMBED_VIDEO_YOUTUBE_DEFAULT_QUERY, 17 | ) 18 | 19 | 20 | class EmbedVideoException(Exception): 21 | """Parental class for all embed_video exceptions""" 22 | 23 | pass 24 | 25 | 26 | class VideoDoesntExistException(EmbedVideoException): 27 | """Exception thrown if video doesn't exist""" 28 | 29 | pass 30 | 31 | 32 | class UnknownBackendException(EmbedVideoException): 33 | """Exception thrown if video backend is not recognized.""" 34 | 35 | pass 36 | 37 | 38 | class UnknownIdException(VideoDoesntExistException): 39 | """ 40 | Exception thrown if backend is detected, but video ID cannot be parsed. 41 | """ 42 | 43 | pass 44 | 45 | 46 | def detect_backend(url): 47 | """ 48 | Detect the right backend for given URL. 49 | 50 | Goes over backends in ``settings.EMBED_VIDEO_BACKENDS``, 51 | calls :py:func:`~VideoBackend.is_valid` and returns backend instance. 52 | 53 | :param url: URL which is passed to `is_valid` methods of VideoBackends. 54 | :type url: str 55 | 56 | :return: Returns recognized VideoBackend 57 | :rtype: VideoBackend 58 | """ 59 | 60 | for backend_name in EMBED_VIDEO_BACKENDS: 61 | backend = import_string(backend_name) 62 | if backend.is_valid(url): 63 | return backend(url) 64 | 65 | raise UnknownBackendException 66 | 67 | 68 | class VideoBackend: 69 | """ 70 | Base class used as parental class for backends. 71 | 72 | 73 | Backend variables: 74 | 75 | .. autosummary:: 76 | 77 | url 78 | code 79 | thumbnail 80 | query 81 | info 82 | is_secure 83 | protocol 84 | template_name 85 | 86 | 87 | .. code-block:: python 88 | 89 | class MyBackend(VideoBackend): 90 | ... 91 | 92 | """ 93 | 94 | re_code = None 95 | """ 96 | Compiled regex (:py:func:`re.compile`) to search code in URL. 97 | 98 | Example: ``re.compile(r'myvideo\\.com/\\?code=(?P\\w+)')`` 99 | """ 100 | 101 | re_detect = None 102 | """ 103 | Compilede regec (:py:func:`re.compile`) to detect, if input URL is valid 104 | for current backend. 105 | 106 | Example: ``re.compile(r'^http://myvideo\\.com/.*')`` 107 | """ 108 | 109 | pattern_url = None 110 | """ 111 | Pattern in which the code is inserted. 112 | 113 | Example: ``http://myvideo.com?code=%s`` 114 | 115 | :type: str 116 | """ 117 | 118 | pattern_thumbnail_url = None 119 | """ 120 | Pattern in which the code is inserted to get thumbnail url. 121 | 122 | Example: ``http://static.myvideo.com/thumbs/%s`` 123 | 124 | :type: str 125 | """ 126 | 127 | allow_https = True 128 | """ 129 | Sets if HTTPS version allowed for specific backend. 130 | 131 | :type: bool 132 | """ 133 | 134 | template_name = "embed_video/embed_code.html" 135 | """ 136 | Name of embed code template used by :py:meth:`get_embed_code`. 137 | 138 | Passed template variables: ``{{ backend }}`` (instance of VideoBackend), 139 | ``{{ width }}``, ``{{ height }}`` 140 | 141 | :type: str 142 | """ 143 | 144 | default_query = "" 145 | """ 146 | Default query string or `QueryDict` appended to url 147 | 148 | :type: str 149 | """ 150 | 151 | is_secure = False 152 | """ 153 | Decides if secured protocol (HTTPS) is used. 154 | 155 | :type: bool 156 | """ 157 | 158 | def __init__(self, url): 159 | """ 160 | First it tries to load data from cache and if it don't succeed, run 161 | :py:meth:`init` and then save it to cache. 162 | 163 | :type url: str 164 | """ 165 | self.backend = self.__class__.__name__ 166 | self._url = url 167 | self.query = QueryDict(self.default_query, mutable=True) 168 | 169 | @property 170 | def code(self): 171 | """ 172 | Code of video. 173 | """ 174 | return self.get_code() 175 | 176 | @property 177 | def url(self): 178 | """ 179 | URL of video. 180 | """ 181 | return self.get_url() 182 | 183 | @property 184 | def protocol(self): 185 | """ 186 | Protocol used to generate URL. 187 | """ 188 | return "https" if self.allow_https and self.is_secure else "http" 189 | 190 | @property 191 | def thumbnail(self): 192 | """ 193 | URL of video thumbnail. 194 | """ 195 | return self.get_thumbnail_url() 196 | 197 | @property 198 | def info(self): 199 | """ 200 | Additional information about video. Not implemented in all backends. 201 | """ 202 | return self.get_info() 203 | 204 | @property 205 | def query(self): 206 | """ 207 | String transformed to QueryDict appended to url. 208 | """ 209 | return self._query 210 | 211 | @query.setter 212 | def query(self, value): 213 | """ 214 | :type value: QueryDict | str 215 | """ 216 | self._query = ( 217 | value if isinstance(value, QueryDict) else QueryDict(value, mutable=True) 218 | ) 219 | 220 | @classmethod 221 | def is_valid(cls, url): 222 | """ 223 | Class method to control if passed url is valid for current backend. By 224 | default it is done by :py:data:`re_detect` regex. 225 | 226 | :type url: str 227 | """ 228 | return True if cls.re_detect.match(url) else False 229 | 230 | def get_code(self): 231 | """ 232 | Returns video code matched from given url by :py:data:`re_code`. 233 | 234 | :rtype: str 235 | """ 236 | match = self.re_code.search(self._url) 237 | if match: 238 | return match.group("code") 239 | 240 | def get_url(self): 241 | """ 242 | Returns URL folded from :py:data:`pattern_url` and parsed code. 243 | """ 244 | url = self.pattern_url.format(code=self.code, protocol=self.protocol) 245 | url += "?" + self.query.urlencode() if self.query else "" 246 | return mark_safe(url) 247 | 248 | def get_thumbnail_url(self): 249 | """ 250 | Returns thumbnail URL folded from :py:data:`pattern_thumbnail_url` and 251 | parsed code. 252 | 253 | :rtype: str 254 | """ 255 | return self.pattern_thumbnail_url.format(code=self.code, protocol=self.protocol) 256 | 257 | def get_embed_code(self, width, height): 258 | """ 259 | Returns embed code rendered from template :py:data:`template_name`. 260 | 261 | :type width: int | str 262 | :type height: int | str 263 | :rtype: str 264 | """ 265 | return render_to_string( 266 | self.template_name, {"backend": self, "width": width, "height": height} 267 | ) 268 | 269 | def get_info(self): 270 | """ 271 | :rtype: dict 272 | """ 273 | raise NotImplementedError 274 | 275 | def set_options(self, options): 276 | """ 277 | :type options: dict 278 | """ 279 | for key in options: 280 | setattr(self, key, options[key]) 281 | 282 | 283 | class YoutubeBackend(VideoBackend): 284 | """ 285 | Backend for YouTube URLs. 286 | """ 287 | 288 | re_detect = re.compile(r"^(http(s)?://)?(www\.|m\.)?youtu(\.?)be(\.com)?/.*", re.I) 289 | 290 | re_code = re.compile( 291 | r"""youtu(\.?)be(\.com)?/ # match youtube's domains 292 | (\#/)? # for mobile urls 293 | (embed/)? # match the embed url syntax 294 | (v/)? 295 | (shorts/)? # match youtube shorts 296 | (watch\?v=)? # match the youtube page url 297 | (ytscreeningroom\?v=)? 298 | (feeds/api/videos/)? 299 | (user\S*[^\w\-\s])? 300 | (?P[\w\-]{11})[a-z0-9;:@?&%=+/\$_.-]* # match and extract 301 | """, 302 | re.I | re.X, 303 | ) 304 | 305 | pattern_url = "{protocol}://www.youtube.com/embed/{code}" 306 | pattern_thumbnail_url = "{protocol}://img.youtube.com/vi/{code}/{resolution}" 307 | default_query = EMBED_VIDEO_YOUTUBE_DEFAULT_QUERY 308 | resolutions = [ 309 | "maxresdefault.jpg", 310 | "sddefault.jpg", 311 | "hqdefault.jpg", 312 | "mqdefault.jpg", 313 | ] 314 | 315 | is_secure = True 316 | """ 317 | Decides if secured protocol (HTTPS) is used. 318 | 319 | :type: bool 320 | """ 321 | 322 | def get_code(self): 323 | code = super().get_code() 324 | 325 | if not code: 326 | parsed_url = urlparse.urlparse(self._url) 327 | parsed_qs = urlparse.parse_qs(parsed_url.query) 328 | 329 | if "v" in parsed_qs: 330 | code = parsed_qs["v"][0] 331 | elif "video_id" in parsed_qs: 332 | code = parsed_qs["video_id"][0] 333 | else: 334 | raise UnknownIdException("Cannot get ID from `{0}`".format(self._url)) 335 | 336 | return code 337 | 338 | def get_thumbnail_url(self): 339 | """ 340 | Returns thumbnail URL folded from :py:data:`pattern_thumbnail_url` and 341 | parsed code. 342 | 343 | :rtype: str 344 | """ 345 | if not EMBED_VIDEO_YOUTUBE_CHECK_THUMBNAIL: 346 | return self.pattern_thumbnail_url.format( 347 | code=self.code, protocol=self.protocol, resolution="hqdefault.jpg" 348 | ) 349 | for resolution in self.resolutions: 350 | temp_thumbnail_url = self.pattern_thumbnail_url.format( 351 | code=self.code, protocol=self.protocol, resolution=resolution 352 | ) 353 | if int(requests.head(temp_thumbnail_url).status_code) < 400: 354 | return temp_thumbnail_url 355 | return None 356 | 357 | 358 | class VimeoBackend(VideoBackend): 359 | """ 360 | Backend for Vimeo URLs. 361 | """ 362 | 363 | re_detect = re.compile(r"^((http(s)?:)?//)?(www\.)?(player\.)?vimeo\.com/.*", re.I) 364 | re_code = re.compile( 365 | r"""vimeo\.com/(video/)?(channels/(.*/)?)?((.+)/review/)?(manage/)?(?P[0-9]+)""", 366 | re.I, 367 | ) 368 | pattern_url = "{protocol}://player.vimeo.com/video/{code}" 369 | pattern_info = "{protocol}://vimeo.com/api/v2/video/{code}.json" 370 | 371 | is_secure = True 372 | """ 373 | Decides if secured protocol (HTTPS) is used. 374 | 375 | :type: bool 376 | """ 377 | 378 | def get_info(self): 379 | try: 380 | response = requests.get( 381 | self.pattern_info.format(code=self.code, protocol=self.protocol), 382 | timeout=EMBED_VIDEO_TIMEOUT, 383 | ) 384 | return json.loads(response.text)[0] 385 | except ValueError: 386 | raise VideoDoesntExistException() 387 | 388 | def get_thumbnail_url(self): 389 | return self.info.get("thumbnail_large") 390 | 391 | 392 | class SoundCloudBackend(VideoBackend): 393 | """ 394 | Backend for SoundCloud URLs. 395 | """ 396 | 397 | base_url = "{protocol}://soundcloud.com/oembed" 398 | 399 | re_detect = re.compile(r"^(http(s)?://(www\.|m\.)?)?soundcloud\.com/.*", re.I) 400 | re_code = re.compile(r'src=".*%2F(?P\d+)&show_artwork.*"', re.I) 401 | re_url = re.compile(r'src="(?P.*?)"', re.I) 402 | 403 | is_secure = True 404 | """ 405 | Decides if secured protocol (HTTPS) is used. 406 | 407 | :type: bool 408 | """ 409 | 410 | @cached_property 411 | def width(self): 412 | """ 413 | :rtype: str 414 | """ 415 | return self.info.get("width") 416 | 417 | @cached_property 418 | def height(self): 419 | """ 420 | :rtype: str 421 | """ 422 | return self.info.get("height") 423 | 424 | def get_info(self): 425 | params = {"format": "json", "url": self._url} 426 | r = requests.get( 427 | self.base_url.format(protocol=self.protocol), 428 | params=params, 429 | timeout=EMBED_VIDEO_TIMEOUT, 430 | ) 431 | 432 | if r.status_code != 200: 433 | raise VideoDoesntExistException( 434 | "SoundCloud returned status code `{status_code}` for URL `{url}`.".format( 435 | status_code=r.status_code, 436 | url=r.url, 437 | ) 438 | ) 439 | 440 | return json.loads(r.text) 441 | 442 | def get_thumbnail_url(self): 443 | return self.info.get("thumbnail_url") 444 | 445 | def get_url(self): 446 | match = self.re_url.search(self.info.get("html")) 447 | return match.group("url") 448 | 449 | def get_code(self): 450 | match = self.re_code.search(self.info.get("html")) 451 | return match.group("code") 452 | 453 | def get_embed_code(self, width, height): 454 | return super().get_embed_code(width=width, height=height) 455 | -------------------------------------------------------------------------------- /embed_video/fields.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.db import models 3 | from django.utils.translation import gettext_lazy as _ 4 | 5 | from embed_video.backends import ( 6 | UnknownBackendException, 7 | UnknownIdException, 8 | VideoDoesntExistException, 9 | detect_backend, 10 | ) 11 | 12 | __all__ = ("EmbedVideoField", "EmbedVideoFormField") 13 | 14 | 15 | class EmbedVideoField(models.URLField): 16 | """ 17 | Model field for embedded video. Descendant of 18 | :py:class:`django.db.models.URLField`. 19 | """ 20 | 21 | def formfield(self, **kwargs): 22 | defaults = {"form_class": EmbedVideoFormField} 23 | defaults.update(kwargs) 24 | return super().formfield(**defaults) 25 | 26 | 27 | class EmbedVideoFormField(forms.URLField): 28 | """ 29 | Form field for embeded video. Descendant of 30 | :py:class:`django.forms.URLField` 31 | """ 32 | 33 | def validate(self, url): 34 | # if empty url is not allowed throws an exception 35 | super().validate(url) 36 | 37 | if not url: 38 | return 39 | 40 | try: 41 | backend = detect_backend(url) 42 | backend.get_code() 43 | except UnknownBackendException: 44 | raise forms.ValidationError(_("URL could not be recognized.")) 45 | except UnknownIdException: 46 | raise forms.ValidationError( 47 | _("ID of this video could not be " "recognized.") 48 | ) 49 | except VideoDoesntExistException: 50 | raise forms.ValidationError(_("This media not found on site.")) 51 | return url 52 | -------------------------------------------------------------------------------- /embed_video/locale/pl/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-embed-video/fb64ba0ef06b4a6518ddcb8f1f29679285c38f91/embed_video/locale/pl/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /embed_video/locale/pl/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2020-07-17 11:40+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n" 20 | "%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n" 21 | "%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" 22 | 23 | #: fields.py:44 24 | msgid "URL could not be recognized." 25 | msgstr "Nieprawidłowy adres URL" 26 | 27 | #: fields.py:47 28 | msgid "ID of this video could not be recognized." 29 | msgstr "Nieprawidłowy identyfikator wideo" 30 | 31 | #: fields.py:50 32 | msgid "This media not found on site." 33 | msgstr "Nie znaleziono pliku na stronie" 34 | -------------------------------------------------------------------------------- /embed_video/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | EMBED_VIDEO_BACKENDS = getattr( 4 | settings, 5 | "EMBED_VIDEO_BACKENDS", 6 | ( 7 | "embed_video.backends.YoutubeBackend", 8 | "embed_video.backends.VimeoBackend", 9 | "embed_video.backends.SoundCloudBackend", 10 | ), 11 | ) 12 | """ :type: tuple[str] """ 13 | 14 | EMBED_VIDEO_TIMEOUT = getattr(settings, "EMBED_VIDEO_TIMEOUT", 10) 15 | """ :type: int """ 16 | 17 | EMBED_VIDEO_YOUTUBE_DEFAULT_QUERY = getattr( 18 | settings, "EMBED_VIDEO_YOUTUBE_DEFAULT_QUERY", "wmode=opaque" 19 | ) 20 | """ :type: django.db.models.QuerySet | str """ 21 | 22 | EMBED_VIDEO_YOUTUBE_CHECK_THUMBNAIL = getattr( 23 | settings, "EMBED_VIDEO_YOUTUBE_CHECK_THUMBNAIL", True 24 | ) 25 | """ :type: bool """ 26 | -------------------------------------------------------------------------------- /embed_video/templates/embed_video/embed_code.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /embed_video/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-embed-video/fb64ba0ef06b4a6518ddcb8f1f29679285c38f91/embed_video/templatetags/__init__.py -------------------------------------------------------------------------------- /embed_video/templatetags/embed_video_tags.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | 4 | import requests 5 | from django.template import Library, Node, TemplateSyntaxError 6 | from django.utils.encoding import smart_str 7 | from django.utils.safestring import mark_safe 8 | 9 | from embed_video.backends import ( 10 | UnknownBackendException, 11 | VideoBackend, 12 | VideoDoesntExistException, 13 | detect_backend, 14 | ) 15 | 16 | register = Library() 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | 21 | @register.tag("video") 22 | class VideoNode(Node): 23 | """ 24 | Template tag ``video``. It gives access to all 25 | :py:class:`~embed_video.backends.VideoBackend` variables. 26 | 27 | Usage (shortcut): 28 | 29 | .. code-block:: html+django 30 | 31 | {% video URL [SIZE] [key1=value1, key2=value2...] %} 32 | 33 | Or as a block: 34 | 35 | .. code-block:: html+django 36 | 37 | {% video URL [SIZE] [key1=value1, key2=value2...] as VAR %} 38 | ... 39 | {% endvideo %} 40 | 41 | Examples: 42 | 43 | .. code-block:: html+django 44 | 45 | {% video item.video %} 46 | {% video item.video "large" %} 47 | {% video item.video "340x200" %} 48 | {% video item.video "100% x 300" query="rel=0&wmode=opaque" %} 49 | 50 | {% video item.video is_secure=True as my_video %} 51 | URL: {{ my_video.url }} 52 | Thumbnail: {{ my_video.thumbnail }} 53 | Backend: {{ my_video.backend }} 54 | {% endvideo %} 55 | 56 | """ 57 | 58 | error_msg = ( 59 | "Syntax error. Expected: ``{% video URL " 60 | "[size] [key1=val1 key2=val2 ...] [as var] %}``" 61 | ) 62 | default_size = "small" 63 | 64 | re_size = re.compile(r'[\'"]?(?P\d+%?) *x *(?P\d+%?)[\'"]?') 65 | re_option = re.compile(r"^(?P[\w]+)=(?P.+)$") 66 | 67 | def __init__(self, parser, token): 68 | """ 69 | :param parser: Django template parser 70 | :type parser: django.template.base.Parser 71 | :param token: Django template token 72 | :type token: django.template.base.Token 73 | """ 74 | self.parser = parser 75 | self.bits = list(token.split_contents()) 76 | self.tag_name = str(self.pop_bit()) 77 | self.url = self.pop_bit() 78 | 79 | if len(self.bits) > 1 and self.bits[-2] == "as": 80 | del self.bits[-2] 81 | self.variable_name = str(self.pop_bit(-1)) 82 | self.nodelist_file = parser.parse(("end" + self.tag_name,)) 83 | parser.delete_first_token() 84 | else: 85 | self.variable_name = None 86 | 87 | self.size = self.pop_bit() if self.bits and "=" not in self.bits[0] else None 88 | self.options = self.parse_options(self.bits) 89 | 90 | def pop_bit(self, index=0): 91 | return self.parser.compile_filter(self.bits.pop(index)) 92 | 93 | def parse_options(self, bits): 94 | options = {} 95 | for bit in bits: 96 | parsed_bit = self.re_option.match(bit) 97 | key = smart_str(parsed_bit.group("key")) 98 | value = self.parser.compile_filter(parsed_bit.group("value")) 99 | options[key] = value 100 | return options 101 | 102 | def render(self, context): 103 | """ 104 | Returns generated HTML. 105 | 106 | :param context: Django template RequestContext 107 | :type context: django.template.RequestContext 108 | :return: Rendered HTML with embed video. 109 | :rtype: django.utils.safestring.SafeText | str 110 | """ 111 | url = self.url.resolve(context) 112 | size = self.size.resolve(context) if self.size else None 113 | options = self.resolve_options(context) 114 | 115 | try: 116 | if not self.variable_name: 117 | return self.embed(url, size, context=context, **options) 118 | backend = self.get_backend(url, context=context, **options) 119 | return self.render_block(context, backend) 120 | except requests.Timeout: 121 | logger.exception( 122 | "Timeout reached during rendering embed video (`{0}`)".format(url) 123 | ) 124 | except UnknownBackendException: 125 | logger.warning("Backend wasn't recognised (`{0}`)".format(url)) 126 | except VideoDoesntExistException: 127 | logger.warning("Attempt to render not existing video (`{0}`)".format(url)) 128 | 129 | return "" 130 | 131 | def resolve_options(self, context): 132 | """ 133 | :param context: Django template RequestContext 134 | :type context: django.template.RequestContext 135 | """ 136 | options = {} 137 | for key in self.options: 138 | value = self.options[key] 139 | options[key] = value.resolve(context) 140 | return options 141 | 142 | def render_block(self, context, backend): 143 | """ 144 | :param context: Django template RequestContext 145 | :type context: django.template.RequestContext 146 | :param backend: Given instance inherited from VideoBackend 147 | :type backend: VideoBackend 148 | :rtype: django.utils.safestring.SafeText 149 | """ 150 | context.push() 151 | context[self.variable_name] = backend 152 | output = self.nodelist_file.render(context) 153 | context.pop() 154 | return output 155 | 156 | @staticmethod 157 | def get_backend(backend_or_url, context=None, **options): 158 | """ 159 | Returns instance of VideoBackend. If context is passed to the method 160 | and request is secure, than the is_secure mark is set to backend. 161 | 162 | A string or VideoBackend instance can be passed to the method. 163 | 164 | :param backend: Given instance inherited from VideoBackend or url 165 | :type backend_or_url: VideoBackend | str 166 | :param context: Django template RequestContext 167 | :type context: django.template.RequestContext | None 168 | :rtype: VideoBackend 169 | """ 170 | 171 | backend = ( 172 | backend_or_url 173 | if isinstance(backend_or_url, VideoBackend) 174 | else detect_backend(str(backend_or_url)) 175 | ) 176 | 177 | if context and "request" in context: 178 | backend.is_secure = context["request"].is_secure() 179 | if options: 180 | backend.set_options(options) 181 | 182 | return backend 183 | 184 | @classmethod 185 | def embed(cls, url, size, context=None, **options): 186 | """ 187 | Direct render of embed video. 188 | 189 | :param url: URL to embed video 190 | :type url: str 191 | :param size: Size of rendered block 192 | :type size: str 193 | :param context: Django template RequestContext 194 | :type context: django.template.RequestContext | None 195 | """ 196 | backend = cls.get_backend(url, context=context, **options) 197 | width, height = cls.get_size(size) 198 | return mark_safe(backend.get_embed_code(width=width, height=height)) 199 | 200 | @classmethod 201 | def get_size(cls, value): 202 | """ 203 | Predefined sizes: 204 | 205 | ======== ======== ========= 206 | size width height 207 | ======== ======== ========= 208 | tiny 420 315 209 | small 480 360 210 | medium 640 480 211 | large 960 720 212 | huge 1280 960 213 | ======== ======== ========= 214 | 215 | You can also use custom size - in format ``WIDTHxHEIGHT`` 216 | (eg. ``500x400``). 217 | 218 | :type value: str 219 | 220 | :return: Returns tuple with (width, height) values. 221 | :rtype: tuple[int, int] 222 | """ 223 | sizes = { 224 | "tiny": (420, 315), 225 | "small": (480, 360), 226 | "medium": (640, 480), 227 | "large": (960, 720), 228 | "huge": (1280, 960), 229 | } 230 | 231 | value = value or cls.default_size 232 | if value in sizes: 233 | return sizes[value] 234 | 235 | try: 236 | size = cls.re_size.match(value) 237 | return size.group("width"), size.group("height") 238 | except AttributeError: 239 | raise TemplateSyntaxError( 240 | "Incorrect size.\nPossible format is WIDTHxHEIGHT or using " 241 | "predefined size ({sizes}).".format(sizes=", ".join(sizes.keys())) 242 | ) 243 | 244 | def __iter__(self): 245 | for node in self.nodelist_file: 246 | yield node 247 | 248 | def __repr__(self): 249 | return '' % self.url 250 | -------------------------------------------------------------------------------- /embed_video/tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import django 4 | 5 | os.environ["DJANGO_SETTINGS_MODULE"] = "embed_video.tests.django_settings" 6 | 7 | django.setup() 8 | -------------------------------------------------------------------------------- /embed_video/tests/backends/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-embed-video/fb64ba0ef06b4a6518ddcb8f1f29679285c38f91/embed_video/tests/backends/__init__.py -------------------------------------------------------------------------------- /embed_video/tests/backends/tests_custom_backend.py: -------------------------------------------------------------------------------- 1 | import re 2 | from unittest import TestCase 3 | 4 | from embed_video.backends import VideoBackend, detect_backend 5 | 6 | 7 | class CustomBackend(VideoBackend): 8 | re_detect = re.compile(r"http://myvideo\.com/[0-9]+") 9 | re_code = re.compile(r"http://myvideo\.com/(?P[0-9]+)") 10 | 11 | pattern_url = "{protocol}://play.myvideo.com/c/{code}/" 12 | pattern_thumbnail_url = "{protocol}://thumb.myvideo.com/c/{code}/" 13 | 14 | 15 | class CustomBackendTestCase(TestCase): 16 | def setUp(self): 17 | self.backend = detect_backend("http://myvideo.com/1530") 18 | 19 | def test_detect_backend(self): 20 | self.assertIsInstance(self.backend, CustomBackend) 21 | 22 | def test_code(self): 23 | self.assertEqual(self.backend.code, "1530") 24 | 25 | def test_url(self): 26 | self.assertEqual(self.backend.get_url(), "http://play.myvideo.com/c/1530/") 27 | 28 | def test_url_https(self): 29 | self.backend.is_secure = True 30 | self.assertEqual(self.backend.get_url(), "https://play.myvideo.com/c/1530/") 31 | 32 | def test_thumbnail(self): 33 | self.assertEqual( 34 | self.backend.get_thumbnail_url(), "http://thumb.myvideo.com/c/1530/" 35 | ) 36 | -------------------------------------------------------------------------------- /embed_video/tests/backends/tests_soundcloud.py: -------------------------------------------------------------------------------- 1 | from json import dumps 2 | from unittest import TestCase 3 | from unittest.mock import patch 4 | 5 | import requests 6 | import requests_mock 7 | 8 | from embed_video.backends import ( 9 | SoundCloudBackend, 10 | VideoDoesntExistException, 11 | detect_backend, 12 | ) 13 | 14 | 15 | class SoundCloudBackendTestCase(TestCase): 16 | urls = ( 17 | ( 18 | "https://soundcloud.com/community/soundcloud-case-study-wildlife", 19 | "82244706", 20 | "https://soundcloud.com/oembed?format=json&url=https%3A%2F%2Fsoundcloud.com%2Fcommunity%2Fsoundcloud-case-study-wildlife", 21 | dumps( 22 | { 23 | "version": 1.0, 24 | "type": "rich", 25 | "provider_name": "SoundCloud", 26 | "provider_url": "https://soundcloud.com", 27 | "height": 400, 28 | "width": "100%", 29 | "title": "SoundCloud Case Study: Wildlife Control by SoundCloud Community", 30 | "description": "Listen to how Wildlife Control makes the most of the SoundCloud platform, and it's API.", 31 | "thumbnail_url": "https://i1.sndcdn.com/artworks-000042390403-ouou1g-t500x500.jpg", 32 | "html": '', 33 | "author_name": "SoundCloud Community", 34 | "author_url": "https://soundcloud.com/community", 35 | } 36 | ), 37 | ), 38 | ( 39 | "https://soundcloud.com/matej-roman/jaromir-nohavica-karel-plihal-mikymauz", 40 | "7834701", 41 | "https://soundcloud.com/oembed?format=json&url=https%3A%2F%2Fsoundcloud.com%2Fmatej-roman%2Fjaromir-nohavica-karel-plihal-mikymauz", 42 | dumps( 43 | { 44 | "version": 1.0, 45 | "type": "rich", 46 | "provider_name": "SoundCloud", 47 | "provider_url": "https://soundcloud.com", 48 | "height": 400, 49 | "width": "100%", 50 | "title": "Jaromír Nohavica, Karel Plíhal - Mikymauz by Matěj Roman", 51 | "description": "", 52 | "thumbnail_url": "https://soundcloud.com/images/fb_placeholder.png", 53 | "html": '', 54 | "author_name": "Matěj Roman", 55 | "author_url": "https://soundcloud.com/matej-roman", 56 | } 57 | ), 58 | ), 59 | ( 60 | "https://soundcloud.com/beny97/sets/jaromir-nohavica-prazska", 61 | "960591", 62 | "https://soundcloud.com/oembed?format=json&url=https%3A%2F%2Fsoundcloud.com%2Fbeny97%2Fsets%2Fjaromir-nohavica-prazska", 63 | dumps( 64 | { 65 | "version": 1.0, 66 | "type": "rich", 67 | "provider_name": "SoundCloud", 68 | "provider_url": "https://soundcloud.com", 69 | "height": 450, 70 | "width": "100%", 71 | "title": "Jaromir Nohavica - Prazska palena by beny97", 72 | "description": "", 73 | "thumbnail_url": "https://soundcloud.com/images/fb_placeholder.png", 74 | "html": '', 75 | "author_name": "beny97", 76 | "author_url": "https://soundcloud.com/beny97", 77 | } 78 | ), 79 | ), 80 | ( 81 | "https://soundcloud.com/jet-silver/couleur-3-downtown-boogie-show-prove", 82 | "257485194", 83 | "https://soundcloud.com/oembed?format=json&url=https%3A%2F%2Fsoundcloud.com%2Fjet-silver%2Fcouleur-3-downtown-boogie-show-prove", 84 | dumps( 85 | { 86 | "version": 1.0, 87 | "type": "rich", 88 | "provider_name": "SoundCloud", 89 | "provider_url": "https://soundcloud.com", 90 | "height": 400, 91 | "width": "100%", 92 | "title": "Couleur 3 - Downtown Boogie : Show & Prove by Jet Silver", 93 | "description": "Show & Prove specially recorded for Downtown Boogie, the best hip-hop radio show in Switzerland, on Couleur 3 radio. Shout out to Vincz Lee, Jiggy Jones, Green Giant, Dynamike and Geos for having us one the ones and twos !", 94 | "thumbnail_url": "https://i1.sndcdn.com/artworks-000156604478-47oo6y-t500x500.jpg", 95 | "html": '', 96 | "author_name": "Jet Silver", 97 | "author_url": "https://soundcloud.com/jet-silver", 98 | } 99 | ), 100 | ), 101 | ) 102 | 103 | instance = SoundCloudBackend 104 | 105 | def setUp(self): 106 | class FooBackend(SoundCloudBackend): 107 | url = "foobar" 108 | 109 | def get_info(self): 110 | return { 111 | "width": 123, 112 | "height": 321, 113 | "thumbnail_url": "xyz", 114 | "html": '\u003Ciframe width="100%" height="400" ' 115 | 'scrolling="no" frameborder="no" ' 116 | 'src="{0}"\u003E\u003C/iframe\u003E'.format(self.url), 117 | } 118 | 119 | self.foo = FooBackend("abcd") 120 | 121 | def test_detect(self): 122 | for url in self.urls: 123 | with requests_mock.Mocker() as m: 124 | m.get(url[2], text=url[3]) 125 | backend = detect_backend(url[0]) 126 | self.assertIsInstance(backend, self.instance) 127 | 128 | def test_code(self): 129 | for url in self.urls: 130 | with requests_mock.Mocker() as m: 131 | m.get(url[2], text=url[3]) 132 | backend = self.instance(url[0]) 133 | self.assertEqual(backend.code, url[1]) 134 | 135 | def test_width(self): 136 | self.assertEqual(self.foo.width, 123) 137 | 138 | def test_height(self): 139 | self.assertEqual(self.foo.height, 321) 140 | 141 | def test_get_thumbnail_url(self): 142 | self.assertEqual(self.foo.get_thumbnail_url(), "xyz") 143 | 144 | def test_get_url(self): 145 | self.assertEqual(self.foo.get_url(), self.foo.url) 146 | 147 | @patch("embed_video.backends.EMBED_VIDEO_TIMEOUT", 0.000001) 148 | def test_timeout_in_get_info(self): 149 | backend = SoundCloudBackend( 150 | "https://soundcloud.com/community/soundcloud-case-study-wildlife" 151 | ) 152 | self.assertRaises(requests.Timeout, backend.get_info) 153 | 154 | def test_invalid_url(self): 155 | """Check if bug #21 is fixed.""" 156 | backend = SoundCloudBackend("https://soundcloud.com/xyz/foo") 157 | self.assertRaises(VideoDoesntExistException, backend.get_info) 158 | -------------------------------------------------------------------------------- /embed_video/tests/backends/tests_videobackend.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from embed_video.backends import UnknownBackendException, VideoBackend, detect_backend 4 | 5 | 6 | class VideoBackendTestCase(TestCase): 7 | unknown_backend_urls = ( 8 | "http://myurl.com/?video=http://www.youtube.com/watch?v=jsrRJyHBvzw", 9 | "http://myurl.com/?video=www.youtube.com/watch?v=jsrRJyHBvzw", 10 | "http://youtube.com.myurl.com/watch?v=jsrRJyHBvzw", 11 | "http://vimeo.com.myurl.com/72304002", 12 | ) 13 | 14 | def test_detect_bad_urls(self): 15 | for url in self.unknown_backend_urls: 16 | self.assertRaises(UnknownBackendException, detect_backend, url) 17 | 18 | def test_not_implemented_get_info(self): 19 | backend = VideoBackend("https://www.example.com") 20 | self.assertRaises(NotImplementedError, backend.get_info) 21 | -------------------------------------------------------------------------------- /embed_video/tests/backends/tests_vimeo.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import patch 3 | 4 | import requests 5 | 6 | from embed_video.backends import VideoDoesntExistException, VimeoBackend, detect_backend 7 | 8 | 9 | class VimeoBackendTestCase(TestCase): 10 | urls = ( 11 | ("http://vimeo.com/72304002", "72304002"), 12 | ("https://vimeo.com/72304002", "72304002"), 13 | ("http://www.vimeo.com/72304002", "72304002"), 14 | ("https://www.vimeo.com/72304002", "72304002"), 15 | ("http://player.vimeo.com/video/72304002", "72304002"), 16 | ("https://player.vimeo.com/video/72304002", "72304002"), 17 | ("http://www.vimeo.com/channels/staffpick/72304002", "72304002"), 18 | ("https://www.vimeo.com/channels/staffpick/72304002", "72304002"), 19 | ("https://vimeo.com/exampleusername/review/72304002/a1b2c3d4", "72304002"), 20 | ("https://vimeo.com/manage/72304002/general", "72304002"), 21 | ) 22 | 23 | instance = VimeoBackend 24 | 25 | def test_detect(self): 26 | for url in self.urls: 27 | backend = detect_backend(url[0]) 28 | self.assertIsInstance(backend, self.instance) 29 | 30 | def test_code(self): 31 | for url in self.urls: 32 | backend = self.instance(url[0]) 33 | self.assertEqual(backend.code, url[1]) 34 | 35 | def test_vimeo_get_info_exception(self): 36 | with self.assertRaises(VideoDoesntExistException): 37 | backend = VimeoBackend("https://vimeo.com/123") 38 | backend.get_info() 39 | 40 | def test_get_thumbnail_url(self): 41 | backend = VimeoBackend("https://vimeo.com/72304002") 42 | self.assertEqual( 43 | backend.get_thumbnail_url(), 44 | "https://i.vimeocdn.com/video/446150690-9621b882540b53788eaa36ef8e303d4e06fc40af3d27918b7f561bb44ed971dc-d_640", 45 | ) 46 | 47 | @patch("embed_video.backends.EMBED_VIDEO_TIMEOUT", 0.000001) 48 | def test_timeout_in_get_info(self): 49 | backend = VimeoBackend("https://vimeo.com/72304002") 50 | self.assertRaises(requests.Timeout, backend.get_info) 51 | -------------------------------------------------------------------------------- /embed_video/tests/backends/tests_youtube.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import patch 3 | 4 | from embed_video.backends import UnknownIdException, YoutubeBackend, detect_backend 5 | 6 | 7 | class YoutubeBackendTestCase(TestCase): 8 | urls = ( 9 | ("http://youtu.be/jsrRJyHBvzw", "jsrRJyHBvzw"), 10 | ("http://youtu.be/n17B_uFF4cA", "n17B_uFF4cA"), 11 | ("http://youtu.be/t-ZRX8984sc", "t-ZRX8984sc"), 12 | ("https://youtu.be/t-ZRX8984sc", "t-ZRX8984sc"), 13 | ("http://youtube.com/watch?v=jsrRJyHBvzw", "jsrRJyHBvzw"), 14 | ("https://youtube.com/watch?v=jsrRJyHBvzw", "jsrRJyHBvzw"), 15 | ("http://www.youtube.com/v/0zM3nApSvMg?rel=0", "0zM3nApSvMg"), 16 | ("https://www.youtube.com/v/0zM3nApSvMg?rel=0", "0zM3nApSvMg"), 17 | ("http://www.youtube.com/embed/0zM3nApSvMg?rel=0", "0zM3nApSvMg"), 18 | ("https://www.youtube.com/embed/0zM3nApSvMg?rel=0", "0zM3nApSvMg"), 19 | ("http://www.youtube.com/watch?v=jsrRJyHBvzw", "jsrRJyHBvzw"), 20 | ("https://www.youtube.com/watch?v=t-ZRX8984sc", "t-ZRX8984sc"), 21 | ("http://www.youtube.com/watch?v=iwGFalTRHDA&feature=related", "iwGFalTRHDA"), 22 | ("https://www.youtube.com/watch?v=iwGFalTRHDA&feature=related", "iwGFalTRHDA"), 23 | ( 24 | "http://www.youtube.com/watch?feature=player_embedded&v=2NpZbaAIXag", 25 | "2NpZbaAIXag", 26 | ), 27 | ( 28 | "https://www.youtube.com/watch?feature=player_embedded&v=2NpZbaAIXag", 29 | "2NpZbaAIXag", 30 | ), 31 | ( 32 | "https://www.youtube.com/watch?v=XPk521voaOE&feature=youtube_gdata_player", 33 | "XPk521voaOE", 34 | ), 35 | ( 36 | "http://www.youtube.com/watch?v=6xu00J3-g2s&list=PLb5n6wzDlPakFKvJ69rJ9AJW24Aaaki2z", 37 | "6xu00J3-g2s", 38 | ), 39 | ("https://m.youtube.com/#/watch?v=IAooXLAPoBQ", "IAooXLAPoBQ"), 40 | ("https://m.youtube.com/watch?v=IAooXLAPoBQ", "IAooXLAPoBQ"), 41 | ("https://www.youtube.com/shorts/cOoQ7pc0CoY", "cOoQ7pc0CoY"), 42 | ("https://youtube.com/shorts/cOoQ7pc0CoY", "cOoQ7pc0CoY"), 43 | ) 44 | 45 | instance = YoutubeBackend 46 | 47 | def test_detect(self): 48 | for url in self.urls: 49 | backend = detect_backend(url[0]) 50 | self.assertIsInstance(backend, self.instance) 51 | 52 | def test_code(self): 53 | for url in self.urls: 54 | backend = self.instance(url[0]) 55 | self.assertEqual(backend.code, url[1]) 56 | 57 | def test_youtube_keyerror(self): 58 | """Test for issue #7""" 59 | backend = self.instance("http://youtube.com/watch?id=5") 60 | self.assertRaises(UnknownIdException, backend.get_code) 61 | 62 | def test_thumbnail(self): 63 | for url in self.urls: 64 | backend = self.instance(url[0]) 65 | self.assertIn(url[1], backend.thumbnail) 66 | 67 | def test_get_better_resolution_youtube(self): 68 | backend = self.instance("https://www.youtube.com/watch?v=1Zo0-sWD7xE") 69 | self.assertIn( 70 | "img.youtube.com/vi/1Zo0-sWD7xE/maxresdefault.jpg", backend.thumbnail 71 | ) 72 | 73 | @patch("embed_video.backends.EMBED_VIDEO_YOUTUBE_CHECK_THUMBNAIL", False) 74 | def test_youtube_not_check_thumbnail(self): 75 | backend = self.instance("https://www.youtube.com/watch?v=not-exist") 76 | self.assertIn("img.youtube.com/vi/not-exist/hqdefault.jpg", backend.thumbnail) 77 | -------------------------------------------------------------------------------- /embed_video/tests/django_settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | DEBUG = True 4 | SECRET_KEY = "testing_key123" 5 | 6 | STATIC_ROOT = MEDIA_ROOT = os.path.join(os.path.dirname(__file__), "static") 7 | STATIC_URL = MEDIA_URL = "/static/" 8 | 9 | TEMPLATES = [ 10 | { 11 | "BACKEND": "django.template.backends.django.DjangoTemplates", 12 | "APP_DIRS": True, 13 | "OPTIONS": { 14 | "context_processors": [ 15 | "django.contrib.auth.context_processors.auth", 16 | "django.template.context_processors.debug", 17 | "django.template.context_processors.i18n", 18 | "django.template.context_processors.media", 19 | "django.template.context_processors.static", 20 | "django.template.context_processors.tz", 21 | "django.contrib.messages.context_processors.messages", 22 | ] 23 | }, 24 | } 25 | ] 26 | 27 | INSTALLED_APPS = ("django.contrib.contenttypes", "django.contrib.auth", "embed_video") 28 | 29 | 30 | EMBED_VIDEO_BACKENDS = ( 31 | "embed_video.backends.YoutubeBackend", 32 | "embed_video.backends.VimeoBackend", 33 | "embed_video.backends.SoundCloudBackend", 34 | "embed_video.tests.backends.tests_custom_backend.CustomBackend", 35 | ) 36 | 37 | 38 | LOGGING = { 39 | "version": 1, 40 | "disable_existing_loggers": False, 41 | "handlers": {"console": {"level": "DEBUG", "class": "logging.StreamHandler"}}, 42 | "loggers": {"less": {"handlers": ["console"], "level": "DEBUG"}}, 43 | } 44 | -------------------------------------------------------------------------------- /embed_video/tests/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-embed-video/fb64ba0ef06b4a6518ddcb8f1f29679285c38f91/embed_video/tests/templatetags/__init__.py -------------------------------------------------------------------------------- /embed_video/tests/templatetags/tests_embed_video_tags.py: -------------------------------------------------------------------------------- 1 | import re 2 | import urllib.parse as urlparse 3 | from json import dumps 4 | from unittest import TestCase 5 | from unittest.mock import Mock, patch 6 | 7 | import requests_mock 8 | from django.http import HttpRequest 9 | from django.template import TemplateSyntaxError 10 | from django.template.base import Template 11 | from django.template.context import RequestContext 12 | from django.test.client import RequestFactory 13 | 14 | from embed_video.templatetags.embed_video_tags import VideoNode 15 | 16 | URL_PATTERN = re.compile(r'src="?\'?([^"\'>]*)"') 17 | 18 | 19 | class EmbedTestCase(TestCase): 20 | def render_template(self, template_string, context=None): 21 | response = RequestContext(HttpRequest(), context) 22 | return Template(template_string).render(response).strip() 23 | 24 | def assertRenderedTemplate(self, template_string, output, context=None): 25 | rendered_output = self.render_template(template_string, context=context) 26 | self.assertEqual(rendered_output, output.strip()) 27 | 28 | def url_dict(self, url): 29 | """ 30 | Parse the URL into a format suitable for comparison, ignoring the query 31 | parameter order. 32 | """ 33 | 34 | parsed = urlparse.urlparse(url) 35 | query = urlparse.parse_qs(parsed.query) 36 | 37 | return { 38 | "scheme": parsed.scheme, 39 | "netloc": parsed.netloc, 40 | "path": parsed.path, 41 | "params": parsed.params, 42 | "query": query, 43 | "fragment": parsed.fragment, 44 | } 45 | 46 | def assertUrlEqual(self, actual, expected, msg=None): 47 | """Assert two URLs are equal, ignoring the query parameter order.""" 48 | actual_dict = self.url_dict(actual) 49 | expected_dict = self.url_dict(expected) 50 | 51 | self.assertEqual(actual_dict, expected_dict, msg=msg) 52 | 53 | def test_embed(self): 54 | template = """ 55 | {% load embed_video_tags %} 56 | {% video 'http://www.youtube.com/watch?v=jsrRJyHBvzw' as ytb %} 57 | {% video ytb 'large' %} 58 | {% endvideo %} 59 | """ 60 | self.assertRenderedTemplate( 61 | template, 62 | '', 65 | ) 66 | 67 | def test_embed_invalid_url(self): 68 | template = """ 69 | {% load embed_video_tags %} 70 | {% video 'http://www.youtube.com/edit?abcd=efgh' as ytb %} 71 | {{ ytb.url }} 72 | {% endvideo %} 73 | """ 74 | self.assertRenderedTemplate(template, "") 75 | 76 | def test_embed_with_none_instance(self): 77 | template = """ 78 | {% with None as my_video %} 79 | {% load embed_video_tags %} 80 | {% video my_video %}{% endwith %} 81 | """ 82 | self.assertRenderedTemplate(template, "") 83 | 84 | def test_embed_empty_string(self): 85 | template = """ 86 | {% load embed_video_tags %} 87 | {% video '' 'large' %} 88 | """ 89 | self.assertRenderedTemplate(template, "") 90 | 91 | def test_direct_embed_tag(self): 92 | template = """ 93 | {% load embed_video_tags %} 94 | {% video "http://www.youtube.com/watch?v=jsrRJyHBvzw" "large" %} 95 | """ 96 | self.assertRenderedTemplate( 97 | template, 98 | '', 101 | ) 102 | 103 | def test_direct_embed_tag_with_default_size(self): 104 | template = """ 105 | {% load embed_video_tags %} 106 | {% video "http://www.youtube.com/watch?v=jsrRJyHBvzw" %} 107 | """ 108 | self.assertRenderedTemplate( 109 | template, 110 | '', 113 | ) 114 | 115 | def test_direct_embed_invalid_url(self): 116 | template = """ 117 | {% load embed_video_tags %} 118 | {% video "https://soundcloud.com/xyz/foo" %} 119 | """ 120 | self.assertRenderedTemplate(template, "") 121 | 122 | def test_user_size(self): 123 | template = """ 124 | {% load embed_video_tags %} 125 | {% video 'http://www.youtube.com/watch?v=jsrRJyHBvzw' as ytb %} 126 | {% video ytb '800x800' %} 127 | {% endvideo %} 128 | """ 129 | self.assertRenderedTemplate( 130 | template, 131 | '', 134 | ) 135 | 136 | def test_wrong_size(self): 137 | template = Template( 138 | """ 139 | {% load embed_video_tags %} 140 | {% video 'http://www.youtube.com/watch?v=jsrRJyHBvzw' 'so x huge' %} 141 | """ 142 | ) 143 | request = RequestContext(HttpRequest()) 144 | self.assertRaises(TemplateSyntaxError, template.render, request) 145 | 146 | def test_tag_youtube(self): 147 | template = """ 148 | {% load embed_video_tags %} 149 | {% video 'http://www.youtube.com/watch?v=jsrRJyHBvzw' as ytb %} 150 | {{ ytb.url }} {{ ytb.backend }} 151 | {% endvideo %} 152 | """ 153 | self.assertRenderedTemplate( 154 | template, 155 | "https://www.youtube.com/embed/jsrRJyHBvzw?wmode=opaque " "YoutubeBackend", 156 | ) 157 | 158 | def test_tag_vimeo(self): 159 | template = """ 160 | {% load embed_video_tags %} 161 | {% video 'https://vimeo.com/72304002' as vimeo %} 162 | {{ vimeo.url }} {{ vimeo.backend }} {{ vimeo.info.duration }} 163 | {% endvideo %} 164 | """ 165 | self.assertRenderedTemplate( 166 | template, "https://player.vimeo.com/video/72304002 VimeoBackend 176" 167 | ) 168 | 169 | def test_tag_soundcloud(self): 170 | template = """ 171 | {% load embed_video_tags %} 172 | {% video 'https://soundcloud.com/community/soundcloud-case-study-wildlife' as soundcloud %} 173 | {{ soundcloud.url }} {{ soundcloud.backend }} 174 | {% endvideo %} 175 | """ 176 | 177 | # Soundcloud backend generates the following embed URL and fetches the payload from it 178 | # you can check the URL contents and update it as needed 179 | url = "https://soundcloud.com/oembed?format=json&url=https%3A%2F%2Fsoundcloud.com%2Fcommunity%2Fsoundcloud-case-study-wildlife" 180 | response = dumps( 181 | { 182 | "version": 1.0, 183 | "type": "rich", 184 | "provider_name": "SoundCloud", 185 | "provider_url": "https://soundcloud.com", 186 | "height": 400, 187 | "width": "100%", 188 | "title": "SoundCloud Case Study: Wildlife Control by SoundCloud Community", 189 | "description": "Listen to how Wildlife Control makes the most of the SoundCloud platform, and it's API.", 190 | "thumbnail_url": "https://i1.sndcdn.com/artworks-000042390403-ouou1g-t500x500.jpg", 191 | "html": "", 195 | "author_name": "SoundCloud Community", 196 | "author_url": "https://soundcloud.com/community", 197 | } 198 | ) 199 | 200 | with requests_mock.Mocker() as m: 201 | m.get(url, text=response) 202 | self.assertRenderedTemplate( 203 | template, 204 | "https://w.soundcloud.com/player/?visual=true&url=https%3A%2F%2Fapi.soundcloud.com%2Ftracks%2F82244706&show_artwork=true " 205 | "SoundCloudBackend", 206 | ) 207 | 208 | @patch("embed_video.backends.EMBED_VIDEO_TIMEOUT", 0.000001) 209 | @patch("urllib3.connectionpool.log") 210 | @patch("embed_video.templatetags.embed_video_tags.logger") 211 | def test_empty_if_timeout(self, embed_video_logger, urllib_logger): 212 | template = """ 213 | {% load embed_video_tags %} 214 | {% video "http://vimeo.com/72304002" as my_video %} 215 | {{ my_video.thumbnail }} 216 | {% endvideo %} 217 | """ 218 | 219 | self.assertRenderedTemplate(template, "") 220 | 221 | urllib_logger.debug.assert_called_with( 222 | "Starting new HTTPS connection (%d): %s:%s", 1, "vimeo.com", 443 223 | ) 224 | 225 | embed_video_logger.exception.assert_called_with( 226 | "Timeout reached during rendering embed video (`http://vimeo.com/72304002`)" 227 | ) 228 | 229 | def test_relative_size(self): 230 | template = """ 231 | {% load embed_video_tags %} 232 | {% video "http://vimeo.com/72304002" "80%x30%" %} 233 | """ 234 | self.assertRenderedTemplate( 235 | template, 236 | '', 239 | ) 240 | 241 | def test_allow_spaces_in_size(self): 242 | template = """ 243 | {% load embed_video_tags %} 244 | {% video "http://vimeo.com/72304002" "80% x 300" %} 245 | """ 246 | self.assertRenderedTemplate( 247 | template, 248 | '', 251 | ) 252 | 253 | def test_embed_with_query(self): 254 | template = """ 255 | {% load embed_video_tags %} 256 | {% video 'http://www.youtube.com/watch?v=jsrRJyHBvzw' query="rel=1&wmode=transparent" as ytb %} 257 | {{ ytb.url }} 258 | {% endvideo %} 259 | """ 260 | 261 | output = self.render_template(template) 262 | self.assertUrlEqual( 263 | output, "https://www.youtube.com/embed/jsrRJyHBvzw?rel=1&wmode=transparent" 264 | ) 265 | 266 | def test_direct_embed_with_query(self): 267 | template = """ 268 | {% load embed_video_tags %} 269 | {% video 'http://www.youtube.com/watch?v=jsrRJyHBvzw' query="rel=1&wmode=transparent" %} 270 | """ 271 | 272 | output = self.render_template(template) 273 | 274 | # The order of query parameters in the URL might change between Python 275 | # versions. Compare the URL and the outer part separately. 276 | 277 | url_pattern = re.compile(r'http[^"]+') 278 | url = url_pattern.search(output).group(0) 279 | 280 | self.assertUrlEqual( 281 | url, "https://www.youtube.com/embed/jsrRJyHBvzw?rel=1&wmode=transparent" 282 | ) 283 | 284 | output_without_url = url_pattern.sub("URL", output) 285 | 286 | self.assertEqual( 287 | output_without_url, 288 | '', 291 | ) 292 | 293 | def test_set_options(self): 294 | template = """ 295 | {% load embed_video_tags %} 296 | {% video 'http://www.youtube.com/watch?v=jsrRJyHBvzw' "300x200" is_secure=True query="rel=1" %} 297 | """ 298 | self.assertRenderedTemplate( 299 | template, 300 | '', 303 | ) 304 | 305 | def test_size_as_variable(self): 306 | template = """ 307 | {% load embed_video_tags %} 308 | {% with size="500x200" %} 309 | {% video 'http://www.youtube.com/watch?v=jsrRJyHBvzw' size %} 310 | {% endwith %} 311 | """ 312 | self.assertRenderedTemplate( 313 | template, 314 | '', 317 | ) 318 | 319 | 320 | class EmbedVideoNodeTestCase(TestCase): 321 | def setUp(self): 322 | self.parser = Mock() 323 | self.token = Mock(methods=["split_contents"]) 324 | 325 | def test_repr(self): 326 | self.token.split_contents.return_value = ( 327 | "video", 328 | "http://youtu.be/v/1234", 329 | "as", 330 | "myvideo", 331 | ) 332 | self.parser.compile_filter.return_value = "some_url" 333 | 334 | node = VideoNode(self.parser, self.token) 335 | self.assertEqual(str(node), '') 336 | 337 | def test_videonode_iter(self): 338 | out = ["a", "b", "c", "d"] 339 | 340 | class FooNode(VideoNode): 341 | nodelist_file = out 342 | 343 | def __init__(self): 344 | pass 345 | 346 | node = FooNode() 347 | self.assertEqual(out, [x for x in node]) 348 | 349 | def test_get_backend_secure(self): 350 | class SecureRequest(RequestFactory): 351 | is_secure = lambda x: True 352 | 353 | context = {"request": SecureRequest()} 354 | backend = VideoNode.get_backend( 355 | "http://www.youtube.com/watch?v=jsrRJyHBvzw", context 356 | ) 357 | self.assertTrue(backend.is_secure) 358 | 359 | def test_get_backend_insecure(self): 360 | class InsecureRequest(RequestFactory): 361 | is_secure = lambda x: False 362 | 363 | context = {"request": InsecureRequest()} 364 | backend = VideoNode.get_backend( 365 | "http://www.youtube.com/watch?v=jsrRJyHBvzw", context 366 | ) 367 | self.assertFalse(backend.is_secure) 368 | -------------------------------------------------------------------------------- /embed_video/tests/tests_admin.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from django.test import SimpleTestCase 4 | 5 | from embed_video.admin import AdminVideoMixin, AdminVideoWidget 6 | from embed_video.backends import VimeoBackend 7 | from embed_video.fields import EmbedVideoField, EmbedVideoFormField 8 | 9 | 10 | class AdminVideoWidgetTestCase(SimpleTestCase): 11 | def test_size(self): 12 | widget = AdminVideoWidget() 13 | self.assertTrue("size" in widget.attrs) 14 | 15 | def test_overwrite_size(self): 16 | widget = AdminVideoWidget(attrs={"size": 0}) 17 | self.assertEqual(widget.attrs["size"], 0) 18 | 19 | def test_add_to_attrs(self): 20 | widget = AdminVideoWidget(attrs={"name": "foo"}) 21 | self.assertEqual(widget.attrs["name"], "foo") 22 | self.assertTrue("size" in widget.attrs) 23 | 24 | def test_render_empty_value(self): 25 | widget = AdminVideoWidget(attrs={"size": "0"}) 26 | self.assertHTMLEqual( 27 | widget.render("foo"), '' 28 | ) 29 | 30 | def test_render(self): 31 | backend = VimeoBackend("https://vimeo.com/1") 32 | widget = AdminVideoWidget(attrs={"size": "0"}) 33 | widget.output_format = "{video}{input}" 34 | 35 | self.assertHTMLEqual( 36 | widget.render("foo", backend.url, size=(100, 100)), 37 | backend.get_embed_code(100, 100) 38 | + '' % backend.url, 39 | ) 40 | 41 | def test_render_unknown_backend(self): 42 | widget = AdminVideoWidget() 43 | self.assertHTMLEqual( 44 | widget.render("foo", "abcd"), 45 | '', 46 | ) 47 | 48 | def test_render_video_doesnt_exist(self): 49 | widget = AdminVideoWidget() 50 | self.assertHTMLEqual( 51 | widget.render("foo", "https://soundcloud.com/xyz/foo"), 52 | '', 53 | ) 54 | 55 | 56 | class AdminVideoMixinTestCase(TestCase): 57 | def test_embedvideofield(self): 58 | foo = EmbedVideoField() 59 | mixin = AdminVideoMixin() 60 | self.assertTrue( 61 | isinstance(mixin.formfield_for_dbfield(foo), EmbedVideoFormField) 62 | ) 63 | 64 | def test_other_fields(self): 65 | class Parent: 66 | def formfield_for_dbfield(*args, **kwargs): 67 | raise Exception 68 | 69 | class MyAdmin(AdminVideoMixin, Parent): 70 | pass 71 | 72 | myadmin = MyAdmin() 73 | self.assertRaises(Exception, myadmin.formfield_for_dbfield, "foo") 74 | -------------------------------------------------------------------------------- /embed_video/tests/tests_fields.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import patch 3 | 4 | from django.forms import ValidationError 5 | 6 | from embed_video.backends import ( 7 | UnknownBackendException, 8 | UnknownIdException, 9 | YoutubeBackend, 10 | ) 11 | from embed_video.fields import EmbedVideoField, EmbedVideoFormField 12 | 13 | 14 | class EmbedVideoFieldTestCase(TestCase): 15 | def setUp(self): 16 | self.field = EmbedVideoField() 17 | 18 | def test_formfield_form_class(self): 19 | self.assertIsInstance(self.field.formfield(), EmbedVideoFormField) 20 | 21 | 22 | class EmbedVideoFormFieldTestCase(TestCase): 23 | def setUp(self): 24 | self.formfield = EmbedVideoFormField() 25 | 26 | def test_validation_unknown_backend(self): 27 | with patch("embed_video.fields.detect_backend") as mock_detect_backend: 28 | mock_detect_backend.return_value = True 29 | mock_detect_backend.side_effect = UnknownBackendException 30 | self.assertRaises( 31 | ValidationError, self.formfield.validate, ("http://youtube.com/v/123/",) 32 | ) 33 | 34 | def test_validation_unknown_id(self): 35 | with patch("embed_video.fields.detect_backend") as mock_detect_backend: 36 | mock_detect_backend.return_value = True 37 | mock_detect_backend.side_effect = UnknownIdException 38 | self.assertRaises( 39 | ValidationError, self.formfield.validate, ("http://youtube.com/v/123/",) 40 | ) 41 | 42 | def test_validation_correct(self): 43 | url = "http://www.youtube.com/watch?v=gauN0gzxTcU" 44 | with patch("embed_video.fields.detect_backend") as mock_detect_backend: 45 | mock_detect_backend.return_value = YoutubeBackend(url) 46 | self.assertEqual(url, self.formfield.validate(url)) 47 | 48 | def test_validation_unknown_code(self): 49 | url = "http://www.youtube.com/edit?abcd=abcd" 50 | self.assertRaises(ValidationError, self.formfield.validate, url) 51 | 52 | def test_validation_super(self): 53 | self.assertRaises(ValidationError, self.formfield.validate, "") 54 | 55 | def test_validation_allowed_empty(self): 56 | formfield = EmbedVideoFormField(required=False) 57 | self.assertIsNone(formfield.validate("")) 58 | -------------------------------------------------------------------------------- /example_project/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite3 2 | -------------------------------------------------------------------------------- /example_project/README.rst: -------------------------------------------------------------------------------- 1 | Running example project 2 | *********************** 3 | 4 | #. Install requirements:: 5 | 6 | pip install -r requirements.txt 7 | 8 | #. Create database:: 9 | 10 | python manage.py migrate 11 | 12 | #. Load sample data:: 13 | 14 | python manage.py loaddata initial_data 15 | 16 | #. Run testing server:: 17 | 18 | python manage.py runserver 19 | 20 | #. Take a look at http://localhost:8000 . You can log in to administration with username ``admin`` 21 | and password ``admin``. 22 | 23 | 24 | Testing HTTPS 25 | ************* 26 | 27 | To test HTTPS on development server, `follow this instructions 28 | `_. 29 | -------------------------------------------------------------------------------- /example_project/embed_video: -------------------------------------------------------------------------------- 1 | ../embed_video -------------------------------------------------------------------------------- /example_project/example_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-embed-video/fb64ba0ef06b4a6518ddcb8f1f29679285c38f91/example_project/example_project/__init__.py -------------------------------------------------------------------------------- /example_project/example_project/fixtures/initial_data.yaml: -------------------------------------------------------------------------------- 1 | - fields: 2 | date_joined: 2013-12-01 05:25:05.084549 3 | email: admin@example.com 4 | first_name: 'John' 5 | groups: [] 6 | is_active: true 7 | is_staff: true 8 | is_superuser: true 9 | last_login: 2013-12-01 05:25:09.185561 10 | last_name: 'Smith' 11 | password: pbkdf2_sha256$12000$scfDItXHnci7$ZloMPgY2YAhaFelKGqZ7eJB3mCYbxwua8TxNnCerwP8= 12 | user_permissions: [] 13 | username: admin 14 | model: auth.user 15 | pk: 1 16 | -------------------------------------------------------------------------------- /example_project/example_project/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-embed-video/fb64ba0ef06b4a6518ddcb8f1f29679285c38f91/example_project/example_project/models.py -------------------------------------------------------------------------------- /example_project/example_project/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for example_project project. 2 | import django 3 | 4 | DEBUG = True 5 | 6 | DATABASES = { 7 | "default": { 8 | "ENGINE": "django.db.backends.sqlite3", 9 | "NAME": "example_project.sqlite3", 10 | } 11 | } 12 | 13 | SITE_ID = 1 14 | 15 | SECRET_KEY = "u%38dln@$1!7w#cxi4np504^sa3_skv5aekad)jy_u0v2mc+nr" 16 | 17 | TEMPLATES = [ 18 | { 19 | "BACKEND": "django.template.backends.django.DjangoTemplates", 20 | "APP_DIRS": True, 21 | "OPTIONS": { 22 | "context_processors": [ 23 | "django.template.context_processors.debug", 24 | "django.template.context_processors.i18n", 25 | "django.template.context_processors.media", 26 | "django.template.context_processors.static", 27 | "django.template.context_processors.tz", 28 | "django.template.context_processors.request", 29 | "django.contrib.auth.context_processors.auth", 30 | "django.contrib.messages.context_processors.messages", 31 | ] 32 | }, 33 | } 34 | ] 35 | 36 | MIDDLEWARE = [ 37 | "django.middleware.security.SecurityMiddleware", 38 | "django.contrib.sessions.middleware.SessionMiddleware", 39 | "django.middleware.common.CommonMiddleware", 40 | "django.middleware.csrf.CsrfViewMiddleware", 41 | "django.contrib.auth.middleware.AuthenticationMiddleware", 42 | "django.contrib.messages.middleware.MessageMiddleware", 43 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 44 | ] 45 | 46 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 47 | 48 | APPEND_SLASH = True 49 | 50 | ROOT_URLCONF = "example_project.urls" 51 | 52 | STATIC_URL = "/static/" 53 | 54 | DJANGO_APPS = ( 55 | "django.contrib.auth", 56 | "django.contrib.contenttypes", 57 | "django.contrib.sessions", 58 | "django.contrib.sites", 59 | "django.contrib.messages", 60 | "django.contrib.admin", 61 | ) 62 | 63 | THIRD_PARTY_APPS = ("embed_video",) 64 | 65 | LOCAL_APPS = ("example_project", "posts") 66 | 67 | INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS 68 | -------------------------------------------------------------------------------- /example_project/example_project/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | django-embed-video example project 9 | 10 | 11 | 21 | 22 | 23 | 24 |
25 | {% block content %}{% endblock %} 26 |
27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /example_project/example_project/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 2 | from django.urls import include, path 3 | 4 | from django.contrib import admin 5 | 6 | admin.autodiscover() 7 | 8 | urlpatterns = staticfiles_urlpatterns() + [ 9 | path("admin/", admin.site.urls), 10 | path("", include(("posts.urls", "posts"))), 11 | ] 12 | -------------------------------------------------------------------------------- /example_project/example_project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for example_project project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks 19 | # if running multiple sites in the same mod_wsgi process. To fix this, use 20 | # mod_wsgi daemon mode with each site in its own daemon process, or use 21 | # os.environ["DJANGO_SETTINGS_MODULE"] = "example_project.settings" 22 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings") 23 | 24 | # This application object is used by any WSGI server configured to use this 25 | # file. This includes Django's development server, if the WSGI_APPLICATION 26 | # setting points here. 27 | from django.core.wsgi import get_wsgi_application 28 | 29 | application = get_wsgi_application() 30 | 31 | # Apply WSGI middleware here. 32 | # from helloworld.wsgi import HelloWorldApplication 33 | # application = HelloWorldApplication(application) 34 | -------------------------------------------------------------------------------- /example_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /example_project/posts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-embed-video/fb64ba0ef06b4a6518ddcb8f1f29679285c38f91/example_project/posts/__init__.py -------------------------------------------------------------------------------- /example_project/posts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from embed_video.admin import AdminVideoMixin 4 | 5 | from .models import Post 6 | 7 | 8 | class PostAdmin(AdminVideoMixin, admin.ModelAdmin): 9 | pass 10 | 11 | 12 | admin.site.register(Post, PostAdmin) 13 | -------------------------------------------------------------------------------- /example_project/posts/fixtures/initial_data.yaml: -------------------------------------------------------------------------------- 1 | - fields: {title: I will wait, video: 'http://www.youtube.com/watch?v=rGKfrgqWcv0'} 2 | model: posts.post 3 | pk: 1 4 | - fields: {title: Alma, video: 'https://vimeo.com/4749536'} 5 | model: posts.post 6 | pk: 2 7 | - fields: {title: I Got Rhythm, video: 'https://www.youtube.com/watch?v=QAaOI0cAjhc'} 8 | model: posts.post 9 | pk: 3 10 | - fields: {title: It Had To Be You, video: 'https://www.youtube.com/watch?v=bbDqp1JV5lU'} 11 | model: posts.post 12 | pk: 4 13 | - fields: {title: Harlem Swing, video: 'https://soundcloud.com/djangoreinhardt/harlem-swing-5'} 14 | model: posts.post 15 | pk: 5 16 | - fields: {title: Yours and Mine, video: 'https://soundcloud.com/djangoreinhardt/yours-and-mine-704002798'} 17 | model: posts.post 18 | pk: 6 19 | - fields: {title: Festival Swing, video: 'https://soundcloud.com/djangoreinhardt/festival-swing-514690263'} 20 | model: posts.post 21 | pk: 7 22 | - fields: {title: Stubborn love, video: 'http://youtu.be/UJWk_KNbDHo'} 23 | model: posts.post 24 | pk: 8 25 | - fields: {title: Timelapse, video: 'http://vimeo.com/14352658'} 26 | model: posts.post 27 | pk: 9 28 | - fields: {title: Slovakia, video: 'https://vimeo.com/72304002'} 29 | model: posts.post 30 | pk: 10 31 | - fields: {title: Django Reinhardt, video: 'https://www.youtube.com/watch?v=ceqeR49t7I4'} 32 | model: posts.post 33 | pk: 11 34 | - fields: {title: What is Django, video: 'https://www.youtube.com/watch?v=c5HlCEjxQ9o&list=PLkQQsbwLvxCXWvKbcSBd7F7qg3x--A-v6'} 35 | model: posts.post 36 | pk: 12 37 | -------------------------------------------------------------------------------- /example_project/posts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.5 on 2022-06-26 08:55 2 | 3 | from django.db import migrations, models 4 | import embed_video.fields 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Post', 17 | fields=[ 18 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('title', models.CharField(max_length=50)), 20 | ('video', embed_video.fields.EmbedVideoField(help_text='This is a help text', verbose_name='My video')), 21 | ], 22 | options={'ordering': ['pk']}, 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /example_project/posts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jazzband/django-embed-video/fb64ba0ef06b4a6518ddcb8f1f29679285c38f91/example_project/posts/migrations/__init__.py -------------------------------------------------------------------------------- /example_project/posts/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.urls import reverse 3 | 4 | from embed_video.fields import EmbedVideoField 5 | 6 | 7 | class Post(models.Model): 8 | title = models.CharField(max_length=50) 9 | video = EmbedVideoField(verbose_name="My video", help_text="This is a help text") 10 | 11 | def __unicode__(self): 12 | return self.title 13 | 14 | def get_absolute_url(self): 15 | return reverse("posts:detail", kwargs={"pk": self.pk}) 16 | 17 | class Meta: 18 | ordering = ["pk"] 19 | -------------------------------------------------------------------------------- /example_project/posts/templates/posts/post_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load embed_video_tags %} 4 | 5 | {% block content %} 6 | 7 |

8 | {{ object.title }} 9 | Back to list 10 |

11 | 12 | {% video object.video as my_video %} 13 |
14 |     Backend: {{ my_video.backend }}
15 |     URL: {{ my_video.url }}
16 |     Code: {{ my_video.code }}
17 |     Thumbnail: {{ my_video.thumbnail }}
18 |     HTTPS: {% if my_video.protocol == 'https' %}Yes{% else %}No{% endif %}
19 | 20 | {% video my_video "large" %} 21 | {% endvideo %} 22 | 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /example_project/posts/templates/posts/post_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% load embed_video_tags %} 4 | 5 | {% block content %} 6 |

Posts

7 | 8 | 9 | {% for post in object_list %} 10 | 11 | {% video post.video as video %} 12 | 13 | 14 | {% endvideo %} 15 | 16 | {% endfor %} 17 |
{{ post.title }}
18 | 19 |
    20 | {% if page_obj.has_previous %} 21 | 22 | {% endif %} 23 | {% if page_obj.has_next %} 24 | 25 | {% endif %} 26 |
27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /example_project/posts/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates writing tests using the unittest module. These will pass 3 | when you run "manage.py test". 4 | 5 | Replace this with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | 11 | class SimpleTest(TestCase): 12 | def test_basic_addition(self): 13 | """ 14 | Tests that 1 + 1 always equals 2. 15 | """ 16 | self.assertEqual(1 + 1, 2) 17 | -------------------------------------------------------------------------------- /example_project/posts/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, re_path 2 | 3 | from .views import PostListView, PostDetailView 4 | 5 | urlpatterns = [ 6 | re_path(r"(?P\d+)/$", PostDetailView.as_view(), name="detail"), 7 | re_path(r"$", PostListView.as_view(), name="list"), 8 | ] 9 | -------------------------------------------------------------------------------- /example_project/posts/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import ListView, DetailView 2 | 3 | from .models import Post 4 | 5 | 6 | class PostListView(ListView): 7 | model = Post 8 | paginate_by = 10 9 | 10 | 11 | class PostDetailView(DetailView): 12 | model = Post 13 | -------------------------------------------------------------------------------- /example_project/requirements.txt: -------------------------------------------------------------------------------- 1 | Django>=3.2 2 | pyyaml 3 | requests 4 | django-embed-video 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import find_packages, setup 4 | 5 | setup( 6 | name="django-embed-video", 7 | packages=find_packages(), 8 | package_data={"embed_video": ["templates/embed_video/*.html"]}, 9 | use_scm_version=True, 10 | author="Cedric Carrard", 11 | author_email="cedric.carrard@gmail.com", 12 | url="https://github.com/jazzband/django-embed-video", 13 | description="Django app for easy embedding YouTube and Vimeo videos and music from SoundCloud.", 14 | long_description="\n".join( 15 | [ 16 | open("README.rst", encoding="utf-8").read(), 17 | open("CHANGES.rst", encoding="utf-8").read(), 18 | ] 19 | ), 20 | classifiers=[ 21 | "Development Status :: 5 - Production/Stable", 22 | "Environment :: Web Environment", 23 | "Environment :: Plugins", 24 | "Framework :: Django", 25 | "Framework :: Django :: 3.2", 26 | "Framework :: Django :: 4.1", 27 | "Framework :: Django :: 4.2", 28 | "Framework :: Django :: 5.0", 29 | "Framework :: Django :: 5.1", 30 | "Intended Audience :: Developers", 31 | "License :: OSI Approved :: MIT License", 32 | "Programming Language :: Python", 33 | "Programming Language :: Python :: 3", 34 | "Programming Language :: Python :: 3.8", 35 | "Programming Language :: Python :: 3.9", 36 | "Programming Language :: Python :: 3.10", 37 | "Programming Language :: Python :: 3.11", 38 | "Programming Language :: Python :: 3.12", 39 | "Programming Language :: Python :: Implementation :: CPython", 40 | "Topic :: Internet :: WWW/HTTP", 41 | ], 42 | keywords=["youtube", "vimeo", "video", "soundcloud"], 43 | install_requires=[ 44 | "requests >= 2.19", 45 | "Django >= 3.2", 46 | "importlib-metadata; python_version < '3.8'", 47 | ], 48 | setup_requires=["readme", "setuptools_scm"], 49 | tests_require=["Django", "requests >= 2.19", "coverage"], 50 | ) 51 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{37,38,39,310}-dj32 4 | py{38,39,310}-dj40 5 | py{38,39,310,311}-dj41 6 | py{38,39,310,311}-dj42 7 | py{310,311,312}-dj50 8 | py{310,311,312}-dj51 9 | py{311}-djmain 10 | py{311}-djqa 11 | 12 | [gh-actions] 13 | python = 14 | 3.7: py37 15 | 3.8: py38 16 | 3.9: py39 17 | 3.10: py310 18 | 3.11: py311 19 | 3.12: py312 20 | 21 | [gh-actions:env] 22 | DJANGO = 23 | 3.2: dj32 24 | 4.0: dj40 25 | 4.1: dj41 26 | 4.2: dj42 27 | 5.0: dj50 28 | 5.1: dj51 29 | main: djmain 30 | qa: djqa 31 | 32 | [testenv] 33 | deps = 34 | dj32: django>=3.2,<3.3 35 | dj40: django>=4.0,<4.1 36 | dj41: django>=4.1,<4.2 37 | dj42: django>=4.2,<4.3 38 | dj50: django>=5.0,<5.1 39 | dj51: django>=5.1,<5.2 40 | djmain: https://github.com/django/django/archive/main.tar.gz 41 | coverage 42 | requests-mock 43 | usedevelop = True 44 | commands = 45 | coverage run -m django test --settings=embed_video.tests.django_settings 46 | coverage report 47 | coverage xml 48 | setenv = 49 | PYTHONDONTWRITEBYTECODE=1 50 | # Django development version is allowed to fail the test matrix 51 | ignore_outcome = 52 | djmain: True 53 | ignore_errors = 54 | djmain: True 55 | 56 | [testenv:py311-djqa] 57 | ignore_errors = true 58 | basepython = 3.11 59 | deps = 60 | black==22.6.0 61 | isort==5.6.4 62 | skip_install = true 63 | commands = 64 | isort --profile black --check-only --diff embed_video setup.py 65 | black -t py38 --check --diff embed_video 66 | --------------------------------------------------------------------------------