Compare commits
83 Commits
7ff2d2e3d5
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| c2bf1c8aaa | |||
| 14be7ef6b4 | |||
| 6885feb980 | |||
| 4cdbffe718 | |||
| f9fadf7f08 | |||
| 8351e6983c | |||
| 93843fdcad | |||
| aa8f4195c7 | |||
| 60accdc618 | |||
| 48473b2a90 | |||
| 926224a6e1 | |||
| 1966bcc371 | |||
| 768def6630 | |||
| 9585eb7416 | |||
| aaea5f4674 | |||
| 8e376319b0 | |||
| 5a87ba6d64 | |||
| 7e7671b6d7 | |||
| ab0148a039 | |||
| ef29d4415a | |||
| d3e071c93d | |||
| 46c6272213 | |||
| 1b480b8ddb | |||
| 7d8eef8387 | |||
| 9f0b6881dd | |||
| c17b558d14 | |||
| c6f2bacf0f | |||
| 2e1c7e7165 | |||
| 019adcba56 | |||
| 8c4ec83993 | |||
| 29fb109b6f | |||
| 8e6a36212d | |||
| 61c178d222 | |||
| 40490d6086 | |||
| e1aff0c7a1 | |||
| 63ce1078e4 | |||
| 2cbd8bf49d | |||
| 1141f6d4a6 | |||
| 8ace8d3312 | |||
| f8a3151672 | |||
| 9b732c55d7 | |||
| a58618c920 | |||
| 046dc94c77 | |||
| 56c7e24969 | |||
| b54ef76881 | |||
| 9492e8445b | |||
| aa4025d138 | |||
| de2d58356b | |||
| 146df2d3b1 | |||
| eb90bca467 | |||
| 5c89842b92 | |||
| 8f3acfef9b | |||
| a92e874371 | |||
| 88de77b261 | |||
| 04c96bcb53 | |||
| 24edb7717a | |||
| c6da876620 | |||
| 1ee58f1b66 | |||
| 7daff47d5c | |||
| 8d80745469 | |||
| b8221ba261 | |||
| fc7af4fa78 | |||
| be747db93f | |||
| 1634bf33c2 | |||
| 8f57e6c0cf | |||
| 8140a77e61 | |||
| 4bff78d0ac | |||
| 3a06e4ec9f | |||
| f5be1b5156 | |||
| 427bf3ac82 | |||
| fcabf013e0 | |||
| 2b5785444f | |||
| c2a9783c3c | |||
| 297a16a0b3 | |||
| 11a7a736c2 | |||
| 2d6e26e474 | |||
| ed1a16b08a | |||
| 287f652fd2 | |||
| ee8d8e0ea8 | |||
| cdca198228 | |||
| 83b3856616 | |||
| a2839c9f91 | |||
| f55a31e12b |
339
LICENSE
Normal file
339
LICENSE
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 2, June 1991
|
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your
|
||||||
|
freedom to share and change it. By contrast, the GNU General Public
|
||||||
|
License is intended to guarantee your freedom to share and change free
|
||||||
|
software--to make sure the software is free for all its users. This
|
||||||
|
General Public License applies to most of the Free Software
|
||||||
|
Foundation's software and to any other program whose authors commit to
|
||||||
|
using it. (Some other Free Software Foundation software is covered by
|
||||||
|
the GNU Lesser General Public License instead.) You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
this service if you wish), that you receive source code or can get it
|
||||||
|
if you want it, that you can change the software or use pieces of it
|
||||||
|
in new free programs; and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid
|
||||||
|
anyone to deny you these rights or to ask you to surrender the rights.
|
||||||
|
These restrictions translate to certain responsibilities for you if you
|
||||||
|
distribute copies of the software, or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must give the recipients all the rights that
|
||||||
|
you have. You must make sure that they, too, receive or can get the
|
||||||
|
source code. And you must show them these terms so they know their
|
||||||
|
rights.
|
||||||
|
|
||||||
|
We protect your rights with two steps: (1) copyright the software, and
|
||||||
|
(2) offer you this license which gives you legal permission to copy,
|
||||||
|
distribute and/or modify the software.
|
||||||
|
|
||||||
|
Also, for each author's protection and ours, we want to make certain
|
||||||
|
that everyone understands that there is no warranty for this free
|
||||||
|
software. If the software is modified by someone else and passed on, we
|
||||||
|
want its recipients to know that what they have is not the original, so
|
||||||
|
that any problems introduced by others will not reflect on the original
|
||||||
|
authors' reputations.
|
||||||
|
|
||||||
|
Finally, any free program is threatened constantly by software
|
||||||
|
patents. We wish to avoid the danger that redistributors of a free
|
||||||
|
program will individually obtain patent licenses, in effect making the
|
||||||
|
program proprietary. To prevent this, we have made it clear that any
|
||||||
|
patent must be licensed for everyone's free use or not licensed at all.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License applies to any program or other work which contains
|
||||||
|
a notice placed by the copyright holder saying it may be distributed
|
||||||
|
under the terms of this General Public License. The "Program", below,
|
||||||
|
refers to any such program or work, and a "work based on the Program"
|
||||||
|
means either the Program or any derivative work under copyright law:
|
||||||
|
that is to say, a work containing the Program or a portion of it,
|
||||||
|
either verbatim or with modifications and/or translated into another
|
||||||
|
language. (Hereinafter, translation is included without limitation in
|
||||||
|
the term "modification".) Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not
|
||||||
|
covered by this License; they are outside its scope. The act of
|
||||||
|
running the Program is not restricted, and the output from the Program
|
||||||
|
is covered only if its contents constitute a work based on the
|
||||||
|
Program (independent of having been made by running the Program).
|
||||||
|
Whether that is true depends on what the Program does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Program's
|
||||||
|
source code as you receive it, in any medium, provided that you
|
||||||
|
conspicuously and appropriately publish on each copy an appropriate
|
||||||
|
copyright notice and disclaimer of warranty; keep intact all the
|
||||||
|
notices that refer to this License and to the absence of any warranty;
|
||||||
|
and give any other recipients of the Program a copy of this License
|
||||||
|
along with the Program.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy, and
|
||||||
|
you may at your option offer warranty protection in exchange for a fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Program or any portion
|
||||||
|
of it, thus forming a work based on the Program, and copy and
|
||||||
|
distribute such modifications or work under the terms of Section 1
|
||||||
|
above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) You must cause the modified files to carry prominent notices
|
||||||
|
stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
b) You must cause any work that you distribute or publish, that in
|
||||||
|
whole or in part contains or is derived from the Program or any
|
||||||
|
part thereof, to be licensed as a whole at no charge to all third
|
||||||
|
parties under the terms of this License.
|
||||||
|
|
||||||
|
c) If the modified program normally reads commands interactively
|
||||||
|
when run, you must cause it, when started running for such
|
||||||
|
interactive use in the most ordinary way, to print or display an
|
||||||
|
announcement including an appropriate copyright notice and a
|
||||||
|
notice that there is no warranty (or else, saying that you provide
|
||||||
|
a warranty) and that users may redistribute the program under
|
||||||
|
these conditions, and telling the user how to view a copy of this
|
||||||
|
License. (Exception: if the Program itself is interactive but
|
||||||
|
does not normally print such an announcement, your work based on
|
||||||
|
the Program is not required to print an announcement.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If
|
||||||
|
identifiable sections of that work are not derived from the Program,
|
||||||
|
and can be reasonably considered independent and separate works in
|
||||||
|
themselves, then this License, and its terms, do not apply to those
|
||||||
|
sections when you distribute them as separate works. But when you
|
||||||
|
distribute the same sections as part of a whole which is a work based
|
||||||
|
on the Program, the distribution of the whole must be on the terms of
|
||||||
|
this License, whose permissions for other licensees extend to the
|
||||||
|
entire whole, and thus to each and every part regardless of who wrote it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest
|
||||||
|
your rights to work written entirely by you; rather, the intent is to
|
||||||
|
exercise the right to control the distribution of derivative or
|
||||||
|
collective works based on the Program.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Program
|
||||||
|
with the Program (or with a work based on the Program) on a volume of
|
||||||
|
a storage or distribution medium does not bring the other work under
|
||||||
|
the scope of this License.
|
||||||
|
|
||||||
|
3. You may copy and distribute the Program (or a work based on it,
|
||||||
|
under Section 2) in object code or executable form under the terms of
|
||||||
|
Sections 1 and 2 above provided that you also do one of the following:
|
||||||
|
|
||||||
|
a) Accompany it with the complete corresponding machine-readable
|
||||||
|
source code, which must be distributed under the terms of Sections
|
||||||
|
1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
b) Accompany it with a written offer, valid for at least three
|
||||||
|
years, to give any third party, for a charge no more than your
|
||||||
|
cost of physically performing source distribution, a complete
|
||||||
|
machine-readable copy of the corresponding source code, to be
|
||||||
|
distributed under the terms of Sections 1 and 2 above on a medium
|
||||||
|
customarily used for software interchange; or,
|
||||||
|
|
||||||
|
c) Accompany it with the information you received as to the offer
|
||||||
|
to distribute corresponding source code. (This alternative is
|
||||||
|
allowed only for noncommercial distribution and only if you
|
||||||
|
received the program in object code or executable form with such
|
||||||
|
an offer, in accord with Subsection b above.)
|
||||||
|
|
||||||
|
The source code for a work means the preferred form of the work for
|
||||||
|
making modifications to it. For an executable work, complete source
|
||||||
|
code means all the source code for all modules it contains, plus any
|
||||||
|
associated interface definition files, plus the scripts used to
|
||||||
|
control compilation and installation of the executable. However, as a
|
||||||
|
special exception, the source code distributed need not include
|
||||||
|
anything that is normally distributed (in either source or binary
|
||||||
|
form) with the major components (compiler, kernel, and so on) of the
|
||||||
|
operating system on which the executable runs, unless that component
|
||||||
|
itself accompanies the executable.
|
||||||
|
|
||||||
|
If distribution of executable or object code is made by offering
|
||||||
|
access to copy from a designated place, then offering equivalent
|
||||||
|
access to copy the source code from the same place counts as
|
||||||
|
distribution of the source code, even though third parties are not
|
||||||
|
compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
4. You may not copy, modify, sublicense, or distribute the Program
|
||||||
|
except as expressly provided under this License. Any attempt
|
||||||
|
otherwise to copy, modify, sublicense or distribute the Program is
|
||||||
|
void, and will automatically terminate your rights under this License.
|
||||||
|
However, parties who have received copies, or rights, from you under
|
||||||
|
this License will not have their licenses terminated so long as such
|
||||||
|
parties remain in full compliance.
|
||||||
|
|
||||||
|
5. You are not required to accept this License, since you have not
|
||||||
|
signed it. However, nothing else grants you permission to modify or
|
||||||
|
distribute the Program or its derivative works. These actions are
|
||||||
|
prohibited by law if you do not accept this License. Therefore, by
|
||||||
|
modifying or distributing the Program (or any work based on the
|
||||||
|
Program), you indicate your acceptance of this License to do so, and
|
||||||
|
all its terms and conditions for copying, distributing or modifying
|
||||||
|
the Program or works based on it.
|
||||||
|
|
||||||
|
6. Each time you redistribute the Program (or any work based on the
|
||||||
|
Program), the recipient automatically receives a license from the
|
||||||
|
original licensor to copy, distribute or modify the Program subject to
|
||||||
|
these terms and conditions. You may not impose any further
|
||||||
|
restrictions on the recipients' exercise of the rights granted herein.
|
||||||
|
You are not responsible for enforcing compliance by third parties to
|
||||||
|
this License.
|
||||||
|
|
||||||
|
7. If, as a consequence of a court judgment or allegation of patent
|
||||||
|
infringement or for any other reason (not limited to patent issues),
|
||||||
|
conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot
|
||||||
|
distribute so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you
|
||||||
|
may not distribute the Program at all. For example, if a patent
|
||||||
|
license would not permit royalty-free redistribution of the Program by
|
||||||
|
all those who receive copies directly or indirectly through you, then
|
||||||
|
the only way you could satisfy both it and this License would be to
|
||||||
|
refrain entirely from distribution of the Program.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under
|
||||||
|
any particular circumstance, the balance of the section is intended to
|
||||||
|
apply and the section as a whole is intended to apply in other
|
||||||
|
circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any
|
||||||
|
patents or other property right claims or to contest validity of any
|
||||||
|
such claims; this section has the sole purpose of protecting the
|
||||||
|
integrity of the free software distribution system, which is
|
||||||
|
implemented by public license practices. Many people have made
|
||||||
|
generous contributions to the wide range of software distributed
|
||||||
|
through that system in reliance on consistent application of that
|
||||||
|
system; it is up to the author/donor to decide if he or she is willing
|
||||||
|
to distribute software through any other system and a licensee cannot
|
||||||
|
impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to
|
||||||
|
be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
8. If the distribution and/or use of the Program is restricted in
|
||||||
|
certain countries either by patents or by copyrighted interfaces, the
|
||||||
|
original copyright holder who places the Program under this License
|
||||||
|
may add an explicit geographical distribution limitation excluding
|
||||||
|
those countries, so that distribution is permitted only in or among
|
||||||
|
countries not thus excluded. In such case, this License incorporates
|
||||||
|
the limitation as if written in the body of this License.
|
||||||
|
|
||||||
|
9. The Free Software Foundation may publish revised and/or new versions
|
||||||
|
of the General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Program
|
||||||
|
specifies a version number of this License which applies to it and "any
|
||||||
|
later version", you have the option of following the terms and conditions
|
||||||
|
either of that version or of any later version published by the Free
|
||||||
|
Software Foundation. If the Program does not specify a version number of
|
||||||
|
this License, you may choose any version ever published by the Free Software
|
||||||
|
Foundation.
|
||||||
|
|
||||||
|
10. If you wish to incorporate parts of the Program into other free
|
||||||
|
programs whose distribution conditions are different, write to the author
|
||||||
|
to ask for permission. For software which is copyrighted by the Free
|
||||||
|
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||||
|
make exceptions for this. Our decision will be guided by the two goals
|
||||||
|
of preserving the free status of all derivatives of our free software and
|
||||||
|
of promoting the sharing and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||||
|
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||||
|
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||||
|
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||||
|
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||||
|
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||||
|
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||||
|
REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||||
|
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||||
|
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||||
|
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||||
|
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||||
|
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
convey the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
{description}
|
||||||
|
Copyright (C) {year} {fullname}
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along
|
||||||
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program is interactive, make it output a short notice like this
|
||||||
|
when it starts in an interactive mode:
|
||||||
|
|
||||||
|
Gnomovision version 69, Copyright (C) year name of author
|
||||||
|
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, the commands you use may
|
||||||
|
be called something other than `show w' and `show c'; they could even be
|
||||||
|
mouse-clicks or menu items--whatever suits your program.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your
|
||||||
|
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||||
|
necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||||
|
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||||
|
|
||||||
|
{signature of Ty Coon}, 1 April 1989
|
||||||
|
Ty Coon, President of Vice
|
||||||
|
|
||||||
|
This General Public License does not permit incorporating your program into
|
||||||
|
proprietary programs. If your program is a subroutine library, you may
|
||||||
|
consider it more useful to permit linking proprietary applications with the
|
||||||
|
library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License.
|
||||||
13
README.md
Normal file
13
README.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
videodinges
|
||||||
|
===========
|
||||||
|
|
||||||
|
Simple video CMS for self hosting of videos with different possible encodings,
|
||||||
|
simply served with the html5 video tag.
|
||||||
|
|
||||||
|
Goal is to have maximal flexilibity with encodings and quality one wants,
|
||||||
|
configurable per video in a convient administration environment,
|
||||||
|
and simply providing pages to show them.
|
||||||
|
|
||||||
|
(C) Copyright 2020 Bastiaan Welmers
|
||||||
|
|
||||||
|
Licenced with GPLv2, see file LICENCE
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
Django==1.11
|
Django==3.1.*
|
||||||
pkg-resources==0.0.0
|
pytz
|
||||||
pytz==2018.7
|
Jinja2==2.11.*
|
||||||
|
|||||||
0
static/.placeholder
Normal file
0
static/.placeholder
Normal file
24
static/js/video.js
Normal file
24
static/js/video.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
function init() {
|
||||||
|
|
||||||
|
var hash = document.location.hash;
|
||||||
|
var res = hash.match(/t=([0-9]+)/);
|
||||||
|
if (res) {
|
||||||
|
var vids = document.getElementsByTagName('video');
|
||||||
|
// only first video
|
||||||
|
var vid = vids[0];
|
||||||
|
vid.currentTime = res[1];
|
||||||
|
vid.autoplay = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
|
||||||
|
function vidTimeInUrl(el) {
|
||||||
|
var vids = document.getElementsByTagName('video');
|
||||||
|
// only first video
|
||||||
|
var vid = vids[0];
|
||||||
|
var currentTime = vid.currentTime;
|
||||||
|
var href = el.href + "#t=" + parseInt(currentTime);
|
||||||
|
el.href = href;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
1
tests/videodinges/__init__.py
Normal file
1
tests/videodinges/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .base import UploadMixin
|
||||||
32
tests/videodinges/base.py
Normal file
32
tests/videodinges/base.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import tempfile
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from django.test import override_settings
|
||||||
|
|
||||||
|
|
||||||
|
class UploadMixin(TestCase):
|
||||||
|
clean_uploads_after_run = True
|
||||||
|
base_upload_dir: tempfile.TemporaryDirectory
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls) -> None:
|
||||||
|
super().setUpClass()
|
||||||
|
cls.base_upload_dir = tempfile.TemporaryDirectory(suffix='-videodinges-tests')
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
super().setUp()
|
||||||
|
self.media_root = tempfile.TemporaryDirectory(suffix='-' + self.__class__.__name__, dir=self.base_upload_dir.name)
|
||||||
|
self.media_root_override_settings = override_settings(MEDIA_ROOT=self.media_root.name)
|
||||||
|
self.media_root_override_settings.enable()
|
||||||
|
|
||||||
|
def tearDown(self) -> None:
|
||||||
|
self.media_root_override_settings.disable()
|
||||||
|
if self.clean_uploads_after_run:
|
||||||
|
self.media_root.cleanup()
|
||||||
|
super().tearDown()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls) -> None:
|
||||||
|
if cls.clean_uploads_after_run:
|
||||||
|
cls.base_upload_dir.cleanup()
|
||||||
|
super().tearDownClass()
|
||||||
84
tests/videodinges/factories.py
Normal file
84
tests/videodinges/factories.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
""" Module generating useful models in 1 place """
|
||||||
|
from inspect import signature
|
||||||
|
from typing import Type, TypeVar
|
||||||
|
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
import django.db.models
|
||||||
|
|
||||||
|
from videodinges import models
|
||||||
|
|
||||||
|
T = TypeVar('T', bound=django.db.models.Model)
|
||||||
|
|
||||||
|
|
||||||
|
def create(model: Type[T], **kwargs) -> T:
|
||||||
|
if model is models.Video:
|
||||||
|
return _create_with_defaults(models.Video, kwargs,
|
||||||
|
title=lambda x: 'Title {}'.format(x),
|
||||||
|
slug=lambda x: 'slug-{}'.format(x),
|
||||||
|
description=lambda x: 'Description {}'.format(x),
|
||||||
|
)
|
||||||
|
|
||||||
|
if model is models.Transcoding:
|
||||||
|
def url():
|
||||||
|
# only URL if no upload for they are mutually exclusive
|
||||||
|
if 'upload' not in kwargs:
|
||||||
|
return 'https://some_url'
|
||||||
|
|
||||||
|
return _create_with_defaults(models.Transcoding, kwargs,
|
||||||
|
video=lambda: create(models.Video),
|
||||||
|
quality=models.qualities[0].name,
|
||||||
|
type=str(models.transcoding_types[0]),
|
||||||
|
url=url,
|
||||||
|
)
|
||||||
|
|
||||||
|
if model is models.Upload:
|
||||||
|
return _create_with_defaults(models.Upload, kwargs, file=SimpleUploadedFile('some_file.txt', b'some contents'))
|
||||||
|
|
||||||
|
if model is models.Track:
|
||||||
|
return _create_with_defaults(models.Track, kwargs,
|
||||||
|
video=lambda: create(models.Video),
|
||||||
|
lang='en',
|
||||||
|
upload=lambda: create(models.Upload)
|
||||||
|
)
|
||||||
|
|
||||||
|
raise NotImplementedError('Factory for %s not implemented' % model)
|
||||||
|
|
||||||
|
|
||||||
|
def _create_with_defaults(model: Type[T], kwargs: dict, **defaults) -> T:
|
||||||
|
"""
|
||||||
|
Return created django model instance.
|
||||||
|
When providing lambda as default item, the result of the lambda will be taken.
|
||||||
|
The lambda will ONLY be executed when not provided in kwargs.
|
||||||
|
|
||||||
|
When a lambda requires an argument, the primary key of the to be created object
|
||||||
|
will be provided to that argument. This is useful for generating unique fields.
|
||||||
|
|
||||||
|
:param model: django model to create
|
||||||
|
:param kwargs: keyword arguments to fill the model
|
||||||
|
:param defaults: default keyword arguments to use when not mentioned in kwargs
|
||||||
|
"""
|
||||||
|
|
||||||
|
_next_pk = 0
|
||||||
|
def next_pk():
|
||||||
|
# Queries next pk only one time during creation
|
||||||
|
nonlocal _next_pk
|
||||||
|
if _next_pk == 0:
|
||||||
|
_next_pk = _query_next_pk(model)
|
||||||
|
return _next_pk
|
||||||
|
|
||||||
|
for k, v in defaults.items():
|
||||||
|
if callable(v) and not k in kwargs:
|
||||||
|
if len(signature(v).parameters) == 1:
|
||||||
|
result = v(next_pk())
|
||||||
|
else:
|
||||||
|
result = v()
|
||||||
|
defaults[k] = result
|
||||||
|
|
||||||
|
return model.objects.create(**{**defaults, **kwargs})
|
||||||
|
|
||||||
|
|
||||||
|
def _query_next_pk(model: Type[T]) -> int:
|
||||||
|
try:
|
||||||
|
return model.objects.order_by('-pk').first().pk + 1
|
||||||
|
except AttributeError:
|
||||||
|
return 1
|
||||||
0
tests/videodinges/models/__init__.py
Normal file
0
tests/videodinges/models/__init__.py
Normal file
96
tests/videodinges/models/test_track.py
Normal file
96
tests/videodinges/models/test_track.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
from django.db.utils import IntegrityError
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from tests.videodinges import factories, UploadMixin
|
||||||
|
from videodinges.models import Track, Video, Upload
|
||||||
|
|
||||||
|
|
||||||
|
class TrackTestCase(UploadMixin, TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.video = Video.objects.create(title='Title', slug='slug', description='Description')
|
||||||
|
|
||||||
|
def test_model_is_created_with_required_fields(self):
|
||||||
|
Track.objects.create(video=self.video, lang='en', upload=factories.create(Upload))
|
||||||
|
track = Track.objects.all()[0]
|
||||||
|
self.assertEqual(track.video.slug, 'slug')
|
||||||
|
self.assertEqual(track.default, False)
|
||||||
|
self.assertEqual(track.kind, 'subtitles')
|
||||||
|
self.assertEqual(track.lang, 'en')
|
||||||
|
self.assertEqual(track.label, None)
|
||||||
|
self.assertEqual(track.upload.file.name, 'some_file.txt')
|
||||||
|
|
||||||
|
def test_model_is_created_with_nonrequired_fields(self):
|
||||||
|
Track.objects.create(
|
||||||
|
video=self.video,
|
||||||
|
lang='en',
|
||||||
|
upload=factories.create(Upload),
|
||||||
|
default=True,
|
||||||
|
label='Something',
|
||||||
|
kind='chapters',
|
||||||
|
)
|
||||||
|
track = Track.objects.all()[0]
|
||||||
|
self.assertEqual(track.video.slug, 'slug')
|
||||||
|
self.assertEqual(track.default, True)
|
||||||
|
self.assertEqual(track.kind, 'chapters')
|
||||||
|
self.assertEqual(track.lang, 'en')
|
||||||
|
self.assertEqual(track.label, 'Something')
|
||||||
|
self.assertEqual(track.upload.file.name, 'some_file.txt')
|
||||||
|
|
||||||
|
def test_can_create_two_models(self):
|
||||||
|
model1 = Track.objects.create(video=self.video, lang='en', upload=factories.create(Upload))
|
||||||
|
model2 = Track.objects.create(video=self.video, lang='nl', upload=factories.create(Upload))
|
||||||
|
self.assertEqual({model1, model2}, set((m for m in Track.objects.all())))
|
||||||
|
|
||||||
|
self.assertEqual(model1.video, self.video)
|
||||||
|
self.assertEqual(model1.default, False)
|
||||||
|
self.assertEqual(model1.kind, 'subtitles')
|
||||||
|
self.assertEqual(model1.lang, 'en')
|
||||||
|
|
||||||
|
self.assertEqual(model2.video, self.video)
|
||||||
|
self.assertEqual(model2.default, False)
|
||||||
|
self.assertEqual(model2.kind, 'subtitles')
|
||||||
|
self.assertEqual(model2.lang, 'nl')
|
||||||
|
|
||||||
|
def test_can_create_two_models_with_one_default(self):
|
||||||
|
model1 = Track.objects.create(video=self.video, default=True, lang='en', upload=factories.create(Upload))
|
||||||
|
model2 = Track.objects.create(video=self.video, lang='nl', upload=factories.create(Upload))
|
||||||
|
self.assertEqual({model1, model2}, set((m for m in Track.objects.all())))
|
||||||
|
|
||||||
|
self.assertEqual(model1.video, self.video)
|
||||||
|
self.assertEqual(model1.default, True)
|
||||||
|
self.assertEqual(model1.kind, 'subtitles')
|
||||||
|
self.assertEqual(model1.lang, 'en')
|
||||||
|
|
||||||
|
self.assertEqual(model2.video, self.video)
|
||||||
|
self.assertEqual(model2.default, False)
|
||||||
|
self.assertEqual(model2.kind, 'subtitles')
|
||||||
|
self.assertEqual(model2.lang, 'nl')
|
||||||
|
|
||||||
|
def test_cannot_set_default_twice(self):
|
||||||
|
video = factories.create(Video)
|
||||||
|
Track.objects.create(video=video, default=True, lang='en', upload=factories.create(Upload))
|
||||||
|
with self.assertRaisesMessage(IntegrityError, 'UNIQUE constraint failed: tracks.video_id'):
|
||||||
|
Track.objects.create(video=video, default=True, lang='nl', upload=factories.create(Upload))
|
||||||
|
|
||||||
|
def test_cannot_set_two_subtitles_with_same_lang(self):
|
||||||
|
video = factories.create(Video)
|
||||||
|
Track.objects.create(video=video, lang='en', upload=factories.create(Upload))
|
||||||
|
with self.assertRaisesMessage(IntegrityError, 'UNIQUE constraint failed: tracks.video_id'):
|
||||||
|
Track.objects.create(video=video, lang='en', upload=factories.create(Upload))
|
||||||
|
|
||||||
|
def test_can_create_two_models_with_same_lang_but_different_kind(self):
|
||||||
|
model1 = Track.objects.create(video=self.video, lang='en', upload=factories.create(Upload))
|
||||||
|
model2 = Track.objects.create(video=self.video, lang='en', kind='chapters', upload=factories.create(Upload))
|
||||||
|
self.assertEqual({model1, model2}, set((m for m in Track.objects.all())))
|
||||||
|
|
||||||
|
self.assertEqual(model1.video, self.video)
|
||||||
|
self.assertEqual(model1.default, False)
|
||||||
|
self.assertEqual(model1.kind, 'subtitles')
|
||||||
|
self.assertEqual(model1.lang, 'en')
|
||||||
|
|
||||||
|
self.assertEqual(model2.video, self.video)
|
||||||
|
self.assertEqual(model2.default, False)
|
||||||
|
self.assertEqual(model2.kind, 'chapters')
|
||||||
|
self.assertEqual(model2.lang, 'en')
|
||||||
62
tests/videodinges/models/test_transcoding.py
Normal file
62
tests/videodinges/models/test_transcoding.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
from django.db.utils import IntegrityError
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from tests.videodinges import factories, UploadMixin
|
||||||
|
from videodinges.models import Transcoding, Video, qualities, transcoding_types, Upload
|
||||||
|
|
||||||
|
|
||||||
|
class TranscodingTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
video = Video.objects.create(title='Title', slug='slug', description='Description')
|
||||||
|
Transcoding.objects.create(video=video, quality=qualities[0].name, type=str(transcoding_types[0]), url='https://some_url')
|
||||||
|
|
||||||
|
def test_model_is_created(self):
|
||||||
|
transcoding = Transcoding.objects.all()[0]
|
||||||
|
self.assertEqual(transcoding.video.slug, 'slug')
|
||||||
|
self.assertEqual(transcoding.quality, '360p')
|
||||||
|
self.assertEqual(transcoding.type, 'video/webm')
|
||||||
|
self.assertEqual(transcoding.url, 'https://some_url')
|
||||||
|
|
||||||
|
|
||||||
|
class CreateTranscodingTestCase(UploadMixin, TestCase):
|
||||||
|
|
||||||
|
def test_upload_and_url_cannot_both_be_filled(self):
|
||||||
|
video = factories.create(Video)
|
||||||
|
with self.assertRaisesMessage(IntegrityError, 'CHECK constraint failed: upload_and_url_cannot_both_be_filled'):
|
||||||
|
Transcoding.objects.create(
|
||||||
|
video=video,
|
||||||
|
quality=qualities[0].name,
|
||||||
|
type=str(transcoding_types[0]),
|
||||||
|
url='https://some_url',
|
||||||
|
upload=factories.create(Upload)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_either_upload_or_url_must_be_filled(self):
|
||||||
|
video = factories.create(Video)
|
||||||
|
|
||||||
|
with self.assertRaisesMessage(IntegrityError, 'CHECK constraint failed: upload_or_url_is_filled'):
|
||||||
|
Transcoding.objects.create(
|
||||||
|
video=video,
|
||||||
|
quality=qualities[0].name,
|
||||||
|
type=str(transcoding_types[0]),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_no_duplicate_qualities_for_same_video_and_type_can_be_created(self):
|
||||||
|
video = factories.create(Video)
|
||||||
|
|
||||||
|
Transcoding.objects.create(
|
||||||
|
video=video,
|
||||||
|
quality=qualities[0].name,
|
||||||
|
type=str(transcoding_types[0]),
|
||||||
|
url='https://some_url',
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaisesMessage(IntegrityError, 'UNIQUE constraint failed: transcodings.video_id, transcodings.quality, transcodings.type'):
|
||||||
|
Transcoding.objects.create(
|
||||||
|
video=video,
|
||||||
|
quality=qualities[0].name,
|
||||||
|
type=str(transcoding_types[0]),
|
||||||
|
url='https://some_url',
|
||||||
|
)
|
||||||
15
tests/videodinges/models/test_upload.py
Normal file
15
tests/videodinges/models/test_upload.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from tests.videodinges import UploadMixin
|
||||||
|
from videodinges.models import Upload
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
|
||||||
|
|
||||||
|
class UploadTestCase(UploadMixin, TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
Upload.objects.create(file=SimpleUploadedFile('some_file.txt', b'some contents'))
|
||||||
|
|
||||||
|
def test_model_is_created(self):
|
||||||
|
upload = Upload.objects.all()[0]
|
||||||
|
self.assertEqual(upload.file.name, 'some_file.txt')
|
||||||
15
tests/videodinges/models/test_video.py
Normal file
15
tests/videodinges/models/test_video.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from videodinges.models import Video
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class VideoTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
Video.objects.create(title='Title', slug='slug', description='Description')
|
||||||
|
|
||||||
|
def test_model_is_created(self):
|
||||||
|
video = Video.objects.get(slug='slug')
|
||||||
|
self.assertEqual(video.slug, 'slug')
|
||||||
|
self.assertEqual(video.title, 'Title')
|
||||||
|
self.assertEqual(video.description, 'Description')
|
||||||
|
self.assertIsInstance(video.created_at, datetime)
|
||||||
|
self.assertIsInstance(video.updated_at, datetime)
|
||||||
0
tests/videodinges/test_factories/__init__.py
Normal file
0
tests/videodinges/test_factories/__init__.py
Normal file
16
tests/videodinges/test_factories/test_create.py
Normal file
16
tests/videodinges/test_factories/test_create.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from tests.videodinges import factories
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
class CreateTestCase(TestCase):
|
||||||
|
|
||||||
|
def test_factory_returns_model(self):
|
||||||
|
|
||||||
|
class NotImplementedModel(models.Model):
|
||||||
|
class Meta:
|
||||||
|
app_label = 'some_test_label'
|
||||||
|
|
||||||
|
with self.assertRaises(NotImplementedError):
|
||||||
|
factories.create(NotImplementedModel)
|
||||||
|
|
||||||
55
tests/videodinges/test_factories/test_transcoding.py
Normal file
55
tests/videodinges/test_factories/test_transcoding.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
from django.test import TestCase
|
||||||
|
from videodinges.models import Transcoding, Video, Upload
|
||||||
|
from tests.videodinges import factories, UploadMixin
|
||||||
|
|
||||||
|
class TranscodingTestCase(TestCase):
|
||||||
|
def test_factory_returns_model(self):
|
||||||
|
transcoding = factories.create(Transcoding)
|
||||||
|
self.assertEqual(transcoding.video.slug, 'slug-1')
|
||||||
|
self.assertEqual(transcoding.quality, '360p')
|
||||||
|
self.assertEqual(transcoding.type, 'video/webm')
|
||||||
|
self.assertEqual(transcoding.url, 'https://some_url')
|
||||||
|
|
||||||
|
def test_can_overwrite_kwargs(self):
|
||||||
|
transcoding = factories.create(
|
||||||
|
Transcoding,
|
||||||
|
quality='720p',
|
||||||
|
type='video/mp4',
|
||||||
|
url='http://another_url',
|
||||||
|
video=factories.create(Video, slug='yet-another-video-slug')
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(transcoding.video.slug, 'yet-another-video-slug')
|
||||||
|
self.assertEqual(transcoding.quality, '720p')
|
||||||
|
self.assertEqual(transcoding.type, 'video/mp4')
|
||||||
|
self.assertEqual(transcoding.url, 'http://another_url')
|
||||||
|
|
||||||
|
def test_does_not_create_video_when_providing_one(self):
|
||||||
|
transcoding = factories.create(
|
||||||
|
Transcoding,
|
||||||
|
quality='720p',
|
||||||
|
type='video/mp4',
|
||||||
|
url='http://another_url',
|
||||||
|
video=factories.create(Video, slug='yet-another-video-slug')
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEquals(Video.objects.all().count(), 1)
|
||||||
|
|
||||||
|
|
||||||
|
class TranscodingWithUploadTestCase(UploadMixin, TestCase):
|
||||||
|
def test_can_assign_upload(self):
|
||||||
|
transcoding = factories.create(
|
||||||
|
Transcoding,
|
||||||
|
quality='720p',
|
||||||
|
type='video/mp4',
|
||||||
|
video=factories.create(Video, slug='yet-another-video-slug'),
|
||||||
|
upload=factories.create(Upload, file=SimpleUploadedFile('my_upload.txt', b'some_contents'))
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(os.path.exists(os.path.join(self.media_root.name, 'my_upload.txt')))
|
||||||
|
with open(os.path.join(self.media_root.name, 'my_upload.txt'), 'rb') as f:
|
||||||
|
self.assertEquals(f.read(), b'some_contents')
|
||||||
|
|
||||||
19
tests/videodinges/test_factories/test_upload.py
Normal file
19
tests/videodinges/test_factories/test_upload.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from tests.videodinges import factories, UploadMixin
|
||||||
|
from videodinges.models import Upload
|
||||||
|
|
||||||
|
class UploadTestCase(UploadMixin, TestCase):
|
||||||
|
def test_model_is_created(self):
|
||||||
|
upload = factories.create(Upload)
|
||||||
|
self.assertEqual(upload.file.name, 'some_file.txt')
|
||||||
|
self.assertTrue(os.path.exists(os.path.join(self.media_root.name, 'some_file.txt')))
|
||||||
|
|
||||||
|
def test_upload_does_not_create_file_when_providing_upload(self):
|
||||||
|
upload = factories.create(Upload, file=SimpleUploadedFile('my_file.txt', b'some contents'))
|
||||||
|
self.assertEqual(upload.file.name, 'my_file.txt')
|
||||||
|
self.assertFalse(os.path.exists(os.path.join(self.media_root.name, 'some_file.txt')))
|
||||||
|
self.assertTrue(os.path.exists(os.path.join(self.media_root.name, 'my_file.txt')))
|
||||||
41
tests/videodinges/test_factories/test_video.py
Normal file
41
tests/videodinges/test_factories/test_video.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from videodinges.models import Video
|
||||||
|
from tests.videodinges import factories
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class VideoTestCase(TestCase):
|
||||||
|
def test_factory_returns_model(self):
|
||||||
|
video = factories.create(Video)
|
||||||
|
self.assertEqual(video.slug, 'slug-1')
|
||||||
|
self.assertEqual(video.title, 'Title 1')
|
||||||
|
self.assertEqual(video.description, 'Description 1')
|
||||||
|
self.assertIsInstance(video.created_at, datetime)
|
||||||
|
self.assertIsInstance(video.updated_at, datetime)
|
||||||
|
|
||||||
|
def test_factory_can_create_multiple_models(self):
|
||||||
|
video1 = factories.create(Video)
|
||||||
|
video2 = factories.create(Video)
|
||||||
|
video3 = factories.create(Video)
|
||||||
|
|
||||||
|
self.assertEqual(video1.slug, 'slug-1')
|
||||||
|
self.assertEqual(video1.title, 'Title 1')
|
||||||
|
self.assertEqual(video1.description, 'Description 1')
|
||||||
|
self.assertIsInstance(video1.created_at, datetime)
|
||||||
|
self.assertIsInstance(video1.updated_at, datetime)
|
||||||
|
|
||||||
|
self.assertEqual(video2.slug, 'slug-2')
|
||||||
|
self.assertEqual(video2.title, 'Title 2')
|
||||||
|
self.assertEqual(video2.description, 'Description 2')
|
||||||
|
self.assertIsInstance(video2.created_at, datetime)
|
||||||
|
self.assertIsInstance(video2.updated_at, datetime)
|
||||||
|
|
||||||
|
self.assertEqual(video3.slug, 'slug-3')
|
||||||
|
self.assertEqual(video3.title, 'Title 3')
|
||||||
|
self.assertEqual(video3.description, 'Description 3')
|
||||||
|
self.assertIsInstance(video3.created_at, datetime)
|
||||||
|
self.assertIsInstance(video3.updated_at, datetime)
|
||||||
|
|
||||||
|
def test_factory_runs_only_2_queries(self):
|
||||||
|
""" Factory should only use 2 queries: one for selecting primary key, and one for inserting record """
|
||||||
|
with self.assertNumQueries(2):
|
||||||
|
video = factories.create(Video)
|
||||||
6
tests/videodinges/unit/__init__.py
Normal file
6
tests/videodinges/unit/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
"""
|
||||||
|
Module for storing 'real' _unit_ tests.
|
||||||
|
Meaning, they should test single small units e.g. methods or functions in an isolated way.
|
||||||
|
|
||||||
|
Try to use django.test.SimpleTestCase to prevent unnecessary database setup and improve speed.
|
||||||
|
"""
|
||||||
0
tests/videodinges/unit/models/__init__.py
Normal file
0
tests/videodinges/unit/models/__init__.py
Normal file
15
tests/videodinges/unit/models/test_get_quality_by_name.py
Normal file
15
tests/videodinges/unit/models/test_get_quality_by_name.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from django.test import SimpleTestCase
|
||||||
|
from videodinges.models import get_quality_by_name
|
||||||
|
|
||||||
|
class GetQualityByNameTestCase(SimpleTestCase):
|
||||||
|
|
||||||
|
def test_returns_quality_if_listed(self):
|
||||||
|
result = get_quality_by_name('480p')
|
||||||
|
self.assertEqual(result.name, '480p')
|
||||||
|
self.assertEqual(result.width, 853)
|
||||||
|
self.assertEqual(result.height, 480)
|
||||||
|
self.assertEqual(result.priority, 2)
|
||||||
|
|
||||||
|
def test_returns_none_if_not_listed(self):
|
||||||
|
result = get_quality_by_name('non-existend')
|
||||||
|
self.assertIsNone(result)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from django.test import SimpleTestCase
|
||||||
|
from videodinges.models import TranscodingType, get_short_name_of_transcoding_type
|
||||||
|
|
||||||
|
class GetShortNameOfTranscodingTypeTestCase(SimpleTestCase):
|
||||||
|
|
||||||
|
def test_gets_transcoding_by_name(self):
|
||||||
|
result = get_short_name_of_transcoding_type('video/webm; codecs="vp8, vorbis"')
|
||||||
|
self.assertEqual(result, 'vp8')
|
||||||
|
|
||||||
|
def test_gets_transcoding_by_transcoding_object(self):
|
||||||
|
result = get_short_name_of_transcoding_type(TranscodingType(name='Looooong naaaaame', short_name='shrt nm',
|
||||||
|
description='Some Description', priority=1))
|
||||||
|
self.assertEqual(result, 'shrt nm')
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from django.test import SimpleTestCase
|
||||||
|
from videodinges.models import get_transcoding_type_by_name
|
||||||
|
|
||||||
|
class GetTranscodingTypeByNameTestCase(SimpleTestCase):
|
||||||
|
|
||||||
|
def test_returns_transcoding_type_if_listed(self):
|
||||||
|
result = get_transcoding_type_by_name('video/webm; codecs="vp9, opus"')
|
||||||
|
self.assertEqual(result.name, 'video/webm; codecs="vp9, opus"')
|
||||||
|
self.assertEqual(result.short_name, 'vp9')
|
||||||
|
self.assertEqual(result.description, 'WebM with VP9 and Opus')
|
||||||
|
self.assertEqual(result.priority, 100)
|
||||||
|
|
||||||
|
def test_returns_none_if_not_listed(self):
|
||||||
|
result = get_transcoding_type_by_name('non-existent')
|
||||||
|
self.assertIsNone(result)
|
||||||
0
tests/videodinges/views/__init__.py
Normal file
0
tests/videodinges/views/__init__.py
Normal file
28
tests/videodinges/views/test_index.py
Normal file
28
tests/videodinges/views/test_index.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from unittest.mock import patch, Mock
|
||||||
|
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.test import TestCase, Client
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from tests.videodinges import factories
|
||||||
|
from videodinges import models
|
||||||
|
|
||||||
|
|
||||||
|
class IndexTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.client = Client()
|
||||||
|
|
||||||
|
#@patch('videodinges.views.render')
|
||||||
|
def test_index(self):
|
||||||
|
|
||||||
|
#render.return_value = HttpResponse(b'data', status=200)
|
||||||
|
|
||||||
|
video1 = factories.create(models.Video, title='Vid 1', slug='vid-1')
|
||||||
|
video2 = factories.create(models.Video, title='Vid 2', slug='vid-2')
|
||||||
|
resp = self.client.get(reverse('index'))
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertContains(resp, 'Vid 1')
|
||||||
|
self.assertContains(resp, 'vid-1.html')
|
||||||
|
|
||||||
|
self.assertContains(resp, 'Vid 2')
|
||||||
|
self.assertContains(resp, 'vid-2.html')
|
||||||
661
tests/videodinges/views/test_video.py
Normal file
661
tests/videodinges/views/test_video.py
Normal file
@@ -0,0 +1,661 @@
|
|||||||
|
""" Test video page """
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.test import TestCase, Client
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from tests.videodinges import factories, UploadMixin
|
||||||
|
from videodinges import models
|
||||||
|
|
||||||
|
|
||||||
|
class VideoTestCase(UploadMixin, TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.client = Client()
|
||||||
|
|
||||||
|
def test_video_view_renders_properly(self):
|
||||||
|
|
||||||
|
video = factories.create(
|
||||||
|
models.Video,
|
||||||
|
title='Vid 1',
|
||||||
|
slug='vid-1',
|
||||||
|
default_quality='480p',
|
||||||
|
)
|
||||||
|
transcoding1 = factories.create(
|
||||||
|
models.Transcoding,
|
||||||
|
video=video,
|
||||||
|
quality='480p',
|
||||||
|
type='video/webm; codecs="vp9, opus"',
|
||||||
|
url='http://480p.webm',
|
||||||
|
)
|
||||||
|
transcoding2 = factories.create(
|
||||||
|
models.Transcoding,
|
||||||
|
video=video,
|
||||||
|
quality='480p',
|
||||||
|
type='video/mp4',
|
||||||
|
url='http://480p.mp4',
|
||||||
|
)
|
||||||
|
transcoding3 = factories.create(
|
||||||
|
models.Transcoding,
|
||||||
|
video=video,
|
||||||
|
quality='720p',
|
||||||
|
type='video/webm; codecs="vp9, opus"',
|
||||||
|
url='http://720p.webm',
|
||||||
|
)
|
||||||
|
transcoding4 = factories.create(
|
||||||
|
models.Transcoding,
|
||||||
|
video=video,
|
||||||
|
quality='720p',
|
||||||
|
type='video/mp4',
|
||||||
|
url='http://720p.mp4',
|
||||||
|
)
|
||||||
|
|
||||||
|
resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
content:str = resp.content.decode(resp.charset)
|
||||||
|
|
||||||
|
srctag = '<source src="{url}" type="{type}" />'
|
||||||
|
|
||||||
|
self.assertInHTML(
|
||||||
|
"""<video width="853" height="480" controls="controls">
|
||||||
|
<source src="http://480p.webm" type='video/webm; codecs="vp9, opus"' />
|
||||||
|
<source src="http://480p.mp4" type='video/mp4' />
|
||||||
|
You need a browser that understands HTML5 video and supports h.264 or vp9 codecs.
|
||||||
|
</video>""",
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertInHTML('<title>Vid 1</title>', content)
|
||||||
|
|
||||||
|
self.assertInHTML('<h1>Vid 1</h1>', content)
|
||||||
|
|
||||||
|
self.assertInHTML('<p>Description 1</p>', content)
|
||||||
|
|
||||||
|
self.assertInHTML('<strong>480p versie</strong>', content)
|
||||||
|
|
||||||
|
self.assertInHTML(
|
||||||
|
'<a href="vid-1.html?quality=720p" onclick="vidTimeInUrl(this);">720p versie</a>',
|
||||||
|
content
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertInHTML(
|
||||||
|
'<script src="static/js/video.js" type="text/javascript"></script>',
|
||||||
|
content
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_video_show_correct_default_quality(self):
|
||||||
|
|
||||||
|
video = factories.create(
|
||||||
|
models.Video,
|
||||||
|
title='Vid 1',
|
||||||
|
slug='vid-1',
|
||||||
|
default_quality='720p',
|
||||||
|
)
|
||||||
|
transcoding1 = factories.create(
|
||||||
|
models.Transcoding,
|
||||||
|
video=video,
|
||||||
|
quality='480p',
|
||||||
|
type='video/webm; codecs="vp8, vorbis"',
|
||||||
|
url='http://480p.webm',
|
||||||
|
)
|
||||||
|
transcoding2 = factories.create(
|
||||||
|
models.Transcoding,
|
||||||
|
video=video,
|
||||||
|
quality='480p',
|
||||||
|
type='video/mp4',
|
||||||
|
url='http://480p.mp4',
|
||||||
|
)
|
||||||
|
transcoding3 = factories.create(
|
||||||
|
models.Transcoding,
|
||||||
|
video=video,
|
||||||
|
quality='720p',
|
||||||
|
type='video/webm; codecs="vp8, vorbis"',
|
||||||
|
url='http://720p.webm',
|
||||||
|
)
|
||||||
|
transcoding4 = factories.create(
|
||||||
|
models.Transcoding,
|
||||||
|
video=video,
|
||||||
|
quality='720p',
|
||||||
|
type='video/mp4',
|
||||||
|
url='http://720p.mp4',
|
||||||
|
)
|
||||||
|
|
||||||
|
resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
content:str = resp.content.decode(resp.charset)
|
||||||
|
|
||||||
|
self.assertInHTML(
|
||||||
|
"""<video width="1280" height="720" controls="controls">
|
||||||
|
<source src="http://720p.webm" type='video/webm; codecs="vp8, vorbis"' />
|
||||||
|
<source src="http://720p.mp4" type='video/mp4' />
|
||||||
|
You need a browser that understands HTML5 video and supports h.264 or vp8 codecs.
|
||||||
|
</video>""",
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
self.assertInHTML(
|
||||||
|
'<a href="vid-1.html?quality=480p" onclick="vidTimeInUrl(this);">480p versie</a>',
|
||||||
|
content
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertInHTML('<strong>720p versie</strong>', content)
|
||||||
|
|
||||||
|
|
||||||
|
def test_video_shows_correct_quality_for_parameter(self):
|
||||||
|
|
||||||
|
video = factories.create(
|
||||||
|
models.Video,
|
||||||
|
title='Vid 1',
|
||||||
|
slug='vid-1',
|
||||||
|
)
|
||||||
|
transcoding1 = factories.create(
|
||||||
|
models.Transcoding,
|
||||||
|
video=video,
|
||||||
|
quality='480p',
|
||||||
|
type='video/webm; codecs="vp8, vorbis"',
|
||||||
|
url='http://480p.webm',
|
||||||
|
)
|
||||||
|
transcoding2 = factories.create(
|
||||||
|
models.Transcoding,
|
||||||
|
video=video,
|
||||||
|
quality='480p',
|
||||||
|
type='video/mp4',
|
||||||
|
url='http://480p.mp4',
|
||||||
|
)
|
||||||
|
transcoding3 = factories.create(
|
||||||
|
models.Transcoding,
|
||||||
|
video=video,
|
||||||
|
quality='720p',
|
||||||
|
type='video/webm; codecs="vp8, vorbis"',
|
||||||
|
url='http://720p.webm',
|
||||||
|
)
|
||||||
|
transcoding4 = factories.create(
|
||||||
|
models.Transcoding,
|
||||||
|
video=video,
|
||||||
|
quality='720p',
|
||||||
|
type='video/mp4',
|
||||||
|
url='http://720p.mp4',
|
||||||
|
)
|
||||||
|
|
||||||
|
resp:HttpResponse = self.client.get(
|
||||||
|
reverse('video', args=['vid-1']) + '?quality=720p')
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
content:str = resp.content.decode(resp.charset)
|
||||||
|
|
||||||
|
self.assertInHTML(
|
||||||
|
"""<video width="1280" height="720" controls="controls">
|
||||||
|
<source src="http://720p.webm" type='video/webm; codecs="vp8, vorbis"' />
|
||||||
|
<source src="http://720p.mp4" type='video/mp4' />
|
||||||
|
You need a browser that understands HTML5 video and supports h.264 or vp8 codecs.
|
||||||
|
</video>""",
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
self.assertInHTML(
|
||||||
|
'<a href="vid-1.html?quality=480p" onclick="vidTimeInUrl(this);">480p versie</a>',
|
||||||
|
content
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertInHTML('<strong>720p versie</strong>', content)
|
||||||
|
|
||||||
|
def test_video_uploads_shows_correctly(self):
|
||||||
|
|
||||||
|
image = factories.create(models.Upload)
|
||||||
|
movie = factories.create(models.Upload)
|
||||||
|
|
||||||
|
video = factories.create(
|
||||||
|
models.Video,
|
||||||
|
title='Vid 1',
|
||||||
|
slug='vid-1',
|
||||||
|
poster=image,
|
||||||
|
og_image=image
|
||||||
|
)
|
||||||
|
transcoding = factories.create(
|
||||||
|
models.Transcoding,
|
||||||
|
video=video,
|
||||||
|
quality='480p',
|
||||||
|
type='video/webm; codecs="vp8, vorbis"',
|
||||||
|
upload=movie,
|
||||||
|
)
|
||||||
|
|
||||||
|
resp:HttpResponse = self.client.get(
|
||||||
|
reverse('video', args=['vid-1']) + '?quality=720p')
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
content:str = resp.content.decode(resp.charset)
|
||||||
|
|
||||||
|
self.assertInHTML(
|
||||||
|
"""<video width="853" height="480" poster="{image}" controls="controls">
|
||||||
|
<source src="{url}" type='video/webm; codecs="vp8, vorbis"' />
|
||||||
|
You need a browser that understands HTML5 video and supports vp8 codecs.
|
||||||
|
</video>""".format(url=movie.file.url, image=image.file.url),
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertInHTML(
|
||||||
|
'<meta property="og:image" content="{image}" />'.format(image=image.file.url),
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VideoWithTrackTestCase(UploadMixin, TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.client = Client()
|
||||||
|
self.video = factories.create(
|
||||||
|
models.Video,
|
||||||
|
title='Vid 1',
|
||||||
|
slug='vid-1',
|
||||||
|
default_quality='480p',
|
||||||
|
)
|
||||||
|
factories.create(
|
||||||
|
models.Transcoding,
|
||||||
|
video=self.video,
|
||||||
|
quality='480p',
|
||||||
|
type='video/webm; codecs="vp9, opus"',
|
||||||
|
url='http://480p.webm',
|
||||||
|
)
|
||||||
|
factories.create(
|
||||||
|
models.Transcoding,
|
||||||
|
video=self.video,
|
||||||
|
quality='480p',
|
||||||
|
type='video/mp4',
|
||||||
|
url='http://480p.mp4',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_video_view_renders_track_properly(self):
|
||||||
|
|
||||||
|
factories.create(
|
||||||
|
models.Track,
|
||||||
|
video=self.video,
|
||||||
|
lang='en',
|
||||||
|
)
|
||||||
|
|
||||||
|
resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
content:str = resp.content.decode(resp.charset)
|
||||||
|
|
||||||
|
self.assertInHTML(
|
||||||
|
"""<video width="853" height="480" controls="controls">
|
||||||
|
<source src="http://480p.webm" type='video/webm; codecs="vp9, opus"' />
|
||||||
|
<source src="http://480p.mp4" type='video/mp4' />
|
||||||
|
<track src="/uploads/some_file.txt" srclang="en" kind="subtitles" label="en" />
|
||||||
|
You need a browser that understands HTML5 video and supports h.264 or vp9 codecs.
|
||||||
|
</video>""",
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_video_view_renders_multiple_tracks_properly(self):
|
||||||
|
|
||||||
|
track_en = factories.create(
|
||||||
|
models.Track,
|
||||||
|
video=self.video,
|
||||||
|
lang='en',
|
||||||
|
)
|
||||||
|
|
||||||
|
track_nl = factories.create(
|
||||||
|
models.Track,
|
||||||
|
video=self.video,
|
||||||
|
lang='nl',
|
||||||
|
)
|
||||||
|
|
||||||
|
resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
content:str = resp.content.decode(resp.charset)
|
||||||
|
|
||||||
|
self.assertInHTML(
|
||||||
|
f"""<video width="853" height="480" controls="controls">
|
||||||
|
<source src="http://480p.webm" type='video/webm; codecs="vp9, opus"' />
|
||||||
|
<source src="http://480p.mp4" type='video/mp4' />
|
||||||
|
<track src="{ track_en.upload.file.url }" srclang="en" kind="subtitles" label="en" />
|
||||||
|
<track src="{ track_nl.upload.file.url }" srclang="nl" kind="subtitles" label="nl" />
|
||||||
|
You need a browser that understands HTML5 video and supports h.264 or vp9 codecs.
|
||||||
|
</video>""",
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_video_view_renders_default_track_properly(self):
|
||||||
|
|
||||||
|
factories.create(
|
||||||
|
models.Track,
|
||||||
|
video=self.video,
|
||||||
|
lang='en',
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
content:str = resp.content.decode(resp.charset)
|
||||||
|
|
||||||
|
self.assertInHTML(
|
||||||
|
"""<video width="853" height="480" controls="controls">
|
||||||
|
<source src="http://480p.webm" type='video/webm; codecs="vp9, opus"' />
|
||||||
|
<source src="http://480p.mp4" type='video/mp4' />
|
||||||
|
<track src="/uploads/some_file.txt" default="default" srclang="en" kind="subtitles" label="en" />
|
||||||
|
You need a browser that understands HTML5 video and supports h.264 or vp9 codecs.
|
||||||
|
</video>""",
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_video_view_renders_multiple_tracks_properly_with_one_default(self):
|
||||||
|
|
||||||
|
track_en = factories.create(
|
||||||
|
models.Track,
|
||||||
|
video=self.video,
|
||||||
|
lang='en',
|
||||||
|
default=True
|
||||||
|
)
|
||||||
|
|
||||||
|
track_nl = factories.create(
|
||||||
|
models.Track,
|
||||||
|
video=self.video,
|
||||||
|
lang='nl',
|
||||||
|
)
|
||||||
|
|
||||||
|
resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
content:str = resp.content.decode(resp.charset)
|
||||||
|
|
||||||
|
self.assertInHTML(
|
||||||
|
f"""<video width="853" height="480" controls="controls">
|
||||||
|
<source src="http://480p.webm" type='video/webm; codecs="vp9, opus"' />
|
||||||
|
<source src="http://480p.mp4" type='video/mp4' />
|
||||||
|
<track src="{ track_en.upload.file.url }" default="default" srclang="en" kind="subtitles" label="en" />
|
||||||
|
<track src="{ track_nl.upload.file.url }" srclang="nl" kind="subtitles" label="nl" />
|
||||||
|
You need a browser that understands HTML5 video and supports h.264 or vp9 codecs.
|
||||||
|
</video>""",
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_video_view_renders_track_label(self):
|
||||||
|
|
||||||
|
factories.create(
|
||||||
|
models.Track,
|
||||||
|
video=self.video,
|
||||||
|
lang='en',
|
||||||
|
label='Some Label',
|
||||||
|
)
|
||||||
|
|
||||||
|
resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
content:str = resp.content.decode(resp.charset)
|
||||||
|
|
||||||
|
self.assertInHTML(
|
||||||
|
"""<video width="853" height="480" controls="controls">
|
||||||
|
<source src="http://480p.webm" type='video/webm; codecs="vp9, opus"' />
|
||||||
|
<source src="http://480p.mp4" type='video/mp4' />
|
||||||
|
<track src="/uploads/some_file.txt" srclang="en" kind="subtitles" label="Some Label" />
|
||||||
|
You need a browser that understands HTML5 video and supports h.264 or vp9 codecs.
|
||||||
|
</video>""",
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_video_view_renders_track_kind(self):
|
||||||
|
|
||||||
|
factories.create(
|
||||||
|
models.Track,
|
||||||
|
video=self.video,
|
||||||
|
lang='en',
|
||||||
|
kind='captions',
|
||||||
|
)
|
||||||
|
|
||||||
|
resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
content:str = resp.content.decode(resp.charset)
|
||||||
|
|
||||||
|
self.assertInHTML(
|
||||||
|
"""<video width="853" height="480" controls="controls">
|
||||||
|
<source src="http://480p.webm" type='video/webm; codecs="vp9, opus"' />
|
||||||
|
<source src="http://480p.mp4" type='video/mp4' />
|
||||||
|
<track src="/uploads/some_file.txt" srclang="en" kind="captions" label="en" />
|
||||||
|
You need a browser that understands HTML5 video and supports h.264 or vp9 codecs.
|
||||||
|
</video>""",
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VideoWithCodecOrderCookieTestCase(UploadMixin, TestCase):
|
||||||
|
""" Test the order of the codecs """
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.client = Client()
|
||||||
|
|
||||||
|
self.video = factories.create(
|
||||||
|
models.Video,
|
||||||
|
title='Vid 1',
|
||||||
|
slug='vid-1',
|
||||||
|
default_quality='480p',
|
||||||
|
)
|
||||||
|
factories.create(
|
||||||
|
models.Transcoding,
|
||||||
|
video=self.video,
|
||||||
|
quality='480p',
|
||||||
|
type='video/mp4; codecs="avc1.64001e,mp4a.40.2"',
|
||||||
|
url='http://480p.mp4',
|
||||||
|
)
|
||||||
|
factories.create(
|
||||||
|
models.Transcoding,
|
||||||
|
video=self.video,
|
||||||
|
quality='480p',
|
||||||
|
type='video/webm; codecs="vp9, opus"',
|
||||||
|
url='http://480p.vp9.webm',
|
||||||
|
)
|
||||||
|
factories.create(
|
||||||
|
models.Transcoding,
|
||||||
|
video=self.video,
|
||||||
|
quality='480p',
|
||||||
|
type='video/webm; codecs="vp8, vorbis"',
|
||||||
|
url='http://480p.vp8.webm',
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_video_view_renders_transcoding_types_in_correct_order_without_cookie(self):
|
||||||
|
|
||||||
|
resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
content:str = resp.content.decode(resp.charset)
|
||||||
|
|
||||||
|
self.assertInHTML(
|
||||||
|
"""<video width="853" height="480" controls="controls">
|
||||||
|
<source src="http://480p.vp9.webm" type='video/webm; codecs="vp9, opus"' />
|
||||||
|
<source src="http://480p.vp8.webm" type='video/webm; codecs="vp8, vorbis"' />
|
||||||
|
<source src="http://480p.mp4" type='video/mp4; codecs="avc1.64001e,mp4a.40.2"' />
|
||||||
|
You need a browser that understands HTML5 video and supports h.264 or vp8 or vp9 codecs.
|
||||||
|
</video>""",
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_video_view_renders_transcoding_types_in_correct_order_with_empty_cookie(self):
|
||||||
|
|
||||||
|
self.client.cookies['video_codecs_prio'] = ''
|
||||||
|
|
||||||
|
resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
content:str = resp.content.decode(resp.charset)
|
||||||
|
|
||||||
|
self.assertInHTML(
|
||||||
|
"""<video width="853" height="480" controls="controls">
|
||||||
|
<source src="http://480p.vp9.webm" type='video/webm; codecs="vp9, opus"' />
|
||||||
|
<source src="http://480p.vp8.webm" type='video/webm; codecs="vp8, vorbis"' />
|
||||||
|
<source src="http://480p.mp4" type='video/mp4; codecs="avc1.64001e,mp4a.40.2"' />
|
||||||
|
You need a browser that understands HTML5 video and supports h.264 or vp8 or vp9 codecs.
|
||||||
|
</video>""",
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_video_view_renders_transcoding_types_in_correct_order_with_only_vp8_mentioned(self):
|
||||||
|
|
||||||
|
self.client.cookies['video_codecs_prio'] = 'vp8'
|
||||||
|
resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
content:str = resp.content.decode(resp.charset)
|
||||||
|
|
||||||
|
self.assertInHTML(
|
||||||
|
"""<video width="853" height="480" controls="controls">
|
||||||
|
<source src="http://480p.vp8.webm" type='video/webm; codecs="vp8, vorbis"' />
|
||||||
|
<source src="http://480p.vp9.webm" type='video/webm; codecs="vp9, opus"' />
|
||||||
|
<source src="http://480p.mp4" type='video/mp4; codecs="avc1.64001e,mp4a.40.2"' />
|
||||||
|
You need a browser that understands HTML5 video and supports h.264 or vp8 or vp9 codecs.
|
||||||
|
</video>""",
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_video_view_renders_transcoding_types_in_correct_order_with_only_h264_mentioned(self):
|
||||||
|
|
||||||
|
self.client.cookies['video_codecs_prio'] = 'h.264'
|
||||||
|
resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
content:str = resp.content.decode(resp.charset)
|
||||||
|
|
||||||
|
self.assertInHTML(
|
||||||
|
"""<video width="853" height="480" controls="controls">
|
||||||
|
<source src="http://480p.mp4" type='video/mp4; codecs="avc1.64001e,mp4a.40.2"' />
|
||||||
|
<source src="http://480p.vp9.webm" type='video/webm; codecs="vp9, opus"' />
|
||||||
|
<source src="http://480p.vp8.webm" type='video/webm; codecs="vp8, vorbis"' />
|
||||||
|
You need a browser that understands HTML5 video and supports h.264 or vp8 or vp9 codecs.
|
||||||
|
</video>""",
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_video_view_renders_transcoding_types_in_correct_order_with_vp8_and_h264_mentioned(self):
|
||||||
|
|
||||||
|
self.client.cookies['video_codecs_prio'] = 'vp8 h.264'
|
||||||
|
resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
content:str = resp.content.decode(resp.charset)
|
||||||
|
|
||||||
|
self.assertInHTML(
|
||||||
|
"""<video width="853" height="480" controls="controls">
|
||||||
|
<source src="http://480p.vp8.webm" type='video/webm; codecs="vp8, vorbis"' />
|
||||||
|
<source src="http://480p.mp4" type='video/mp4; codecs="avc1.64001e,mp4a.40.2"' />
|
||||||
|
<source src="http://480p.vp9.webm" type='video/webm; codecs="vp9, opus"' />
|
||||||
|
You need a browser that understands HTML5 video and supports h.264 or vp8 or vp9 codecs.
|
||||||
|
</video>""",
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_video_view_renders_transcoding_types_in_correct_order_with_h264_and_vp8_mentioned(self):
|
||||||
|
|
||||||
|
self.client.cookies['video_codecs_prio'] = 'h.264 vp8'
|
||||||
|
resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
content:str = resp.content.decode(resp.charset)
|
||||||
|
|
||||||
|
self.assertInHTML(
|
||||||
|
"""<video width="853" height="480" controls="controls">
|
||||||
|
<source src="http://480p.mp4" type='video/mp4; codecs="avc1.64001e,mp4a.40.2"' />
|
||||||
|
<source src="http://480p.vp8.webm" type='video/webm; codecs="vp8, vorbis"' />
|
||||||
|
<source src="http://480p.vp9.webm" type='video/webm; codecs="vp9, opus"' />
|
||||||
|
You need a browser that understands HTML5 video and supports h.264 or vp8 or vp9 codecs.
|
||||||
|
</video>""",
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_video_view_renders_transcoding_types_in_correct_order_with_h264_and_vp9_and_vp8_mentioned(self):
|
||||||
|
|
||||||
|
self.client.cookies['video_codecs_prio'] = 'h.264 vp9 vp8'
|
||||||
|
resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
content:str = resp.content.decode(resp.charset)
|
||||||
|
|
||||||
|
self.assertInHTML(
|
||||||
|
"""<video width="853" height="480" controls="controls">
|
||||||
|
<source src="http://480p.mp4" type='video/mp4; codecs="avc1.64001e,mp4a.40.2"' />
|
||||||
|
<source src="http://480p.vp9.webm" type='video/webm; codecs="vp9, opus"' />
|
||||||
|
<source src="http://480p.vp8.webm" type='video/webm; codecs="vp8, vorbis"' />
|
||||||
|
You need a browser that understands HTML5 video and supports h.264 or vp8 or vp9 codecs.
|
||||||
|
</video>""",
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_video_view_renders_transcoding_types_in_correct_order_with_h264_and_vp8_and_vp9_mentioned(self):
|
||||||
|
|
||||||
|
self.client.cookies['video_codecs_prio'] = 'h.264 vp8 vp9'
|
||||||
|
resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
content:str = resp.content.decode(resp.charset)
|
||||||
|
|
||||||
|
self.assertInHTML(
|
||||||
|
"""<video width="853" height="480" controls="controls">
|
||||||
|
<source src="http://480p.mp4" type='video/mp4; codecs="avc1.64001e,mp4a.40.2"' />
|
||||||
|
<source src="http://480p.vp8.webm" type='video/webm; codecs="vp8, vorbis"' />
|
||||||
|
<source src="http://480p.vp9.webm" type='video/webm; codecs="vp9, opus"' />
|
||||||
|
You need a browser that understands HTML5 video and supports h.264 or vp8 or vp9 codecs.
|
||||||
|
</video>""",
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_video_view_renders_transcoding_types_ignores_garbage_in_cookie(self):
|
||||||
|
|
||||||
|
self.client.cookies['video_codecs_prio'] = 'bwarpblergh crap bla'
|
||||||
|
resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
content:str = resp.content.decode(resp.charset)
|
||||||
|
|
||||||
|
self.assertInHTML(
|
||||||
|
"""<video width="853" height="480" controls="controls">
|
||||||
|
<source src="http://480p.vp9.webm" type='video/webm; codecs="vp9, opus"' />
|
||||||
|
<source src="http://480p.vp8.webm" type='video/webm; codecs="vp8, vorbis"' />
|
||||||
|
<source src="http://480p.mp4" type='video/mp4; codecs="avc1.64001e,mp4a.40.2"' />
|
||||||
|
You need a browser that understands HTML5 video and supports h.264 or vp8 or vp9 codecs.
|
||||||
|
</video>""",
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_video_view_renders_transcoding_types_in_correct_order_with_h264_and_vp8_and_crap_and_vp9_mentioned(self):
|
||||||
|
|
||||||
|
self.client.cookies['video_codecs_prio'] = 'h.264 vp8 nonexistent-thing vp9'
|
||||||
|
resp:HttpResponse = self.client.get(reverse('video', args=['vid-1']))
|
||||||
|
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
content:str = resp.content.decode(resp.charset)
|
||||||
|
|
||||||
|
self.assertInHTML(
|
||||||
|
"""<video width="853" height="480" controls="controls">
|
||||||
|
<source src="http://480p.mp4" type='video/mp4; codecs="avc1.64001e,mp4a.40.2"' />
|
||||||
|
<source src="http://480p.vp8.webm" type='video/webm; codecs="vp8, vorbis"' />
|
||||||
|
<source src="http://480p.vp9.webm" type='video/webm; codecs="vp9, opus"' />
|
||||||
|
You need a browser that understands HTML5 video and supports h.264 or vp8 or vp9 codecs.
|
||||||
|
</video>""",
|
||||||
|
content,
|
||||||
|
)
|
||||||
0
tmp/.placeholder
Normal file
0
tmp/.placeholder
Normal file
0
uploads/.placeholder
Normal file
0
uploads/.placeholder
Normal file
57
videodinges/admin.py
Normal file
57
videodinges/admin.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
class TranscodingsForm(forms.ModelForm):
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
if not cleaned_data['url'] and not cleaned_data['upload']:
|
||||||
|
validation_error = ValidationError('Either url or upload must be given', code='invalid')
|
||||||
|
self.add_error('url', validation_error)
|
||||||
|
self.add_error('upload', validation_error)
|
||||||
|
if cleaned_data['url'] and cleaned_data['upload']:
|
||||||
|
validation_error = ValidationError('Cannot fill both url and upload', code='invalid')
|
||||||
|
self.add_error('url', validation_error)
|
||||||
|
self.add_error('upload', validation_error)
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
class TrackInlineFormset(forms.BaseInlineFormSet):
|
||||||
|
forms: Iterable[forms.ModelForm]
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
default_cnt = 0
|
||||||
|
for form in self.forms:
|
||||||
|
try:
|
||||||
|
if form.cleaned_data['default'] is True:
|
||||||
|
default_cnt += 1
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
if default_cnt > 1:
|
||||||
|
form.add_error('default', ValidationError('Can set only one track as default'))
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
class TranscodingsInline(admin.StackedInline):
|
||||||
|
model = models.Transcoding
|
||||||
|
form = TranscodingsForm
|
||||||
|
fields = ['quality', 'type', 'url', 'upload']
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
class TracksInline(admin.StackedInline):
|
||||||
|
model = models.Track
|
||||||
|
formset = TrackInlineFormset
|
||||||
|
fields = ('default', 'kind', 'lang', 'label', 'upload')
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
class VideoAdmin(admin.ModelAdmin):
|
||||||
|
model = models.Video
|
||||||
|
fields = ['title', 'description', 'slug', 'poster', 'og_image', 'created_at', 'default_quality']
|
||||||
|
inlines = (TranscodingsInline, TracksInline)
|
||||||
|
list_display = ('title', 'slug', 'created_at', 'updated_at')
|
||||||
|
ordering = ('-created_at', )
|
||||||
|
|
||||||
|
admin.site.register(models.Video, VideoAdmin)
|
||||||
|
admin.site.register(models.Upload)
|
||||||
@@ -1,22 +1,73 @@
|
|||||||
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from collections import namedtuple
|
from typing import NamedTuple, Optional, Union
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
Quality = namedtuple('Quality', ['name', 'width', 'height', 'priority'])
|
from django.db import models
|
||||||
|
from django.db.models import constraints
|
||||||
|
from django.db.models.query_utils import Q
|
||||||
|
|
||||||
|
class Quality(NamedTuple):
|
||||||
|
name: str
|
||||||
|
width: int
|
||||||
|
height: int
|
||||||
|
priority: int
|
||||||
|
|
||||||
|
class TranscodingType(NamedTuple):
|
||||||
|
name: str
|
||||||
|
short_name: str
|
||||||
|
description: str
|
||||||
|
priority: int
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
qualities = (
|
qualities = (
|
||||||
Quality(name='360p', width=640, height=360, priority=1),
|
Quality(name='360p', width=640, height=360, priority=1),
|
||||||
Quality(name='480p', width=853, height=480, priority=2),
|
Quality(name='480p', width=853, height=480, priority=2),
|
||||||
Quality(name='480p', width=1280, height=720, priority=2),
|
Quality(name='720p', width=1280, height=720, priority=2),
|
||||||
Quality(name='1080p', width=1920, height=1080, priority=1),
|
Quality(name='1080p', width=1920, height=1080, priority=1),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
transcoding_types = (
|
||||||
|
TranscodingType(name='video/webm', short_name='webm', description='Generic WebM', priority=1),
|
||||||
|
TranscodingType(name='video/webm; codecs="vp8, vorbis"', short_name='vp8', description='WebM with VP8 and Vorbis',
|
||||||
|
priority=80),
|
||||||
|
TranscodingType(name='video/webm; codecs="vp9, opus"', short_name='vp9', description='WebM with VP9 and Opus',
|
||||||
|
priority=100),
|
||||||
|
TranscodingType(name='video/mp4', short_name='h.264', description='Generic MP4 with H.264', priority=1),
|
||||||
|
TranscodingType(name='video/mp4; codecs="avc1.64001e,mp4a.40.2"', short_name='h.264',
|
||||||
|
description='MP4 with H.264 (AVC1 profile High, Level 3.0) and AAC-LC', priority=50),
|
||||||
|
TranscodingType(name='video/mp4; codecs="avc1.64001f,mp4a.40.2"', short_name='h.264',
|
||||||
|
description='MP4 with H.264 (AVC1 profile High, Level 3.1) and AAC-LC', priority=50),
|
||||||
|
TranscodingType(name='video/mp4; codecs="avc1.640028,mp4a.40.2"', short_name='h.264',
|
||||||
|
description='MP4 with H.264 (AVC1 profile High, Level 4.0) and AAC-LC', priority=50),
|
||||||
|
TranscodingType(name='video/mp4; codecs="avc1.640032,mp4a.40.2"', short_name='h.264',
|
||||||
|
description='MP4 with H.264 (AVC1 profile High, Level 5.0) and AAC-LC', priority=50),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Upload(models.Model):
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
file = models.FileField()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return os.path.basename(self.file.path)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'uploads'
|
||||||
|
|
||||||
class Video(models.Model):
|
class Video(models.Model):
|
||||||
id = models.AutoField(primary_key=True)
|
id = models.AutoField(primary_key=True)
|
||||||
title = models.CharField()
|
title = models.CharField(max_length=256)
|
||||||
slug = models.CharField()
|
slug = models.CharField(max_length=256, unique=True)
|
||||||
description = models.TextField()
|
description = models.TextField()
|
||||||
|
poster = models.OneToOneField(Upload, on_delete=models.PROTECT, blank=True, null=True, related_name='video_poster')
|
||||||
|
og_image = models.OneToOneField(Upload, on_delete=models.PROTECT, blank=True, null=True, related_name='video_og_image')
|
||||||
|
default_quality = models.CharField(
|
||||||
|
choices=((quality.name, quality.name) for quality in qualities),
|
||||||
|
max_length=128,
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
created_at = models.DateTimeField(default=datetime.now)
|
created_at = models.DateTimeField(default=datetime.now)
|
||||||
updated_at = models.DateTimeField(default=datetime.now)
|
updated_at = models.DateTimeField(default=datetime.now)
|
||||||
|
|
||||||
@@ -24,9 +75,84 @@ class Video(models.Model):
|
|||||||
self.updated_at = datetime.now()
|
self.updated_at = datetime.now()
|
||||||
super().save(force_insert, force_update, using, update_fields)
|
super().save(force_insert, force_update, using, update_fields)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
indexes = [models.Index(fields=['slug']), models.Index(fields=['created_at'])]
|
||||||
|
db_table = 'videos'
|
||||||
|
|
||||||
class Transcoding(models.Model):
|
class Transcoding(models.Model):
|
||||||
id = models.AutoField(primary_key=True)
|
id = models.AutoField(primary_key=True)
|
||||||
video = models.ForeignKey(Video, on_delete=models.CASCADE)
|
video = models.ForeignKey(Video, on_delete=models.CASCADE, related_name='transcodings')
|
||||||
quality = models.CharField(choices=(quality.name, quality.name) for quality in qualities)
|
quality = models.CharField(choices=((quality.name, quality.name) for quality in qualities), max_length=128)
|
||||||
file = models.FileField()
|
type = models.CharField(choices=((str(type_), str(type_)) for type_ in transcoding_types), max_length=128)
|
||||||
|
upload = models.OneToOneField(Upload, on_delete=models.PROTECT, blank=True, null=True)
|
||||||
|
url = models.CharField(max_length=256, null=True, blank=True, unique=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.quality
|
||||||
|
|
||||||
|
@property
|
||||||
|
def quality_obj(self):
|
||||||
|
return get_quality_by_name(self.quality)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('video', 'quality', 'type')
|
||||||
|
constraints = [
|
||||||
|
constraints.CheckConstraint(
|
||||||
|
check=Q(upload__isnull=False) | Q(url__isnull=False),
|
||||||
|
name='upload_or_url_is_filled'
|
||||||
|
),
|
||||||
|
constraints.CheckConstraint(
|
||||||
|
check=~(Q(upload__isnull=False) & Q(url__isnull=False)),
|
||||||
|
name='upload_and_url_cannot_both_be_filled'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
db_table = 'transcodings'
|
||||||
|
|
||||||
|
class Track(models.Model):
|
||||||
|
KINDS = (
|
||||||
|
'subtitles',
|
||||||
|
'captions',
|
||||||
|
'descriptions',
|
||||||
|
'chapters',
|
||||||
|
'metadata',
|
||||||
|
)
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
video = models.ForeignKey(Video, on_delete=models.CASCADE, related_name='tracks')
|
||||||
|
default = models.BooleanField(default=False)
|
||||||
|
kind = models.CharField(choices=((kind, kind) for kind in KINDS), max_length=128, default=KINDS[0])
|
||||||
|
lang = models.CharField(max_length=128)
|
||||||
|
label = models.CharField(max_length=128, blank=True, null=True)
|
||||||
|
upload = models.OneToOneField(Upload, on_delete=models.PROTECT)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '%s_%s' % (self.kind, self.lang)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('video', 'kind', 'lang')
|
||||||
|
constraints = (
|
||||||
|
constraints.UniqueConstraint(
|
||||||
|
fields=('video',), condition=Q(default=True), name='only_one_default_per_video'),
|
||||||
|
)
|
||||||
|
db_table = 'tracks'
|
||||||
|
|
||||||
|
def get_quality_by_name(name: str) -> Optional[Quality]:
|
||||||
|
for quality in qualities:
|
||||||
|
if quality.name == name:
|
||||||
|
return quality
|
||||||
|
|
||||||
|
def get_transcoding_type_by_name(name: str) -> Optional[TranscodingType]:
|
||||||
|
for t in transcoding_types:
|
||||||
|
if t.name == name:
|
||||||
|
return t
|
||||||
|
|
||||||
|
def get_short_name_of_transcoding_type(transcoding_type: Union[str, TranscodingType]) -> str:
|
||||||
|
if isinstance(transcoding_type, str):
|
||||||
|
for type_ in transcoding_types:
|
||||||
|
if type_.name == transcoding_type:
|
||||||
|
transcoding_type = type_
|
||||||
|
|
||||||
|
if isinstance(transcoding_type, TranscodingType):
|
||||||
|
return transcoding_type.short_name
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
"""
|
|
||||||
Django settings for videodinges project.
|
|
||||||
|
|
||||||
Generated by 'django-admin startproject' using Django 1.11.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/1.11/topics/settings/
|
|
||||||
|
|
||||||
For the full list of settings and their values, see
|
|
||||||
https://docs.djangoproject.com/en/1.11/ref/settings/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
|
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
|
||||||
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
|
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
|
||||||
SECRET_KEY = '_$9059r$6diz+m+z91(sn_mx3yarj%h)j_+2z$6j8yp+(-h-1c'
|
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
|
||||||
DEBUG = True
|
|
||||||
|
|
||||||
ALLOWED_HOSTS = []
|
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
|
||||||
'django.contrib.admin',
|
|
||||||
'django.contrib.auth',
|
|
||||||
'django.contrib.contenttypes',
|
|
||||||
'django.contrib.sessions',
|
|
||||||
'django.contrib.messages',
|
|
||||||
'django.contrib.staticfiles',
|
|
||||||
]
|
|
||||||
|
|
||||||
MIDDLEWARE = [
|
|
||||||
'django.middleware.security.SecurityMiddleware',
|
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
|
||||||
'django.middleware.common.CommonMiddleware',
|
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|
||||||
]
|
|
||||||
|
|
||||||
ROOT_URLCONF = 'videodinges.urls'
|
|
||||||
|
|
||||||
TEMPLATES = [
|
|
||||||
{
|
|
||||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
|
||||||
'DIRS': [],
|
|
||||||
'APP_DIRS': True,
|
|
||||||
'OPTIONS': {
|
|
||||||
'context_processors': [
|
|
||||||
'django.template.context_processors.debug',
|
|
||||||
'django.template.context_processors.request',
|
|
||||||
'django.contrib.auth.context_processors.auth',
|
|
||||||
'django.contrib.messages.context_processors.messages',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
WSGI_APPLICATION = 'videodinges.wsgi.application'
|
|
||||||
|
|
||||||
|
|
||||||
# Database
|
|
||||||
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
|
|
||||||
|
|
||||||
DATABASES = {
|
|
||||||
'default': {
|
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
|
||||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Password validation
|
|
||||||
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
|
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
# Internationalization
|
|
||||||
# https://docs.djangoproject.com/en/1.11/topics/i18n/
|
|
||||||
|
|
||||||
LANGUAGE_CODE = 'en-us'
|
|
||||||
|
|
||||||
TIME_ZONE = 'UTC'
|
|
||||||
|
|
||||||
USE_I18N = True
|
|
||||||
|
|
||||||
USE_L10N = True
|
|
||||||
|
|
||||||
USE_TZ = True
|
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
|
||||||
# https://docs.djangoproject.com/en/1.11/howto/static-files/
|
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
|
||||||
4
videodinges/settings/__init__.py
Normal file
4
videodinges/settings/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
try:
|
||||||
|
from .localsettings import *
|
||||||
|
except ImportError:
|
||||||
|
from .defaultsettings import *
|
||||||
141
videodinges/settings/defaultsettings.py
Normal file
141
videodinges/settings/defaultsettings.py
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
"""
|
||||||
|
Django settings for videodinges project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 1.11.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/1.11/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/1.11/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
|
BASE_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..', '..', '..'))
|
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = '_$9059r$6diz+m+z91(sn_mx3yarj%h)j_+2z$6j8yp+(-h-1c'
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
'videodinges',
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'videodinges.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.jinja2.Jinja2',
|
||||||
|
'DIRS': [os.path.join(BASE_DIR, 'videodinges', 'templates')],
|
||||||
|
'APP_DIRS': False,
|
||||||
|
'OPTIONS': {},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'videodinges.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/1.11/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_L10N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/1.11/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
|
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
|
||||||
|
|
||||||
|
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
|
||||||
|
|
||||||
|
MEDIA_URL = '/uploads/'
|
||||||
|
|
||||||
|
FILE_UPLOAD_HANDLERS = ['django.core.files.uploadhandler.TemporaryFileUploadHandler']
|
||||||
|
|
||||||
|
FILE_UPLOAD_MAX_MEMORY_SIZE = 2147483648 # 2GB
|
||||||
|
|
||||||
|
FILE_UPLOAD_TEMP_DIR = os.path.join(BASE_DIR, 'tmp') # probably default /tmp is too small for video files
|
||||||
|
|
||||||
|
URL_BASE = '' # usefull to prefix the application URL on deployment
|
||||||
15
videodinges/settings/localsettings-example.py
Normal file
15
videodinges/settings/localsettings-example.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
"""
|
||||||
|
Copy this file to localsettings.py to make local overrides.
|
||||||
|
BEWARE to always import defaultsettings as well if activate this file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .defaultsettings import *
|
||||||
|
|
||||||
|
DATABASES['default'] = {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||||
|
'NAME': 'videos', # database name
|
||||||
|
'USER': 'videos',
|
||||||
|
'PASSWORD': 'v3r7s3cr3t',
|
||||||
|
'HOST': 'localhost',
|
||||||
|
'PORT': '5432',
|
||||||
|
}
|
||||||
10
videodinges/templates/index.html.j2
Normal file
10
videodinges/templates/index.html.j2
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Video's</h1>
|
||||||
|
<ul>
|
||||||
|
{% for video in videos %}
|
||||||
|
<li><a href="{{ video.slug }}.html">{{ video.title }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
39
videodinges/templates/video.html.j2
Normal file
39
videodinges/templates/video.html.j2
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
{# <link rel="stylesheet" href="style.css" type="text/css" media="screen" /> #}
|
||||||
|
{% if og_image %}
|
||||||
|
<meta property="og:image" content="{{og_image}}" />
|
||||||
|
{% endif %}
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
<video width="{{ width }}" height="{{ height }}" {% if poster %}poster="{{ poster }}" {% endif %}controls="controls">
|
||||||
|
{% for source in sources %}
|
||||||
|
<source src="{{ source.src }}" type='{{ source.type|safe }}' />
|
||||||
|
{% endfor %}
|
||||||
|
{% for track in tracks %}
|
||||||
|
<track{% if track.default %} default="default"{% endif %} src="{{ track.src }}" srclang="{{ track.srclang }}" kind="{{ track.kind }}" label="{{ track.label }}" />
|
||||||
|
{% endfor %}
|
||||||
|
You need a browser that understands HTML5 video and supports {% for i in used_codecs %}{{ i }}{% if not loop.last %} or {% endif %}{% endfor %} codecs.
|
||||||
|
</video><br />
|
||||||
|
<p>
|
||||||
|
{% for quality in qualities %}
|
||||||
|
{% if quality == current_quality %}
|
||||||
|
<strong>{{ quality }} versie</strong>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ slug }}.html?quality={{ quality }}" onclick="vidTimeInUrl(this);">{{ quality }} versie</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if not loop.last %}|{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{ description|safe }}
|
||||||
|
</p>
|
||||||
|
<div id="commenter-container" data-object-id="welmers-video-{{ slug }}">
|
||||||
|
<div class="commenter-count-container"><span class="commenter-count">0</span> comments total</div>
|
||||||
|
</div>
|
||||||
|
<script data-container="commenter-container" src="//www.welmers.net/commenter/js/commenter.js" type="text/javascript"></script>
|
||||||
|
<script src="static/js/video.js" type="text/javascript"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -13,9 +13,23 @@ Including another URLconf
|
|||||||
1. Import the include() function: from django.conf.urls import url, include
|
1. Import the include() function: from django.conf.urls import url, include
|
||||||
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.urls import include
|
||||||
|
|
||||||
urlpatterns = [
|
from . import views
|
||||||
|
|
||||||
|
_urlpatterns = [
|
||||||
url(r'^admin/', admin.site.urls),
|
url(r'^admin/', admin.site.urls),
|
||||||
|
url(r'^$', views.index, name='index'),
|
||||||
|
url(r'^(?P<slug>[\w-]+).html', views.video, name='video')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
_urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
||||||
|
if settings.URL_BASE:
|
||||||
|
urlpatterns = [url(r'^{}/'.format(settings.URL_BASE), include(_urlpatterns))]
|
||||||
|
else:
|
||||||
|
urlpatterns = _urlpatterns
|
||||||
|
|||||||
111
videodinges/views.py
Normal file
111
videodinges/views.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
|
from django.http import HttpResponse, HttpRequest, Http404
|
||||||
|
from django.shortcuts import render
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
def video(request: HttpRequest, slug: str) -> HttpResponse:
|
||||||
|
try:
|
||||||
|
video = models.Video.objects.get(slug=slug)
|
||||||
|
except models.Video.DoesNotExist:
|
||||||
|
raise Http404('Video not found')
|
||||||
|
|
||||||
|
template_data = dict(
|
||||||
|
og_image=video.og_image.file.url if video.og_image else None,
|
||||||
|
title=video.title,
|
||||||
|
poster=video.poster.file.url if video.poster else None,
|
||||||
|
description=video.description,
|
||||||
|
slug=video.slug
|
||||||
|
)
|
||||||
|
|
||||||
|
qualities = _get_qualities(video)
|
||||||
|
try:
|
||||||
|
# find quality specified by URL param
|
||||||
|
quality = qualities[request.GET['quality']]
|
||||||
|
except:
|
||||||
|
# find quality specified by default quality specified for video
|
||||||
|
try:
|
||||||
|
quality = qualities[video.default_quality]
|
||||||
|
except:
|
||||||
|
# take default first quality
|
||||||
|
quality = next(iter(qualities.values()))
|
||||||
|
|
||||||
|
template_data.update(
|
||||||
|
width=quality[0].quality_obj.width,
|
||||||
|
height=quality[0].quality_obj.height,
|
||||||
|
current_quality=quality[0].quality_obj.name
|
||||||
|
)
|
||||||
|
|
||||||
|
sources = [
|
||||||
|
{
|
||||||
|
'src': _url_for(transcoding),
|
||||||
|
'type': transcoding.type,
|
||||||
|
}
|
||||||
|
for transcoding in quality
|
||||||
|
]
|
||||||
|
|
||||||
|
client_codec_prios = _get_codec_prios_from_client(request.COOKIES.get('video_codecs_prio', ''))
|
||||||
|
# sort by client desired order, or transcoding type priority
|
||||||
|
sources.sort(
|
||||||
|
key=lambda i: client_codec_prios.get(models.get_transcoding_type_by_name(i['type']).short_name) or \
|
||||||
|
models.get_transcoding_type_by_name(i['type']).priority,
|
||||||
|
reverse=True
|
||||||
|
)
|
||||||
|
template_data['sources'] = sources
|
||||||
|
|
||||||
|
template_data['used_codecs'] = [
|
||||||
|
models.get_short_name_of_transcoding_type(transcoding.type)
|
||||||
|
for transcoding in quality
|
||||||
|
]
|
||||||
|
|
||||||
|
template_data['qualities'] = qualities.keys()
|
||||||
|
|
||||||
|
template_data['tracks'] = [
|
||||||
|
{
|
||||||
|
'default': track.default,
|
||||||
|
'src': track.upload.file.url,
|
||||||
|
'srclang': track.lang,
|
||||||
|
'kind': track.kind,
|
||||||
|
'label': track.label or track.lang,
|
||||||
|
} for track in video.tracks.all()
|
||||||
|
]
|
||||||
|
|
||||||
|
return render(request, 'video.html.j2', template_data, using='jinja2')
|
||||||
|
|
||||||
|
def index(request: HttpRequest) -> HttpResponse:
|
||||||
|
videos = models.Video.objects.order_by('-created_at').all()
|
||||||
|
return render(request, 'index.html.j2', dict(videos=videos), using='jinja2')
|
||||||
|
|
||||||
|
def _get_dict_from_models_with_fields(model, *fields: str) -> Dict[str, Any]:
|
||||||
|
ret = {}
|
||||||
|
for field in fields:
|
||||||
|
ret[field] = model.__dict__[field]
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def _get_qualities(video: models.Video) -> Dict[str, List[models.Transcoding]]:
|
||||||
|
transcodings: List[models.Transcoding] = video.transcodings.order_by('quality').all()
|
||||||
|
qualities = defaultdict(list)
|
||||||
|
for transcoding in transcodings:
|
||||||
|
qualities[transcoding.quality_obj.name].append(transcoding)
|
||||||
|
return dict(qualities)
|
||||||
|
|
||||||
|
def _url_for(transcoding: models.Transcoding) -> str:
|
||||||
|
if transcoding.url:
|
||||||
|
return transcoding.url
|
||||||
|
elif transcoding.upload:
|
||||||
|
return transcoding.upload.file.url
|
||||||
|
|
||||||
|
def _get_codec_prios_from_client(prio_string: str) -> Dict[str, int]:
|
||||||
|
"""
|
||||||
|
Get prios from prio string.
|
||||||
|
For they are more important than build-in prios, they have
|
||||||
|
values higher than the max value of the build-in prios.
|
||||||
|
"""
|
||||||
|
max_prio = max(tt.priority for tt in models.transcoding_types) + 1
|
||||||
|
client_codecs_prio = prio_string.split(' ')
|
||||||
|
client_codecs_prio.reverse()
|
||||||
|
return {
|
||||||
|
v: max_prio + i
|
||||||
|
for i, v in enumerate(client_codecs_prio)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user