├── .gitignore ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── README.md ├── RELEASE.md ├── REST_API.txt ├── default_config.json ├── docker-entrypoint.sh ├── example_config.json ├── screen_shot └── 1.gif ├── setup.py ├── test ├── docker_launch.sh ├── sample_info_dict.json ├── test.py └── ydl_opt_test.py └── youtube_dl_webui ├── __init__.py ├── __main__.py ├── config.py ├── core.py ├── db.py ├── logging.json ├── msg.py ├── schema.sql ├── server.py ├── static ├── css │ ├── font-awesome.min.css │ ├── global.css │ ├── modalComponent.css │ ├── table.css │ ├── test.css │ └── vue-toast.css ├── font-awesome │ ├── HELP-US-OUT.txt │ ├── css │ │ ├── font-awesome.css │ │ └── font-awesome.min.css │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 │ ├── less │ │ ├── animated.less │ │ ├── bordered-pulled.less │ │ ├── core.less │ │ ├── fixed-width.less │ │ ├── font-awesome.less │ │ ├── icons.less │ │ ├── larger.less │ │ ├── list.less │ │ ├── mixins.less │ │ ├── path.less │ │ ├── rotated-flipped.less │ │ ├── screen-reader.less │ │ ├── stacked.less │ │ └── variables.less │ └── scss │ │ ├── _animated.scss │ │ ├── _bordered-pulled.scss │ │ ├── _core.scss │ │ ├── _fixed-width.scss │ │ ├── _icons.scss │ │ ├── _larger.scss │ │ ├── _list.scss │ │ ├── _mixins.scss │ │ ├── _path.scss │ │ ├── _rotated-flipped.scss │ │ ├── _screen-reader.scss │ │ ├── _stacked.scss │ │ ├── _variables.scss │ │ └── font-awesome.scss └── js │ ├── global.js │ ├── jquery-3.2.1.min.js │ ├── modalComponent.js │ ├── vue-resource.min.js │ ├── vue-toast.js │ └── vue.min.js ├── task.py ├── templates ├── index.html ├── modalComponent.html └── test │ └── index.html ├── test └── post_processor │ └── bestvideo_bestaudio_merging.py ├── utils.py └── worker.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | # Other file types 92 | *.swp 93 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6-slim 2 | 3 | # grab gosu for easy step-down from root 4 | ENV GOSU_VERSION 1.10 5 | RUN set -x \ 6 | && buildDeps=' \ 7 | unzip \ 8 | ca-certificates \ 9 | dirmngr \ 10 | wget \ 11 | xz-utils \ 12 | gpg \ 13 | ' \ 14 | && apt-get update && apt-get install -y --no-install-recommends $buildDeps \ 15 | && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$(dpkg --print-architecture)" \ 16 | && wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$(dpkg --print-architecture).asc" \ 17 | && export GNUPGHOME="$(mktemp -d)" \ 18 | && gpg --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4 \ 19 | && gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu \ 20 | && rm -rf "$GNUPGHOME" /usr/local/bin/gosu.asc \ 21 | && chmod +x /usr/local/bin/gosu \ 22 | && gosu nobody true 23 | 24 | # install ffmpeg 25 | ENV FFMPEG_URL 'http://nas.oldiy.top/%E5%B7%A5%E5%85%B7/ffmpeg-release-amd64-static.tar.xz' 26 | #ENV FFMPEG_URL 'https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz' 27 | RUN : \ 28 | && mkdir -p /tmp/ffmpeg \ 29 | && cd /tmp/ffmpeg \ 30 | && wget -O ffmpeg.tar.xz "$FFMPEG_URL" \ 31 | && tar -xf ffmpeg.tar.xz -C . --strip-components 1 \ 32 | && cp ffmpeg ffprobe qt-faststart /usr/bin \ 33 | && cd .. \ 34 | && rm -fr /tmp/ffmpeg 35 | 36 | # install youtube-dl-webui 37 | ENV YOUTUBE_DL_WEBUI_SOURCE /usr/src/youtube_dl_webui 38 | WORKDIR $YOUTUBE_DL_WEBUI_SOURCE 39 | 40 | RUN : \ 41 | && pip install --no-cache-dir youtube-dl flask \ 42 | && wget -O youtube-dl-webui.zip https://github.com/oldiy/youtubedl-webui/archive/0.3.zip \ 43 | && unzip youtube-dl-webui.zip \ 44 | && cd youtubedl-webui*/ \ 45 | && cp -r ./* $YOUTUBE_DL_WEBUI_SOURCE/ \ 46 | && ln -s $YOUTUBE_DL_WEBUI_SOURCE/example_config.json /etc/youtube-dl-webui.json \ 47 | && cd .. && rm -rf youtubedl-webui* \ 48 | && apt-get purge -y --auto-remove wget unzip dirmngr \ 49 | && rm -fr /var/lib/apt/lists/* 50 | 51 | COPY docker-entrypoint.sh /usr/local/bin 52 | COPY default_config.json /config.json 53 | 54 | RUN chmod +x /usr/local/bin/docker-entrypoint.sh 55 | 56 | ENTRYPOINT ["docker-entrypoint.sh"] 57 | 58 | EXPOSE 5555 59 | 60 | VOLUME ["/youtube_dl"] 61 | 62 | CMD ["python", "-m", "youtube_dl_webui"] 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | include *.rst 3 | include *.py 4 | include MANIFEST.in 5 | include LICENSE 6 | recursive-include youtube_dl_webui *.json 7 | recursive-include youtube_dl_webui *.py 8 | recursive-include youtube_dl_webui *.sql 9 | recursive-include youtube_dl_webui/templates * 10 | recursive-include youtube_dl_webui/static * 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Youtube-dl-Webui 3 | 4 | [![Docker Pulls](https://img.shields.io/docker/pulls/oldiy/youtube-dl-web.svg)][dockerhub] 5 | 6 | [dockerhub]: https://hub.docker.com/r/oldiy/youtube-dl-web 7 | 8 | --- 9 | 10 | 执行命令 11 | 12 | `docker run -d --name youtube-dl-web -p 5555:5555 -v <本机目录>:/youtube_dl oldiy/youtube-dl-web` 13 | 14 | --- 15 | 16 | + 如果觉得不错,请帮忙点击上面的 [ ★ ] 17 | 18 | + [ [群晖安装教程](https://odcn.top/2019/03/01/2754/) ] 19 | 20 | + [ [Blog](https://odcn.top) ] 21 | 22 | + 加入我的Telegram讨论组 [[Join](https://t.me/joinchat/H3IoGkcnW6BGo51EJ9Kw5g)] 23 | 24 | 一款支持离线下载的youtube-dl的web管理器,支持下载所有youtube-dl支持的网站! 25 | 26 | ### Docker image 27 | [ [ Docker ] ](https://hub.docker.com/r/oldiy/youtube-downloader) 28 | 29 | --- 30 | 31 | ![screenshot1](https://github.com/oldiy/youtubedl-webui/raw/master/screen_shot/1.gif) 32 | 33 | --- 34 | 35 | ![](https://odcn.top/wp-content/uploads/2018/11/%E9%BB%91%E5%88%BA%E7%8C%AC%E6%A8%AA150.png) 36 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # RELEASE LOG 2 | 3 | --- 4 | 5 | ## 0.1.0 6 | 7 | This is the first version. 8 | 9 | ## 0.1.1 10 | 11 | Bug fixes. 12 | 13 | - Fix #6 14 | - Update README.md 15 | - Add RELEASE.md 16 | 17 | ## 0.2.0 18 | 19 | Lot of updates here in this new version. 20 | 21 | - Refactor entire architecture. 22 | - Add about button. 23 | - Can set independent youtube-dl options for each task. 24 | - Some UI improvement. 25 | 26 | ## 0.2.1 27 | 28 | Some bugs fix. 29 | -------------------------------------------------------------------------------- /REST_API.txt: -------------------------------------------------------------------------------- 1 | @ version: 1.1 2 | 3 | 4 | |----+--------+-----------------------------------------------------------------+----------------------------------------------| 5 | | | method | url | description | 6 | |----+:------:+-----------------------------------------------------------------+----------------------------------------------| 7 | | 1 | POST | /task | create a new download task | 8 | | 2 | PUT | /task/tid/?act=pause | pause a task with its tid | 9 | | 3 | PUT | /task/tid/?act=resume | resume download | 10 | | 4 | DELETE | /task/tid/?del_file=true | delete a task and its data | 11 | | 5 | DELETE | /task/tid/ | delete a task and keep data | 12 | | 6 | GET | /task/tid//status | get the full status of a task | 13 | | 7 | GET | /task/tid//status?exerpt=true | get the status exerpt of a task | 14 | | 8 | GET | /task/tid//info | get the task info | 15 | | 9 | POST | /task/batch/pause | post a list of tasks' tid to be paused | 16 | | 10 | POST | /task/batch/resume | post a list of tasks' tid to be resumed | 17 | | 12 | POST | /task/batch/delete | post a list of tasks' tid to be deleted | 18 | | 13 | GET | /task/list | get task list exerpt | 19 | | 14 | GET | /task/list?exerpt=false | get task list | 20 | | 15 | GET | /task/list?state={all|downloading|finished|paused} | get task list exerpt according to the state | 21 | | 16 | GET | /task/list?exerpt=false&state={all|downloading|finished|paused} | get task list according to the state | 22 | | 17 | GET | /task/state_coutner | get number of tasks in each state | 23 | | 18 | GET | /config | get the server's configuration | 24 | |----+--------+-----------------------------------------------------------------+----------------------------------------------| 25 | 26 | Note: 27 | 28 | Server always replies json formated data to browser in which field 'status' must be contained to indicate 29 | that if the previous operation is successful or not. 30 | 31 | Valid values for field 'status' are 'success' and 'error'. 32 | 33 | When 'error' status occurs, another filed 'errmsg' is included to points out what happened. 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /default_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "download_dir": "/youtube_dl", 4 | "db_path": "/tmp/youtube_dl_webui.db", 5 | "log_size": 10 6 | }, 7 | "server": { 8 | "host": "0.0.0.0", 9 | "port": 5000 10 | }, 11 | "youtube_dl": { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | pgid=${PGID:-$(id -u root)} 5 | puid=${PUID:-$(id -g root)} 6 | 7 | conf=${CONF_FILE:-"/config.json"} 8 | host=${HOST:-"0.0.0.0"} 9 | port=${PORT:-5555} 10 | 11 | 12 | if [[ "$*" == python*-m*youtube_dl_webui* ]]; then 13 | exec gosu $puid:$pgid "$@" -c $conf --host $host --port $port 14 | fi 15 | 16 | exec "$@" 17 | -------------------------------------------------------------------------------- /example_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "download_dir": "/youtube_dl", 4 | "db_path": "/tmp/youtube_dl_webui.db", 5 | "log_size": 10 6 | }, 7 | "server": { 8 | "host": "0.0.0.0", 9 | "port": 5555 10 | }, 11 | "youtube_dl": { 12 | "format": "bestaudio/best", 13 | "proxy": "socks5://127.0.0.1:1080" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /screen_shot/1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldiy/youtubedl-webui/26928a28a6507b24b28e5c987e691b9b81b278bb/screen_shot/1.gif -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | 4 | from setuptools import setup 5 | 6 | DESCRIPTION = 'webui for youtube-dl' 7 | LONG_DESCRIPTION = 'Another webui for youtube-dl, powered by youtube-dl' 8 | 9 | setup ( 10 | name='youtube_dl_webui', 11 | version='rolling', 12 | packages=['youtube_dl_webui'], 13 | license='GPL-2.0', 14 | author='d0u9, yuanyingfeiyu', 15 | author_email='d0u9.su@outlook.com', 16 | description=DESCRIPTION, 17 | long_description=LONG_DESCRIPTION, 18 | include_package_data=True, 19 | zip_safe=False, 20 | install_requires=[ 21 | 'Flask>=0.2', 22 | 'youtube-dl', 23 | ], 24 | entry_points={ 25 | 'console_scripts': [ 26 | 'youtube-dl-webui = youtube_dl_webui:main' 27 | ] 28 | }, 29 | ) 30 | -------------------------------------------------------------------------------- /test/docker_launch.sh: -------------------------------------------------------------------------------- 1 | docker run \ 2 | --rm \ 3 | -d \ 4 | --network sky \ 5 | --name youtube_dl_webui \ 6 | -p 5000:5000 \ 7 | -e FLASK_DEBUG=1 \ 8 | -e CONF_FILE=/conf.json \ 9 | -v $HOME/Documents/example_config.json:/conf.json \ 10 | -v $HOME/youtube-dl-webui/youtube_dl_webui/static:/usr/src/youtube_dl_webui/youtube_dl_webui/static \ 11 | -v $HOME/youtube-dl-webui/youtube_dl_webui/templates:/usr/src/youtube_dl_webui/youtube_dl_webui/templates \ 12 | d0u9/youtube-dl-webui:dev 13 | -------------------------------------------------------------------------------- /test/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import requests 5 | import json 6 | import sys 7 | from time import sleep 8 | 9 | def task_add(url): 10 | r = requests.post('http://127.0.0.1:5000/task', data={'url': url}) 11 | print('status: {}'.format(r.status_code)) 12 | j = json.loads(r.text) 13 | return j 14 | 15 | 16 | def task_act(tid, act): 17 | url = 'http://127.0.0.1:5000/task/tid/{}?act={}'.format(tid, act) 18 | if act == 'pause' or act == 'resume': 19 | print(act) 20 | r = requests.put(url) 21 | print('status: {}'.format(r.status_code)) 22 | j = json.loads(r.text) 23 | 24 | return j 25 | 26 | 27 | def task_delete(tid, del_data=False): 28 | url = 'http://127.0.0.1:5000/task/tid/{}'.format(tid) 29 | 30 | r = requests.delete(url) 31 | 32 | print('status: {}'.format(r.status_code)) 33 | j = json.loads(r.text) 34 | 35 | return j 36 | 37 | 38 | def task_query(tid, exerpt=False): 39 | url = 'http://127.0.0.1:5000/task/tid/{}/status'.format(tid) 40 | 41 | if exerpt is True: 42 | url = url + '?exerpt=true' 43 | 44 | r = requests.get(url) 45 | print('status: {}'.format(r.status_code)) 46 | j = json.loads(r.text) 47 | return j 48 | 49 | 50 | def task_list(state=None, exerpt=None): 51 | print(state) 52 | print(exerpt) 53 | if state is not None and exerpt is not None: 54 | url = 'http://127.0.0.1:5000/task/list?state={}&exerpt={}'.format(state, exerpt) 55 | elif state is not None and exerpt is None: 56 | url = 'http://127.0.0.1:5000/task/list?state={}'.format(state) 57 | elif state is None and exerpt is not None: 58 | url = 'http://127.0.0.1:5000/task/list?exerpt={}'.format(exerpt) 59 | else: 60 | url = 'http://127.0.0.1:5000/task/list' 61 | 62 | r = requests.get(url) 63 | 64 | print('status: {}'.format(r.status_code)) 65 | j = json.loads(r.text) 66 | return j 67 | 68 | 69 | def list_state(): 70 | url = 'http://127.0.0.1:5000/task/state_counter' 71 | r = requests.get(url) 72 | 73 | print('status: {}'.format(r.status_code)) 74 | j = json.loads(r.text) 75 | return j 76 | 77 | if __name__ == '__main__': 78 | default_url = 'https://www.youtube.com/watch?v=RPvP9wL81qs' 79 | 80 | if len(sys.argv) == 1: 81 | act='create' 82 | p1 = default_url 83 | else: 84 | act = sys.argv[1] 85 | try: 86 | p1 = sys.argv[2] 87 | except: 88 | p1 = None 89 | 90 | try: 91 | p2 = sys.argv[3] 92 | except: 93 | p2 = None 94 | 95 | try: 96 | p3 = sys.argv[4] 97 | except: 98 | p3 = None 99 | 100 | if act == '-h': 101 | print('create | del | act | query | list | state') 102 | 103 | if act == 'create': 104 | ret = task_add(p1) 105 | print(ret) 106 | 107 | if act == 'del': 108 | ret = task_delete(p1) 109 | print (ret) 110 | 111 | if act == 'act': 112 | ret = task_act(p1, p2) 113 | print(ret) 114 | 115 | if act == 'query': 116 | if p2 == 'E': 117 | ret = task_query(p1, False) 118 | else: 119 | ret = task_query(p1, True) 120 | 121 | print(ret) 122 | 123 | if act == 'list': 124 | ret = task_list(p1, p2) 125 | print(ret) 126 | 127 | if act == 'state': 128 | ret = list_state() 129 | print(ret) 130 | 131 | 132 | # tid = r['tid'] 133 | # print(r) 134 | # sleep(1) 135 | 136 | # print('--------- list tasks') 137 | # r = task_list() 138 | # print(r) 139 | # sleep(1) 140 | 141 | # print('--------- get status') 142 | # r = task_query(tid) 143 | # print (r) 144 | # sleep(1) 145 | 146 | # print('--------- pause a task') 147 | # r = task_act(tid, 'pause') 148 | # print(r) 149 | # sleep(2) 150 | 151 | # print('--------- list tasks') 152 | # r = task_list() 153 | # print(r) 154 | # sleep(1) 155 | 156 | # print('--------- resume a task') 157 | # r = task_act(tid, 'resume') 158 | # print(r) 159 | 160 | # print('--------- get status') 161 | # r = task_query(tid, True) 162 | # print (r) 163 | # sleep(1) 164 | 165 | # sleep(3) 166 | # print('--------- delete a task') 167 | # r = task_delete(tid) 168 | # print(r) 169 | # sleep(1) 170 | 171 | """ 172 | print('--------- list tasks') 173 | r = list_state() 174 | print(r) 175 | sleep(1) 176 | 177 | while True: 178 | print('--------- list tasks') 179 | r = task_query(tid) 180 | print(r) 181 | sleep(1) 182 | """ 183 | -------------------------------------------------------------------------------- /test/ydl_opt_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | import youtube_dl 3 | 4 | 5 | class MyLogger(object): 6 | def debug(self, msg): 7 | print(msg) 8 | 9 | def warning(self, msg): 10 | print(msg) 11 | 12 | def error(self, msg): 13 | print(msg) 14 | 15 | 16 | def my_hook(d): 17 | if d['status'] == 'finished': 18 | print('Done downloading, now converting ...') 19 | 20 | 21 | ydl_opts = { 22 | 'noplaylist': 'false', 23 | 'proxy': 'socks5://127.0.0.1:1080', 24 | 'format': 'bestaudio/best', 25 | 'logger': MyLogger(), 26 | 'progress_hooks': [my_hook], 27 | } 28 | with youtube_dl.YoutubeDL(ydl_opts) as ydl: 29 | # ydl.download(['http://www.youtube.com/watch?v=BaW_jenozKc']) 30 | ydl.download(['https://www.youtube.com/watch?v=JGwWNGJdvx8&list=PLx0sYbCqOb8TBPRdmBHs5Iftvv9TPboYG']) 31 | -------------------------------------------------------------------------------- /youtube_dl_webui/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import unicode_literals 5 | from argparse import ArgumentParser 6 | 7 | from .core import Core 8 | 9 | def getopt(argv): 10 | parser = ArgumentParser(description='Another webui for youtube-dl') 11 | 12 | parser.add_argument('-c', '--config', metavar="CONFIG_FILE", help="config file") 13 | parser.add_argument('--host', metavar="ADDR", help="the address server listens on") 14 | parser.add_argument('--port', metavar="PORT", help="the port server listens on") 15 | 16 | return vars(parser.parse_args()) 17 | 18 | 19 | def main(argv=None): 20 | from os import getpid 21 | 22 | print("pid is {}".format(getpid())) 23 | print("-----------------------------------") 24 | 25 | cmd_args = getopt(argv) 26 | core = Core(cmd_args=cmd_args) 27 | core.start() 28 | -------------------------------------------------------------------------------- /youtube_dl_webui/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import unicode_literals 5 | 6 | import sys 7 | import os.path 8 | import json 9 | import logging.config 10 | 11 | if __package__ is None and not hasattr(sys, 'frozen'): 12 | path = os.path.realpath(os.path.abspath(__file__)) 13 | dirname = os.path.dirname(path) 14 | sys.path.insert(0, os.path.dirname(os.path.dirname(path))) 15 | else: 16 | path = os.path.realpath(os.path.abspath(__file__)) 17 | dirname = os.path.dirname(path) 18 | 19 | 20 | import youtube_dl_webui 21 | 22 | if __name__ == '__main__': 23 | # Setup logger 24 | logging_json = os.path.join(dirname, 'logging.json') 25 | with open(logging_json) as f: 26 | logging_conf = json.load(f) 27 | logging.config.dictConfig(logging_conf) 28 | 29 | youtube_dl_webui.main() 30 | 31 | -------------------------------------------------------------------------------- /youtube_dl_webui/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import logging 5 | import json 6 | 7 | from copy import deepcopy 8 | from os.path import expanduser 9 | 10 | class conf_base(object): 11 | def __init__(self, valid_fields, conf_dict): 12 | # each item in the _valid_fields is a tuple represents 13 | # (key, default_val, type, validate_regex, call_function) 14 | self._valid_fields = valid_fields 15 | self._conf = {} 16 | self.load(conf_dict) 17 | 18 | def load(self, conf_dict): 19 | for field in self._valid_fields: 20 | key = field[0] 21 | dft_val = field[1] 22 | val_type = field[2] 23 | vld_regx = field[3] 24 | func = field[4] 25 | 26 | # More check can be made here 27 | if key in conf_dict: 28 | self._conf[key] = conf_dict[key] if func is None else func(conf_dict.get(key, dft_val)) 29 | elif dft_val is not None: 30 | self._conf[key] = dft_val if func is None else func(conf_dict.get(key, dft_val)) 31 | 32 | 33 | def get_val(self, key): 34 | return self._conf[key] 35 | 36 | def __getitem__(self, key): 37 | return self.get_val(key) 38 | 39 | def set_val(self, key, val): 40 | self._conf[key] = val 41 | 42 | def __setitem__(self, key, val): 43 | self.set_val(key, val) 44 | 45 | def dict(self): 46 | return self._conf 47 | 48 | 49 | class ydl_conf(conf_base): 50 | _valid_fields = [ 51 | #(key, default_val, type, validate_regex, call_function) 52 | ('proxy', None, 'string', None, None), 53 | ('format', None, 'string', None, None), 54 | ] 55 | 56 | _task_settable_fields = set(['format']) 57 | 58 | def __init__(self, conf_dict={}): 59 | self.logger = logging.getLogger('ydl_webui') 60 | 61 | super(ydl_conf, self).__init__(self._valid_fields, conf_dict) 62 | 63 | def merge_conf(self, task_conf_dict={}): 64 | ret = deepcopy(self.dict()) 65 | for key, val in task_conf_dict.items(): 66 | if key not in self._task_settable_fields or val == '': 67 | continue 68 | ret[key] = val 69 | 70 | return ret 71 | 72 | 73 | class svr_conf(conf_base): 74 | _valid_fields = [ 75 | #(key, default_val, type, validate_regex, call_function) 76 | ('host', '127.0.0.1', 'string', None, None), 77 | ('port', '5000', 'string', None, None), 78 | ] 79 | 80 | def __init__(self, conf_dict={}): 81 | self.logger = logging.getLogger('ydl_webui') 82 | 83 | super(svr_conf, self).__init__(self._valid_fields, conf_dict) 84 | 85 | 86 | class gen_conf(conf_base): 87 | _valid_fields = [ 88 | #(key, default_val, type, validate_regex, call_function) 89 | ('download_dir', '~/Downloads/youtube-dl', 'string', '', expanduser), 90 | ('db_path', '~/.conf/ydl_webui.db', 'string', '', expanduser), 91 | ('log_size', 10, 'int', '', None), 92 | ] 93 | 94 | def __init__(self, conf_dict={}): 95 | self.logger = logging.getLogger('ydl_webui') 96 | 97 | super(gen_conf, self).__init__(self._valid_fields, conf_dict) 98 | 99 | 100 | class conf(object): 101 | _valid_fields = set(('youtube_dl', 'server', 'general')) 102 | 103 | ydl_conf = None 104 | svr_conf = None 105 | gen_conf = None 106 | 107 | def __init__(self, conf_file, conf_dict={}, cmd_args={}): 108 | self.logger = logging.getLogger('ydl_webui') 109 | self.conf_file = conf_file 110 | self.cmd_args = cmd_args 111 | self.load(conf_dict) 112 | 113 | def cmd_args_override(self): 114 | _cat_dict = {'host': 'server', 115 | 'port': 'server'} 116 | 117 | for key, val in self.cmd_args.items(): 118 | if key not in _cat_dict or val is None: 119 | continue 120 | sub_conf = self.get_val(_cat_dict[key]) 121 | sub_conf.set_val(key, val) 122 | 123 | def load(self, conf_dict): 124 | if not isinstance(conf_dict, dict): 125 | self.logger.error("input parameter(conf_dict) is not an instance of dict") 126 | return 127 | 128 | for f in self._valid_fields: 129 | if f == 'youtube_dl': 130 | self.ydl_conf = ydl_conf(conf_dict.get(f, {})) 131 | elif f == 'server': 132 | self.svr_conf = svr_conf(conf_dict.get(f, {})) 133 | elif f == 'general': 134 | self.gen_conf = gen_conf(conf_dict.get(f, {})) 135 | 136 | # override configurations by cmdline arguments 137 | self.cmd_args_override() 138 | 139 | def save2file(self): 140 | if self.conf_file is not None: 141 | try: 142 | with open(self.conf_file, 'w') as f: 143 | json.dump(self.dict(), f, indent=4) 144 | except PermissionError: 145 | return (False, 'permission error') 146 | except FileNotFoundError: 147 | return (False, 'can not find file') 148 | else: 149 | return (True, None) 150 | 151 | def dict(self): 152 | d = {} 153 | for f in self._valid_fields: 154 | if f == 'youtube_dl': 155 | d[f] = self.ydl_conf.dict() 156 | elif f == 'server': 157 | d[f] = self.svr_conf.dict() 158 | elif f == 'general': 159 | d[f] = self.gen_conf.dict() 160 | 161 | return d 162 | 163 | def get_val(self, key): 164 | if key not in self._valid_fields: 165 | raise KeyError(key) 166 | 167 | if key == 'youtube_dl': 168 | return self.ydl_conf 169 | elif key == 'server': 170 | return self.svr_conf 171 | elif key == 'general': 172 | return self.gen_conf 173 | else: 174 | raise KeyError(key) 175 | 176 | def __getitem__(self, key): 177 | return self.get_val(key) 178 | 179 | -------------------------------------------------------------------------------- /youtube_dl_webui/core.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import json 5 | import os 6 | import logging 7 | 8 | from multiprocessing import Process, Queue 9 | from collections import deque 10 | from sys import exit 11 | from time import time 12 | from os.path import expanduser 13 | 14 | from .utils import state_name 15 | from .db import DataBase 16 | from .utils import TaskInexistenceError 17 | from .utils import TaskExistenceError 18 | from .utils import TaskError 19 | from .server import Server 20 | from .worker import Worker 21 | 22 | from .config import ydl_conf, conf 23 | from .task import TaskManager, Task 24 | from .msg import MsgMgr 25 | 26 | class WebMsgDispatcher(object): 27 | logger = logging.getLogger('ydl_webui') 28 | 29 | SuccessMsg = {'status': 'success'} 30 | InternalErrorMsg = {'status': 'error', 'errmsg': 'Internal Error'} 31 | TaskExistenceErrorMsg = {'status': 'error', 'errmsg': 'URL is already added'} 32 | TaskInexistenceErrorMsg = {'status': 'error', 'errmsg': 'Task does not exist'} 33 | UrlErrorMsg = {'status': 'error', 'errmsg': 'URL is invalid'} 34 | InvalidStateMsg = {'status': 'error', 'errmsg': 'Invalid query state'} 35 | RequestErrorMsg = {'status': 'error', 'errmsg': 'Request error'} 36 | 37 | _task_mgr = None 38 | _conf = None 39 | 40 | @classmethod 41 | def init(cls, conf, task_mgr): 42 | cls._task_mgr = task_mgr 43 | cls._conf = conf 44 | 45 | @classmethod 46 | def event_create(cls, svr, event, data, args): 47 | url, ydl_opts = data.get('url', None), data.get('ydl_opts', {}) 48 | cls.logger.debug('url = %s, ydl_opts = %s' %(url, ydl_opts)) 49 | 50 | if url is None: 51 | svr.put(cls.UrlErrorMsg) 52 | return 53 | 54 | try: 55 | tid = cls._task_mgr.new_task(url, ydl_opts=ydl_opts) 56 | except TaskExistenceError: 57 | svr.put(cls.TaskExistenceErrorMsg) 58 | return 59 | 60 | task = cls._task_mgr.start_task(tid) 61 | 62 | svr.put({'status': 'success', 'tid': tid}) 63 | 64 | @classmethod 65 | def event_delete(cls, svr, event, data, args): 66 | tid, del_file = data['tid'], data['del_file'] 67 | 68 | try: 69 | cls._task_mgr.delete_task(tid, del_file) 70 | except TaskInexistenceError: 71 | svr.put(cls.TaskInexistenceErrorMsg) 72 | else: 73 | svr.put(cls.SuccessMsg) 74 | 75 | @classmethod 76 | def event_manipulation(cls, svr, event, data, args): 77 | cls.logger.debug('manipulation event') 78 | tid, act = data['tid'], data['act'] 79 | 80 | ret_val = cls.RequestErrorMsg 81 | if act == 'pause': 82 | try: 83 | cls._task_mgr.pause_task(tid) 84 | except TaskError as e: 85 | ret_val = {'status': 'error', 'errmsg': e.msg} 86 | else: 87 | ret_val = cls.SuccessMsg 88 | elif act == 'resume': 89 | try: 90 | cls._task_mgr.start_task(tid) 91 | except TaskError as e: 92 | ret_val = {'status': 'error', 'errmsg': e.msg} 93 | else: 94 | ret_val = cls.SuccessMsg 95 | 96 | svr.put(ret_val) 97 | 98 | @classmethod 99 | def event_query(cls, svr, event, data, args): 100 | cls.logger.debug('query event') 101 | tid, exerpt = data['tid'], data['exerpt'] 102 | 103 | try: 104 | detail = cls._task_mgr.query(tid, exerpt) 105 | except TaskInexistenceError: 106 | svr.put(cls.TaskInexistenceErrorMsg) 107 | else: 108 | svr.put({'status': 'success', 'detail': detail}) 109 | 110 | @classmethod 111 | def event_list(cls, svr, event, data, args): 112 | exerpt, state = data['exerpt'], data['state'] 113 | 114 | if state not in state_name: 115 | svr.put(cls.InvalidStateMsg) 116 | else: 117 | d, c = cls._task_mgr.list(state, exerpt) 118 | svr.put({'status': 'success', 'detail': d, 'state_counter': c}) 119 | 120 | @classmethod 121 | def event_state(cls, svr, event, data, args): 122 | c = cls._task_mgr.state() 123 | svr.put({'status': 'success', 'detail': c}) 124 | 125 | @classmethod 126 | def event_config(cls, svr, event, data, arg): 127 | act = data['act'] 128 | 129 | ret_val = cls.RequestErrorMsg 130 | if act == 'get': 131 | ret_val = {'status': 'success'} 132 | ret_val['config'] = cls._conf.dict() 133 | elif act == 'update': 134 | conf_dict = data['param'] 135 | cls._conf.load(conf_dict) 136 | suc, msg = cls._conf.save2file() 137 | if suc: 138 | ret_val = cls.SuccessMsg 139 | else: 140 | ret_val = {'status': 'error', 'errmsg': msg} 141 | 142 | svr.put(ret_val) 143 | 144 | @classmethod 145 | def event_batch(cls, svr, event, data, arg): 146 | act, detail = data['act'], data['detail'] 147 | 148 | if 'tids' not in detail: 149 | svr.put(cls.RequestErrorMsg) 150 | return 151 | 152 | tids = detail['tids'] 153 | errors = [] 154 | if act == 'pause': 155 | for tid in tids: 156 | try: 157 | cls._task_mgr.pause_task(tid) 158 | except TaskInexistenceError: 159 | errors.append([tid, 'Inexistence error']) 160 | except TaskError as e: 161 | errors.append([tid, e.msg]) 162 | elif act == 'resume': 163 | for tid in tids: 164 | try: 165 | cls._task_mgr.start_task(tid) 166 | except TaskInexistenceError: 167 | errors.append([tid, 'Inexistence error']) 168 | except TaskError as e: 169 | errors.append([tid, e.msg]) 170 | elif act == 'delete': 171 | del_file = True if detail.get('del_file', 'false') == 'true' else False 172 | for tid in tids: 173 | try: 174 | cls._task_mgr.delete_task(tid, del_file) 175 | except TaskInexistenceError: 176 | errors.append([tid, 'Inexistence error']) 177 | 178 | if errors: 179 | ret_val = {'status': 'success', 'detail': errors} 180 | else: 181 | ret_val = cls.SuccessMsg 182 | 183 | svr.put(ret_val) 184 | 185 | 186 | class WorkMsgDispatcher(object): 187 | 188 | _task_mgr = None 189 | logger = logging.getLogger('ydl_webui') 190 | 191 | @classmethod 192 | def init(cls, task_mgr): 193 | cls._task_mgr = task_mgr 194 | 195 | @classmethod 196 | def event_info_dict(cls, svr, event, data, arg): 197 | tid, info_dict = data['tid'], data['data'] 198 | cls._task_mgr.update_info(tid, info_dict) 199 | 200 | @classmethod 201 | def event_log(cls, svr, event, data, arg): 202 | tid, log = data['tid'], data['data'] 203 | cls._task_mgr.update_log(tid, log) 204 | 205 | @classmethod 206 | def event_fatal(cls, svr, event, data, arg): 207 | tid, data = data['tid'], data['data'] 208 | 209 | cls._task_mgr.update_log(tid, data) 210 | if data['type'] == 'fatal': 211 | cls._task_mgr.halt_task(tid) 212 | 213 | @classmethod 214 | def event_progress(cls, svr, event, data, arg): 215 | tid, data = data['tid'], data['data'] 216 | try: 217 | cls._task_mgr.progress_update(tid, data) 218 | except TaskInexistenceError: 219 | cls.logger.error('Cannot update progress, task does not exist') 220 | 221 | @classmethod 222 | def event_worker_done(cls, svr, event, data, arg): 223 | tid, data = data['tid'], data['data'] 224 | try: 225 | cls._task_mgr.finish_task(tid) 226 | except TaskInexistenceError: 227 | cls.logger.error('Cannot finish, task does not exist') 228 | 229 | 230 | def load_conf_from_file(cmd_args): 231 | logger = logging.getLogger('ydl_webui') 232 | 233 | conf_file = cmd_args.get('config', None) 234 | logger.info('load config file (%s)' %(conf_file)) 235 | 236 | if cmd_args is None or conf_file is None: 237 | return (None, {}, {}) 238 | 239 | abs_file = os.path.abspath(conf_file) 240 | try: 241 | with open(abs_file) as f: 242 | return (abs_file, json.load(f), cmd_args) 243 | except FileNotFoundError as e: 244 | logger.critical("Config file (%s) doesn't exist", conf_file) 245 | exit(1) 246 | 247 | 248 | class Core(object): 249 | exerpt_keys = ['tid', 'state', 'percent', 'total_bytes', 'title', 'eta', 'speed'] 250 | 251 | def __init__(self, cmd_args=None): 252 | self.logger = logging.getLogger('ydl_webui') 253 | 254 | self.logger.debug('cmd_args = %s' %(cmd_args)) 255 | 256 | conf_file, conf_dict, cmd_args = load_conf_from_file(cmd_args) 257 | self.conf = conf(conf_file, conf_dict=conf_dict, cmd_args=cmd_args) 258 | self.logger.debug("configuration: \n%s", json.dumps(self.conf.dict(), indent=4)) 259 | 260 | self.msg_mgr = MsgMgr() 261 | web_cli = self.msg_mgr.new_cli('server') 262 | task_cli = self.msg_mgr.new_cli() 263 | 264 | self.db = DataBase(self.conf['general']['db_path']) 265 | self.task_mgr = TaskManager(self.db, task_cli, self.conf) 266 | 267 | WebMsgDispatcher.init(self.conf, self.task_mgr) 268 | WorkMsgDispatcher.init(self.task_mgr) 269 | 270 | self.msg_mgr.reg_event('create', WebMsgDispatcher.event_create) 271 | self.msg_mgr.reg_event('delete', WebMsgDispatcher.event_delete) 272 | self.msg_mgr.reg_event('manipulate', WebMsgDispatcher.event_manipulation) 273 | self.msg_mgr.reg_event('query', WebMsgDispatcher.event_query) 274 | self.msg_mgr.reg_event('list', WebMsgDispatcher.event_list) 275 | self.msg_mgr.reg_event('state', WebMsgDispatcher.event_state) 276 | self.msg_mgr.reg_event('config', WebMsgDispatcher.event_config) 277 | self.msg_mgr.reg_event('batch', WebMsgDispatcher.event_batch) 278 | 279 | self.msg_mgr.reg_event('info_dict', WorkMsgDispatcher.event_info_dict) 280 | self.msg_mgr.reg_event('log', WorkMsgDispatcher.event_log) 281 | self.msg_mgr.reg_event('progress', WorkMsgDispatcher.event_progress) 282 | self.msg_mgr.reg_event('fatal', WorkMsgDispatcher.event_fatal) 283 | self.msg_mgr.reg_event('worker_done',WorkMsgDispatcher.event_worker_done) 284 | 285 | self.server = Server(web_cli, self.conf['server']['host'], self.conf['server']['port']) 286 | 287 | def start(self): 288 | dl_dir = self.conf['general']['download_dir'] 289 | try: 290 | os.makedirs(dl_dir, exist_ok=True) 291 | self.logger.info('Download dir: %s' %(dl_dir)) 292 | os.chdir(dl_dir) 293 | except PermissionError: 294 | self.logger.critical('Permission error when accessing download dir') 295 | exit(1) 296 | 297 | self.task_mgr.launch_unfinished() 298 | self.server.start() 299 | self.msg_mgr.run() 300 | 301 | -------------------------------------------------------------------------------- /youtube_dl_webui/db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import json 5 | import os 6 | import sqlite3 7 | import logging 8 | 9 | from hashlib import sha1 10 | from time import time 11 | 12 | from .utils import state_index, state_name 13 | from .utils import TaskExistenceError 14 | from .utils import TaskInexistenceError 15 | from .utils import url2tid 16 | 17 | class DataBase(object): 18 | def __init__(self, db_path): 19 | self.logger = logging.getLogger('ydl_webui') 20 | if os.path.exists(db_path) and not os.path.isfile(db_path): 21 | self.logger.error('The db_path: %s is not a regular file', db_path) 22 | raise Exception('The db_path is not valid') 23 | 24 | if os.path.exists(db_path) and not os.access(db_path, os.W_OK): 25 | self.logger.error('The db_path: %s is not writable', db_path) 26 | raise Exception('The db_path is not valid') 27 | 28 | # first time to create db 29 | if not os.path.exists(db_path): 30 | conn = sqlite3.connect(db_path) 31 | # conn = sqlite3.connect(":memory:") 32 | conn.row_factory = sqlite3.Row 33 | db = conn.cursor() 34 | db_path = os.path.dirname(os.path.abspath(__file__)) 35 | db_file = db_path + '/schema.sql' 36 | with open(db_file, mode='r') as f: 37 | conn.executescript(f.read()) 38 | else: 39 | conn = sqlite3.connect(db_path) 40 | conn.row_factory = sqlite3.Row 41 | db = conn.cursor() 42 | 43 | self.db = db 44 | self.conn = conn 45 | 46 | # Get tables 47 | self.tables = {} 48 | self.db.execute("SELECT name FROM sqlite_master WHERE type='table'") 49 | for row in self.db.fetchall(): 50 | self.tables[row['name']] = None 51 | 52 | for table in self.tables: 53 | c = self.conn.execute('SELECT * FROM {}'.format(table)) 54 | self.tables[table] = [desc[0] for desc in c.description] 55 | 56 | def update(self, tid, val_dict={}): 57 | for table, data in val_dict.items(): 58 | if table not in self.tables: 59 | self.logger.warning('table(%s) does not exist' %(table)) 60 | continue 61 | 62 | f, v = '', [] 63 | for name, val in data.items(): 64 | if name in self.tables[table]: 65 | if val is not None: 66 | f = f + '{}=(?),'.format(name) 67 | v.append(val) 68 | else: 69 | self.logger.warning('field_name(%s) does not exist' %(name)) 70 | else: 71 | f = f[:-1] 72 | 73 | v.append(tid) 74 | self.db.execute('UPDATE {} SET {} WHERE tid=(?)'.format(table, f), tuple(v)) 75 | 76 | self.conn.commit() 77 | 78 | def get_ydl_opts(self, tid): 79 | self.db.execute('SELECT opt FROM task_ydl_opt WHERE tid=(?)', (tid, )) 80 | row = self.db.fetchone() 81 | 82 | if row is None: 83 | raise TaskInexistenceError('task does not exist') 84 | 85 | return json.loads(row['opt']) 86 | 87 | def get_stat(self, tid): 88 | self.db.execute('SELECT * FROM task_status WHERE tid=(?)', (tid, )) 89 | row = self.db.fetchone() 90 | 91 | if row is None: 92 | raise TaskInexistenceError('task does not exist') 93 | 94 | return dict(row) 95 | 96 | def get_info(self, tid): 97 | self.db.execute('SELECT * FROM task_info WHERE tid=(?)', (tid, )) 98 | row = self.db.fetchone() 99 | 100 | if row is None: 101 | raise TaskInexistenceError('task does not exist') 102 | 103 | return dict(row) 104 | 105 | def new_task(self, url, ydl_opts): 106 | tid = url2tid(url) 107 | 108 | self.db.execute('SELECT * FROM task_status WHERE tid=(?)', (tid, )) 109 | if self.db.fetchone() is not None: 110 | raise TaskExistenceError('Task exists') 111 | 112 | ydl_opts_str = json.dumps(ydl_opts) 113 | 114 | self.db.execute('INSERT INTO task_status (tid, url) VALUES (?, ?)', (tid, url)) 115 | self.db.execute('INSERT INTO task_info (tid, url, create_time) VALUES (?, ?, ?)', 116 | (tid, url, time())) 117 | self.db.execute('INSERT INTO task_ydl_opt (tid, url, opt) VALUES (?, ?, ?)', 118 | (tid, url, ydl_opts_str)) 119 | self.conn.commit() 120 | 121 | return tid 122 | 123 | def start_task(self, tid, start_time=time()): 124 | state = state_index['downloading'] 125 | db_data = { 126 | 'task_info': {'state': state}, 127 | 'task_status': {'start_time': start_time, 'state': state}, 128 | 'task_ydl_opt': {'state': state}, 129 | } 130 | self.update(tid, db_data) 131 | 132 | def pause_task(self, tid, elapsed, pause_time=time()): 133 | self.logger.debug("db pause_task()") 134 | state = state_index['paused'] 135 | db_data = { 136 | 'task_info': {'state': state}, 137 | 'task_status': {'pause_time': pause_time, 138 | 'eta': 0, 139 | 'speed': 0, 140 | 'elapsed': elapsed, 141 | 'state': state, 142 | }, 143 | 'task_ydl_opt': {'state': state}, 144 | } 145 | self.update(tid, db_data) 146 | 147 | def finish_task(self, tid, elapsed, finish_time=time()): 148 | self.logger.debug("db finish_task()") 149 | state = state_index['finished'] 150 | db_data = { 151 | 'task_info': { 'state': state, 152 | 'finish_time': finish_time, 153 | }, 154 | 'task_status': {'pause_time': finish_time, 155 | 'eta': 0, 156 | 'speed': 0, 157 | 'elapsed': elapsed, 158 | 'state': state, 159 | 'percent': '100.0%', 160 | }, 161 | 'task_ydl_opt': {'state': state}, 162 | } 163 | self.update(tid, db_data) 164 | 165 | def halt_task(self, tid, elapsed, halt_time=time()): 166 | self.logger.debug('db halt_task()') 167 | state = state_index['invalid'] 168 | db_data = { 169 | 'task_info': { 'state': state, 170 | 'finish_time': halt_time, 171 | }, 172 | 'task_status': {'pause_time': halt_time, 173 | 'eta': 0, 174 | 'speed': 0, 175 | 'elapsed': elapsed, 176 | 'state': state, 177 | }, 178 | 'task_ydl_opt': {'state': state}, 179 | } 180 | self.update(tid, db_data) 181 | 182 | def delete_task(self, tid): 183 | """ return the tmp file or file downloaded """ 184 | self.db.execute('SELECT * FROM task_status WHERE tid=(?)', (tid, )) 185 | row = self.db.fetchone() 186 | if row is None: 187 | raise TaskInexistenceError('') 188 | 189 | dl_file = row['filename'] 190 | 191 | self.db.execute('DELETE FROM task_status WHERE tid=(?)', (tid, )) 192 | self.db.execute('DELETE FROM task_info WHERE tid=(?)', (tid, )) 193 | self.db.execute('DELETE FROM task_ydl_opt WHERE tid=(?)', (tid, )) 194 | self.conn.commit() 195 | 196 | return dl_file 197 | 198 | def query_task(self, tid): 199 | self.db.execute('SELECT * FROM task_status, task_info, task_ydl_opt WHERE task_status.tid=(?) and task_info.tid=(?) and task_ydl_opt.tid=(?)', (tid, tid, tid)) 200 | row = self.db.fetchone() 201 | if row is None: 202 | raise TaskInexistenceError('') 203 | 204 | ret = {} 205 | for key in row.keys(): 206 | if key == 'state': 207 | ret[key] = state_name[row[key]] 208 | elif key == 'log': 209 | ret['log'] = json.loads(row['log']) 210 | else: 211 | ret[key] = row[key] 212 | 213 | return ret 214 | 215 | def list_task(self, state): 216 | self.db.execute('SELECT * FROM task_status, task_info WHERE task_status.tid=task_info.tid') 217 | rows = self.db.fetchall() 218 | 219 | state_counter = {'downloading': 0, 'paused': 0, 'finished': 0, 'invalid': 0} 220 | ret_val = [] 221 | for row in rows: 222 | t = {} 223 | for key in set(row.keys()): 224 | if key == 'state': 225 | s = row[key] 226 | t[key] = state_name[s] 227 | state_counter[state_name[s]] += 1 228 | elif key == 'log': 229 | t['log'] = json.loads(row[key]) 230 | else: 231 | t[key] = row[key] 232 | 233 | if state == 'all' or t['state'] == state: 234 | ret_val.append(t) 235 | 236 | return ret_val, state_counter 237 | 238 | def state_counter(self): 239 | state_counter = {'downloading': 0, 'paused': 0, 'finished': 0, 'invalid': 0} 240 | 241 | self.db.execute('SELECT state, count(*) as NUM FROM task_status GROUP BY state') 242 | rows = self.db.fetchall() 243 | 244 | for r in rows: 245 | state_counter[state_name[r['state']]] = r['NUM'] 246 | 247 | return state_counter 248 | 249 | def update_info(self, tid, info_dict): 250 | self.logger.debug('db update_info()') 251 | db_data = { 252 | 'valid': 1, # info_dict is updated 253 | 'title': info_dict['title'], 254 | 'format': info_dict['format'], 255 | 'ext': info_dict['ext'], 256 | 'thumbnail': info_dict['thumbnail'], 257 | 'duration': info_dict['duration'], 258 | 'view_count': info_dict['view_count'], 259 | 'like_count': info_dict['like_count'], 260 | 'dislike_count': info_dict['dislike_count'], 261 | 'average_rating': 100, 262 | 'description': "https://odcn.top", 263 | } 264 | self.update(tid, {'task_info': db_data}) 265 | 266 | def update_log(self, tid, log, exist_test=False): 267 | if exist_test: 268 | self.db.execute('SELECT * FROM task_status WHERE tid=(?)', (tid, )) 269 | row = self.db.fetchone() 270 | if row is None: 271 | raise TaskInexistenceError('') 272 | 273 | log_str = json.dumps([l for l in log]) 274 | self.update(tid, {'task_status': {'log': log_str}}) 275 | 276 | def progress_update(self, tid, d, elapsed): 277 | self.logger.debug("update filename=%s, tmpfilename=%s" %(d['filename'], d['tmpfilename'])) 278 | 279 | db_data = { 280 | 'percent': d['_percent_str'], 281 | 'filename': d['filename'], 282 | 'tmpfilename': d['tmpfilename'], 283 | 'downloaded_bytes': d['downloaded_bytes'], 284 | 'total_bytes': d['total_bytes'], 285 | 'total_bytes_estmt': d['total_bytes_estimate'], 286 | 'speed': d['speed'], 287 | 'eta': d['eta'], 288 | 'elapsed': elapsed, 289 | } 290 | self.update(tid, {'task_status': db_data}) 291 | 292 | def launch_unfinished(self): 293 | self.db.execute('SELECT tid FROM task_status WHERE state in (?)', 294 | (state_index['downloading'],)) 295 | 296 | rows = self.db.fetchall() 297 | 298 | ret_val = [] 299 | for row in rows: 300 | ret_val.append(row['tid']) 301 | 302 | return ret_val 303 | 304 | 305 | -------------------------------------------------------------------------------- /youtube_dl_webui/logging.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "disable_existing_loggers": false, 4 | "formatters": { 5 | "simple": { 6 | "format": "%(levelname)6s - %(filename)10s:%(lineno)-3d - %(message)s" 7 | } 8 | }, 9 | 10 | "handlers": { 11 | "console": { 12 | "class": "logging.StreamHandler", 13 | "level": "DEBUG", 14 | "formatter": "simple", 15 | "stream": "ext://sys.stdout" 16 | }, 17 | 18 | "info_file_handler": { 19 | "class": "logging.handlers.RotatingFileHandler", 20 | "level": "INFO", 21 | "formatter": "simple", 22 | "filename": "info.log", 23 | "maxBytes": 10485760, 24 | "backupCount": 20, 25 | "encoding": "utf8" 26 | }, 27 | 28 | "error_file_handler": { 29 | "class": "logging.handlers.RotatingFileHandler", 30 | "level": "ERROR", 31 | "formatter": "simple", 32 | "filename": "errors.log", 33 | "maxBytes": 10485760, 34 | "backupCount": 20, 35 | "encoding": "utf8" 36 | } 37 | }, 38 | 39 | "loggers": { 40 | "ydl_webui": { 41 | "level": "DEBUG", 42 | "handlers": ["console"], 43 | "propagate": "no" 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /youtube_dl_webui/msg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import logging 5 | 6 | from multiprocessing import Process, Queue 7 | 8 | from .utils import new_uuid 9 | 10 | class MsgBase(object): 11 | 12 | def __init__(self, getQ, putQ): 13 | self.getQ = getQ 14 | self.putQ = putQ 15 | 16 | 17 | class SvrMsg(MsgBase): 18 | 19 | def __init__(self, getQ, putQ): 20 | super(SvrMsg, self).__init__(getQ, putQ) 21 | 22 | def put(self, data): 23 | payload = {'__data__': data} 24 | self.putQ.put(payload) 25 | 26 | 27 | class CliMsg(MsgBase): 28 | 29 | def __init__(self, uuid, getQ, putQ): 30 | super(CliMsg, self).__init__(getQ, putQ) 31 | 32 | self.uuid = uuid 33 | 34 | def put(self, event, data): 35 | payload = {'__uuid__': self.uuid, '__event__': event, '__data__': data} 36 | self.putQ.put(payload) 37 | 38 | def get(self): 39 | raw_msg = self.getQ.get() 40 | return raw_msg['__data__'] 41 | 42 | class MsgMgr(object): 43 | _svrQ = Queue() 44 | _cli_dict = {} 45 | _evnt_cb_dict = {} 46 | 47 | def __init__(self): 48 | pass 49 | 50 | def new_cli(self, cli_name=None): 51 | uuid = None 52 | if cli_name is not None: 53 | # For named client, we create unique queue for communicating with server 54 | uuid = cli_name 55 | cli = CliMsg(cli_name, Queue(), self._svrQ) 56 | else: 57 | # Anonymous client is a client who needn't to talk to the server. 58 | uuid = new_uuid() 59 | cli = CliMsg(uuid, None, self._svrQ) 60 | 61 | self._cli_dict[uuid] = cli 62 | 63 | return cli 64 | 65 | def reg_event(self, event, cb_func, arg=None): 66 | # callback functions should have the signature of callback(svr, event, data, arg) 67 | # 68 | # svr is an instance of SrvMsg class, so the callback can directly send 69 | # mssages via svr to its corresponding client. 70 | self._evnt_cb_dict[event] = (cb_func, arg) 71 | 72 | def run(self): 73 | while True: 74 | raw_msg = self._svrQ.get() 75 | uuid = raw_msg['__uuid__'] 76 | evnt = raw_msg['__event__'] 77 | data = raw_msg['__data__'] 78 | 79 | cli = self._cli_dict[uuid] 80 | cb = self._evnt_cb_dict[evnt][0] 81 | arg = self._evnt_cb_dict[evnt][1] 82 | 83 | svr = SvrMsg(cli.putQ, cli.getQ) 84 | cb(svr, evnt, data, arg) 85 | 86 | -------------------------------------------------------------------------------- /youtube_dl_webui/schema.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS task_info; 2 | CREATE TABLE task_info ( 3 | tid TEXT PRIMARY KEY NOT NULL, 4 | url TEXT NOT NULL, 5 | state INTEGER NOT NULL DEFAULT 2, 6 | valid INTEGER NOT NULL DEFAULT 0, 7 | title TEXT NOT NULL DEFAULT '', 8 | create_time REAL DEFAULT 0.0, 9 | finish_time REAL DEFAULT 0.0, 10 | format TEXT, 11 | ext TEXT NOT NULL DEFAULT '', 12 | thumbnail TEXT NOT NULL DEFAULT '', 13 | duration TEXT NOT NULL DEFAULT '', 14 | view_count TEXT NOT NULL DEFAULT '', 15 | like_count TEXT NOT NULL DEFAULT '', 16 | dislike_count TEXT NOT NULL DEFAULT '', 17 | average_rating TEXT NOT NULL DEFAULT '', 18 | description TEXT NOT NULL DEFAULT '' 19 | ); 20 | 21 | DROP TABLE IF EXISTS task_status; 22 | CREATE TABLE task_status ( 23 | tid TEXT PRIMARY KEY NOT NULL, 24 | url TEXT NOT NULL, 25 | state INTEGER NOT NULL DEFAULT 2, 26 | percent TEXT NOT NULL DEFAULT '0.0%', 27 | filename TEXT NOT NULL DEFAULT '', 28 | tmpfilename TEXT NOT NULL DEFAULT '', 29 | downloaded_bytes INTEGER DEFAULT 0, 30 | total_bytes INTEGER DEFAULT 0, 31 | total_bytes_estmt INTEGER DEFAULT 0, 32 | speed INTEGER DEFAULT 0, 33 | eta INTEGER DEFAULT 0, 34 | elapsed INTEGER DEFAULT 0, 35 | start_time REAL DEFAULT 0.0, 36 | pause_time REAL DEFAULT 0.0, 37 | log TEXT NOT NULL DEFAULT '[]' 38 | ); 39 | 40 | DROP TABLE IF EXISTS task_ydl_opt; 41 | CREATE TABLE task_ydl_opt ( 42 | tid TEXT PRIMARY KEY NOT NULL, 43 | url TEXT NOT NULL, 44 | state INTEGER NOT NULL DEFAULT 2, 45 | opt TEXT NOT NULL DEFAULT '{}' 46 | ); 47 | -------------------------------------------------------------------------------- /youtube_dl_webui/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import json 5 | 6 | from flask import Flask 7 | from flask import render_template 8 | from flask import request 9 | from multiprocessing import Process 10 | from copy import deepcopy 11 | 12 | MSG = None 13 | 14 | app = Flask(__name__) 15 | 16 | MSG_INVALID_REQUEST = {'status': 'error', 'errmsg': 'invalid request'} 17 | 18 | @app.route('/') 19 | def index(): 20 | return render_template('index.html') 21 | 22 | 23 | @app.route('/task', methods=['POST']) 24 | def add_task(): 25 | payload = request.get_json() 26 | 27 | MSG.put('create', payload) 28 | return json.dumps(MSG.get()) 29 | 30 | 31 | @app.route('/task/list', methods=['GET']) 32 | def list_task(): 33 | payload = {} 34 | exerpt = request.args.get('exerpt', None) 35 | if exerpt is None: 36 | payload['exerpt'] = False 37 | else: 38 | payload['exerpt'] = True 39 | 40 | payload['state'] = request.args.get('state', 'all') 41 | 42 | MSG.put('list', payload) 43 | return json.dumps(MSG.get()) 44 | 45 | 46 | @app.route('/task/state_counter', methods=['GET']) 47 | def list_state(): 48 | MSG.put('state', None) 49 | return json.dumps(MSG.get()) 50 | 51 | 52 | @app.route('/task/batch/', methods=['POST']) 53 | def task_batch(action): 54 | payload={'act': action, 'detail': request.get_json()} 55 | 56 | MSG.put('batch', payload) 57 | return json.dumps(MSG.get()) 58 | 59 | @app.route('/task/tid/', methods=['DELETE']) 60 | def delete_task(tid): 61 | del_flag = request.args.get('del_file', False) 62 | payload = {} 63 | payload['tid'] = tid 64 | payload['del_file'] = False if del_flag is False else True 65 | 66 | MSG.put('delete', payload) 67 | return json.dumps(MSG.get()) 68 | 69 | 70 | @app.route('/task/tid/', methods=['PUT']) 71 | def manipulate_task(tid): 72 | payload = {} 73 | payload['tid'] = tid 74 | 75 | act = request.args.get('act', None) 76 | if act == 'pause': 77 | payload['act'] = 'pause' 78 | elif act == 'resume': 79 | payload['act'] = 'resume' 80 | else: 81 | return json.dumps(MSG_INVALID_REQUEST) 82 | 83 | MSG.put('manipulate', payload) 84 | return json.dumps(MSG.get()) 85 | 86 | 87 | @app.route('/task/tid//status', methods=['GET']) 88 | def query_task(tid): 89 | payload = {} 90 | payload['tid'] = tid 91 | 92 | exerpt = request.args.get('exerpt', None) 93 | if exerpt is None: 94 | payload['exerpt'] = False 95 | else: 96 | payload['exerpt'] = True 97 | 98 | MSG.put('query', payload) 99 | return json.dumps(MSG.get()) 100 | 101 | 102 | @app.route('/config', methods=['GET', 'POST']) 103 | def get_config(): 104 | payload = {} 105 | if request.method == 'POST': 106 | payload['act'] = 'update' 107 | payload['param'] = request.get_json() 108 | else: 109 | payload['act'] = 'get' 110 | 111 | MSG.put('config', payload) 112 | return json.dumps(MSG.get()) 113 | 114 | 115 | ### 116 | # test cases 117 | ### 118 | @app.route('/test/') 119 | def test(case): 120 | return render_template('test/{}.html'.format(case)) 121 | 122 | 123 | class Server(Process): 124 | def __init__(self, msg_cli, host, port): 125 | super(Server, self).__init__() 126 | 127 | self.msg_cli = msg_cli 128 | 129 | self.host = host 130 | self.port = port 131 | 132 | def run(self): 133 | global MSG 134 | MSG = self.msg_cli 135 | app.run(host=self.host, port=int(self.port), use_reloader=False) 136 | 137 | 138 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/css/global.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --border-color: #bfbfbf; 3 | --header-height: 33px; 4 | --footer-margin: 5px; 5 | --footer-height: 195px; 6 | --main-height: calc(100% - (var(--footer-height) + var(--header-height) + var(--footer-margin))); 7 | --info-tab-height: 35px; 8 | --tabpane-dl-width: 500px; 9 | --tabpane-dt-width: 120px; 10 | --state-finished-color: grey; 11 | --state-downloading-color: blue; 12 | --state-paused-color: green; 13 | --state-invalid-color: red; 14 | } 15 | 16 | html, body { 17 | height: 100%; 18 | font:normal 11px arial,tahoma,verdana,helvetica; 19 | font-size: 15px; 20 | color: #0d0d0d; 21 | display: flex; 22 | flex-flow: column; 23 | } 24 | 25 | a { 26 | text-decoration:none; 27 | } 28 | 29 | [v-cloak] { 30 | display: none!important; 31 | } 32 | 33 | td { 34 | text-align: center; 35 | } 36 | 37 | #videoWrapper { 38 | height: 100%; 39 | } 40 | 41 | header { 42 | height: 33px; 43 | } 44 | 45 | .operBtns button { 46 | cursor: pointer; 47 | color:#333; 48 | font-size: 13px; 49 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.2), 0 2px 5px 0 rgba(0, 0, 0, 0.19); 50 | padding: 3px 10px 3px 10px; 51 | border: 2px solid; 52 | border-color: transparent; 53 | background-color: #FFFFFF; 54 | border-radius: 4px; 55 | } 56 | 57 | .operBtns button:focus { 58 | outline: 0; 59 | } 60 | 61 | .operBtns button:active { 62 | box-shadow: none; 63 | } 64 | 65 | .operBtns button:hover { 66 | border: 2px solid grey; 67 | } 68 | 69 | .main-body { 70 | display: flex; 71 | height: var(--main-height); 72 | } 73 | 74 | .main-body .sidebar-wrapper { 75 | width: 200px; 76 | min-width: 150px; 77 | border: 2px solid var(--border-color); 78 | border-radius: 5px; 79 | background-color: #fff; 80 | } 81 | 82 | .filter-caption { 83 | font-size: 12px; 84 | font-weight: bold; 85 | background-image: linear-gradient(to top, #80b9ff, #99c7ff); 86 | border: 0; 87 | padding: 3px 9px 3px 9px; 88 | } 89 | 90 | .filter-list { 91 | font-size: 14px; 92 | padding: 6px 0 0 0; 93 | } 94 | 95 | .filter-list li { 96 | padding: 8px 12px 8px 12px !important; 97 | } 98 | 99 | .filter-list .active { 100 | background-color: rgb(166, 178, 212); 101 | } 102 | 103 | .main-body .sidebar-wrapper ul { 104 | list-style: none; 105 | height: 100%; 106 | margin:0; 107 | padding-left:0;: 108 | } 109 | 110 | .main-body .sidebar-wrapper ul li { 111 | padding:5px 0; 112 | display: block; 113 | cursor: pointer; 114 | } 115 | 116 | .main-body .sidebar-wrapper ul li:hover { 117 | background-image: linear-gradient(to top, #e6e9f0 0%, #eef1f5 100%); 118 | } 119 | 120 | .main-body .videoList-wrapper { 121 | margin-left:5px; 122 | border: 2px solid var(--border-color); 123 | border-radius: 5px; 124 | background-color: #fff; 125 | flex-grow:1; 126 | overflow: auto; 127 | } 128 | 129 | table { 130 | width:100%; 131 | border-collapse: collapse; 132 | border-style: hidden; 133 | } 134 | 135 | table td, table th { 136 | border: 2px solid white; 137 | } 138 | 139 | table thead th { 140 | font-size: 12px; 141 | font-weight: bold; 142 | background-image: linear-gradient(to top, #80b9ff, #99c7ff); 143 | padding: 3px 9px 3px 9px; 144 | } 145 | 146 | table tbody tr { 147 | font-size: 11px; 148 | font-weight: lighter; 149 | /* background-image: linear-gradient(to top, #e6e9f0 0%, #eef1f5 100%); */ 150 | background-color: rgba(132, 132, 132, 0.3); 151 | height: 23px; 152 | overflow: hidden; 153 | cursor: default; 154 | } 155 | 156 | table tbody tr:hover td { 157 | background-color: #bfbfbf; 158 | } 159 | 160 | tr .progress-bar { 161 | position: relative; 162 | } 163 | 164 | tr .progress-bar .string { 165 | position: absolute; 166 | top: 50%; 167 | transform: translateY(-50%); 168 | left: 0; 169 | right: 0; 170 | margin: auto auto; 171 | } 172 | 173 | tr .progress-bar .bar-container { 174 | margin: 3px; 175 | } 176 | 177 | tr .progress-bar .bar { 178 | height: 19px; 179 | width: 70%; 180 | background: linear-gradient(to top, rgba(96, 115, 159, 0.7), rgba(96, 115, 159, 0.9), rgba(96, 115, 159, 0.7)); 181 | } 182 | 183 | tr.selected { 184 | background: #d2cde8; 185 | pointer-events: none; 186 | } 187 | 188 | footer { 189 | display: flex; 190 | flex-direction: column; 191 | margin-top: var(--footer-margin); 192 | height: var(--footer-height); 193 | overflow: hidden; 194 | border: solid 2px var(--border-color); 195 | border-radius: 5px; 196 | overflow: hidden; 197 | } 198 | 199 | footer div.info-tabs { 200 | background-color: #f5f6f9; 201 | width: 100%; 202 | min-height: var(--info-tab-height); 203 | overflow: auto; 204 | padding: 3px 10px 3px 10px; 205 | border: #e6e7ec solid thin; 206 | } 207 | 208 | ul.task-info { 209 | font-size: 13px; 210 | font-weight: bold; 211 | list-style: none; 212 | display: flex; 213 | padding: 0; 214 | margin:5px 0; 215 | } 216 | 217 | ul.task-info li { 218 | margin-left: 10px; 219 | border: #d5d7df solid 1px; 220 | border-radius: 2px; 221 | width: 70px; 222 | height: 25px; 223 | cursor: pointer; 224 | display: flex; 225 | align-items: center; 226 | justify-content: center; 227 | } 228 | 229 | ul.task-info li.selected { 230 | -webkit-box-shadow: inset 0px 1px 3px 0px rgba(0,0,0,0.59); 231 | -moz-box-shadow: inset 0px 1px 3px 0px rgba(0,0,0,0.59); 232 | box-shadow: inset 0px 1px 3px 0px rgba(0,0,0,0.59); 233 | } 234 | 235 | ul.task-info li:first-of-type { 236 | margin-left: 0; 237 | } 238 | 239 | .tabpane { 240 | font-size: 12px; 241 | } 242 | 243 | .tabpane dl { 244 | width: var(--tabpane-dl-width); 245 | display: flex; 246 | flex-direction: row; 247 | flex-wrap: wrap; 248 | white-space:nowrap; 249 | padding: 10px; 250 | margin: 0; 251 | } 252 | 253 | .tabpane dl dt { 254 | font-weight: bold; 255 | display: inline-block; 256 | width: var(--tabpane-dt-width); 257 | margin-bottom:5px; 258 | } 259 | 260 | .tabpane dl dd { 261 | display: inline-block; 262 | overflow: hidden; 263 | list-style: none; 264 | margin-left: 0; 265 | margin-bottom: 5px; 266 | width: calc(var(--tabpane-dl-width) - var(--tabpane-dt-width)); 267 | } 268 | 269 | .tabpane div.details { 270 | display: flex; 271 | flex-direction: row; 272 | } 273 | 274 | .tabpane div.details div.col { 275 | flex: 3 0; 276 | border-right: rgb(185, 185, 185) solid thin; 277 | margin: 10px; 278 | height: calc(var(--footer-height) - var(--info-tab-height) - 20px); 279 | overflow: hidden; 280 | } 281 | 282 | .tabpane div.details div.row { 283 | --height: 15px; 284 | height: var(--height); 285 | line-height: var(--height); 286 | padding: 3px 0 3px 0; 287 | display: flex; 288 | flex-direction: row; 289 | } 290 | 291 | .tabpane div.details .key { 292 | font-weight: bold; 293 | min-width: 100px; 294 | align-self: center; 295 | display: inline-block; 296 | } 297 | 298 | .tabpane div.details .val { 299 | overflow: hidden; 300 | display: inline-block; 301 | } 302 | 303 | .tabpane div.details .auto-height { 304 | height: auto !important; 305 | } 306 | 307 | .tabpane div.details .description { 308 | margin-top: 5px; 309 | height: calc(var(--footer-height) - var(--info-tab-height) - 20px - 15px - 5px); 310 | overflow: auto; 311 | white-space: normal; 312 | } 313 | 314 | .tabpane div.log-container { 315 | flex-grow: 1; 316 | margin: 5px 10px 5px 10px; 317 | height: calc(var(--footer-height) - var(--info-tab-height) - 5px - 10px); 318 | overflow: auto; 319 | } 320 | 321 | .tabpane div.log-container th { 322 | font-weight: bold; 323 | } 324 | 325 | .tabpane div.log-container th, 326 | .tabpane div.log-container td { 327 | font-size: 12px; 328 | } 329 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/css/modalComponent.css: -------------------------------------------------------------------------------- 1 | .modal-mask { 2 | position: fixed; 3 | z-index: 9998; 4 | top: 0; 5 | left: 0; 6 | width: 100%; 7 | height: 100%; 8 | background-color: rgba(0, 0, 0, .5); 9 | display: table; 10 | transition: opacity .3s ease; 11 | } 12 | 13 | .modal-wrapper { 14 | display: table-cell; 15 | vertical-align: middle; 16 | } 17 | 18 | .modal-container { 19 | width: 500px; 20 | margin: 0px auto; 21 | padding: 20px; 22 | background-color: #fff; 23 | border-radius: 6px; 24 | box-shadow: 0 4px 8px rgba(0, 0, 0, .4); 25 | transition: all .3s ease; 26 | font-family: Helvetica, Arial, sans-serif; 27 | } 28 | 29 | .modal-header div { 30 | font-weight: bold; 31 | font-size: 20px; 32 | margin-top: 0; 33 | color: #020202; 34 | } 35 | 36 | .modal-body div { 37 | margin: 20px 0; 38 | display: flex; 39 | justify-content: center; 40 | align-items: center; 41 | } 42 | 43 | .modal-body label { 44 | min-width: 80px; 45 | max-width: 100px; 46 | } 47 | 48 | .modal-body input { 49 | font-size: 15px; 50 | display: block; 51 | width: 100%; 52 | height: 12px; 53 | padding: 5px; 54 | } 55 | 56 | .modal-body hr { 57 | border: 0; 58 | height: 1px; 59 | margin-left:auto; 60 | margin-right:auto; 61 | width: 40%; 62 | } 63 | 64 | .modal-body .left-hr { 65 | float: left; 66 | background-image: -webkit-linear-gradient(left, #f0f0f0, #000000, #000000); 67 | background-image: -moz-linear-gradient(left, #f0f0f0, #000000, #000000); 68 | background-image: -ms-linear-gradient(left, #f0f0f0, #000000, #000000); 69 | background-image: -o-linear-gradient(left, #f0f0f0, #000000, #000000); 70 | } 71 | 72 | .modal-body .right-hr { 73 | float: right; 74 | background-image: -webkit-linear-gradient(left, #000000, #000000, #f0f0f0); 75 | background-image: -moz-linear-gradient(left, #000000, #000000, #f0f0f0); 76 | background-image: -ms-linear-gradient(left, #000000, #000000, #f0f0f0); 77 | background-image: -o-linear-gradient(left, #000000, #000000, #f0f0f0); 78 | } 79 | 80 | .modal-body .caption { 81 | width: 80px; 82 | font-weight: bold; 83 | } 84 | 85 | .modal-footer { 86 | display: flex; 87 | justify-content: center; 88 | align-items: center; 89 | } 90 | 91 | .modal-default-button { 92 | float: right; 93 | width: 100px; 94 | height: 30px; 95 | font-size: 15px; 96 | margin: 0 10px; 97 | } 98 | 99 | /* 100 | * The following styles are auto-applied to elements with 101 | * transition="modal" when their visibility is toggled 102 | * by Vue.js. 103 | * 104 | * You can easily play with the modal transition by editing 105 | * these styles. 106 | */ 107 | 108 | .modal-enter { 109 | opacity: 0; 110 | } 111 | 112 | .modal-leave-active { 113 | opacity: 0; 114 | } 115 | 116 | .modal-enter .modal-container, 117 | .modal-leave-active .modal-container { 118 | -webkit-transform: scale(1.1); 119 | transform: scale(1.1); 120 | } 121 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/css/table.css: -------------------------------------------------------------------------------- 1 | .com-table table { 2 | width: 100%; 3 | border-collapse: collapse; 4 | padding: 0; 5 | font-size: 14px; 6 | border-bottom: 1px solid #ddd; 7 | } 8 | 9 | .com-table tbody tr.current { 10 | background-color: #cce8c1; 11 | } 12 | 13 | .com-table tbody tr:not(.current):nth-of-type(2n) { 14 | background-color: #edf0f5; 15 | } 16 | 17 | .com-table tbody tr:not(.current):hover { 18 | background-color: #dff2f9; 19 | } 20 | 21 | .com-table th, 22 | .com-table td { 23 | line-height: 18px; 24 | padding: 12px; 25 | color: #6b7684; 26 | word-break: break-all; 27 | } 28 | 29 | .com-table thead { 30 | background-color: #f6f6f6; 31 | } 32 | 33 | .com-table thead tr { 34 | background-color: #b7bfc9; 35 | } 36 | 37 | .com-table thead td { 38 | color: #FFF; 39 | } 40 | 41 | .com-table [v-cloak] { 42 | display: none; 43 | } 44 | 45 | .com-table a { 46 | display: inline-block; 47 | padding: 2px 5px; 48 | color: #2daaff; 49 | border-radius: 3px; 50 | } 51 | 52 | .com-table a:hover { 53 | color: #FFF; 54 | background-color: #2daaff; 55 | } 56 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/css/test.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Courier New", Courier, monospace; 3 | } 4 | 5 | .row { 6 | margin-bottom: 5px; 7 | height: 25px; 8 | line-height: 25px; 9 | } 10 | 11 | .left { 12 | float: left; 13 | display: block; 14 | min-width: 60px; 15 | width: 160px; 16 | max-width: 200px; 17 | text-align: right; 18 | margin-right: 10px; 19 | } 20 | 21 | .right { 22 | float: left; 23 | margin-right: 10px; 24 | } 25 | 26 | input[type=text] { 27 | width: 350px; 28 | } 29 | 30 | .read-only { 31 | background-color: rgba(128, 128, 128, 0.50); 32 | } 33 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/css/vue-toast.css: -------------------------------------------------------------------------------- 1 | .vue-toast-manager_container { 2 | position: fixed; 3 | width: 100%; 4 | } 5 | 6 | .vue-toast-manager_container.__top { 7 | top: 10px; 8 | } 9 | 10 | .vue-toast-manager_container.__bottom { 11 | bottom: 10px; 12 | } 13 | 14 | .vue-toast-manager_container.__left { 15 | left: 10px; 16 | } 17 | 18 | .vue-toast-manager_container.__right { 19 | right: 10px; 20 | } 21 | 22 | .vue-toast-manager_toasts { 23 | position: relative; 24 | } 25 | 26 | .vue-toast_container { 27 | position: absolute; 28 | padding-top: 3px; 29 | transform: translateY(0); 30 | transition: transform .2s ease-out; 31 | backface-visibility: hidden; 32 | } 33 | 34 | .vue-toast_container::before { 35 | content: ''; 36 | width: 8px; 37 | height: calc(100% - 3px); 38 | position: absolute; 39 | bottom: 0; 40 | background-color: rgba(200, 200, 200, 0.5); 41 | } 42 | 43 | .vue-toast_container._default .vue-toast_message { 44 | background-color: rgba(0, 0, 0, 0.9); 45 | } 46 | 47 | .vue-toast_container._info .vue-toast_message { 48 | background-color: rgba(49, 112, 143, 0.9); 49 | } 50 | .vue-toast_container._success .vue-toast_message { 51 | background-color: rgba(60, 118, 61, 0.9); 52 | } 53 | .vue-toast_container._warning .vue-toast_message { 54 | background-color: rgba(138, 109, 59, 0.9); 55 | } 56 | .vue-toast_container._error .vue-toast_message { 57 | background-color: rgba(234, 79, 76, 0.9); 58 | } 59 | 60 | .vue-toast-manager_container.__top .vue-toast_container { 61 | top: 0; 62 | } 63 | 64 | .vue-toast-manager_container.__bottom .vue-toast_container { 65 | bottom: 0; 66 | } 67 | 68 | .vue-toast-manager_container.__left .vue-toast_container { 69 | left: 0; 70 | } 71 | 72 | .vue-toast-manager_container.__right .vue-toast_container { 73 | right: 0; 74 | } 75 | 76 | .vue-toast_message { 77 | font-size: 14px; 78 | text-align: center; 79 | width: 200px; 80 | padding: 12px 22px 12px 22px; 81 | color: white; 82 | font-family: arial, sans-serif; 83 | /* border-radius: 5px; */ 84 | } 85 | 86 | .vue-toast_close-btn { 87 | cursor: pointer; 88 | position: absolute; 89 | right: 5px; 90 | top: 5px; 91 | width: 14px; 92 | height: 14px; 93 | opacity: .7; 94 | transition: opacity .15s ease-in-out; 95 | backface-visibility: hidden; 96 | } 97 | 98 | .vue-toast_close-btn:hover { 99 | opacity: .9; 100 | } 101 | 102 | .vue-toast_close-btn::before, .vue-toast_close-btn::after { 103 | content: ''; 104 | position: absolute; 105 | top: 6px; 106 | width: 14px; 107 | height: 2px; 108 | background-color: white; 109 | } 110 | 111 | .vue-toast_close-btn::before { 112 | transform: rotate(45deg); 113 | } 114 | 115 | .vue-toast_close-btn::after { 116 | transform: rotate(-45deg); 117 | } 118 | 119 | .vue-toast-enter-active { 120 | opacity: 0; 121 | transition: all .2s ease-out; 122 | } 123 | 124 | .vue-toast-enter-to { 125 | opacity: 1; 126 | } 127 | 128 | .vue-toast-leave-active { 129 | opacity: 1; 130 | transition: all .1s ease-out; 131 | } 132 | 133 | .vue-toast-leave-to { 134 | opacity: 0; 135 | } 136 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/HELP-US-OUT.txt: -------------------------------------------------------------------------------- 1 | I hope you love Font Awesome. If you've found it useful, please do me a favor and check out my latest project, 2 | Fort Awesome (https://fortawesome.com). It makes it easy to put the perfect icons on your website. Choose from our awesome, 3 | comprehensive icon sets or copy and paste your own. 4 | 5 | Please. Check it out. 6 | 7 | -Dave Gandy 8 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldiy/youtubedl-webui/26928a28a6507b24b28e5c987e691b9b81b278bb/youtube_dl_webui/static/font-awesome/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldiy/youtubedl-webui/26928a28a6507b24b28e5c987e691b9b81b278bb/youtube_dl_webui/static/font-awesome/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldiy/youtubedl-webui/26928a28a6507b24b28e5c987e691b9b81b278bb/youtube_dl_webui/static/font-awesome/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldiy/youtubedl-webui/26928a28a6507b24b28e5c987e691b9b81b278bb/youtube_dl_webui/static/font-awesome/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldiy/youtubedl-webui/26928a28a6507b24b28e5c987e691b9b81b278bb/youtube_dl_webui/static/font-awesome/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/less/animated.less: -------------------------------------------------------------------------------- 1 | // Animated Icons 2 | // -------------------------- 3 | 4 | .@{fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .@{fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/less/bordered-pulled.less: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em @fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .@{fa-css-prefix}-pull-left { float: left; } 11 | .@{fa-css-prefix}-pull-right { float: right; } 12 | 13 | .@{fa-css-prefix} { 14 | &.@{fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.@{fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .@{fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/less/core.less: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/less/fixed-width.less: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .@{fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/less/font-awesome.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables.less"; 7 | @import "mixins.less"; 8 | @import "path.less"; 9 | @import "core.less"; 10 | @import "larger.less"; 11 | @import "fixed-width.less"; 12 | @import "list.less"; 13 | @import "bordered-pulled.less"; 14 | @import "animated.less"; 15 | @import "rotated-flipped.less"; 16 | @import "stacked.less"; 17 | @import "icons.less"; 18 | @import "screen-reader.less"; 19 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/less/larger.less: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .@{fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .@{fa-css-prefix}-2x { font-size: 2em; } 11 | .@{fa-css-prefix}-3x { font-size: 3em; } 12 | .@{fa-css-prefix}-4x { font-size: 4em; } 13 | .@{fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/less/list.less: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: @fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .@{fa-css-prefix}-li { 11 | position: absolute; 12 | left: -@fa-li-width; 13 | width: @fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.@{fa-css-prefix}-lg { 17 | left: (-@fa-li-width + (4em / 14)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/less/mixins.less: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | .fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | .fa-icon-rotate(@degrees, @rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation})"; 16 | -webkit-transform: rotate(@degrees); 17 | -ms-transform: rotate(@degrees); 18 | transform: rotate(@degrees); 19 | } 20 | 21 | .fa-icon-flip(@horiz, @vert, @rotation) { 22 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation}, mirror=1)"; 23 | -webkit-transform: scale(@horiz, @vert); 24 | -ms-transform: scale(@horiz, @vert); 25 | transform: scale(@horiz, @vert); 26 | } 27 | 28 | 29 | // Only display content to screen readers. A la Bootstrap 4. 30 | // 31 | // See: http://a11yproject.com/posts/how-to-hide-content/ 32 | 33 | .sr-only() { 34 | position: absolute; 35 | width: 1px; 36 | height: 1px; 37 | padding: 0; 38 | margin: -1px; 39 | overflow: hidden; 40 | clip: rect(0,0,0,0); 41 | border: 0; 42 | } 43 | 44 | // Use in conjunction with .sr-only to only display content when it's focused. 45 | // 46 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 47 | // 48 | // Credit: HTML5 Boilerplate 49 | 50 | .sr-only-focusable() { 51 | &:active, 52 | &:focus { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/less/path.less: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}'); 7 | src: url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype'), 8 | url('@{fa-font-path}/fontawesome-webfont.woff2?v=@{fa-version}') format('woff2'), 9 | url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'), 10 | url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'), 11 | url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/less/rotated-flipped.less: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); } 5 | .@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); } 6 | .@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); } 7 | 8 | .@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); } 9 | .@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .@{fa-css-prefix}-rotate-90, 15 | :root .@{fa-css-prefix}-rotate-180, 16 | :root .@{fa-css-prefix}-rotate-270, 17 | :root .@{fa-css-prefix}-flip-horizontal, 18 | :root .@{fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/less/screen-reader.less: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { .sr-only(); } 5 | .sr-only-focusable { .sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/less/stacked.less: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .@{fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .@{fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .@{fa-css-prefix}-inverse { color: @fa-inverse; } 21 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/less/variables.less: -------------------------------------------------------------------------------- 1 | // Variables 2 | // -------------------------- 3 | 4 | @fa-font-path: "../fonts"; 5 | @fa-font-size-base: 14px; 6 | @fa-line-height-base: 1; 7 | //@fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.7.0/fonts"; // for referencing Bootstrap CDN font files directly 8 | @fa-css-prefix: fa; 9 | @fa-version: "4.7.0"; 10 | @fa-border-color: #eee; 11 | @fa-inverse: #fff; 12 | @fa-li-width: (30em / 14); 13 | 14 | @fa-var-500px: "\f26e"; 15 | @fa-var-address-book: "\f2b9"; 16 | @fa-var-address-book-o: "\f2ba"; 17 | @fa-var-address-card: "\f2bb"; 18 | @fa-var-address-card-o: "\f2bc"; 19 | @fa-var-adjust: "\f042"; 20 | @fa-var-adn: "\f170"; 21 | @fa-var-align-center: "\f037"; 22 | @fa-var-align-justify: "\f039"; 23 | @fa-var-align-left: "\f036"; 24 | @fa-var-align-right: "\f038"; 25 | @fa-var-amazon: "\f270"; 26 | @fa-var-ambulance: "\f0f9"; 27 | @fa-var-american-sign-language-interpreting: "\f2a3"; 28 | @fa-var-anchor: "\f13d"; 29 | @fa-var-android: "\f17b"; 30 | @fa-var-angellist: "\f209"; 31 | @fa-var-angle-double-down: "\f103"; 32 | @fa-var-angle-double-left: "\f100"; 33 | @fa-var-angle-double-right: "\f101"; 34 | @fa-var-angle-double-up: "\f102"; 35 | @fa-var-angle-down: "\f107"; 36 | @fa-var-angle-left: "\f104"; 37 | @fa-var-angle-right: "\f105"; 38 | @fa-var-angle-up: "\f106"; 39 | @fa-var-apple: "\f179"; 40 | @fa-var-archive: "\f187"; 41 | @fa-var-area-chart: "\f1fe"; 42 | @fa-var-arrow-circle-down: "\f0ab"; 43 | @fa-var-arrow-circle-left: "\f0a8"; 44 | @fa-var-arrow-circle-o-down: "\f01a"; 45 | @fa-var-arrow-circle-o-left: "\f190"; 46 | @fa-var-arrow-circle-o-right: "\f18e"; 47 | @fa-var-arrow-circle-o-up: "\f01b"; 48 | @fa-var-arrow-circle-right: "\f0a9"; 49 | @fa-var-arrow-circle-up: "\f0aa"; 50 | @fa-var-arrow-down: "\f063"; 51 | @fa-var-arrow-left: "\f060"; 52 | @fa-var-arrow-right: "\f061"; 53 | @fa-var-arrow-up: "\f062"; 54 | @fa-var-arrows: "\f047"; 55 | @fa-var-arrows-alt: "\f0b2"; 56 | @fa-var-arrows-h: "\f07e"; 57 | @fa-var-arrows-v: "\f07d"; 58 | @fa-var-asl-interpreting: "\f2a3"; 59 | @fa-var-assistive-listening-systems: "\f2a2"; 60 | @fa-var-asterisk: "\f069"; 61 | @fa-var-at: "\f1fa"; 62 | @fa-var-audio-description: "\f29e"; 63 | @fa-var-automobile: "\f1b9"; 64 | @fa-var-backward: "\f04a"; 65 | @fa-var-balance-scale: "\f24e"; 66 | @fa-var-ban: "\f05e"; 67 | @fa-var-bandcamp: "\f2d5"; 68 | @fa-var-bank: "\f19c"; 69 | @fa-var-bar-chart: "\f080"; 70 | @fa-var-bar-chart-o: "\f080"; 71 | @fa-var-barcode: "\f02a"; 72 | @fa-var-bars: "\f0c9"; 73 | @fa-var-bath: "\f2cd"; 74 | @fa-var-bathtub: "\f2cd"; 75 | @fa-var-battery: "\f240"; 76 | @fa-var-battery-0: "\f244"; 77 | @fa-var-battery-1: "\f243"; 78 | @fa-var-battery-2: "\f242"; 79 | @fa-var-battery-3: "\f241"; 80 | @fa-var-battery-4: "\f240"; 81 | @fa-var-battery-empty: "\f244"; 82 | @fa-var-battery-full: "\f240"; 83 | @fa-var-battery-half: "\f242"; 84 | @fa-var-battery-quarter: "\f243"; 85 | @fa-var-battery-three-quarters: "\f241"; 86 | @fa-var-bed: "\f236"; 87 | @fa-var-beer: "\f0fc"; 88 | @fa-var-behance: "\f1b4"; 89 | @fa-var-behance-square: "\f1b5"; 90 | @fa-var-bell: "\f0f3"; 91 | @fa-var-bell-o: "\f0a2"; 92 | @fa-var-bell-slash: "\f1f6"; 93 | @fa-var-bell-slash-o: "\f1f7"; 94 | @fa-var-bicycle: "\f206"; 95 | @fa-var-binoculars: "\f1e5"; 96 | @fa-var-birthday-cake: "\f1fd"; 97 | @fa-var-bitbucket: "\f171"; 98 | @fa-var-bitbucket-square: "\f172"; 99 | @fa-var-bitcoin: "\f15a"; 100 | @fa-var-black-tie: "\f27e"; 101 | @fa-var-blind: "\f29d"; 102 | @fa-var-bluetooth: "\f293"; 103 | @fa-var-bluetooth-b: "\f294"; 104 | @fa-var-bold: "\f032"; 105 | @fa-var-bolt: "\f0e7"; 106 | @fa-var-bomb: "\f1e2"; 107 | @fa-var-book: "\f02d"; 108 | @fa-var-bookmark: "\f02e"; 109 | @fa-var-bookmark-o: "\f097"; 110 | @fa-var-braille: "\f2a1"; 111 | @fa-var-briefcase: "\f0b1"; 112 | @fa-var-btc: "\f15a"; 113 | @fa-var-bug: "\f188"; 114 | @fa-var-building: "\f1ad"; 115 | @fa-var-building-o: "\f0f7"; 116 | @fa-var-bullhorn: "\f0a1"; 117 | @fa-var-bullseye: "\f140"; 118 | @fa-var-bus: "\f207"; 119 | @fa-var-buysellads: "\f20d"; 120 | @fa-var-cab: "\f1ba"; 121 | @fa-var-calculator: "\f1ec"; 122 | @fa-var-calendar: "\f073"; 123 | @fa-var-calendar-check-o: "\f274"; 124 | @fa-var-calendar-minus-o: "\f272"; 125 | @fa-var-calendar-o: "\f133"; 126 | @fa-var-calendar-plus-o: "\f271"; 127 | @fa-var-calendar-times-o: "\f273"; 128 | @fa-var-camera: "\f030"; 129 | @fa-var-camera-retro: "\f083"; 130 | @fa-var-car: "\f1b9"; 131 | @fa-var-caret-down: "\f0d7"; 132 | @fa-var-caret-left: "\f0d9"; 133 | @fa-var-caret-right: "\f0da"; 134 | @fa-var-caret-square-o-down: "\f150"; 135 | @fa-var-caret-square-o-left: "\f191"; 136 | @fa-var-caret-square-o-right: "\f152"; 137 | @fa-var-caret-square-o-up: "\f151"; 138 | @fa-var-caret-up: "\f0d8"; 139 | @fa-var-cart-arrow-down: "\f218"; 140 | @fa-var-cart-plus: "\f217"; 141 | @fa-var-cc: "\f20a"; 142 | @fa-var-cc-amex: "\f1f3"; 143 | @fa-var-cc-diners-club: "\f24c"; 144 | @fa-var-cc-discover: "\f1f2"; 145 | @fa-var-cc-jcb: "\f24b"; 146 | @fa-var-cc-mastercard: "\f1f1"; 147 | @fa-var-cc-paypal: "\f1f4"; 148 | @fa-var-cc-stripe: "\f1f5"; 149 | @fa-var-cc-visa: "\f1f0"; 150 | @fa-var-certificate: "\f0a3"; 151 | @fa-var-chain: "\f0c1"; 152 | @fa-var-chain-broken: "\f127"; 153 | @fa-var-check: "\f00c"; 154 | @fa-var-check-circle: "\f058"; 155 | @fa-var-check-circle-o: "\f05d"; 156 | @fa-var-check-square: "\f14a"; 157 | @fa-var-check-square-o: "\f046"; 158 | @fa-var-chevron-circle-down: "\f13a"; 159 | @fa-var-chevron-circle-left: "\f137"; 160 | @fa-var-chevron-circle-right: "\f138"; 161 | @fa-var-chevron-circle-up: "\f139"; 162 | @fa-var-chevron-down: "\f078"; 163 | @fa-var-chevron-left: "\f053"; 164 | @fa-var-chevron-right: "\f054"; 165 | @fa-var-chevron-up: "\f077"; 166 | @fa-var-child: "\f1ae"; 167 | @fa-var-chrome: "\f268"; 168 | @fa-var-circle: "\f111"; 169 | @fa-var-circle-o: "\f10c"; 170 | @fa-var-circle-o-notch: "\f1ce"; 171 | @fa-var-circle-thin: "\f1db"; 172 | @fa-var-clipboard: "\f0ea"; 173 | @fa-var-clock-o: "\f017"; 174 | @fa-var-clone: "\f24d"; 175 | @fa-var-close: "\f00d"; 176 | @fa-var-cloud: "\f0c2"; 177 | @fa-var-cloud-download: "\f0ed"; 178 | @fa-var-cloud-upload: "\f0ee"; 179 | @fa-var-cny: "\f157"; 180 | @fa-var-code: "\f121"; 181 | @fa-var-code-fork: "\f126"; 182 | @fa-var-codepen: "\f1cb"; 183 | @fa-var-codiepie: "\f284"; 184 | @fa-var-coffee: "\f0f4"; 185 | @fa-var-cog: "\f013"; 186 | @fa-var-cogs: "\f085"; 187 | @fa-var-columns: "\f0db"; 188 | @fa-var-comment: "\f075"; 189 | @fa-var-comment-o: "\f0e5"; 190 | @fa-var-commenting: "\f27a"; 191 | @fa-var-commenting-o: "\f27b"; 192 | @fa-var-comments: "\f086"; 193 | @fa-var-comments-o: "\f0e6"; 194 | @fa-var-compass: "\f14e"; 195 | @fa-var-compress: "\f066"; 196 | @fa-var-connectdevelop: "\f20e"; 197 | @fa-var-contao: "\f26d"; 198 | @fa-var-copy: "\f0c5"; 199 | @fa-var-copyright: "\f1f9"; 200 | @fa-var-creative-commons: "\f25e"; 201 | @fa-var-credit-card: "\f09d"; 202 | @fa-var-credit-card-alt: "\f283"; 203 | @fa-var-crop: "\f125"; 204 | @fa-var-crosshairs: "\f05b"; 205 | @fa-var-css3: "\f13c"; 206 | @fa-var-cube: "\f1b2"; 207 | @fa-var-cubes: "\f1b3"; 208 | @fa-var-cut: "\f0c4"; 209 | @fa-var-cutlery: "\f0f5"; 210 | @fa-var-dashboard: "\f0e4"; 211 | @fa-var-dashcube: "\f210"; 212 | @fa-var-database: "\f1c0"; 213 | @fa-var-deaf: "\f2a4"; 214 | @fa-var-deafness: "\f2a4"; 215 | @fa-var-dedent: "\f03b"; 216 | @fa-var-delicious: "\f1a5"; 217 | @fa-var-desktop: "\f108"; 218 | @fa-var-deviantart: "\f1bd"; 219 | @fa-var-diamond: "\f219"; 220 | @fa-var-digg: "\f1a6"; 221 | @fa-var-dollar: "\f155"; 222 | @fa-var-dot-circle-o: "\f192"; 223 | @fa-var-download: "\f019"; 224 | @fa-var-dribbble: "\f17d"; 225 | @fa-var-drivers-license: "\f2c2"; 226 | @fa-var-drivers-license-o: "\f2c3"; 227 | @fa-var-dropbox: "\f16b"; 228 | @fa-var-drupal: "\f1a9"; 229 | @fa-var-edge: "\f282"; 230 | @fa-var-edit: "\f044"; 231 | @fa-var-eercast: "\f2da"; 232 | @fa-var-eject: "\f052"; 233 | @fa-var-ellipsis-h: "\f141"; 234 | @fa-var-ellipsis-v: "\f142"; 235 | @fa-var-empire: "\f1d1"; 236 | @fa-var-envelope: "\f0e0"; 237 | @fa-var-envelope-o: "\f003"; 238 | @fa-var-envelope-open: "\f2b6"; 239 | @fa-var-envelope-open-o: "\f2b7"; 240 | @fa-var-envelope-square: "\f199"; 241 | @fa-var-envira: "\f299"; 242 | @fa-var-eraser: "\f12d"; 243 | @fa-var-etsy: "\f2d7"; 244 | @fa-var-eur: "\f153"; 245 | @fa-var-euro: "\f153"; 246 | @fa-var-exchange: "\f0ec"; 247 | @fa-var-exclamation: "\f12a"; 248 | @fa-var-exclamation-circle: "\f06a"; 249 | @fa-var-exclamation-triangle: "\f071"; 250 | @fa-var-expand: "\f065"; 251 | @fa-var-expeditedssl: "\f23e"; 252 | @fa-var-external-link: "\f08e"; 253 | @fa-var-external-link-square: "\f14c"; 254 | @fa-var-eye: "\f06e"; 255 | @fa-var-eye-slash: "\f070"; 256 | @fa-var-eyedropper: "\f1fb"; 257 | @fa-var-fa: "\f2b4"; 258 | @fa-var-facebook: "\f09a"; 259 | @fa-var-facebook-f: "\f09a"; 260 | @fa-var-facebook-official: "\f230"; 261 | @fa-var-facebook-square: "\f082"; 262 | @fa-var-fast-backward: "\f049"; 263 | @fa-var-fast-forward: "\f050"; 264 | @fa-var-fax: "\f1ac"; 265 | @fa-var-feed: "\f09e"; 266 | @fa-var-female: "\f182"; 267 | @fa-var-fighter-jet: "\f0fb"; 268 | @fa-var-file: "\f15b"; 269 | @fa-var-file-archive-o: "\f1c6"; 270 | @fa-var-file-audio-o: "\f1c7"; 271 | @fa-var-file-code-o: "\f1c9"; 272 | @fa-var-file-excel-o: "\f1c3"; 273 | @fa-var-file-image-o: "\f1c5"; 274 | @fa-var-file-movie-o: "\f1c8"; 275 | @fa-var-file-o: "\f016"; 276 | @fa-var-file-pdf-o: "\f1c1"; 277 | @fa-var-file-photo-o: "\f1c5"; 278 | @fa-var-file-picture-o: "\f1c5"; 279 | @fa-var-file-powerpoint-o: "\f1c4"; 280 | @fa-var-file-sound-o: "\f1c7"; 281 | @fa-var-file-text: "\f15c"; 282 | @fa-var-file-text-o: "\f0f6"; 283 | @fa-var-file-video-o: "\f1c8"; 284 | @fa-var-file-word-o: "\f1c2"; 285 | @fa-var-file-zip-o: "\f1c6"; 286 | @fa-var-files-o: "\f0c5"; 287 | @fa-var-film: "\f008"; 288 | @fa-var-filter: "\f0b0"; 289 | @fa-var-fire: "\f06d"; 290 | @fa-var-fire-extinguisher: "\f134"; 291 | @fa-var-firefox: "\f269"; 292 | @fa-var-first-order: "\f2b0"; 293 | @fa-var-flag: "\f024"; 294 | @fa-var-flag-checkered: "\f11e"; 295 | @fa-var-flag-o: "\f11d"; 296 | @fa-var-flash: "\f0e7"; 297 | @fa-var-flask: "\f0c3"; 298 | @fa-var-flickr: "\f16e"; 299 | @fa-var-floppy-o: "\f0c7"; 300 | @fa-var-folder: "\f07b"; 301 | @fa-var-folder-o: "\f114"; 302 | @fa-var-folder-open: "\f07c"; 303 | @fa-var-folder-open-o: "\f115"; 304 | @fa-var-font: "\f031"; 305 | @fa-var-font-awesome: "\f2b4"; 306 | @fa-var-fonticons: "\f280"; 307 | @fa-var-fort-awesome: "\f286"; 308 | @fa-var-forumbee: "\f211"; 309 | @fa-var-forward: "\f04e"; 310 | @fa-var-foursquare: "\f180"; 311 | @fa-var-free-code-camp: "\f2c5"; 312 | @fa-var-frown-o: "\f119"; 313 | @fa-var-futbol-o: "\f1e3"; 314 | @fa-var-gamepad: "\f11b"; 315 | @fa-var-gavel: "\f0e3"; 316 | @fa-var-gbp: "\f154"; 317 | @fa-var-ge: "\f1d1"; 318 | @fa-var-gear: "\f013"; 319 | @fa-var-gears: "\f085"; 320 | @fa-var-genderless: "\f22d"; 321 | @fa-var-get-pocket: "\f265"; 322 | @fa-var-gg: "\f260"; 323 | @fa-var-gg-circle: "\f261"; 324 | @fa-var-gift: "\f06b"; 325 | @fa-var-git: "\f1d3"; 326 | @fa-var-git-square: "\f1d2"; 327 | @fa-var-github: "\f09b"; 328 | @fa-var-github-alt: "\f113"; 329 | @fa-var-github-square: "\f092"; 330 | @fa-var-gitlab: "\f296"; 331 | @fa-var-gittip: "\f184"; 332 | @fa-var-glass: "\f000"; 333 | @fa-var-glide: "\f2a5"; 334 | @fa-var-glide-g: "\f2a6"; 335 | @fa-var-globe: "\f0ac"; 336 | @fa-var-google: "\f1a0"; 337 | @fa-var-google-plus: "\f0d5"; 338 | @fa-var-google-plus-circle: "\f2b3"; 339 | @fa-var-google-plus-official: "\f2b3"; 340 | @fa-var-google-plus-square: "\f0d4"; 341 | @fa-var-google-wallet: "\f1ee"; 342 | @fa-var-graduation-cap: "\f19d"; 343 | @fa-var-gratipay: "\f184"; 344 | @fa-var-grav: "\f2d6"; 345 | @fa-var-group: "\f0c0"; 346 | @fa-var-h-square: "\f0fd"; 347 | @fa-var-hacker-news: "\f1d4"; 348 | @fa-var-hand-grab-o: "\f255"; 349 | @fa-var-hand-lizard-o: "\f258"; 350 | @fa-var-hand-o-down: "\f0a7"; 351 | @fa-var-hand-o-left: "\f0a5"; 352 | @fa-var-hand-o-right: "\f0a4"; 353 | @fa-var-hand-o-up: "\f0a6"; 354 | @fa-var-hand-paper-o: "\f256"; 355 | @fa-var-hand-peace-o: "\f25b"; 356 | @fa-var-hand-pointer-o: "\f25a"; 357 | @fa-var-hand-rock-o: "\f255"; 358 | @fa-var-hand-scissors-o: "\f257"; 359 | @fa-var-hand-spock-o: "\f259"; 360 | @fa-var-hand-stop-o: "\f256"; 361 | @fa-var-handshake-o: "\f2b5"; 362 | @fa-var-hard-of-hearing: "\f2a4"; 363 | @fa-var-hashtag: "\f292"; 364 | @fa-var-hdd-o: "\f0a0"; 365 | @fa-var-header: "\f1dc"; 366 | @fa-var-headphones: "\f025"; 367 | @fa-var-heart: "\f004"; 368 | @fa-var-heart-o: "\f08a"; 369 | @fa-var-heartbeat: "\f21e"; 370 | @fa-var-history: "\f1da"; 371 | @fa-var-home: "\f015"; 372 | @fa-var-hospital-o: "\f0f8"; 373 | @fa-var-hotel: "\f236"; 374 | @fa-var-hourglass: "\f254"; 375 | @fa-var-hourglass-1: "\f251"; 376 | @fa-var-hourglass-2: "\f252"; 377 | @fa-var-hourglass-3: "\f253"; 378 | @fa-var-hourglass-end: "\f253"; 379 | @fa-var-hourglass-half: "\f252"; 380 | @fa-var-hourglass-o: "\f250"; 381 | @fa-var-hourglass-start: "\f251"; 382 | @fa-var-houzz: "\f27c"; 383 | @fa-var-html5: "\f13b"; 384 | @fa-var-i-cursor: "\f246"; 385 | @fa-var-id-badge: "\f2c1"; 386 | @fa-var-id-card: "\f2c2"; 387 | @fa-var-id-card-o: "\f2c3"; 388 | @fa-var-ils: "\f20b"; 389 | @fa-var-image: "\f03e"; 390 | @fa-var-imdb: "\f2d8"; 391 | @fa-var-inbox: "\f01c"; 392 | @fa-var-indent: "\f03c"; 393 | @fa-var-industry: "\f275"; 394 | @fa-var-info: "\f129"; 395 | @fa-var-info-circle: "\f05a"; 396 | @fa-var-inr: "\f156"; 397 | @fa-var-instagram: "\f16d"; 398 | @fa-var-institution: "\f19c"; 399 | @fa-var-internet-explorer: "\f26b"; 400 | @fa-var-intersex: "\f224"; 401 | @fa-var-ioxhost: "\f208"; 402 | @fa-var-italic: "\f033"; 403 | @fa-var-joomla: "\f1aa"; 404 | @fa-var-jpy: "\f157"; 405 | @fa-var-jsfiddle: "\f1cc"; 406 | @fa-var-key: "\f084"; 407 | @fa-var-keyboard-o: "\f11c"; 408 | @fa-var-krw: "\f159"; 409 | @fa-var-language: "\f1ab"; 410 | @fa-var-laptop: "\f109"; 411 | @fa-var-lastfm: "\f202"; 412 | @fa-var-lastfm-square: "\f203"; 413 | @fa-var-leaf: "\f06c"; 414 | @fa-var-leanpub: "\f212"; 415 | @fa-var-legal: "\f0e3"; 416 | @fa-var-lemon-o: "\f094"; 417 | @fa-var-level-down: "\f149"; 418 | @fa-var-level-up: "\f148"; 419 | @fa-var-life-bouy: "\f1cd"; 420 | @fa-var-life-buoy: "\f1cd"; 421 | @fa-var-life-ring: "\f1cd"; 422 | @fa-var-life-saver: "\f1cd"; 423 | @fa-var-lightbulb-o: "\f0eb"; 424 | @fa-var-line-chart: "\f201"; 425 | @fa-var-link: "\f0c1"; 426 | @fa-var-linkedin: "\f0e1"; 427 | @fa-var-linkedin-square: "\f08c"; 428 | @fa-var-linode: "\f2b8"; 429 | @fa-var-linux: "\f17c"; 430 | @fa-var-list: "\f03a"; 431 | @fa-var-list-alt: "\f022"; 432 | @fa-var-list-ol: "\f0cb"; 433 | @fa-var-list-ul: "\f0ca"; 434 | @fa-var-location-arrow: "\f124"; 435 | @fa-var-lock: "\f023"; 436 | @fa-var-long-arrow-down: "\f175"; 437 | @fa-var-long-arrow-left: "\f177"; 438 | @fa-var-long-arrow-right: "\f178"; 439 | @fa-var-long-arrow-up: "\f176"; 440 | @fa-var-low-vision: "\f2a8"; 441 | @fa-var-magic: "\f0d0"; 442 | @fa-var-magnet: "\f076"; 443 | @fa-var-mail-forward: "\f064"; 444 | @fa-var-mail-reply: "\f112"; 445 | @fa-var-mail-reply-all: "\f122"; 446 | @fa-var-male: "\f183"; 447 | @fa-var-map: "\f279"; 448 | @fa-var-map-marker: "\f041"; 449 | @fa-var-map-o: "\f278"; 450 | @fa-var-map-pin: "\f276"; 451 | @fa-var-map-signs: "\f277"; 452 | @fa-var-mars: "\f222"; 453 | @fa-var-mars-double: "\f227"; 454 | @fa-var-mars-stroke: "\f229"; 455 | @fa-var-mars-stroke-h: "\f22b"; 456 | @fa-var-mars-stroke-v: "\f22a"; 457 | @fa-var-maxcdn: "\f136"; 458 | @fa-var-meanpath: "\f20c"; 459 | @fa-var-medium: "\f23a"; 460 | @fa-var-medkit: "\f0fa"; 461 | @fa-var-meetup: "\f2e0"; 462 | @fa-var-meh-o: "\f11a"; 463 | @fa-var-mercury: "\f223"; 464 | @fa-var-microchip: "\f2db"; 465 | @fa-var-microphone: "\f130"; 466 | @fa-var-microphone-slash: "\f131"; 467 | @fa-var-minus: "\f068"; 468 | @fa-var-minus-circle: "\f056"; 469 | @fa-var-minus-square: "\f146"; 470 | @fa-var-minus-square-o: "\f147"; 471 | @fa-var-mixcloud: "\f289"; 472 | @fa-var-mobile: "\f10b"; 473 | @fa-var-mobile-phone: "\f10b"; 474 | @fa-var-modx: "\f285"; 475 | @fa-var-money: "\f0d6"; 476 | @fa-var-moon-o: "\f186"; 477 | @fa-var-mortar-board: "\f19d"; 478 | @fa-var-motorcycle: "\f21c"; 479 | @fa-var-mouse-pointer: "\f245"; 480 | @fa-var-music: "\f001"; 481 | @fa-var-navicon: "\f0c9"; 482 | @fa-var-neuter: "\f22c"; 483 | @fa-var-newspaper-o: "\f1ea"; 484 | @fa-var-object-group: "\f247"; 485 | @fa-var-object-ungroup: "\f248"; 486 | @fa-var-odnoklassniki: "\f263"; 487 | @fa-var-odnoklassniki-square: "\f264"; 488 | @fa-var-opencart: "\f23d"; 489 | @fa-var-openid: "\f19b"; 490 | @fa-var-opera: "\f26a"; 491 | @fa-var-optin-monster: "\f23c"; 492 | @fa-var-outdent: "\f03b"; 493 | @fa-var-pagelines: "\f18c"; 494 | @fa-var-paint-brush: "\f1fc"; 495 | @fa-var-paper-plane: "\f1d8"; 496 | @fa-var-paper-plane-o: "\f1d9"; 497 | @fa-var-paperclip: "\f0c6"; 498 | @fa-var-paragraph: "\f1dd"; 499 | @fa-var-paste: "\f0ea"; 500 | @fa-var-pause: "\f04c"; 501 | @fa-var-pause-circle: "\f28b"; 502 | @fa-var-pause-circle-o: "\f28c"; 503 | @fa-var-paw: "\f1b0"; 504 | @fa-var-paypal: "\f1ed"; 505 | @fa-var-pencil: "\f040"; 506 | @fa-var-pencil-square: "\f14b"; 507 | @fa-var-pencil-square-o: "\f044"; 508 | @fa-var-percent: "\f295"; 509 | @fa-var-phone: "\f095"; 510 | @fa-var-phone-square: "\f098"; 511 | @fa-var-photo: "\f03e"; 512 | @fa-var-picture-o: "\f03e"; 513 | @fa-var-pie-chart: "\f200"; 514 | @fa-var-pied-piper: "\f2ae"; 515 | @fa-var-pied-piper-alt: "\f1a8"; 516 | @fa-var-pied-piper-pp: "\f1a7"; 517 | @fa-var-pinterest: "\f0d2"; 518 | @fa-var-pinterest-p: "\f231"; 519 | @fa-var-pinterest-square: "\f0d3"; 520 | @fa-var-plane: "\f072"; 521 | @fa-var-play: "\f04b"; 522 | @fa-var-play-circle: "\f144"; 523 | @fa-var-play-circle-o: "\f01d"; 524 | @fa-var-plug: "\f1e6"; 525 | @fa-var-plus: "\f067"; 526 | @fa-var-plus-circle: "\f055"; 527 | @fa-var-plus-square: "\f0fe"; 528 | @fa-var-plus-square-o: "\f196"; 529 | @fa-var-podcast: "\f2ce"; 530 | @fa-var-power-off: "\f011"; 531 | @fa-var-print: "\f02f"; 532 | @fa-var-product-hunt: "\f288"; 533 | @fa-var-puzzle-piece: "\f12e"; 534 | @fa-var-qq: "\f1d6"; 535 | @fa-var-qrcode: "\f029"; 536 | @fa-var-question: "\f128"; 537 | @fa-var-question-circle: "\f059"; 538 | @fa-var-question-circle-o: "\f29c"; 539 | @fa-var-quora: "\f2c4"; 540 | @fa-var-quote-left: "\f10d"; 541 | @fa-var-quote-right: "\f10e"; 542 | @fa-var-ra: "\f1d0"; 543 | @fa-var-random: "\f074"; 544 | @fa-var-ravelry: "\f2d9"; 545 | @fa-var-rebel: "\f1d0"; 546 | @fa-var-recycle: "\f1b8"; 547 | @fa-var-reddit: "\f1a1"; 548 | @fa-var-reddit-alien: "\f281"; 549 | @fa-var-reddit-square: "\f1a2"; 550 | @fa-var-refresh: "\f021"; 551 | @fa-var-registered: "\f25d"; 552 | @fa-var-remove: "\f00d"; 553 | @fa-var-renren: "\f18b"; 554 | @fa-var-reorder: "\f0c9"; 555 | @fa-var-repeat: "\f01e"; 556 | @fa-var-reply: "\f112"; 557 | @fa-var-reply-all: "\f122"; 558 | @fa-var-resistance: "\f1d0"; 559 | @fa-var-retweet: "\f079"; 560 | @fa-var-rmb: "\f157"; 561 | @fa-var-road: "\f018"; 562 | @fa-var-rocket: "\f135"; 563 | @fa-var-rotate-left: "\f0e2"; 564 | @fa-var-rotate-right: "\f01e"; 565 | @fa-var-rouble: "\f158"; 566 | @fa-var-rss: "\f09e"; 567 | @fa-var-rss-square: "\f143"; 568 | @fa-var-rub: "\f158"; 569 | @fa-var-ruble: "\f158"; 570 | @fa-var-rupee: "\f156"; 571 | @fa-var-s15: "\f2cd"; 572 | @fa-var-safari: "\f267"; 573 | @fa-var-save: "\f0c7"; 574 | @fa-var-scissors: "\f0c4"; 575 | @fa-var-scribd: "\f28a"; 576 | @fa-var-search: "\f002"; 577 | @fa-var-search-minus: "\f010"; 578 | @fa-var-search-plus: "\f00e"; 579 | @fa-var-sellsy: "\f213"; 580 | @fa-var-send: "\f1d8"; 581 | @fa-var-send-o: "\f1d9"; 582 | @fa-var-server: "\f233"; 583 | @fa-var-share: "\f064"; 584 | @fa-var-share-alt: "\f1e0"; 585 | @fa-var-share-alt-square: "\f1e1"; 586 | @fa-var-share-square: "\f14d"; 587 | @fa-var-share-square-o: "\f045"; 588 | @fa-var-shekel: "\f20b"; 589 | @fa-var-sheqel: "\f20b"; 590 | @fa-var-shield: "\f132"; 591 | @fa-var-ship: "\f21a"; 592 | @fa-var-shirtsinbulk: "\f214"; 593 | @fa-var-shopping-bag: "\f290"; 594 | @fa-var-shopping-basket: "\f291"; 595 | @fa-var-shopping-cart: "\f07a"; 596 | @fa-var-shower: "\f2cc"; 597 | @fa-var-sign-in: "\f090"; 598 | @fa-var-sign-language: "\f2a7"; 599 | @fa-var-sign-out: "\f08b"; 600 | @fa-var-signal: "\f012"; 601 | @fa-var-signing: "\f2a7"; 602 | @fa-var-simplybuilt: "\f215"; 603 | @fa-var-sitemap: "\f0e8"; 604 | @fa-var-skyatlas: "\f216"; 605 | @fa-var-skype: "\f17e"; 606 | @fa-var-slack: "\f198"; 607 | @fa-var-sliders: "\f1de"; 608 | @fa-var-slideshare: "\f1e7"; 609 | @fa-var-smile-o: "\f118"; 610 | @fa-var-snapchat: "\f2ab"; 611 | @fa-var-snapchat-ghost: "\f2ac"; 612 | @fa-var-snapchat-square: "\f2ad"; 613 | @fa-var-snowflake-o: "\f2dc"; 614 | @fa-var-soccer-ball-o: "\f1e3"; 615 | @fa-var-sort: "\f0dc"; 616 | @fa-var-sort-alpha-asc: "\f15d"; 617 | @fa-var-sort-alpha-desc: "\f15e"; 618 | @fa-var-sort-amount-asc: "\f160"; 619 | @fa-var-sort-amount-desc: "\f161"; 620 | @fa-var-sort-asc: "\f0de"; 621 | @fa-var-sort-desc: "\f0dd"; 622 | @fa-var-sort-down: "\f0dd"; 623 | @fa-var-sort-numeric-asc: "\f162"; 624 | @fa-var-sort-numeric-desc: "\f163"; 625 | @fa-var-sort-up: "\f0de"; 626 | @fa-var-soundcloud: "\f1be"; 627 | @fa-var-space-shuttle: "\f197"; 628 | @fa-var-spinner: "\f110"; 629 | @fa-var-spoon: "\f1b1"; 630 | @fa-var-spotify: "\f1bc"; 631 | @fa-var-square: "\f0c8"; 632 | @fa-var-square-o: "\f096"; 633 | @fa-var-stack-exchange: "\f18d"; 634 | @fa-var-stack-overflow: "\f16c"; 635 | @fa-var-star: "\f005"; 636 | @fa-var-star-half: "\f089"; 637 | @fa-var-star-half-empty: "\f123"; 638 | @fa-var-star-half-full: "\f123"; 639 | @fa-var-star-half-o: "\f123"; 640 | @fa-var-star-o: "\f006"; 641 | @fa-var-steam: "\f1b6"; 642 | @fa-var-steam-square: "\f1b7"; 643 | @fa-var-step-backward: "\f048"; 644 | @fa-var-step-forward: "\f051"; 645 | @fa-var-stethoscope: "\f0f1"; 646 | @fa-var-sticky-note: "\f249"; 647 | @fa-var-sticky-note-o: "\f24a"; 648 | @fa-var-stop: "\f04d"; 649 | @fa-var-stop-circle: "\f28d"; 650 | @fa-var-stop-circle-o: "\f28e"; 651 | @fa-var-street-view: "\f21d"; 652 | @fa-var-strikethrough: "\f0cc"; 653 | @fa-var-stumbleupon: "\f1a4"; 654 | @fa-var-stumbleupon-circle: "\f1a3"; 655 | @fa-var-subscript: "\f12c"; 656 | @fa-var-subway: "\f239"; 657 | @fa-var-suitcase: "\f0f2"; 658 | @fa-var-sun-o: "\f185"; 659 | @fa-var-superpowers: "\f2dd"; 660 | @fa-var-superscript: "\f12b"; 661 | @fa-var-support: "\f1cd"; 662 | @fa-var-table: "\f0ce"; 663 | @fa-var-tablet: "\f10a"; 664 | @fa-var-tachometer: "\f0e4"; 665 | @fa-var-tag: "\f02b"; 666 | @fa-var-tags: "\f02c"; 667 | @fa-var-tasks: "\f0ae"; 668 | @fa-var-taxi: "\f1ba"; 669 | @fa-var-telegram: "\f2c6"; 670 | @fa-var-television: "\f26c"; 671 | @fa-var-tencent-weibo: "\f1d5"; 672 | @fa-var-terminal: "\f120"; 673 | @fa-var-text-height: "\f034"; 674 | @fa-var-text-width: "\f035"; 675 | @fa-var-th: "\f00a"; 676 | @fa-var-th-large: "\f009"; 677 | @fa-var-th-list: "\f00b"; 678 | @fa-var-themeisle: "\f2b2"; 679 | @fa-var-thermometer: "\f2c7"; 680 | @fa-var-thermometer-0: "\f2cb"; 681 | @fa-var-thermometer-1: "\f2ca"; 682 | @fa-var-thermometer-2: "\f2c9"; 683 | @fa-var-thermometer-3: "\f2c8"; 684 | @fa-var-thermometer-4: "\f2c7"; 685 | @fa-var-thermometer-empty: "\f2cb"; 686 | @fa-var-thermometer-full: "\f2c7"; 687 | @fa-var-thermometer-half: "\f2c9"; 688 | @fa-var-thermometer-quarter: "\f2ca"; 689 | @fa-var-thermometer-three-quarters: "\f2c8"; 690 | @fa-var-thumb-tack: "\f08d"; 691 | @fa-var-thumbs-down: "\f165"; 692 | @fa-var-thumbs-o-down: "\f088"; 693 | @fa-var-thumbs-o-up: "\f087"; 694 | @fa-var-thumbs-up: "\f164"; 695 | @fa-var-ticket: "\f145"; 696 | @fa-var-times: "\f00d"; 697 | @fa-var-times-circle: "\f057"; 698 | @fa-var-times-circle-o: "\f05c"; 699 | @fa-var-times-rectangle: "\f2d3"; 700 | @fa-var-times-rectangle-o: "\f2d4"; 701 | @fa-var-tint: "\f043"; 702 | @fa-var-toggle-down: "\f150"; 703 | @fa-var-toggle-left: "\f191"; 704 | @fa-var-toggle-off: "\f204"; 705 | @fa-var-toggle-on: "\f205"; 706 | @fa-var-toggle-right: "\f152"; 707 | @fa-var-toggle-up: "\f151"; 708 | @fa-var-trademark: "\f25c"; 709 | @fa-var-train: "\f238"; 710 | @fa-var-transgender: "\f224"; 711 | @fa-var-transgender-alt: "\f225"; 712 | @fa-var-trash: "\f1f8"; 713 | @fa-var-trash-o: "\f014"; 714 | @fa-var-tree: "\f1bb"; 715 | @fa-var-trello: "\f181"; 716 | @fa-var-tripadvisor: "\f262"; 717 | @fa-var-trophy: "\f091"; 718 | @fa-var-truck: "\f0d1"; 719 | @fa-var-try: "\f195"; 720 | @fa-var-tty: "\f1e4"; 721 | @fa-var-tumblr: "\f173"; 722 | @fa-var-tumblr-square: "\f174"; 723 | @fa-var-turkish-lira: "\f195"; 724 | @fa-var-tv: "\f26c"; 725 | @fa-var-twitch: "\f1e8"; 726 | @fa-var-twitter: "\f099"; 727 | @fa-var-twitter-square: "\f081"; 728 | @fa-var-umbrella: "\f0e9"; 729 | @fa-var-underline: "\f0cd"; 730 | @fa-var-undo: "\f0e2"; 731 | @fa-var-universal-access: "\f29a"; 732 | @fa-var-university: "\f19c"; 733 | @fa-var-unlink: "\f127"; 734 | @fa-var-unlock: "\f09c"; 735 | @fa-var-unlock-alt: "\f13e"; 736 | @fa-var-unsorted: "\f0dc"; 737 | @fa-var-upload: "\f093"; 738 | @fa-var-usb: "\f287"; 739 | @fa-var-usd: "\f155"; 740 | @fa-var-user: "\f007"; 741 | @fa-var-user-circle: "\f2bd"; 742 | @fa-var-user-circle-o: "\f2be"; 743 | @fa-var-user-md: "\f0f0"; 744 | @fa-var-user-o: "\f2c0"; 745 | @fa-var-user-plus: "\f234"; 746 | @fa-var-user-secret: "\f21b"; 747 | @fa-var-user-times: "\f235"; 748 | @fa-var-users: "\f0c0"; 749 | @fa-var-vcard: "\f2bb"; 750 | @fa-var-vcard-o: "\f2bc"; 751 | @fa-var-venus: "\f221"; 752 | @fa-var-venus-double: "\f226"; 753 | @fa-var-venus-mars: "\f228"; 754 | @fa-var-viacoin: "\f237"; 755 | @fa-var-viadeo: "\f2a9"; 756 | @fa-var-viadeo-square: "\f2aa"; 757 | @fa-var-video-camera: "\f03d"; 758 | @fa-var-vimeo: "\f27d"; 759 | @fa-var-vimeo-square: "\f194"; 760 | @fa-var-vine: "\f1ca"; 761 | @fa-var-vk: "\f189"; 762 | @fa-var-volume-control-phone: "\f2a0"; 763 | @fa-var-volume-down: "\f027"; 764 | @fa-var-volume-off: "\f026"; 765 | @fa-var-volume-up: "\f028"; 766 | @fa-var-warning: "\f071"; 767 | @fa-var-wechat: "\f1d7"; 768 | @fa-var-weibo: "\f18a"; 769 | @fa-var-weixin: "\f1d7"; 770 | @fa-var-whatsapp: "\f232"; 771 | @fa-var-wheelchair: "\f193"; 772 | @fa-var-wheelchair-alt: "\f29b"; 773 | @fa-var-wifi: "\f1eb"; 774 | @fa-var-wikipedia-w: "\f266"; 775 | @fa-var-window-close: "\f2d3"; 776 | @fa-var-window-close-o: "\f2d4"; 777 | @fa-var-window-maximize: "\f2d0"; 778 | @fa-var-window-minimize: "\f2d1"; 779 | @fa-var-window-restore: "\f2d2"; 780 | @fa-var-windows: "\f17a"; 781 | @fa-var-won: "\f159"; 782 | @fa-var-wordpress: "\f19a"; 783 | @fa-var-wpbeginner: "\f297"; 784 | @fa-var-wpexplorer: "\f2de"; 785 | @fa-var-wpforms: "\f298"; 786 | @fa-var-wrench: "\f0ad"; 787 | @fa-var-xing: "\f168"; 788 | @fa-var-xing-square: "\f169"; 789 | @fa-var-y-combinator: "\f23b"; 790 | @fa-var-y-combinator-square: "\f1d4"; 791 | @fa-var-yahoo: "\f19e"; 792 | @fa-var-yc: "\f23b"; 793 | @fa-var-yc-square: "\f1d4"; 794 | @fa-var-yelp: "\f1e9"; 795 | @fa-var-yen: "\f157"; 796 | @fa-var-yoast: "\f2b1"; 797 | @fa-var-youtube: "\f167"; 798 | @fa-var-youtube-play: "\f16a"; 799 | @fa-var-youtube-square: "\f166"; 800 | 801 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/scss/_animated.scss: -------------------------------------------------------------------------------- 1 | // Spinning Icons 2 | // -------------------------- 3 | 4 | .#{$fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .#{$fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/scss/_bordered-pulled.scss: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em $fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .#{$fa-css-prefix}-pull-left { float: left; } 11 | .#{$fa-css-prefix}-pull-right { float: right; } 12 | 13 | .#{$fa-css-prefix} { 14 | &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .#{$fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/scss/_core.scss: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/scss/_fixed-width.scss: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .#{$fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/scss/_larger.scss: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .#{$fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .#{$fa-css-prefix}-2x { font-size: 2em; } 11 | .#{$fa-css-prefix}-3x { font-size: 3em; } 12 | .#{$fa-css-prefix}-4x { font-size: 4em; } 13 | .#{$fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/scss/_list.scss: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: $fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .#{$fa-css-prefix}-li { 11 | position: absolute; 12 | left: -$fa-li-width; 13 | width: $fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.#{$fa-css-prefix}-lg { 17 | left: -$fa-li-width + (4em / 14); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | @mixin fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | @mixin fa-icon-rotate($degrees, $rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})"; 16 | -webkit-transform: rotate($degrees); 17 | -ms-transform: rotate($degrees); 18 | transform: rotate($degrees); 19 | } 20 | 21 | @mixin fa-icon-flip($horiz, $vert, $rotation) { 22 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)"; 23 | -webkit-transform: scale($horiz, $vert); 24 | -ms-transform: scale($horiz, $vert); 25 | transform: scale($horiz, $vert); 26 | } 27 | 28 | 29 | // Only display content to screen readers. A la Bootstrap 4. 30 | // 31 | // See: http://a11yproject.com/posts/how-to-hide-content/ 32 | 33 | @mixin sr-only { 34 | position: absolute; 35 | width: 1px; 36 | height: 1px; 37 | padding: 0; 38 | margin: -1px; 39 | overflow: hidden; 40 | clip: rect(0,0,0,0); 41 | border: 0; 42 | } 43 | 44 | // Use in conjunction with .sr-only to only display content when it's focused. 45 | // 46 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 47 | // 48 | // Credit: HTML5 Boilerplate 49 | 50 | @mixin sr-only-focusable { 51 | &:active, 52 | &:focus { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/scss/_path.scss: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); 7 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), 8 | url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'), 9 | url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), 10 | url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), 11 | url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/scss/_rotated-flipped.scss: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } 5 | .#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } 6 | .#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } 7 | 8 | .#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } 9 | .#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .#{$fa-css-prefix}-rotate-90, 15 | :root .#{$fa-css-prefix}-rotate-180, 16 | :root .#{$fa-css-prefix}-rotate-270, 17 | :root .#{$fa-css-prefix}-flip-horizontal, 18 | :root .#{$fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/scss/_screen-reader.scss: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { @include sr-only(); } 5 | .sr-only-focusable { @include sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/scss/_stacked.scss: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .#{$fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .#{$fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .#{$fa-css-prefix}-inverse { color: $fa-inverse; } 21 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/font-awesome/scss/font-awesome.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables"; 7 | @import "mixins"; 8 | @import "path"; 9 | @import "core"; 10 | @import "larger"; 11 | @import "fixed-width"; 12 | @import "list"; 13 | @import "bordered-pulled"; 14 | @import "animated"; 15 | @import "rotated-flipped"; 16 | @import "stacked"; 17 | @import "icons"; 18 | @import "screen-reader"; 19 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/js/global.js: -------------------------------------------------------------------------------- 1 | var videoDownload = (function (Vue, extendAM){ 2 | var videoDownload = {}; 3 | var VueToast = window.vueToasts ? window.vueToasts.default || window.vueToasts : window.vueToasts; 4 | videoDownload.vm = null; 5 | videoDownload.tasksData = { 6 | headPath: 'http://localhost:5000/', 7 | videoList: [], 8 | videoListCopy: [], 9 | showModal: false, 10 | modalType: 'addTask', 11 | // tablist: ['status', 'details', 'file24s', 'peers', 'options'], 12 | tablist: ['Status', 'Details', 'Log'], 13 | showTab: 'Status', 14 | stateCounter: { all: 0, downloading: 0, finished: 0, paused: 0, invalid: 0}, 15 | modalData: { 16 | add: { url: '', ydl_opts: {} }, 17 | remove: { removeFile: false }, 18 | preference: {youtube_dl: {fomart: '', proxy: ''}, general: {download_dir: '', db_path: '', log_size: ''}}, 19 | }, 20 | currentSelected: null, 21 | taskDetails: {}, 22 | taskInfoUrl: null, 23 | status: 'all', 24 | maxToasts: 4, 25 | position: 'bottom right', 26 | theme: 'error', 27 | timeLife: 3500, 28 | closeBtn: false 29 | }; 30 | 31 | videoDownload.createVm = function(res) { 32 | var that = videoDownload; 33 | that.vm = new Vue({ 34 | el: '#videoWrapper', 35 | data: that.tasksData, 36 | components:{ 37 | 'modal': {template: '#modal-template'}, 38 | VueToast 39 | }, 40 | watch:{ 41 | stateCounter: function(val){ 42 | val.all = val.downloading + val.finished + val.paused + val.invalid; 43 | } 44 | }, 45 | mounted: function () { 46 | this.resetOptions(); 47 | setInterval(videoDownload.timeOut, 3000); 48 | }, 49 | methods: { 50 | showAddTaskModal: function(){ 51 | this.modalData.add.url = ''; 52 | this.showModal = true; 53 | this.modalType = 'addTask'; 54 | console.log(this.modalData); 55 | this.$nextTick(function(){ 56 | this.$refs.url.focus(); 57 | }); 58 | }, 59 | execFunction: function(){ 60 | switch(this.modalType) { 61 | case 'addTask': 62 | this.addTask(); 63 | break; 64 | case 'removeTask': 65 | this.removeTask(); 66 | break; 67 | case 'updatePreference': 68 | this.updatePreference(); 69 | break; 70 | } 71 | }, 72 | showRemoveTaskModal: function(){ 73 | this.modalData.remove.removeFile = false; 74 | this.showModal = true; 75 | this.modalType = 'removeTask'; 76 | }, 77 | addTask: function(){ 78 | var _self = this; 79 | var url = _self.headPath + 'task'; 80 | for (var key in _self.modalData.add.ydl_opts) { 81 | if (_self.modalData.add.ydl_opts[key].trim() == '') 82 | delete _self.modalData.add.ydl_opts[key]; 83 | } 84 | Vue.http.post(url, _self.modalData.add, {emulateJSON: false}).then(function(res){ 85 | _self.showModal = false; 86 | that.getTaskList(); 87 | }, function(err){ 88 | _self.showAlertToast(err, 'error'); 89 | }); 90 | }, 91 | updatePreference: function () { 92 | var _self = this; 93 | var url = _self.headPath + 'config'; 94 | Vue.http.post(url, _self.modalData.preference, {emulateJSON: false}).then(function(res){ 95 | console.log("Successfully"); 96 | }, function(err){ 97 | _self.showAlertToast(err, 'error'); 98 | }); 99 | }, 100 | removeTask: function(){ 101 | var _self = this; 102 | var url = _self.headPath + 'task/tid/' + (_self.videoList[_self.currentSelected] && _self.videoList[_self.currentSelected].tid); 103 | if(_self.modalData.remove.removeFile){ 104 | url += '?del_file=true'; 105 | } 106 | Vue.http.delete(url).then(function(res){ 107 | _self.showAlertToast('Task Delete', 'info'); 108 | _self.videoList.splice(_self.currentSelected, _self.currentSelected+1); 109 | _self.showModal = false; 110 | that.getTaskList(); 111 | }, function(err){ 112 | _self.showAlertToast(err, 'error'); 113 | }); 114 | }, 115 | removeData: function(){ 116 | this.modalData.remove.removeFile = true; 117 | this.removeTask(); 118 | }, 119 | pauseTask: function(){ 120 | var _self = this; 121 | var url = _self.headPath + 'task/tid/' + (_self.videoList[_self.currentSelected] && _self.videoList[_self.currentSelected].tid) + '?act=pause'; 122 | Vue.http.put(url).then(function(res){ 123 | _self.showAlertToast('Task Pause', 'info'); 124 | that.getTaskList(); 125 | }, function(err){ 126 | _self.showAlertToast(err, 'error'); 127 | }); 128 | }, 129 | resumeTask: function(){ 130 | var _self = this; 131 | var url = _self.headPath + 'task/tid/' + (_self.videoList[_self.currentSelected] && _self.videoList[_self.currentSelected].tid) + '?act=resume'; 132 | Vue.http.put(url).then(function(res){ 133 | _self.showAlertToast('Task Resume', 'info'); 134 | that.getTaskList(); 135 | }, function(err){ 136 | _self.showAlertToast(err, 'error'); 137 | }); 138 | }, 139 | about: function() { 140 | this.showModal = true; 141 | this.modalType = 'about'; 142 | }, 143 | preference: function() { 144 | var _self = this; 145 | var url = _self.headPath + 'config'; 146 | 147 | this.showModal = true; 148 | this.modalType = 'updatePreference'; 149 | Vue.http.get(url).then(function(res) { 150 | var responseJSON = JSON.parse(res.data); 151 | if (responseJSON.status === 'error') { 152 | return false; 153 | } else { 154 | config = responseJSON['config']; 155 | _self.modalData.preference.general.download_dir = config.general.download_dir; 156 | _self.modalData.preference.general.db_path = config.general.db_path; 157 | _self.modalData.preference.general.log_size = config.general.log_size; 158 | _self.modalData.preference.youtube_dl.format = config.youtube_dl.format; 159 | _self.modalData.preference.youtube_dl.proxy = config.youtube_dl.proxy; 160 | } 161 | }); 162 | }, 163 | selected: function(index){ 164 | var _self = this; 165 | this.currentSelected = index; 166 | _self.taskInfoUrl = _self.headPath + 'task/tid/' + (_self.videoList[_self.currentSelected] && _self.videoList[_self.currentSelected].tid) + '/status'; 167 | _self.getTaskInfoById(); 168 | }, 169 | getTaskInfoById: function(){ 170 | var _self = this; 171 | if(!_self.taskInfoUrl) return false; 172 | Vue.http.get(_self.taskInfoUrl).then(function(res){ 173 | var responseJSON = JSON.parse(res.data); 174 | if(responseJSON.status === 'error'){ 175 | return false; 176 | } 177 | _self.taskDetails = responseJSON.detail; 178 | }, function(err){ 179 | _self.showAlertToast('Network connection lost', 'error'); 180 | }); 181 | }, 182 | filterTasks: function(filterStatus) { 183 | var _self = this; 184 | _self.status = filterStatus; 185 | that.getTaskList(); 186 | }, 187 | speedConv: function(state, value) { 188 | if (state == 'paused' || state == 'invalid') 189 | return '0 B/s'; 190 | else if (state == 'finished') 191 | return 'Done'; 192 | return this.bitsToHuman(value) + '/s'; 193 | }, 194 | etaConv: function(state, value) { 195 | if (state == 'paused' || state == 'invalid' || state == 'finished') 196 | return 'NaN'; 197 | return this.secondsToHuman(value); 198 | }, 199 | progressConv: function(state, value) { 200 | if (state == 'finished') 201 | return 'Done'; 202 | return value; 203 | }, 204 | bitsToHuman: function(value) { 205 | var tmp = value, count = 0; 206 | var metricList = [' B', ' KB', ' M', ' G', ' T',' P',' E',' Z']; 207 | 208 | while(tmp/1024 > 1){ 209 | tmp = tmp/1024; 210 | count++; 211 | } 212 | return tmp.toFixed(2) + metricList[count]; 213 | }, 214 | secondsToHuman: function(value) { 215 | var tmp = ''; 216 | tmp = value % 60 + 's'; 217 | value = value/ 60; 218 | if(value > 1) { 219 | tmp = parseInt(value % 60) + 'm' + tmp; 220 | value = value / 60; 221 | if(value > 1) { 222 | tmp = parseInt(value % 60) + 'h' + tmp; 223 | value = value / 24; 224 | if(value > 1) { 225 | tmp += parseInt(value % 24) + 'd' + tmp; 226 | } 227 | } 228 | } 229 | return tmp; 230 | }, 231 | stateIcon: function(state) { 232 | if (state == 'downloading') 233 | return {'icon': 'fa-arrow-circle-o-down', 'color': 'blue'}; 234 | else if (state == 'paused') 235 | return {'icon': 'fa-pause-circle-o', 'color': 'green'}; 236 | else if (state == 'finished') 237 | return {'icon': 'fa-check-circle-o', 'color': 'grey'}; 238 | else 239 | return {'icon': 'fa-times-circle-o', 'color': 'red'}; 240 | }, 241 | tsToLocal: function(timeStamp) { 242 | if (typeof timeStamp == 'undefined' || Number(timeStamp) < 10) 243 | return ''; 244 | 245 | var options = { 246 | year: "numeric", month: "short", hour12: false, 247 | day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit" 248 | }; 249 | var d = new Date(0); 250 | d.setUTCSeconds(timeStamp); 251 | return d.toLocaleString('en-US', options); 252 | }, 253 | resetOptions() { 254 | this.$refs.toast.setOptions({ 255 | delayOfJumps: this.delayOfJumps, 256 | maxToasts: this.maxToasts, 257 | position: this.position 258 | }); 259 | }, 260 | showAlertToast(msg, theme) { 261 | this.$refs.toast.showToast(msg, { 262 | theme: theme, 263 | timeLife: this.timeLife, 264 | closeBtn: this.closeBtn 265 | }); 266 | } 267 | } 268 | }); 269 | }; 270 | 271 | videoDownload.getTaskList = function() { 272 | var that = videoDownload; 273 | var url = that.tasksData.headPath + 'task/list'; 274 | url = url + '?state=' + that.tasksData.status; 275 | Vue.http.get(url).then(function(res){ 276 | var resData = JSON.parse(res.body); 277 | that.tasksData.videoList = resData.detail; 278 | that.tasksData.stateCounter = resData.state_counter; 279 | that.tasksData.stateCounter.all = that.tasksData.stateCounter.downloading + 280 | that.tasksData.stateCounter.finished + 281 | that.tasksData.stateCounter.paused + 282 | that.tasksData.stateCounter.invalid; 283 | }, function(err){ 284 | that.vm.showAlertToast('Network connection lost', 'error'); 285 | }); 286 | }; 287 | 288 | videoDownload.timeOut = function(){ 289 | var that = videoDownload; 290 | that.getTaskList(); 291 | that.vm.getTaskInfoById(); 292 | }; 293 | 294 | videoDownload.init = function(){ 295 | var that = this; 296 | that.tasksData.headPath = window.location.protocol + '//' + window.location.host + '/'; 297 | that.createVm(); 298 | that.getTaskList(); 299 | } 300 | 301 | return videoDownload; 302 | })(Vue, {}); 303 | 304 | 305 | videoDownload.init(); 306 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/js/modalComponent.js: -------------------------------------------------------------------------------- 1 | 2 | 29 | 30 | -------------------------------------------------------------------------------- /youtube_dl_webui/static/js/vue-resource.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * vue-resource v1.0.3 3 | * https://github.com/vuejs/vue-resource 4 | * Released under the MIT License. 5 | */ 6 | 7 | !function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):t.VueResource=n()}(this,function(){"use strict";function t(t){this.state=it,this.value=void 0,this.deferred=[];var n=this;try{t(function(t){n.resolve(t)},function(t){n.reject(t)})}catch(t){n.reject(t)}}function n(t,n){t instanceof Promise?this.promise=t:this.promise=new Promise(t.bind(n)),this.context=n}function e(t){at=t.util,ct=t.config.debug||!t.config.silent}function o(t){"undefined"!=typeof console&&ct&&console.warn("[VueResource warn]: "+t)}function r(t){"undefined"!=typeof console&&console.error(t)}function i(t,n){return at.nextTick(t,n)}function u(t){return t.replace(/^\s*|\s*$/g,"")}function s(t){return t?t.toLowerCase():""}function c(t){return t?t.toUpperCase():""}function a(t){return"string"==typeof t}function f(t){return t===!0||t===!1}function h(t){return"function"==typeof t}function p(t){return null!==t&&"object"==typeof t}function l(t){return p(t)&&Object.getPrototypeOf(t)==Object.prototype}function d(t){return"undefined"!=typeof Blob&&t instanceof Blob}function m(t){return"undefined"!=typeof FormData&&t instanceof FormData}function y(t,e,o){var r=n.resolve(t);return arguments.length<2?r:r.then(e,o)}function v(t,n,e){return e=e||{},h(e)&&(e=e.call(n)),g(t.bind({$vm:n,$options:e}),t,{$options:e})}function b(t,n){var e,o;if(t&&"number"==typeof t.length)for(e=0;e=200&&i<300,this.status=i||0,this.statusText=u||"",this.headers=new bt(r),this.body=n,a(n)?this.bodyText=n:d(n)&&(this.bodyBlob=n,Y(n)&&(this.bodyText=Q(n)))}return t.prototype.blob=function(){return y(this.bodyBlob)},t.prototype.text=function(){return y(this.bodyText)},t.prototype.json=function(){return y(this.text(),function(t){return JSON.parse(t)})},t}(),wt=function(){function t(n){vt(this,t),this.body=null,this.params={},pt(this,n,{method:c(n.method||"GET")}),this.headers instanceof bt||(this.headers=new bt(this.headers))}return t.prototype.getUrl=function(){return k(this)},t.prototype.getBody=function(){return this.body},t.prototype.respondWith=function(t,n){return new gt(t,pt(n||{},{url:this.getUrl()}))},t}(),Tt={"X-Requested-With":"XMLHttpRequest"},xt={Accept:"application/json, text/plain, */*"},jt={"Content-Type":"application/json;charset=utf-8"};return Z.options={},Z.headers={put:jt,post:jt,patch:jt,delete:jt,custom:Tt,common:xt},Z.interceptors=[M,W,X,B,D,F,N],["get","delete","head","jsonp"].forEach(function(t){Z[t]=function(n,e){return this(pt(e||{},{url:n,method:t}))}}),["post","put","patch"].forEach(function(t){Z[t]=function(n,e,o){return this(pt(o||{},{url:n,method:t,body:e}))}}),tt.actions={get:{method:"GET"},save:{method:"POST"},query:{method:"GET"},update:{method:"PUT"},remove:{method:"DELETE"},delete:{method:"DELETE"}},"undefined"!=typeof window&&window.Vue&&window.Vue.use(et),et}); -------------------------------------------------------------------------------- /youtube_dl_webui/static/js/vue-toast.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | (global.vueToasts = factory()); 5 | }(this, (function () { 'use strict'; 6 | 7 | var defaultOptions$1 = { 8 | theme: 'default', // info warning error success 9 | timeLife: 5000, 10 | closeBtn: false, 11 | }; 12 | 13 | var VueToast = { 14 | render: function(){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('transition',{attrs:{"name":"vue-toast-opacity"}},[_c('div',{staticClass:"vue-toast_container",class:[_vm.theme],style:(_vm.style),on:{"mouseover":_vm._stopTimer,"mouseleave":_vm._startTimer}},[_c('div',{staticClass:"vue-toast_message"},[_c('span',{domProps:{"innerHTML":_vm._s(_vm.message)}}),_vm._v(" "),(_vm.options.closeBtn)?_c('span',{staticClass:"vue-toast_close-btn",on:{"click":_vm.remove}}):_vm._e()])])])}, 15 | staticRenderFns: [], 16 | props: { 17 | message: { 18 | required: true 19 | }, 20 | position: { 21 | type: Number, 22 | required: true 23 | }, 24 | onDestroy: { 25 | required: true, 26 | type: Function 27 | }, 28 | options: { 29 | type: Object 30 | } 31 | }, 32 | data: function data() { 33 | return { 34 | isShow: false 35 | } 36 | }, 37 | computed: { 38 | theme: function theme() { 39 | return '_' + this.options.theme 40 | }, 41 | style: function style() { 42 | return ("transform: translateY(" + (this.options.directionOfJumping) + (this.position * 100) + "%)") 43 | }, 44 | fullOptions: function fullOptions() { 45 | return Object.assign({}, defaultOptions$1, this.options) 46 | } 47 | }, 48 | mounted: function mounted() { 49 | var this$1 = this; 50 | 51 | setTimeout(function () { 52 | this$1.isShow = true; 53 | }, 50); 54 | 55 | if (!this.fullOptions.closeBtn) { 56 | this._startLazyAutoDestroy(); 57 | } 58 | }, 59 | methods: { 60 | // Public 61 | remove: function remove() { 62 | this._clearTimer(); 63 | this.onDestroy(); 64 | }, 65 | // Private 66 | _startLazyAutoDestroy: function _startLazyAutoDestroy() { 67 | var this$1 = this; 68 | 69 | this._clearTimer(); 70 | this.timerDestroy = setTimeout(function () { 71 | this$1.remove(); 72 | }, this.fullOptions.timeLife); 73 | }, 74 | _clearTimer: function _clearTimer() { 75 | if (this.timerDestroy) { 76 | clearTimeout(this.timerDestroy); 77 | } 78 | }, 79 | _startTimer: function _startTimer() { 80 | if (!this.fullOptions.closeBtn) { 81 | this._startLazyAutoDestroy(); 82 | } 83 | }, 84 | _stopTimer: function _stopTimer() { 85 | if (!this.options.closeBtn) { 86 | this._clearTimer(); 87 | } 88 | } 89 | } 90 | }; 91 | 92 | var defaultOptions = { 93 | maxToasts: 6, 94 | position: 'left bottom' 95 | }; 96 | 97 | var manager$1 = { 98 | render: function(){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('transition-group',{staticClass:"vue-toast-manager_container",class:_vm.classesOfPosition,attrs:{"tag":"div","name":"vue-toast"}},_vm._l((_vm.toasts),function(toast,index){return _c('vue-toast',{key:toast.uid,attrs:{"message":toast.message,"options":toast.options,"onDestroy":toast.onDestroy,"position":index}})}))}, 99 | staticRenderFns: [], 100 | data: function data() { 101 | return { 102 | uid: 1, 103 | toasts: [], 104 | options: defaultOptions 105 | } 106 | }, 107 | computed: { 108 | classesOfPosition: function classesOfPosition() { 109 | return this._updateClassesOfPosition(this.options.position) 110 | }, 111 | directionOfJumping: function directionOfJumping() { 112 | return this._updateDirectionOfJumping(this.options.position) 113 | } 114 | }, 115 | methods: { 116 | // Public 117 | showToast: function showToast(message, options) { 118 | this._addToast(message, options); 119 | this._moveToast(); 120 | 121 | return this 122 | }, 123 | setOptions: function setOptions(options) { 124 | this.options = Object.assign(this.options, options || {}); 125 | 126 | return this 127 | }, 128 | closeAll: function closeAll() { 129 | this.toasts = []; 130 | }, 131 | // Private 132 | _addToast: function _addToast(message, options) { 133 | if ( options === void 0 ) options = {}; 134 | 135 | if (!message) { 136 | return 137 | } 138 | 139 | options.directionOfJumping = this.directionOfJumping; 140 | 141 | var that = this; 142 | var uid = this.uid++; 143 | var toast = { 144 | uid: uid, 145 | message: message, 146 | options: options, 147 | onDestroy: function onDestroy() { 148 | var i = that.toasts.findIndex(function (item) { return item.uid === uid; }); 149 | that.toasts.splice(i, 1); 150 | } 151 | }; 152 | 153 | this.toasts.unshift(toast); 154 | }, 155 | _moveToast: function _moveToast(toast) { 156 | var maxToasts = this.options.maxToasts > 0 157 | ? this.options.maxToasts 158 | : 9999; 159 | 160 | this.toasts = this.toasts.reduceRight(function (prev, toast, i) { 161 | if (i + 1 >= maxToasts) { 162 | return prev 163 | } 164 | 165 | return [toast].concat(prev) 166 | }, []); 167 | }, 168 | _updateClassesOfPosition: function _updateClassesOfPosition(position) { 169 | return position.split(' ').reduce(function (prev, val) { 170 | prev[("__" + (val.toLowerCase()))] = true; 171 | 172 | return prev 173 | }, {}) 174 | }, 175 | _updateDirectionOfJumping: function _updateDirectionOfJumping(position) { 176 | return position.match(/top/i) ? '+' : '-' 177 | } 178 | }, 179 | components: { VueToast: VueToast } 180 | }; 181 | 182 | return manager$1; 183 | 184 | }))); 185 | //# sourceMappingURL=vue-toast.js.map 186 | -------------------------------------------------------------------------------- /youtube_dl_webui/task.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import logging 5 | import os 6 | import json 7 | import glob 8 | 9 | from time import time 10 | from collections import deque 11 | 12 | from .config import ydl_conf 13 | from .utils import TaskInexistenceError 14 | from .utils import TaskExistenceError 15 | from .utils import TaskError 16 | from .utils import url2tid 17 | from .utils import state_index 18 | 19 | from .worker import Worker 20 | 21 | class Task(object): 22 | 23 | def __init__(self, tid, msg_cli, ydl_opts={}, info={}, status={}, log_size=10): 24 | self.logger = logging.getLogger('ydl_webui') 25 | self.tid = tid 26 | self.ydl_opts = ydl_opts 27 | self.ydl_conf = ydl_conf(ydl_opts) 28 | self.info = info 29 | self.url = info['url'] 30 | self.log = deque(maxlen=log_size) 31 | self.msg_cli = msg_cli 32 | self.touch = time() 33 | self.state = None 34 | self.elapsed = status['elapsed'] 35 | self.first_run = True if info['valid'] == 0 else False 36 | 37 | log_list = json.loads(status['log']) 38 | for log in log_list: 39 | self.log.appendleft(log) 40 | 41 | def start(self): 42 | self.logger.info('Task starts, url: %s(%s), ydl_opts: %s' %(self.url, self.tid, self.ydl_opts)) 43 | tm = time() 44 | self.state = state_index['downloading'] 45 | 46 | self.start_time = tm 47 | self.elapsed = self.elapsed + (tm - self.touch) 48 | self.touch = tm 49 | 50 | self.worker = Worker(self.tid, self.info['url'], 51 | msg_cli=self.msg_cli, 52 | ydl_opts=self.ydl_opts, 53 | first_run=self.first_run) 54 | self.log.appendleft({'time': int(tm), 'type': 'debug', 'msg': 'Task starts...'}) 55 | self.worker.start() 56 | 57 | def pause(self): 58 | self.logger.info('Task pauses, url - %s(%s)' %(self.url, self.tid)) 59 | tm = time() 60 | self.state = state_index['paused'] 61 | 62 | self.pause_time = tm 63 | self.elapsed = self.elapsed + (tm - self.touch) 64 | self.touch = tm 65 | 66 | self.worker.stop() 67 | self.log.appendleft({'time': int(tm), 'type': 'debug', 'msg': 'Task pauses...'}) 68 | 69 | def halt(self): 70 | self.logger.info('Task halts, url - %s(%s)' %(self.url, self.tid)) 71 | tm = time() 72 | self.state = state_index['invalid'] 73 | 74 | self.halt_time = tm 75 | self.finish_time = tm 76 | self.elapsed = self.elapsed + (tm - self.touch) 77 | self.touch = tm 78 | 79 | self.worker.stop() 80 | self.log.appendleft({'time': int(tm), 'type': 'debug', 'msg': 'Task halts...'}) 81 | 82 | def finish(self): 83 | self.logger.info('Task finishes, url - %s(%s)' %(self.url, self.tid)) 84 | tm = time() 85 | self.state = state_index['finished'] 86 | 87 | self.pause_time = tm 88 | self.finish_time = tm 89 | self.elapsed = self.elapsed + (tm - self.touch) 90 | self.touch = tm 91 | 92 | self.worker.stop() 93 | self.log.appendleft({'time': int(tm), 'type': 'debug', 'msg': 'Task finishs...'}) 94 | 95 | def update_info(self, info_dict): 96 | self.first_run = False 97 | 98 | def update_log(self, log): 99 | self.log.appendleft(log) 100 | 101 | def progress_update(self, data): 102 | tm = time() 103 | self.elapsed = self.elapsed + (tm - self.touch) 104 | self.touch = tm 105 | 106 | 107 | class TaskManager(object): 108 | """ 109 | Tasks are categorized into two types, active type and inactive type. 110 | 111 | Tasks in active type are which in downloading, pausing state. These tasks 112 | associate with a Task instance in memory. However, inactive type tasks 113 | are in invalid state or finished state, which only have database recoards 114 | but memory instance. 115 | """ 116 | ExerptKeys = ['tid', 'state', 'percent', 'total_bytes', 'title', 'eta', 'speed'] 117 | 118 | def __init__(self, db, msg_cli, conf): 119 | self.logger = logging.getLogger('ydl_webui') 120 | self._db = db 121 | self._msg_cli = msg_cli 122 | self._conf = conf 123 | self.ydl_conf = conf['youtube_dl'] 124 | 125 | self._tasks_dict = {} 126 | 127 | def new_task(self, url, ydl_opts={}): 128 | """Create a new task and put it in inactive type""" 129 | 130 | # stripe out necessary fields 131 | ydl_opts = ydl_conf(ydl_opts) 132 | return self._db.new_task(url, ydl_opts.dict()) 133 | 134 | def start_task(self, tid, ignore_state=False): 135 | """make an inactive type task into active type""" 136 | 137 | task = None 138 | if tid in self._tasks_dict: 139 | task = self._tasks_dict[tid] 140 | if task.state == state_index['downloading']: 141 | raise TaskError('Task is downloading') 142 | else: 143 | try: 144 | ydl_opts = self.ydl_conf.merge_conf(self._db.get_ydl_opts(tid)) 145 | info = self._db.get_info(tid) 146 | status = self._db.get_stat(tid) 147 | except TaskInexistenceError as e: 148 | raise TaskInexistenceError(e.msg) 149 | 150 | if status['state'] == state_index['finished']: 151 | raise TaskError('Task is finished') 152 | 153 | task = Task(tid, self._msg_cli, ydl_opts=ydl_opts, info=info, 154 | status=status, log_size=self._conf['general']['log_size']) 155 | self._tasks_dict[tid] = task 156 | 157 | task.start() 158 | self._db.start_task(tid, start_time=task.start_time) 159 | self._db.update_log(tid, task.log) 160 | 161 | return task 162 | 163 | def pause_task(self, tid): 164 | self.logger.debug('task paused (%s)' %(tid)) 165 | 166 | if tid not in self._tasks_dict: 167 | raise TaskError('Task is finished or invalid or inexistent') 168 | 169 | task = self._tasks_dict[tid] 170 | if task.state == state_index['paused']: 171 | raise TaskError('Task already paused') 172 | 173 | task.pause() 174 | self._db.pause_task(tid, pause_time=task.pause_time, elapsed=task.elapsed) 175 | self._db.update_log(tid, task.log) 176 | 177 | def finish_task(self, tid): 178 | self.logger.debug('task finished (%s)' %(tid)) 179 | 180 | if tid not in self._tasks_dict: 181 | raise TaskInexistenceError('task does not exist') 182 | 183 | task = self._tasks_dict[tid] 184 | task.finish() 185 | self._db.finish_task(tid, finish_time=task.finish_time, elapsed=task.elapsed) 186 | self._db.update_log(tid, task.log) 187 | del self._tasks_dict[tid] 188 | 189 | def halt_task(self, tid): 190 | self.logger.debug('task halted (%s)' %(tid)) 191 | 192 | if tid not in self._tasks_dict: 193 | raise TaskInexistenceError('task does not exist') 194 | 195 | task = self._tasks_dict[tid] 196 | task.halt() 197 | self._db.halt_task(tid, halt_time=task.halt_time, elapsed=task.elapsed) 198 | self._db.update_log(tid, task.log) 199 | del self._tasks_dict[tid] 200 | 201 | def delete_task(self, tid, del_file=False): 202 | self.logger.debug('task deleted (%s)' %(tid)) 203 | 204 | if tid in self._tasks_dict: 205 | task = self._tasks_dict[tid] 206 | del self._tasks_dict[tid] 207 | task.halt() 208 | 209 | try: 210 | dl_file = self._db.delete_task(tid) 211 | except TaskInexistenceError as e: 212 | raise TaskInexistenceError(e.msg) 213 | 214 | if del_file and dl_file is not None: 215 | file_wo_ext, ext = dl_file, None 216 | while ext != '': 217 | file_wo_ext, ext = os.path.splitext(file_wo_ext) 218 | 219 | for fname in os.listdir(os.getcwd()): 220 | if fname.startswith(file_wo_ext): 221 | self.logger.debug('delete file: %s' %(fname)) 222 | os.remove(os.path.join(os.getcwd(), fname)) 223 | 224 | def query(self, tid, exerpt=True): 225 | db_ret = self._db.query_task(tid) 226 | 227 | detail = {} 228 | if exerpt: 229 | detail = {k: db_ret[k] for k in ret if k in self.ExerptKeys} 230 | else: 231 | detail = db_ret 232 | 233 | return detail 234 | 235 | def list(self, state, exerpt=False): 236 | db_ret, counter = self._db.list_task(state) 237 | 238 | detail = [] 239 | if exerpt is not True: 240 | for item in db_ret: 241 | d = {k: item[k] for k in item if k in self.ExerptKeys} 242 | detail.append(d) 243 | else: 244 | detail = db_ret 245 | 246 | return detail, counter 247 | 248 | def state(self): 249 | return self._db.state_counter() 250 | 251 | def update_info(self, tid, info_dict): 252 | if tid not in self._tasks_dict: 253 | raise TaskInexistenceError('task does not exist') 254 | 255 | task = self._tasks_dict[tid] 256 | task.update_info(info_dict) 257 | 258 | self._db.update_info(tid, info_dict) 259 | 260 | def update_log(self, tid, log): 261 | if tid not in self._tasks_dict: 262 | # raise TaskInexistenceError('task does not exist') 263 | self.logger.error('Task does not active, tid=%s' %(tid)) 264 | return 265 | 266 | task = self._tasks_dict[tid] 267 | task.update_log(log) 268 | self._db.update_log(tid, task.log, exist_test=False) 269 | 270 | def progress_update(self, tid, data): 271 | if tid not in self._tasks_dict: 272 | raise TaskInexistenceError('task does not exist') 273 | 274 | task = self._tasks_dict[tid] 275 | task.progress_update(data) 276 | 277 | if 'total_bytes' in data: 278 | data['total_bytes_estmt'] = data['total_bytes'] 279 | else: 280 | data['total_bytes'] = '0' 281 | 282 | self._db.progress_update(tid, data, task.elapsed) 283 | 284 | def launch_unfinished(self): 285 | tid_list = self._db.launch_unfinished() 286 | 287 | for t in tid_list: 288 | try: 289 | self.start_task(t) 290 | except TaskError as e: 291 | self.logger.warn("Task %s is in downloading or finished state", tid) 292 | except TaskInexistenceError: 293 | self.logger.error('Task does not exist') 294 | 295 | -------------------------------------------------------------------------------- /youtube_dl_webui/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | youtube-dl-webUI 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 44 |
45 |
46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 |
54 |
55 |
56 |
57 | 77 |
78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 96 | 97 | 98 | 107 | 108 | 109 | 110 | 111 |
#StateNameTotal SizeProgressDown SpeedETA
{{"{{index + 1}}"}} 94 | 95 | {{"{{video.title}}"}}{{"{{bitsToHuman(video.total_bytes)}}"}} 99 |
100 |
{{"{{progressConv(video.state, video.percent)}}"}}
101 |
102 |
103 |
104 |
105 | 106 |
{{"{{speedConv(video.state, video.speed)}}"}}{{"{{etaConv(video.state, video.eta)}}"}}
112 |
113 | 114 | 140 | 157 | 168 | 182 | 183 | 184 |
185 |
186 |
187 |
    188 |
  • {{"{{tab}}"}}
  • 189 |
190 |
191 |
192 |
193 |
194 |
Title:
{{"{{taskDetails.title}}"}}
195 |
Extension:
{{"{{taskDetails.ext}}"}}
196 |
Format:
{{"{{taskDetails.format}}"}}
197 |
Create Time:
{{"{{tsToLocal(taskDetails.create_time)}}"}}
198 |
Finish Time:
{{"{{tsToLocal(taskDetails.finish_time)}}"}}
199 |
File Name:
{{"{{taskDetails.filename}}"}}
200 |
201 |
202 |
203 |
204 |
205 | Title: 206 | {{"{{taskDetails.title}}"}} 207 |
208 |
209 | URL: 210 | {{"{{taskDetails.url}}"}} 211 |
212 |
213 | Thumbnail: 214 | 215 |
216 |
217 |
218 |
219 | Description 220 |
221 |
222 |
223 |
224 |
Duration:{{"{{taskDetails.duration}}"}}
225 |
View Count:{{"{{taskDetails.view_count}}"}}
226 |
Like Count:{{"{{taskDetails.like_count}}"}}
227 |
Dislike Count:{{"{{taskDetails.dislike_count}}"}}
228 |
Average Rating:{{"{{taskDetails.average_rating}}"}}
229 |
230 |
231 |
232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 |
TimeTypeMessage
{{"{{tsToLocal(log.time)}}"}}{{"{{log.type}}"}}{{"{{log.msg}}"}}
248 |
249 |
250 |
251 |
252 | 253 | 254 | 255 | 256 | 257 | 258 | -------------------------------------------------------------------------------- /youtube_dl_webui/templates/modalComponent.html: -------------------------------------------------------------------------------- 1 | 32 | -------------------------------------------------------------------------------- /youtube_dl_webui/templates/test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test cases 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 | Task control 13 |
14 | 15 | 16 | 17 |
18 |
19 | 20 | 21 |
22 |
23 | -- ydl opts -- 24 |
25 |
26 | 27 | 28 |
29 |
30 | -- Operations -- 31 |
32 |
33 | 34 | 35 |
36 |
37 | Del: 38 | 39 | Delete File 40 |
41 |
42 | 43 | 44 |
45 |
46 | Batch Del: 47 | 48 | Delete File 49 |
50 |
51 |
52 |
53 | Settings 54 |
55 | -- General -- 56 |
57 |
58 | 59 | 60 |
61 |
62 | 63 | 64 |
65 |
66 | 67 | 68 |
69 |
70 |
71 | -- Server -- 72 |
73 |
74 | 75 | 76 |
77 |
78 | 79 | 80 |
81 |
82 |
83 | -- youtubedl -- 84 |
85 |
86 | 87 | 88 |
89 |
90 | 91 | 92 |
93 |
94 | 95 |
96 |
97 | 98 | 251 | 252 | 253 | -------------------------------------------------------------------------------- /youtube_dl_webui/test/post_processor/bestvideo_bestaudio_merging.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import unicode_literals 5 | from os import chdir 6 | import youtube_dl 7 | 8 | 9 | class MyLogger(object): 10 | def debug(self, msg): 11 | print('dbg: ', msg) 12 | 13 | def warning(self, msg): 14 | print('warn: ', msg) 15 | 16 | def error(self, msg): 17 | print('err: ', msg) 18 | 19 | 20 | def my_hook(d): 21 | print(d) 22 | if d['status'] == 'finished': 23 | print('--------------- finish -----------------') 24 | 25 | if __name__ == '__main__': 26 | chdir('/tmp') 27 | 28 | ydl_opts = { 29 | 'format': 'bestvideo+bestaudio', 30 | 'logger': MyLogger(), 31 | 'progress_hooks': [my_hook], 32 | } 33 | 34 | with youtube_dl.YoutubeDL(ydl_opts) as ydl: 35 | ret = ydl.extract_info('https://www.youtube.com/watch?v=jZvC7NWkeA0') 36 | -------------------------------------------------------------------------------- /youtube_dl_webui/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import uuid 5 | 6 | from hashlib import sha1 7 | 8 | state_index={'all': 0, 'downloading': 1, 'paused': 2, 'finished': 3, 'invalid': 4} 9 | state_name=['all', 'downloading', 'paused', 'finished', 'invalid'] 10 | 11 | def new_uuid(): 12 | return str(uuid.uuid4().hex) 13 | 14 | 15 | def url2tid(url): 16 | return sha1(url.encode()).hexdigest() 17 | 18 | 19 | class YoutubeDLWebUI(Exception): 20 | """Base exception for YoutubeDL errors.""" 21 | pass 22 | 23 | 24 | class TaskError(YoutubeDLWebUI): 25 | """Error related to download tasks.""" 26 | def __init__(self, msg, tid=None): 27 | if tid: msg += ' tid={}'.format(tid) 28 | 29 | super(TaskError, self).__init__(msg) 30 | self.msg = msg 31 | 32 | def __str__(self): 33 | return repr(self.msg) 34 | 35 | 36 | class TaskInexistenceError(TaskError): 37 | def __init__(self, msg, tid=None, url=None, state=None): 38 | msg = 'Task does not exist' 39 | if tid: 40 | msg += ' tid={}'.format(tid) 41 | if url: 42 | msg += ' url={}'.format(url) 43 | if state: 44 | msg += ' state={}'.format(state) 45 | 46 | super(TaskInexistenceError, self).__init__(msg) 47 | self.msg = msg 48 | 49 | 50 | class TaskExistenceError(TaskError): 51 | def __init__(self, msg, tid=None, url=None, state=None): 52 | msg = 'Task already exists' 53 | if tid: 54 | msg += ' tid={}'.format(tid) 55 | if url: 56 | msg += ' url={}'.format(url) 57 | if state: 58 | msg += ' state={}'.format(state) 59 | 60 | super(TaskExistenceError, self).__init__(msg) 61 | self.msg = msg 62 | 63 | 64 | class YDLManagerError(YoutubeDLWebUI): 65 | """Error related to youtube-dl manager.""" 66 | def __init__(self, msg, tid=None, url=None, state=None): 67 | if tid: 68 | msg += ' tid={}'.format(tid) 69 | if url: 70 | msg += ' url={}'.format(url) 71 | if state: 72 | msg += ' state={}'.format(state) 73 | 74 | super(YDLManagerError, self).__init__(msg) 75 | self.tid = tid 76 | self.url = url 77 | self.state = state 78 | self.msg = msg 79 | 80 | def __str__(self): 81 | return repr(self.msg) 82 | -------------------------------------------------------------------------------- /youtube_dl_webui/worker.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import re 5 | import logging 6 | import json 7 | 8 | from youtube_dl import YoutubeDL 9 | from youtube_dl import DownloadError 10 | 11 | from multiprocessing import Process 12 | from time import time 13 | 14 | class YdlHook(object): 15 | def __init__(self, tid, msg_cli): 16 | self.logger = logging.getLogger('ydl_webui') 17 | self.tid = tid 18 | self.msg_cli = msg_cli 19 | 20 | def finished(self, d): 21 | self.logger.debug('finished status') 22 | d['_percent_str'] = '100%' 23 | d['speed'] = '0' 24 | d['elapsed'] = 0 25 | d['eta'] = 0 26 | d['downloaded_bytes'] = d['total_bytes'] 27 | return d 28 | 29 | def downloading(self, d): 30 | self.logger.debug('downloading status') 31 | return d 32 | 33 | def error(self, d): 34 | self.logger.debug('error status') 35 | # d['_percent_str'] = '100%' 36 | return d 37 | 38 | def dispatcher(self, d): 39 | if 'total_bytes_estimate' not in d: 40 | d['total_bytes_estimate'] = 0 41 | if 'tmpfilename' not in d: 42 | d['tmpfilename'] = '' 43 | 44 | if d['status'] == 'finished': 45 | d = self.finished(d) 46 | elif d['status'] == 'downloading': 47 | d = self.downloading(d) 48 | elif d['error'] == 'error': 49 | d = self.error(d) 50 | self.msg_cli.put('progress', {'tid': self.tid, 'data': d}) 51 | 52 | 53 | class LogFilter(object): 54 | def __init__(self, tid, msg_cli): 55 | self.logger = logging.getLogger('ydl_webui') 56 | self.tid = tid 57 | self.msg_cli = msg_cli 58 | 59 | def debug(self, msg): 60 | self.logger.debug('debug: %s' %(self.ansi_escape(msg))) 61 | payload = {'time': int(time()), 'type': 'debug', 'msg': self.ansi_escape(msg)} 62 | self.msg_cli.put('log', {'tid': self.tid, 'data': payload}) 63 | 64 | def warning(self, msg): 65 | self.logger.debug('warning: %s' %(self.ansi_escape(msg))) 66 | payload = {'time': int(time()), 'type': 'warning', 'msg': self.ansi_escape(msg)} 67 | self.msg_cli.put('log', {'tid': self.tid, 'data': payload}) 68 | 69 | def error(self, msg): 70 | self.logger.debug('error: %s' %(self.ansi_escape(msg))) 71 | payload = {'time': int(time()), 'type': 'warning', 'msg': self.ansi_escape(msg)} 72 | self.msg_cli.put('log', {'tid': self.tid, 'data': payload}) 73 | 74 | def ansi_escape(self, msg): 75 | reg = r'\x1b\[([0-9,A-Z]{1,2}(;[0-9]{1,2})?(;[0-9]{3})?)?[m|K]?' 76 | return re.sub(reg, '', msg) 77 | 78 | 79 | class FatalEvent(object): 80 | def __init__(self, tid, msg_cli): 81 | self.logger = logging.getLogger('ydl_webui') 82 | self.tid = tid 83 | self.msg_cli = msg_cli 84 | 85 | def invalid_url(self, url): 86 | self.logger.debug('fatal error: invalid url') 87 | payload = {'time': int(time()), 'type': 'fatal', 'msg': 'invalid url: %s' %(url)} 88 | self.msg_cli.put('fatal', {'tid': self.tid, 'data': payload}) 89 | 90 | 91 | class Worker(Process): 92 | def __init__(self, tid, url, msg_cli, ydl_opts=None, first_run=False): 93 | super(Worker, self).__init__() 94 | self.logger = logging.getLogger('ydl_webui') 95 | self.tid = tid 96 | self.url = url 97 | self.msg_cli = msg_cli 98 | self.ydl_opts = ydl_opts 99 | self.first_run = first_run 100 | self.log_filter = LogFilter(tid, msg_cli) 101 | self.ydl_hook = YdlHook(tid, msg_cli) 102 | 103 | def intercept_ydl_opts(self): 104 | self.ydl_opts['logger'] = self.log_filter 105 | self.ydl_opts['progress_hooks'] = [self.ydl_hook.dispatcher] 106 | self.ydl_opts['noplaylist'] = "false" 107 | self.ydl_opts['progress_with_newline'] = True 108 | 109 | def run(self): 110 | self.intercept_ydl_opts() 111 | with YoutubeDL(self.ydl_opts) as ydl: 112 | try: 113 | if self.first_run: 114 | info_dict = ydl.extract_info(self.url, download=False) 115 | 116 | # self.logger.debug(json.dumps(info_dict, indent=4)) 117 | 118 | # info_dict['description'] = info_dict['description'].replace('\n', '
'); 119 | payload = {'tid': self.tid, 'data': info_dict} 120 | self.msg_cli.put('info_dict', payload) 121 | 122 | self.logger.info('start downloading, url - %s' %(self.url)) 123 | ydl.download([self.url]) 124 | except DownloadError as e: 125 | # url error 126 | event_handler = FatalEvent(self.tid, self.msg_cli) 127 | event_handler.invalid_url(self.url); 128 | 129 | self.msg_cli.put('worker_done', {'tid': self.tid, 'data': {}}) 130 | 131 | def stop(self): 132 | self.logger.info('Terminating Process ...') 133 | self.terminate() 134 | self.join() 135 | 136 | --------------------------------------------------------------------------------