├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md └── foxford_downloader ├── .gitignore ├── README.md ├── fdl.py ├── lib ├── __init__.py ├── browser.py ├── fns.py ├── helpers.py └── requests_cache.py └── requirements.txt /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at Telegram: @limitedeternity. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | --------------- 3 | 4 | ### **Make sure that all requirements are satisfied:** 5 | 6 | * You are using the latest version of script. 7 | 8 | * You have the latest Node.js installed. 9 | 10 | **ONLY THEN YOU CAN SUBMIT ANY BUG REPORTS** 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Marise Hayashi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Types of changes 2 | 3 | - [ ] Bug fix (non-breaking change which fixes an issue) 4 | - [ ] New feature (non-breaking change which adds functionality) 5 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 6 | - [ ] I have read the **CONTRIBUTING.md** document. 7 | - [ ] My code follows the code style of this project. 8 | - [ ] My change requires a change to the documentation. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # /un/ 2 | 3 |

Incoming message:

Друзья, много времени прошло с того момента, как я начал работу над FDL. Три года, если быть точным. И последней версией оказалась 6.5. Я внёс последнюю строчку кода с этим коммитом и решил, что мне пора двигаться дальше, поэтому я архивирую репозиторий, никаких изменений вноситься больше не будет. Я его оставлю для себя на память.
4 | Пирожковую я передал доверенному лицу, которое сможет продолжать вести её.
5 | Тред в скором времени утонет. Создайте новый, если нужно.
6 | Весь код я оставляю открытым: любой может посмотреть его и модифицировать под свои нужды. На его полное понимание может уйти не один день, но оно того стоит. Я гарантирую это.
7 | Используйте форк от этого репозитория в качестве шапки. Изменения в FDL тоже вносите в форк.
8 | И убедительная просьба: не пишите мне по поводу этого всего. Игнорирую и блокирую.
9 | Ну, вот и всё, сайонара. Люблю всех. 10 | 11 | --- 12 | 13 | ## **_2016 г._** 14 | 15 | ... 16 | 17 |
18 | ⚛️ [Технические] 19 | 20 | - [Математика. Подготовка к ЕГЭ. Часть С](https://rutracker.org/forum/viewtopic.php?t=5257235) 21 | 22 | - [Физика. Подготовка к ЕГЭ. Часть С](https://rutracker.org/forum/viewtopic.php?t=5257249) 23 | 24 | - [Информатика. Экспресс-подготовка к ЕГЭ](https://rutracker.org/forum/viewtopic.php?t=5257220) 25 | 26 | - [Алгебра. 10 класс](https://rutracker.org/forum/viewtopic.php?t=5427254) 27 | 28 | - [Геометрия. 10 класс](https://rutracker.org/forum/viewtopic.php?t=5429370) 29 | 30 |
31 |
32 |
33 | [Гуманитарные] 34 | 35 | - [Русский Язык. Подготовка к ЕГЭ. Сочинение](https://rutracker.org/forum/viewtopic.php?t=5257263) 36 | 37 | - [Экспресс-курс. Учи английский легко.](https://cloud.mail.ru/public/6og2/YZeFbTwYT/) 38 | 39 |
40 |
41 | 42 | **_Части B Математики и Физики находятся здесь:_** 43 | 44 | https://mega.nz/#!BFNV3Q7b!kioo4rQuzS5l7GyGpXJyGhlgPlnSQ2c15DYMsuOi8kw 45 | 46 | --- 47 | 48 | ## **_2017 г._** 49 | 50 | ... 51 | 52 |
53 | ⚛️ [Технические] 54 | 55 | - [Подготовка к ОГЭ. Физика](https://rutracker.org/forum/viewtopic.php?t=5446633) 56 | 57 | - [Подготовка к олимпиадам. Математика. 9 класс](https://rutracker.org/forum/viewtopic.php?t=5446632) 58 | 59 | - [Экспресс-подготовка к ОГЭ. Физика](https://rutracker.org/forum/viewtopic.php?t=5446621) 60 | 61 | - [Подготовка к ОГЭ. Математика](https://rutracker.org/forum/viewtopic.php?t=5446635) 62 | 63 | - [Экспресс-подготовка к ОГЭ. Математика](https://rutracker.org/forum/viewtopic.php?t=5446623) 64 | 65 | - [Углубленный курс. Алгебра](https://rutracker.org/forum/viewtopic.php?t=5446627) 66 | 67 | - [Углубленный курс. Геометрия](https://rutracker.org/forum/viewtopic.php?t=5446626) 68 | 69 | - [Подготовка к олимпиадам "Физтех" по математике](https://rutracker.org/forum/viewtopic.php?t=5418196) 70 | 71 | - [Подготовка к олимпиадам "Физтех" по физике](https://rutracker.org/forum/viewtopic.php?t=5441240) 72 | 73 | - [Подготовка к олимпиадам по математике](https://rutracker.org/forum/viewtopic.php?t=5418108) 74 | 75 | - [Подготовка к олимпиадам по физике](https://rutracker.org/forum/viewtopic.php?t=5442687) 76 | 77 | - [Программирование (9-11 классы). Подготовка к олимпиадам, базовый уровень](https://rutracker.org/forum/viewtopic.php?t=5444437) 78 | 79 | - [Программирование (9-11 классы). Подготовка к олимпиадам, продвинутый уровень](https://rutracker.org/forum/viewtopic.php?t=5417314) 80 | 81 | - [Математика. Подготовка к ЕГЭ / Часть С](https://rutracker.org/forum/viewtopic.php?t=5417886) 82 | 83 | - [Математика. Экспресс-подготовка к ЕГЭ / Часть С](https://rutracker.org/forum/viewtopic.php?t=5444510) 84 | 85 | - [Математика. Экспресс-подготовка к ЕГЭ / Часть B](https://rutracker.org/forum/viewtopic.php?t=5444960) 86 | 87 | - [Физика. Экспресс-подготовка к ЕГЭ / Часть С](https://rutracker.org/forum/viewtopic.php?t=5444953) 88 | 89 | - [Физика. Экспресс-подготовка к ЕГЭ / Часть B](https://rutracker.org/forum/viewtopic.php?t=5444954) 90 | 91 | - [Информатика. Подготовка к ЕГЭ](https://rutracker.org/forum/viewtopic.php?t=5417807) 92 | 93 | - [Изучение языков С и С++ / Язык С++](https://rutracker.org/forum/viewtopic.php?t=5417828) 94 | 95 | - [Web-программирование](https://rutracker.org/forum/viewtopic.php?t=5418437) 96 | 97 | - [Программирование на языке Python](https://rutracker.org/forum/viewtopic.php?t=5444423) 98 | 99 |
100 |
101 |
102 | [Гуманитарные] 103 | 104 | - [Подготовка к ОГЭ. Обществознание](https://rutracker.org/forum/viewtopic.php?t=5446634) 105 | 106 | - [Подготовка к олимпиадам. Обществознание](https://rutracker.org/forum/viewtopic.php?t=5446630) 107 | 108 | - [Русский язык. Подготовка к ЕГЭ. Часть 1](https://rutracker.org/forum/viewtopic.php?t=5444409) 109 | 110 | - [Русский язык. Экспресс-подготовка к ЕГЭ. Часть 1](https://rutracker.org/forum/viewtopic.php?t=5444957) 111 | 112 | - [Русский язык. Сочинение. Экспресс-подготовка к ЕГЭ](https://rutracker.org/forum/viewtopic.php?t=5444449) 113 | 114 | - [Английский язык. Подготовка к ЕГЭ](https://rutracker.org/forum/viewtopic.php?t=5444419) 115 | 116 | - [Английский язык. Экспресс-подготовка к ЕГЭ](https://rutracker.org/forum/viewtopic.php?t=5444434) 117 | 118 | - [Стань сильнее. Pre-Intermediate (A2-B1)](https://rutracker.org/forum/viewtopic.php?t=5444412) 119 | 120 | - [Обществознание. Экспресс-подготовка к ЕГЭ](https://rutracker.org/forum/viewtopic.php?t=5444307) 121 | 122 | - [Обществознание. Подготовка к ЕГЭ](https://rutracker.org/forum/viewtopic.php?t=5444303) 123 | 124 | - [Обществознание. Подготовка к олимпиадам](https://mega.nz/#F!Vv4AmCpS!ClUpGarpD8yXyrx1MEoeLQ) 125 | 126 | - [История. Подготовка к ЕГЭ](https://mega.nz/#F!NyxmnDzT!x9kTW9VsdY28oCT4KvNBBA) 127 | 128 |
129 |
130 |
131 | 🔬 [Естественно-научные] 132 | 133 | - [Биология. Подготовка к ЕГЭ](https://rutracker.org/forum/viewtopic.php?t=5438805) 134 | 135 | - [Биология. Экспресс-подготовка к ЕГЭ](https://rutracker.org/forum/viewtopic.php?t=5444414) 136 | 137 | - [Биология. Подготовка к олимпиадам](https://rutracker.org/forum/viewtopic.php?t=5445005) 138 | 139 | - [Химия. Подготовка к ЕГЭ](https://rutracker.org/forum/viewtopic.php?t=5441118) 140 | 141 | - [Химия. Подоготовка к олимпиадам](https://rutracker.org/forum/viewtopic.php?t=5444426) 142 | 143 | - [Химия. Экспресс-подготовка к ЕГЭ](https://rutracker.org/forum/viewtopic.php?t=5444500) 144 | 145 |
146 |
147 |
148 | 💡 [Другое] 149 | 150 | - [Словесность. Работа с информацией](https://rutracker.org/forum/viewtopic.php?t=5446832) 151 | 152 | - - 153 | 154 |
155 |
156 | 157 | --- 158 | 159 | ## **_2018 г._** 160 | 161 | ... 162 | 163 |
164 | ⚛️ [Технические] 165 | 166 | - [Подготовка к ОГЭ. Математика. 9 класс](https://cloud.mail.ru/public/95XE/g3e1XZCrE) 167 | 168 | - Подготовка к олимпиадам. Математика. 9 класс: 169 | 170 | - [Видео](https://mega.nz/#F!br4g1CRb!Yi_hw2wmK4BPe7fXCQmA4Q) 171 | - [Д/З](https://yadi.sk/d/dNIoDTyW3ajwh7) 172 | 173 | - [Подготовка к ОГЭ. Физика. 9 класс](https://mega.nz/#F!qfxHUA7I!vV1DaKK0-tUVxo4ocBBA3A) 174 | 175 | - [Геометрия. Углубленный уровень. 10 класс](https://yadi.sk/d/-Rv0BQU-3aYjgM) 176 | 177 | - Подготовка к ЕГЭ. Математика. 10 класс: 178 | 179 | - [Видео](https://mega.nz/#F!vagE3aCa!i20C7ttAZavCPhe4SAfqeg) 180 | - [Д/З](https://yadi.sk/d/Ij3LuPWs3aUjTn) 181 | 182 | - Подготовка к ЕГЭ. Математика. C-часть: 183 | 184 | - [Презентации](https://mega.nz/#F!HXgwTLzQ!5VgTKJvGKh_3VxfNctx9HQ) 185 | - [Видео](https://mega.nz/#F!Ln40BSpa!ciyrGIRZhD6vsn-x0EMUUA) 186 | - [Д/З](https://yadi.sk/d/ll2e8ATk3a3xPB) 187 | 188 | - Экспресс-подготовка к ЕГЭ. Математика. В-часть. 189 | 190 | - Экспресс-подготовка к ЕГЭ. Математика. С-часть. 191 | 192 | - Физика. Подготовка к ЕГЭ. С-часть: 193 | 194 | - [Презентации](https://mega.nz/#F!bOp2FbrJ!eR7EbmgcBX82xEVJZpY4QA) 195 | - [Видео](https://mega.nz/#F!vrhllCKB!Mo5ebF8JJGsULfJgu3f9Lg) 196 | - [Д/З](https://mega.nz/#F!nWJAHQDI!mjP9Z_C7LuSTgkZW0Nm-0w) 197 | 198 | - [Физика. Экспресс-подготовка к ЕГЭ. С-часть](https://yadi.sk/d/JCp76aCp3aVxHd) 199 | 200 | - [Физика. Экспресс-подготовка к ЕГЭ. В-часть](https://yadi.sk/d/jlvSYm5t3aVhqQ) 201 | 202 | - [Математика. Подготовка к олимпиаде Физтех](https://yadi.sk/d/PFwGVYZn3aWX9x) 203 | 204 | - Курс подготовки к олимпиадам «Ломоносов», ОММО, ПВГ по математике: 205 | 206 | - [Теория](https://cloud.mail.ru/public/26aB/YRvzhyJe1) 207 | - [Видео](https://mega.nz/#F!ezxWSCaL!3XPe3dRspzkoL74uVz_tLg) 208 | 209 | - Физика. Подготовка к олимпиаде Физтех: 210 | 211 | - [Видео](https://mega.nz/#F!XWBAHQLZ!bFdDHnqx1uUq0h7gLxdfxw) 212 | - [Д/З](https://yadi.sk/d/h7kaU9Mj3a5fGa) 213 | 214 | - Физика. Подготовка к олимпиадам. 10 класс: 215 | 216 | - [Видео](https://mega.nz/#F!nvhniQxD!5p07SQGsfjOGsZ0T-A2u1w) 217 | - [Д/З](https://yadi.sk/d/BzMMJBXl3aNgiv) 218 | 219 | - [Физика. Подготовка к олимпиадам. 9 класс](https://yadi.sk/d/DNN651RR3aC5Qa) 220 | 221 | - [Физика. Подготовка к олимпиадам. 8 класс](https://yadi.sk/d/eum01uh1tzdlcw) 222 | 223 | - Информатика. Подготовка к ЕГЭ. 224 | 225 | - [Информатика. Подготовка к олимпиадам](https://mega.nz/#F!zS431KpL!9J9wwxYsf49aw5Jbd-Py4Q) 226 | 227 | - Мини-курс по математике "Векторный метод в пространстве" 228 | 229 | - Мини-курс по математике "Логарифм и экспонента" 230 | 231 | - [Мини-курс по математике "Теория вероятностей"](https://cloud.mail.ru/public/99iR/ydhDPcVQm) 232 | 233 | - [Мини-курс по математике "Сравнения по модулю"](https://cloud.mail.ru/public/4qPd/N65dNQCNP) 234 | 235 | - [Интенсивный курс по математике "Задачи с параметрами на ЕГЭ"](https://yadi.sk/d/Ban_jjxM3WbeQY) 236 | 237 | - [Интенсивный курс по математике "Задачи по теории чисел на ЕГЭ"](https://yadi.sk/d/LfusFOrL3WbkxS) 238 | 239 | - Мини-курс по физике "Магнетизм и электромагнитная индукция": 240 | 241 | - [Видео](https://yadi.sk/d/qTfLE88L3WYDwV) 242 | - [Д/З](https://mega.nz/#F!Di5HWB5C!6qxfCkgSms6ulB-EzThz7w) 243 | 244 | - Мини-курс по физике "Метод потенциалов": 245 | 246 | - [Видео](https://cloud.mail.ru/public/9ksB/xhg2QYGsx) 247 | - [Д/З](https://yadi.sk/d/2pezugnf3aXrUR) 248 | 249 | - [Мини-курс по физике "Олимпиадная механика"](https://cloud.mail.ru/public/2Eu5/zkaih3SBm) 250 | 251 | - [Мини-курс по физике "Эксперементальный практикум по гидростатике"](https://cloud.mail.ru/public/F8Pi/TuJj8LMxm) 252 | 253 | - [Мини-курс по физике "Эксперементальный практикум по тепловым и электрическим явлениям"](https://cloud.mail.ru/public/5vvV/qWLiAi5kx) 254 | 255 | - Мини-курс по физике "Разные подходы к решению задач по гидростатике": 256 | 257 | - [Видео](https://cloud.mail.ru/public/JZCp/p3eiaBapy) 258 | - [Д/З](https://yadi.sk/d/oWVeHXOp3aXrUh) 259 | 260 | - [Курс по программированию в среде "Swift Playgrounds"](https://yadi.sk/d/JMhZwtQO3Uf9aU) 261 | 262 | - Язык Python 263 | 264 | - [Язык С++](https://mega.nz/#F!f3ZlwYIJ!5Mc6LPZv4Z-eHkXcqZJ4Yw) 265 | 266 |
267 |
268 |
269 | [Гуманитарные] 270 | 271 | - [Подготовка к ОГЭ. Русский язык. 9 класс](https://cloud.mail.ru/public/Lrgj/hEMedDyVS) 272 | 273 | - [Подготовка к олимпиадам. Русский язык. 8-11 класс](https://mega.nz/#F!DWhgXaLC!gxoZCsPOOK-0dI6kwiK_ug) 274 | 275 | - [Курс подготовки к написанию сочинений и изложений на ОГЭ и ГВЭ 9 класс](https://cloud.mail.ru/public/GfZf/EKm5vTgbJ) 276 | 277 | - Английский язык. Подготовка к ЕГЭ. 278 | 279 | - [Русский язык. Экспресс-курс по подготовке к сочинению.](https://yadi.sk/d/P1Dv8v2V3WurE3) 280 | 281 | - Русский язык. Подготовка к декабрьскому сочинению. 282 | 283 | - [Русский язык. 1 часть.](https://cloud.mail.ru/public/151p/52H17pYrV) 284 | 285 | - Русский язык. 1 часть - Экспресс. 286 | 287 | - Литература. 11 класс: 288 | 289 | - [Видео 1](https://cloud.mail.ru/public/fW2v/tmUfA3VjJ) 290 | - [Видео 2](https://cloud.mail.ru/public/DVTH/vwQo4AP2P) 291 | 292 | - [История. Подготовка к ЕГЭ.](https://mega.nz/#F!zzxk1C7C!EI3o8bquUt8cmYUOjxHHZQ) 293 | 294 | - [История. Подготовка к ОГЭ.](https://cloud.mail.ru/public/Esxr/wePkYv1XB) 295 | 296 | - [Подготовка к олимпиадам по праву](https://yadi.sk/d/j5coXPRF3X2dwV) 297 | 298 | - [Обществознание. Подготовка к ЕГЭ.](https://drive.google.com/drive/u/4/folders/1LQL1AYK5R0ZmqCUagDFPq8UWz2knyh1E) 299 | 300 | - [Обществознание. Подготовка к ОГЭ.](https://cloud.mail.ru/public/EB4p/2sQ3aiwiw) 301 | 302 | - Обществознание. Подготовка к олимпиадам: 303 | 304 | - [Видео](https://yadi.sk/d/ZvZKfGJC3YtGQi) 305 | - [Материалы](https://yadi.sk/d/5O-mW7i63ajfhX) 306 | 307 |
308 |
309 |
310 | 🔬 [Естественно-научные] 311 | 312 | - [Химия. Подготовка к ЕГЭ](https://yadi.sk/d/YdnVWd7H3aWR4C) 313 | 314 | - [Химия. Экспресс-подготовка к ЕГЭ](https://yadi.sk/d/kiXwNckX3ZBKeC) 315 | 316 | - [Биология. Подготовка к ЕГЭ](https://yadi.sk/d/3rtloxF43aVTnp) 317 | 318 |
319 |
320 |
321 | 💡 [Другое] 322 | 323 | - [Финансовая грамотность и современные платежные технологии](https://yadi.sk/d/gcrg9MWI3Wveax) 324 | 325 | - [Шахматы - Начальный уровень](https://cloud.mail.ru/public/2z81/zTmxF1w9t) 326 | 327 | - [Серия курсов "Эмоциональный интеллект" - "Научиться учиться" и "Навыки будущего"](https://cloud.mail.ru/public/ESjC/XxmDqZGsj) 328 | 329 | - [Основы информатики и программирования](https://cloud.mail.ru/public/DQUN/ZV3K6kVQ7) 330 | 331 | - [Эффективное мышление на основе ТРИЗ. 7-8 класс](https://cloud.mail.ru/public/AS7U/SsBGoVsJx) 332 | 333 |
334 |
335 | 336 | --- 337 | 338 | ## **_2019 г._** 339 | 340 | ... 341 | 342 |
343 | ⚛️ [Технические] 344 | 345 | - [Подготовка к олимпиаде "Физтех" по математике](https://mega.nz/#F!LO5ThIyJ!O2bAf058daQ_z57fXxhpyQ) 346 | 347 | - [Подготовка к олимпиадам «Ломоносов», ОММО, ПВГ](https://yadi.sk/d/QOIk2Kqovc0yTA) 348 | 349 | - [Подготовка к региональному этапу Всероссийской олимпиады по математике](https://yadi.sk/d/AxRwHbEx9gJnqg) 350 | 351 | - [Подготовка к ДВИ в МГУ по математике](https://mega.nz/#F!upoWxSpD!k1oTK1U0p6t4wLZFFS6naQ) 352 | 353 | - [Математика. Подготовка к ЕГЭ. Часть С](https://mega.nz/#F!IZ9CDarJ!ol_8oe2BQRSWz6L3_ps8IA) 354 | 355 | - [Математика. Подготовка к ЕГЭ. Часть Б](https://rutracker.org/forum/viewtopic.php?p=77546191) 356 | 357 | - [Мини-курс по математике "Векторный метод в пространстве"](https://yadi.sk/d/VpO8uoXWqzfwVg) 358 | 359 | - [Мини-курс по математике "Логарифм и экспонента"](https://yadi.sk/d/ZjwU9gwjjsRP3g) 360 | 361 | - [Мини-курс по математике "Сравнения по модулю"](https://mega.nz/#F!wWhAzS7J!yFx74dbv66CZGLWkTiFgjQ) 362 | 363 | - [Мини-курс по математике "Свойства пределов последовательности"](https://mega.nz/#F!tSonXY6J!GE75bbyXw7cF0P4y1yJTdg) 364 | 365 | - [Интенсивный курс по математике "Задачи с параметрами на ЕГЭ"](https://yadi.sk/d/KBOGGfuw4EGfkA) 366 | 367 | - [Интенсивный курс по математике "Задачи по теории чисел на ЕГЭ"](https://yadi.sk/d/p5EdqMRD8xLZBA) 368 | 369 | - [Подготовка к олимпиадам "Физтех", "Росатом", "Ломоносов" по физике](https://mega.nz/#F!phNGGCSA!bOnNCKQqNFRuuCC6C2ceRQ) 370 | 371 | - [Мини-курс по физике "Магнетизм и электромагнитная индукция"](https://mega.nz/#F!ckkhTIRY!D0WREaqE2jCCk8op0BfDfw) 372 | 373 | - [Физика. Классическая астрономия](https://mega.nz/#F!dWZ3ESAJ!7AvKeNfd-dDPH9b0LwEihw) 374 | 375 | - [Физика. Геометрическая оптика](https://mega.nz/#F!W7QUDayS!YgJiM2rU45TVpokrZTLeKQ) 376 | 377 | - [Физика. Методы расчёта разветвлённых цепей](https://mega.nz/#F!D3QGQCKS!RCcL2gdroOETqR8CYqW-rA) 378 | 379 | - [Физика. Тепловые явления](https://mega.nz/#F!SmQmmCjD!knIpGZMIDEXgS5rVmDUxsg) 380 | 381 | - [Мини-курс "Экспериментальная физика"](https://mega.nz/#F!TrBk0I6R!K2DtfwTMUeVj-wddZZx-VA) 382 | 383 | - [Физика. Кинематические связи в задачах](https://mega.nz/#F!anB2xIaZ!BwslrBueo0utACSnGXL8ZA) 384 | 385 | - [Физика. Подготовка к ЕГЭ. Часть С](https://yadi.sk/d/xjco_AKXdTx99A) 386 | 387 | - [Физика. Подготовка к ЕГЭ. Часть Б](https://yadi.sk/d/1oARnGevJVwAew) 388 | 389 | - [Информатика. Экспресс-подготовка к ЕГЭ](https://t.me/joinchat/AAAAAEZLIl-XWzOC5SIpEw) 390 | 391 | - [Информатика. Подготовка к ЕГЭ](https://mega.nz/#F!M0sWlSyK!x2o0BcBymqJ9yEF54WnIZw) 392 | 393 | - [Программирование. Подготовка к окружному этапу олимпиады](https://yadi.sk/d/INsWH9BHTFapXQ) 394 | 395 | - [Программирование. Подготовка к региональному этапу олимпиады](https://yadi.sk/d/MK4m1lvIM3jmIw) 396 | 397 | - [Программирование. Курс подготовки к олимпиадам, продвинутый уровень](https://yadi.sk/d/_1HXesluVTZp9Q) 398 | 399 | - [Языки С/С++](https://mega.nz/#F!qjIwhQ6T!vwjiDExbiiDLjTO6vsCULA) 400 | 401 | - [Язык Python для начинающих](https://t.me/joinchat/AAAAAFFbpvBSQ0NRqvYK3g) 402 | 403 | - [Язык Python для продолжающих](https://mega.nz/#F!uiQGUY5Y!uJaqRZ2C6IRzOYIzXyUfjQ) 404 | 405 | - [Информатика за пределами ЕГЭ](https://mega.nz/#F!PvIQHYDD!UrgW5M6--ttg-vnnhrNbDQ) 406 | 407 |
408 |
409 |
410 | [Гуманитарные] 411 | 412 | - [Два выпускных сочинения - декабрьское и ЕГЭ](https://mega.nz/#F!a3xXCQKB!ScDw8eROMoq5tGW9-tyZzA) 413 | 414 | - [Русский язык. Тест](https://mega.nz/#F!S3gTXKQL!18qfHt1NtecA3mLxfjK0Sg) 415 | 416 | - [Подготовка к олимпиадам и международным экзаменам по английскому языку](https://mega.nz/#F!S3oCSIhC!AXXh5rl-Tt5vvZTlQ0rYhA) 417 | 418 | - [История. Базовый уровень](https://mega.nz/#F!T7xxUKbD!Nvd5DWo2z2bXReMqmFnXDg) 419 | 420 | - [История. Подготовка к ЕГЭ](https://mega.nz/#F!WmpTEYhS!0JNn1OGL0fq48qtWCiE5RA) 421 | 422 | - [Обществознание. Базовый уровень](https://mega.nz/#F!wHBE3YQY!FocKT3Lt8sLUIBgACGgw3w) 423 | 424 | - [Обществознание. Подготовка к ЕГЭ](https://mega.nz/#F!wDw3HaAZ!XtxUVJDg_eVFFBfxFqSyHQ) 425 | 426 | - [Литература. Базовый уровень](https://mega.nz/#F!cKwGwSLB!1zdADwaRsRaqEWdkZNfJug) 427 | 428 | - [Литература без границ. Читательский дайвинг клуб](https://mega.nz/#F!FOAXhKjR!Jn9ki9KNEDYErfN4O7Wmng) 429 | 430 |
431 |
432 |
433 | 🔬 [Естественно-научные] 434 | 435 | - [Химия. Базовый уровень](https://mega.nz/#F!DvZAwSaJ!-wkNQlzKPGuSeF5Yop9ogw) 436 | 437 | - [Химия. Подготовка к ЕГЭ](https://rutracker.org/forum/viewtopic.php?t=5746100) 438 | 439 | - [Практикум. Органическая химия](https://mega.nz/#F!23oEWSSD!kGIBxWfen7Rjt7YHrrz5OQ) 440 | 441 | - [Практикум. Неорганическая химия](https://mega.nz/#F!PzgRUCbC!lQ_bIvGZGlDTaHXZhIYNEg) 442 | 443 | - [Практикум. Общая химия](https://mega.nz/#F!bvBTQKpY!cq3yAgjspLMS-0GakdjUhw) 444 | 445 | - [Практикум. Олимпиадная химия](https://mega.nz/#F!znYVGSzD!bIKrPKNjDpYIOOaPq1B0CA) 446 | 447 | - [Биология. Базовый уровень](https://mega.nz/#F!n7AWTQrA!YYPfc_l10RaJ_Ir5liuD0w) 448 | 449 | - [Биология. Подготовка к ЕГЭ](https://mega.nz/#F!z3whlCDI!7WllZIhXidu9yEeFnkXa_g) 450 | 451 | - [Ботаника](https://mega.nz/#F!iuhxXYib!n6otON815nAJ0FAAXP6GuA) 452 | 453 |
454 |
455 |
456 | 💡 [Другое] 457 | 458 | - [Шахматы. Продвинутый уровень](https://mega.nz/#F!H6I0hY7Z!5an9b-wzH_-llTWqSZY7PA) 459 | 460 | - [Эмоциональный интеллект - Навыки XXI века](https://drive.google.com/drive/folders/1z3RItBOKTBVg8Dglv2ek5f53PGGFX-2P?usp=sharing) 461 | 462 |
463 |
464 | 465 | --- 466 | 467 | **Дополнительно** 468 | 469 | _(2017) Недостающая теория для:_ 470 | 471 | - Подготовка к олимпиадам по математике (11 класс) 472 | - Подготовка к олимпиаде "Физтех" по математике 473 | - Подготовка к олимпиадам по программированию. Продвинутый уровень (9-11 класс) 474 | - Подготовка к олимпиадам по программированию. Базовый уровень (7-9 класс) 475 | - Язык С++ 476 | - Физика (Часть В и С) 477 | 478 | https://mega.nz/#F!T9pR3STQ!pjIy_jvZOMvg9ucrMENKSw 479 | 480 | ... 481 | 482 | --- 483 | 484 | **Каналы** 485 | 486 | - [_47-я Пирожковая (бывает не только Фоксфорд)_](https://t.me/joinchat/AAAAAFAGr87npHU8ras3zQ) 487 | 488 | - [_Сливы со сливы (умскул)_](https://t.me/shokavosliv) 489 | 490 | --- 491 | 492 | **Теория для мобильных устройств** 493 | 494 | - [_Android_](https://play.google.com/store/apps/details?id=ru.foxford.foxfordtextbook) 495 | 496 | - [_iOS_](https://itunes.apple.com/us/app/foksford.ucebnik/id930911649?l=ru&ls=1&mt=8) 497 | 498 | --- 499 | 500 | **Основные ударения для ЕГЭ по русскому можно отработать здесь:** 501 | 502 | https://russianpy.marisehayashi.repl.run/ 503 | 504 | --- 505 | 506 | **Учебные материалы** 507 | 508 | - _ВМК, Поляков, Ткаучук и прочее:_ 509 | 510 | https://mega.nz/#F!Ei4GmBCL!eKqBEOF9fmCwnOnuSELowQ 511 | 512 | - _Платина разного рода:_ 513 | 514 | https://github.com/tanookki/FizMatInf 515 | 516 | --- 517 | 518 | [**Извлечение данных**](https://github.com/limitedeternity/foxford_courses/tree/master/foxford_downloader/) 519 | 520 | --- 521 | 522 | **ОГРОМНАЯ ПРОСЬБА: НЕ УХОДИТЕ С РАЗДАЧИ - ВНЕСИТЕ СВОЙ ПОСИЛЬНЫЙ ВКЛАД В ОБЩЕЕ ДЕЛО.** 523 | 524 | _Есть что-то, что вы хотите раздать? Хотите починить нерабочие ссылки и можете это сделать? Или дополнить? Редактируйте и делайте pull request._ 525 | 526 | _Курсы предоставлены в ознакомительных целях. Обновленная версия курсов будет читаться на сайте foxford.ru с сентября, преимуществами покупки актуальной версии являются доступ к домашним заданиям и возможность задать вопросы лектору в онлайн-режиме._ 527 | 528 | **Благодарности:** 529 | 530 | - `limitedeternity (Zamazka03)` : Скрипт, координация команды, поддержка. Маг программного кода. 531 | 532 | - `Stanley Kowalski` : Оказание поддержки, тестирование, идеи для скрипта. Страж программного кода. 533 | 534 | - `Paravozik_Lesha` : Титан трудоспособности, перезаливка/заливка на рутрекер; рыцарь Света, охраняющий раздачи и участвовавший абсолютно во всех сражениях (сливах). Предпочитает меч снайперской винтовке. Щедро раздал необходимые курсы, что имелись на его аккаунте. Свидетель Первой Версии. 535 | 536 | - `TmLev (soloLev)` : Основа последнего слива, именуемого Крупнейшим. Любезно предоставил аккаунт с практически всеми необходимыми курсами. 537 | Помог извлечь историю и обществознание. Паладин Света. 538 | 539 | - `Yakui_The_Maid` : Анонимный союзник сил Света. Предоставил стандартные курсы биологии, химии и физики части С. 540 | 541 | - `Г-жа Н` : Ещё один союзник. Основатель канала, загрузка почти всех курсов. 542 | 543 | - `Pavel Павлик` : 90% курсов 2019 года 544 | 545 | - И многие другие. 546 | -------------------------------------------------------------------------------- /foxford_downloader/.gitignore: -------------------------------------------------------------------------------- 1 | **/ 2 | !lib/ 3 | -------------------------------------------------------------------------------- /foxford_downloader/README.md: -------------------------------------------------------------------------------- 1 | # Как сохранить материалы? 2 | 3 | ## Метод 1 4 | 5 | _Сложность: средняя (для пользователей десктопов)_ 6 | 7 | 1. Устанавливаем Python 3.7+ и Git. На Linux есть `apt-get` (или что там), на OSX есть [`homebrew`](https://brew.sh/), под Windows есть [`chocolatey`](https://chocolatey.org/install). 8 | 9 | 2. [Клонируем репозиторий](https://github.com/limitedeternity/foxford_courses/archive/master.zip), распаковываем, переходим в папку, где лежит **этот** гайд. 10 | 11 | 3. Открываем **здесь** терминал и выполняем `pip install -Ur requirements.txt`. (На Linux/OSX - `pip3 install -Ur requirements.txt`) 12 | 13 | 4. Выполняем `python fdl.py` (На Linux/OSX - `python3 fdl.py`). 14 | 15 | 5. Логинимся, выбираем курс и скачиваемые материалы. 16 | 17 | 6. Всё. 18 | 19 | ### Примечания: 20 | 21 | - Это метод для скачивания всего необходимого. 22 | 23 | - Есть возобновление процесса. Нужно удалить поврежденные материалы и запустить `fdl.py` снова. 24 | 25 | - Если во время скачивания выдало ошибку, нужно остановить выполнение программы, нажав в терминале `Ctrl + C`, и перезапустить `fdl.py`. 26 | 27 | - Для проверки целостности **обязательно** запускай `fdl.py` повторно до тех пор, пока программа не завершится "со всеми галочками" и без ошибок. 28 | 29 | ## Метод 2 30 | 31 | _Сложность: средняя (для любителей эмуляторов)_ 32 | 33 | 1. [Устаналиваем MEmu](https://www.memuplay.com/). 34 | 35 | 2. [Включаем Root-права](https://youtu.be/UYl5zPSnugA). 36 | 37 | 3. Устанавливаем приложение Фоксфорд.Курсы. 38 | 39 | 4. Логинимся, выбираем курс, жмем рядом с уроком кнопочку, как бы намекающую на "Скачать". 40 | 41 | 5. [Устанавливаем Root Browser](https://play.google.com/store/apps/details?id=com.jrummy.root.browserfree). 42 | 43 | 6. Переходим в `/data/data/ru.foxford.webinars`, ищем .mp4 файлы, копируем их в `/storage/emulated/0/Download/`. 44 | 45 | 7. [Забираем на ПК](https://www.memuplay.com/blog/2016/06/04/how-to-share-file-between-android-and-windows/). 46 | 47 | ### Примечания: 48 | 49 | - Это метод для скачивания видео 50 | 51 | # Чейнджлог (Крупные апдейты) 52 | 53 | ## 18.06.2017 (v1) 54 | 55 | Реализована первая версия методом "проб и ошибок". 56 | 57 | С помощью расширения (https://chrome.google.com/webstore/detail/network-sniffer/coblekblkacfilmgdghecpekhadldjfj) необходимо перехватить момент перехода к видео. Нажав на поле со ссылкой, соответствующей видео, откроется поле, где в Request Headers будет параметр Cookie. 58 | 59 | В скрипт вводится 2 значения: адрес видео и куки. На выходе получается файл b64.html, в котором находится ссылка, при переходе по которой происходило перенаправление на плеер, выдающий mp4, который качался с помощью расширения (https://chrome.google.com/webstore/detail/video-downloader-pro/ilppkoakomgpcblpemgbloapenijdcho). 60 | 61 | (Отдельное спасибо _Paravozik_Lesha_ за тестирование пре-релизной версии и терпение моего характера c: ) 62 | 63 | --- 64 | 65 | ## 21.06.2017 (v2) 66 | 67 | Реализована полу-автоматическая система. 68 | 69 | Вбивается ссылка на видео и человека перенаправляет сразу в плеер, где с помощью нажатия "Сохранить как..." можно сохранить вебинар на диск (логично). 70 | 71 | Примерно тогда же была реализована система сохранения ДЗ. 72 | 73 | --- 74 | 75 | ## 30.07.2017 (v3) 76 | 77 | Реализована полная автоматика и какой-никакой интерфейс. 78 | 79 | Все скрипты были объединены в один. 80 | 81 | Перевод на русский язык (да-да, наканецта). 82 | 83 | Загрузка курсов была переписана _Stanley Kowalski_. Пришлось много чинить, конечно, но это не отменяет моего "спасиба" за факт реализации "другим способом" и то, что именно он подтолкнул меня на объединение обоих скриптов. 84 | 85 | --- 86 | 87 | ## 05.08.2017 (v4) 88 | 89 | Исправлено множество ошибок. 90 | 91 | Разделение на модули для легкости починки. 92 | 93 | Добавлено сохранение теории и ДЗ. 94 | 95 | Объединение всех действий в единый оператор. За один проход теперь можно скачать весь материал полностью. 96 | 97 | Сделан режим "только видео" и сортировка материала. (Отдельное спасибо _@kuzminovdmit_) 98 | 99 | Добавлено возобновление загрузки на случай, если что-то пойдет не так, чтобы не ждать по-новой. Необходимо просто выбрать после перезапуска пункт меню, который выбирался до этого. 100 | 101 | Создание файла "video.skips", содержащего число, приведет к пропуску видео на соответствующее число. (0 - Вводное занятие, 1 - Первое + Вводное ...) 102 | 103 | --- 104 | 105 | ## 25.12.2017 (v5) 106 | 107 | Перенесен на Node.js. Прекращена поддержка сохранения ДЗ. 108 | 109 | --- 110 | 111 | ## 29.08.2018 (v5.5) 112 | 113 | Скомпилирован. Проведена работа над дизайном. 114 | 115 | --- 116 | 117 | ## 29.01.2019 (v5.6) 118 | 119 | Возвращение легенды - автоматическое сохранение ДЗ. 120 | 121 | --- 122 | 123 | ## 16.07.2019 (v6) 124 | 125 | FDL, написанный на Node.js, был переименован в HWDL и сделан утилитой для сохранения ДЗ. 126 | 127 | Сам же скрипт и его алгоритмическая составляющая были переписаны на Python 3.7. 128 | 129 | Добавлено сохранение сообщений из чата и презентаций. 130 | 131 | --- 132 | 133 | ## 07.08.2019 (v6.5) 134 | 135 | HWDL, написанный на Node.js, был удалён, а его функционал - встроен в FDL. 136 | 137 | --- 138 | 139 | _Текущая версия_: **v6.5** 140 | 141 | --- 142 | 143 | - _Идея, поддержка и написание: `limitedeternity`_ 144 | 145 | - _Тестирование и идеи: `Stanley Kowalski` и `Paravozik_Lesha` (v1-v3)_ 146 | 147 | - _VideoDownloader(): `Stanley Kowalski` (v3)_ 148 | 149 | (Специально для [2ch.hk/un/](https://2ch.hk/un/)) 150 | -------------------------------------------------------------------------------- /foxford_downloader/fdl.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from argparse import ArgumentParser, Namespace 3 | from itertools import chain 4 | from multiprocessing import Pool, cpu_count 5 | from pathlib import Path 6 | from typing import Dict, Iterable, List, Tuple 7 | 8 | from PyInquirer import prompt 9 | 10 | from lib.browser import terminate_browser_instance 11 | from lib.fns import * 12 | from lib.helpers import Logger, pipe 13 | from lib.requests_cache import CachedSession 14 | 15 | 16 | def main(params: Dict) -> None: 17 | session: CachedSession = CachedSession() 18 | credential_query: Dict[str, str] = params if params["email"] and params["password"] else prompt([ 19 | { 20 | "type": "input", 21 | "name": "email", 22 | "message": "Email" 23 | }, 24 | { 25 | "type": "password", 26 | "name": "password", 27 | "message": "Password" 28 | } 29 | ]) 30 | 31 | Logger.log("Fetching course list...") 32 | 33 | user_courses: Tuple[Dict] = get_user_courses( 34 | login( 35 | credential_query["email"], 36 | credential_query["password"], 37 | session 38 | ) 39 | ) 40 | 41 | course_query: Dict[str, str] = prompt([ 42 | { 43 | "type": "list", 44 | "name": "course", 45 | "message": "Select course", 46 | "choices": map(lambda obj: f"({obj['grades_range']}) {obj['name']} - {obj['subtitle']}", user_courses) 47 | } 48 | ]) 49 | 50 | selected_course: Dict = next( 51 | filter( 52 | lambda obj: f"({obj['grades_range']}) {obj['name']} - {obj['subtitle']}" == course_query["course"], 53 | user_courses 54 | ) 55 | ) 56 | 57 | Logger.log("Fetching lesson list...") 58 | 59 | ( 60 | course_lessons_with_video, 61 | course_lessons_with_homework, 62 | course_lessons_with_conspect 63 | ) = pipe( 64 | lambda course_id: get_course_lessons(course_id, session), 65 | lambda all_lessons: filter( 66 | lambda lesson: lesson["access_state"] == "available" and not lesson["is_locked"], 67 | all_lessons 68 | ), 69 | tuple, 70 | lambda available_lessons: map( 71 | lambda that_include: filter( 72 | lambda lesson: 73 | "available" in lesson[f"{that_include}_status"] and 74 | "not" not in lesson[f"{that_include}_status"], 75 | available_lessons 76 | ), [ 77 | "webinar", 78 | "homework", 79 | "conspect" 80 | ] 81 | ), 82 | lambda map_of_filters: map(tuple, map_of_filters) 83 | )(selected_course["resource_id"]) 84 | 85 | options_check: Dict[str, List[str]] = prompt([ 86 | { 87 | "type": "checkbox", 88 | "message": "What to fetch", 89 | "name": "actions", 90 | "choices": [ 91 | { 92 | "name": "Resources", 93 | "checked": True 94 | }, 95 | { 96 | "name": "Homework" 97 | }, 98 | { 99 | "name": "Conspects" 100 | } 101 | ] 102 | } 103 | ]) 104 | 105 | if "Resources" in options_check["actions"]: 106 | Logger.warn("Resources collection started") 107 | Logger.log("Fetching resources links...") 108 | 109 | resources_for_lessons: Tuple[Dict] = get_resources_for_lessons( 110 | selected_course["resource_id"], 111 | map( 112 | lambda obj: obj["webinar_id"], 113 | course_lessons_with_video 114 | ), 115 | session 116 | ) 117 | 118 | paths: Iterable[Path] = build_dir_hierarchy( 119 | selected_course["name"], 120 | selected_course["subtitle"], 121 | selected_course["grades_range"], 122 | course_lessons_with_video 123 | ) 124 | 125 | Logger.log("Downloading resources...") 126 | 127 | pool = Pool(cpu_count()) 128 | pool.starmap( 129 | download_resources, 130 | map( 131 | lambda res_obj, path: [ 132 | { 133 | **res_obj, 134 | "destination": path 135 | }, 136 | session 137 | ], 138 | resources_for_lessons, 139 | paths 140 | ) 141 | ) 142 | 143 | pool.close() 144 | pool.join() 145 | Logger.warn("Resources collection finished") 146 | 147 | coro_list = [] 148 | semaphore = asyncio.Semaphore(2 if cpu_count() > 1 else 1) 149 | 150 | if "Homework" in options_check["actions"]: 151 | Logger.warn("Homework collection started") 152 | Logger.log("Collecting tasks...") 153 | 154 | lesson_tasks: Iterable[List[Dict]] = get_lesson_tasks( 155 | map( 156 | lambda obj: obj["id"], 157 | course_lessons_with_homework 158 | ), 159 | session 160 | ) 161 | 162 | task_urls: Iterable[Iterable[str]] = construct_task_urls( 163 | map( 164 | lambda obj: obj["id"], 165 | course_lessons_with_homework 166 | ), 167 | lesson_tasks 168 | ) 169 | 170 | paths: Iterable[Path] = build_dir_hierarchy( 171 | selected_course["name"], 172 | selected_course["subtitle"], 173 | selected_course["grades_range"], 174 | course_lessons_with_homework 175 | ) 176 | 177 | Logger.warn( 178 | "Fetched tasks details. Homework collection will start soon..." 179 | ) 180 | 181 | coro_list.extend( 182 | chain.from_iterable( 183 | map( 184 | lambda url_tuple, path: map( 185 | lambda url: save_page( 186 | url, 187 | path, 188 | "homework", 189 | map( 190 | lambda item: { 191 | "name": item[0], 192 | "value": item[1], 193 | "domain": ".foxford.ru", 194 | "path": "/" 195 | }, 196 | session.cookies.get_dict().items() 197 | ), 198 | semaphore 199 | ), 200 | url_tuple 201 | ), 202 | task_urls, 203 | paths 204 | ) 205 | ) 206 | ) 207 | 208 | if "Conspects" in options_check["actions"]: 209 | Logger.warn("Conspects collection started") 210 | 211 | conspect_urls: Iterable[Tuple[str]] = construct_conspect_urls( 212 | map( 213 | lambda obj: obj["id"], 214 | course_lessons_with_conspect 215 | ), 216 | map( 217 | lambda obj: obj["conspect_blocks_count"], 218 | course_lessons_with_conspect 219 | ) 220 | ) 221 | 222 | paths: Iterable[Path] = build_dir_hierarchy( 223 | selected_course["name"], 224 | selected_course["subtitle"], 225 | selected_course["grades_range"], 226 | course_lessons_with_conspect 227 | ) 228 | 229 | Logger.warn( 230 | "Fetched conspects details. Conspects collection will start soon..." 231 | ) 232 | 233 | coro_list.extend( 234 | chain.from_iterable( 235 | map( 236 | lambda url_tuple, path: map( 237 | lambda url: save_page( 238 | url, 239 | path, 240 | "conspects", 241 | map( 242 | lambda item: { 243 | "name": item[0], 244 | "value": item[1], 245 | "domain": ".foxford.ru", 246 | "path": "/" 247 | }, 248 | session.cookies.get_dict().items() 249 | ), 250 | semaphore 251 | ), 252 | url_tuple 253 | ), 254 | conspect_urls, 255 | paths 256 | ) 257 | ) 258 | ) 259 | 260 | if coro_list: 261 | Logger.warn("Actual collection started") 262 | 263 | asyncio.get_event_loop().run_until_complete( 264 | asyncio.wait( 265 | coro_list 266 | ) 267 | ) 268 | 269 | Logger.warn("Collection finished. Quitting...") 270 | asyncio.get_event_loop().run_until_complete(asyncio.sleep(0.5)) 271 | asyncio.get_event_loop().run_until_complete(terminate_browser_instance()) 272 | 273 | 274 | if __name__ == "__main__": 275 | parser: ArgumentParser = ArgumentParser() 276 | parser.add_argument("--email", type=str, required=False) 277 | parser.add_argument("--password", type=str, required=False) 278 | 279 | args: Namespace = parser.parse_args() 280 | main(args.__dict__) 281 | -------------------------------------------------------------------------------- /foxford_downloader/lib/__init__.py: -------------------------------------------------------------------------------- 1 | # module init 2 | -------------------------------------------------------------------------------- /foxford_downloader/lib/browser.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from async_lru import alru_cache 4 | from pyppeteer import connect, launch 5 | 6 | 7 | @alru_cache(maxsize=1, typed=False) 8 | async def get_browser_connection_url() -> str: 9 | browser = await launch( 10 | ignoreHTTPSErrors=True, 11 | headless=True, 12 | slowMo=0, 13 | args=[ 14 | "--no-sandbox", 15 | "--disable-setuid-sandbox", 16 | "--disable-gpu", 17 | "--disable-dev-shm-usage", 18 | '--proxy-server="direct://"', 19 | "--proxy-bypass-list=*" 20 | ] 21 | ) 22 | 23 | connectionUrl = browser.wsEndpoint 24 | await browser.disconnect() 25 | return connectionUrl 26 | 27 | 28 | async def terminate_browser_instance() -> None: 29 | browser_endpoint = await get_browser_connection_url() 30 | browser = await connect(browserWSEndpoint=browser_endpoint) 31 | get_browser_connection_url.cache_clear() 32 | await browser.close() 33 | -------------------------------------------------------------------------------- /foxford_downloader/lib/fns.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from collections import deque 3 | from datetime import datetime 4 | from pathlib import Path 5 | from re import Match, match 6 | from typing import Dict, Iterable, List, Tuple, Union 7 | from urllib import parse 8 | 9 | import requests 10 | from bs4 import BeautifulSoup, Tag 11 | from more_itertools import unique_everseen 12 | from pyppeteer import connect 13 | 14 | from .browser import get_browser_connection_url 15 | from .helpers import error_handler, pipe 16 | from .requests_cache import CachedResponse, CachedSession 17 | 18 | 19 | @error_handler 20 | def get_csrf_token(session: CachedSession) -> str: 21 | csrf_token_get_response: CachedResponse = session.get( 22 | "https://foxford.ru/api/csrf_token", 23 | headers={ 24 | "X-Requested-With": "XMLHttpRequest" 25 | } 26 | ) 27 | 28 | if csrf_token_get_response.status_code != 200: 29 | return {"fatal_error": "CSRF token fetch has failed"} 30 | 31 | if "token" not in csrf_token_get_response.json(): 32 | return {"fatal_error": "CSRF token structure is unknown"} 33 | 34 | return csrf_token_get_response.json()["token"] 35 | 36 | 37 | @error_handler 38 | def login(email: str, password: str, session: CachedSession) -> CachedSession: 39 | if not email or not password: 40 | return {"fatal_error": "No credentials provided"} 41 | 42 | credential_post_response: CachedResponse = session.post( 43 | "https://foxford.ru/user/login", 44 | headers={ 45 | "X-CSRF-Token": get_csrf_token(session), 46 | "X-Requested-With": "XMLHttpRequest" 47 | }, 48 | json={ 49 | "user": { 50 | "email": email, 51 | "password": password 52 | } 53 | } 54 | ) 55 | 56 | if credential_post_response.status_code != 200: 57 | return {"fatal_error": "Wrong credentials"} 58 | 59 | return session 60 | 61 | 62 | def get_user_courses(session: CachedSession) -> Tuple[Dict]: 63 | @error_handler 64 | def recursive_collection(page_num: int) -> Tuple[Dict]: 65 | course_list_response: CachedResponse = session.get( 66 | f"https://foxford.ru/api/user/bookmarks?page={page_num}&archived=false", 67 | headers={ 68 | "X-CSRF-Token": get_csrf_token(session), 69 | "X-Requested-With": "XMLHttpRequest" 70 | } 71 | ) 72 | 73 | if course_list_response.status_code != 200: 74 | return {"fatal_error": "Course list fetch has failed"} 75 | 76 | if "bookmarks" not in course_list_response.json(): 77 | return {"fatal_error": "Course list structure is unknown"} 78 | 79 | if all(False for _ in course_list_response.json()["bookmarks"]): 80 | return () 81 | 82 | if not {"name", "subtitle", "resource_id"}.issubset(set(course_list_response.json()["bookmarks"][0])): 83 | return {"fatal_error": "Course structure is unknown"} 84 | 85 | return ( 86 | *course_list_response.json()["bookmarks"], 87 | *recursive_collection(page_num + 1) 88 | ) 89 | 90 | return recursive_collection(1) 91 | 92 | 93 | class get_course_lessons(): 94 | @error_handler 95 | def __new__(self, course_id: int, session: CachedSession) -> Iterable[Dict]: 96 | lesson_list_at_somewhere_response: CachedResponse = session.get( 97 | f"https://foxford.ru/api/courses/{course_id}/lessons", 98 | headers={ 99 | "X-Requested-With": "XMLHttpRequest" 100 | } 101 | ) 102 | 103 | if lesson_list_at_somewhere_response.status_code != 200: 104 | return {"fatal_error": "Lesson list fetch has failed"} 105 | 106 | if not {"lessons", "cursors"}.issubset(set(lesson_list_at_somewhere_response.json())): 107 | return {"fatal_error": "Lesson list structure is unknown"} 108 | 109 | if "id" not in lesson_list_at_somewhere_response.json()["lessons"][0]: 110 | return {"fatal_error": "Lesson structure is unknown"} 111 | 112 | self.course_id = course_id 113 | self.session = session 114 | 115 | return pipe( 116 | lambda json: ( 117 | *self.recursive_collection( 118 | self, 119 | "before", 120 | json["cursors"]["before"] 121 | ), 122 | *json["lessons"], 123 | *self.recursive_collection( 124 | self, 125 | "after", 126 | json["cursors"]["after"] 127 | ) 128 | ), 129 | lambda lessons: map( 130 | lambda lesson: self.lesson_extension(self, lesson), 131 | lessons 132 | ) 133 | )(lesson_list_at_somewhere_response.json()) 134 | 135 | @error_handler 136 | def recursive_collection(self, direction: str, cursor: Union[int, None]) -> Tuple[Dict]: 137 | if not cursor: 138 | return () 139 | 140 | lesson_list_at_direction_response: CachedResponse = self.session.get( 141 | f"https://foxford.ru/api/courses/{self.course_id}/lessons?{direction}={cursor}", 142 | headers={ 143 | "X-Requested-With": "XMLHttpRequest" 144 | } 145 | ) 146 | 147 | if lesson_list_at_direction_response.status_code != 200: 148 | return {"fatal_error": "Lesson list fetch has failed"} 149 | 150 | if not {"lessons", "cursors"}.issubset(set(lesson_list_at_direction_response.json())): 151 | return {"fatal_error": "Lesson list structure is unknown"} 152 | 153 | if "id" not in lesson_list_at_direction_response.json()["lessons"][0]: 154 | return {"fatal_error": "Lesson structure is unknown"} 155 | 156 | if direction == "before": 157 | return ( 158 | *self.recursive_collection( 159 | self, 160 | direction, 161 | lesson_list_at_direction_response 162 | .json()["cursors"][direction] 163 | ), 164 | *lesson_list_at_direction_response.json()["lessons"] 165 | ) 166 | else: 167 | return ( 168 | *lesson_list_at_direction_response.json()["lessons"], 169 | *self.recursive_collection( 170 | self, 171 | direction, 172 | lesson_list_at_direction_response 173 | .json()["cursors"][direction] 174 | ) 175 | ) 176 | 177 | @error_handler 178 | def lesson_extension(self, lesson: Dict) -> Dict: 179 | lesson_extension_response: CachedResponse = self.session.get( 180 | f"https://foxford.ru/api/courses/{self.course_id}/lessons/{lesson['id']}", 181 | headers={ 182 | "X-Requested-With": "XMLHttpRequest" 183 | } 184 | ) 185 | 186 | if lesson_extension_response.status_code != 200: 187 | return {"fatal_error": "Lesson extension fetch has failed"} 188 | 189 | if not {"webinar_id", "access_state", "webinar_status", "is_locked"}.issubset(set(lesson_extension_response.json())): 190 | return {"fatal_error": "Lesson extension structure is unknown"} 191 | 192 | return lesson_extension_response.json() 193 | 194 | 195 | class get_resources_for_lessons(): 196 | def __new__(self, course_id: int, webinar_ids: Iterable[int], session: CachedSession) -> Tuple[Dict]: 197 | self.course_id = course_id 198 | self.webinar_ids = webinar_ids 199 | self.session = session 200 | return self.recursive_collection(self) 201 | 202 | @error_handler 203 | def recursive_collection(self) -> Tuple[Dict]: 204 | webinar_id: Union[int, None] = next(self.webinar_ids, None) 205 | 206 | if not webinar_id: 207 | return () 208 | 209 | video_source_response: CachedResponse = self.session.get( 210 | f"https://foxford.ru/groups/{webinar_id}" 211 | ) 212 | 213 | if video_source_response.status_code != 200: 214 | return {"fatal_error": "Video source fetch has failed"} 215 | 216 | return ( 217 | pipe( 218 | lambda res: self.retrieve_erly_iframe_src(self, res), 219 | lambda src: self.construct_resource_links(self, src) 220 | )(video_source_response), 221 | *self.recursive_collection(self) 222 | ) 223 | 224 | @error_handler 225 | def retrieve_erly_iframe_src(self, video_source_response: CachedResponse) -> str: 226 | erly_iframe: Union[Tag, None] = pipe( 227 | lambda r_content: BeautifulSoup( 228 | r_content, 229 | "html.parser" 230 | ), 231 | lambda soup: soup.select_one( 232 | "div.full_screen > iframe" 233 | ) 234 | )(video_source_response.content) 235 | 236 | if not erly_iframe: 237 | return {"fatal_error": ".full_screen > iframe wasn't found"} 238 | 239 | erly_iframe_src: Union[str, None] = erly_iframe.get("src") 240 | 241 | if not erly_iframe_src: 242 | return {"fatal_error": ".full_screen > iframe doesn't have src attribute"} 243 | 244 | return erly_iframe_src 245 | 246 | @error_handler 247 | def construct_resource_links(self, erly_iframe_src: str) -> Dict: 248 | search_params: Dict = dict( 249 | parse.parse_qsl( 250 | parse.urlparse(erly_iframe_src).query 251 | ) 252 | ) 253 | 254 | if not {"conf", "access_token"}.issubset(set(search_params)): 255 | return {"fatal_error": "Iframe src search params structure is unknown"} 256 | 257 | webinar_id_match: Union[Match, None] = match( 258 | r"^webinar-(\d+)$", search_params.get("conf") 259 | ) 260 | 261 | if not webinar_id_match: 262 | return {"fatal_error": "Unable to extract webinar id"} 263 | 264 | return { 265 | "video": f"https://storage.netology-group.services/api/v1/buckets/ms.webinar.foxford.ru/sets/{webinar_id_match[1]}/objects/mp4?access_token={search_params.get('access_token')}", 266 | "events": f"https://storage.netology-group.services/api/v1/buckets/meta.webinar.foxford.ru/sets/{webinar_id_match[1]}/objects/events.json?access_token={search_params.get('access_token')}" 267 | } 268 | 269 | 270 | def get_lesson_tasks(lesson_ids: Iterable[int], session: CachedSession) -> Iterable[List[Dict]]: 271 | @error_handler 272 | def fetch(lesson_id: int) -> List[Dict]: 273 | tasks_response: CachedResponse = session.get( 274 | f"https://foxford.ru/api/lessons/{lesson_id}/tasks", 275 | headers={ 276 | "X-Requested-With": "XMLHttpRequest" 277 | } 278 | ) 279 | 280 | if tasks_response.status_code != 200: 281 | return {"fatal_error": "Tasks fetch has failed"} 282 | 283 | if "id" not in tasks_response.json()[0]: 284 | return {"fatal_error": "Task structure is unknown"} 285 | 286 | return tasks_response.json() 287 | 288 | return map(fetch, lesson_ids) 289 | 290 | 291 | def construct_task_urls(lesson_ids: Iterable[int], lesson_tasks: Iterable[List[Dict]]) -> Iterable[Iterable[str]]: 292 | def combination(lesson_id: int, task_list: List[Dict]) -> Iterable[str]: 293 | return map( 294 | lambda task: f"https://foxford.ru/lessons/{lesson_id}/tasks/{task['id']}", 295 | task_list 296 | ) 297 | 298 | return map( 299 | combination, 300 | lesson_ids, 301 | lesson_tasks 302 | ) 303 | 304 | 305 | def construct_conspect_urls(lesson_ids: Iterable[int], conspect_amount: Iterable[int]) -> Iterable[Tuple[str]]: 306 | def recursive_collection(lesson_id: int, amount: int) -> Tuple[str]: 307 | if amount == 0: 308 | return () 309 | 310 | return ( 311 | *recursive_collection(lesson_id, amount - 1), 312 | f"https://foxford.ru/lessons/{lesson_id}/conspects/{amount}" 313 | ) 314 | 315 | return map( 316 | recursive_collection, 317 | lesson_ids, 318 | conspect_amount 319 | ) 320 | 321 | 322 | def build_dir_hierarchy(course_name: str, course_subtitle: str, grade: str, lessons: Iterable[Dict]) -> Iterable[Path]: 323 | def sanitize_string(string: str) -> str: 324 | return pipe( 325 | lambda char_list: filter( 326 | lambda char: char.isalpha() or char.isdigit() or char == " ", char_list 327 | ), 328 | lambda iterable: "".join(iterable), 329 | lambda filtered_char_list: filtered_char_list[:30].strip() 330 | )(string) 331 | 332 | def create_dir(lesson: Dict) -> Path: 333 | constructed_path: Path = Path( 334 | Path.cwd(), 335 | ( 336 | f"({grade}) " + 337 | sanitize_string(course_name) + 338 | " - " + 339 | sanitize_string(course_subtitle) 340 | ).strip(), 341 | ( 342 | f"({lesson['number']}) " + 343 | sanitize_string(lesson['title']) 344 | ).strip() 345 | ) 346 | 347 | if not constructed_path.exists(): 348 | constructed_path.mkdir(parents=True) 349 | 350 | return constructed_path 351 | 352 | return map( 353 | create_dir, 354 | lessons 355 | ) 356 | 357 | 358 | def download_resources(res_with_path: Dict, session: CachedSession) -> None: 359 | @error_handler 360 | def download_url(url: str, dest: Path) -> None: 361 | with requests.get(url, stream=True) as r: 362 | if r.status_code != 200: 363 | return {"fatal_error": "Video fetch has failed"} 364 | 365 | with dest.open("wb") as f: 366 | deque( 367 | map( 368 | lambda chunk: f.write(chunk), 369 | filter(None, r.iter_content(10 * 1024)) 370 | ), 371 | 0 372 | ) 373 | 374 | def save_video() -> None: 375 | if res_with_path["destination"].joinpath("video.mp4").exists(): 376 | return 377 | 378 | download_url( 379 | res_with_path["video"], 380 | res_with_path["destination"].joinpath("video.mp4") 381 | ) 382 | 383 | @error_handler 384 | def parse_and_save_event_data() -> None: 385 | if res_with_path["destination"].joinpath("message_log.txt").exists(): 386 | return 387 | 388 | events_response: CachedResponse = session.get( 389 | res_with_path["events"] 390 | ) 391 | 392 | if events_response.status_code != 200: 393 | return {"fatal_error": "Events fetch has failed"} 394 | 395 | if "meta" not in events_response.json()[0]: 396 | return {"fatal_error": "Events structure is unknown"} 397 | 398 | with res_with_path["destination"].joinpath("message_log.txt").open("w", errors="replace") as f: 399 | pipe( 400 | lambda json: filter( 401 | lambda obj: obj["meta"]["action"] == "message", 402 | json 403 | ), 404 | lambda messages: map( 405 | lambda msg: f"[{datetime.fromtimestamp(msg['meta']['time'])}] {msg['meta']['user_name']}: {parse.unquote(msg['meta']['body'])}", 406 | messages 407 | ), 408 | lambda message_log: "\n".join(message_log), 409 | f.write 410 | )(events_response.json()) 411 | 412 | pipe( 413 | lambda json: filter( 414 | lambda obj: 415 | (obj["meta"]["action"] == "add_tab" or 416 | obj["meta"]["action"] == "change_tab") and 417 | obj["meta"]["content_type"] == "pdf", 418 | json 419 | ), 420 | lambda pdfs: map( 421 | lambda pdf: pdf["meta"]["url"], 422 | pdfs 423 | ), 424 | unique_everseen, 425 | lambda urls: enumerate(urls, 1), 426 | lambda enumed_urls: map( 427 | lambda item: download_url( 428 | item[1], 429 | res_with_path["destination"] 430 | .joinpath(f"{item[0]}.pdf") 431 | ), 432 | enumed_urls 433 | ), 434 | lambda task_map: deque(task_map, 0) 435 | )(events_response.json()) 436 | 437 | save_video() 438 | parse_and_save_event_data() 439 | print( 440 | f"-> {res_with_path['destination'].name}: \033[92m\u2713\033[0m" 441 | ) 442 | 443 | 444 | async def save_page(url: str, path: Path, folder: str, cookies: Iterable[Dict], semaphore: asyncio.Semaphore) -> None: 445 | async with semaphore: 446 | if not path.joinpath(folder).joinpath(url.split("/")[-1] + ".pdf").exists(): 447 | browser_endpoint = await get_browser_connection_url() 448 | browser = await connect(browserWSEndpoint=browser_endpoint) 449 | page = await browser.newPage() 450 | await page.emulateMedia("screen") 451 | await page.setViewport({"width": 411, "height": 823}) 452 | await page.setCookie(*cookies) 453 | await page.goto(url, {"waitUntil": "domcontentloaded"}) 454 | 455 | if await page.waitForFunction("() => window.MathJax", timeout=10000): 456 | await asyncio.sleep(3.5) 457 | await page.evaluate(""" 458 | async function() { 459 | await new Promise(function(resolve) { 460 | window.MathJax.Hub.Register.StartupHook( 461 | "End", 462 | resolve 463 | ) 464 | }) 465 | } 466 | """) 467 | await asyncio.sleep(0.1) 468 | 469 | await page.evaluate(""" 470 | document.querySelectorAll(".toggle_element > .toggle_content").forEach(el => el.style.display = "block") 471 | """, force_expr=True) 472 | await asyncio.sleep(0.1) 473 | 474 | await page.evaluate(""" 475 | document.querySelector("#cc_container").remove() 476 | """, force_expr=True) 477 | await asyncio.sleep(0.1) 478 | 479 | if not path.joinpath(folder).exists(): 480 | path.joinpath(folder).mkdir() 481 | 482 | path.joinpath(folder).joinpath(url.split("/")[-1] + ".pdf").touch() 483 | 484 | await page.pdf({ 485 | "path": str(path.joinpath(folder).joinpath(url.split("/")[-1] + ".pdf")), 486 | "printBackground": True 487 | }) 488 | 489 | await page.close() 490 | await browser.disconnect() 491 | 492 | print( 493 | f"-> {folder}/{url.split('/')[-3]}/{url.split('/')[-1]}: \033[92m\u2713\033[0m" 494 | ) 495 | -------------------------------------------------------------------------------- /foxford_downloader/lib/helpers.py: -------------------------------------------------------------------------------- 1 | from functools import reduce 2 | from traceback import format_exc 3 | from typing import Any, Callable, Dict, Tuple, Union 4 | 5 | 6 | class Logger(): 7 | @staticmethod 8 | def error(message: str) -> None: 9 | print(f"[\033[91mE\033[0m]: \033[1m{message}\033[0m") 10 | 11 | @staticmethod 12 | def warn(message: str) -> None: 13 | print(f"[\033[93mW\033[0m]: \033[1m{message}\033[0m") 14 | 15 | @staticmethod 16 | def log(message: str) -> None: 17 | print(f"[\033[94mL\033[0m]: \033[1m{message}\033[0m") 18 | 19 | 20 | def pipe(*args: Tuple[Callable]) -> Callable: 21 | return lambda val: reduce(lambda prev, fn: fn(prev), args, val) 22 | 23 | 24 | def error_handler(fn: Callable) -> Callable: 25 | def wrapper(*args: Tuple, **kwargs: Dict): 26 | try: 27 | result: Any = fn(*args, **kwargs) 28 | if isinstance(result, dict) and "fatal_error" in result: 29 | Logger.error(result["fatal_error"]) 30 | exit(1) 31 | 32 | return result 33 | except Exception: 34 | Logger.error(format_exc()) 35 | exit(1) 36 | 37 | return wrapper 38 | -------------------------------------------------------------------------------- /foxford_downloader/lib/requests_cache.py: -------------------------------------------------------------------------------- 1 | from functools import lru_cache 2 | 3 | from requests import Response, Session 4 | from requests.adapters import HTTPAdapter 5 | from requests.cookies import cookiejar_from_dict, extract_cookies_to_jar 6 | from requests.structures import CaseInsensitiveDict 7 | from requests.utils import get_encoding_from_headers 8 | 9 | 10 | class CachedResponse(Response): 11 | @property 12 | @lru_cache(maxsize=1, typed=False) 13 | def content(self): 14 | return super().content 15 | 16 | @property 17 | @lru_cache(maxsize=1, typed=False) 18 | def text(self): 19 | return super().text 20 | 21 | @lru_cache(maxsize=1, typed=False) 22 | def json(self, **kwargs): 23 | return super().json(**kwargs) 24 | 25 | 26 | class CachedHTTPAdapter(HTTPAdapter): 27 | def build_response(self, req, resp): 28 | response = CachedResponse() 29 | response.status_code = getattr(resp, "status", None) 30 | response.headers = CaseInsensitiveDict(getattr(resp, "headers", {})) 31 | response.encoding = get_encoding_from_headers(response.headers) 32 | response.raw = resp 33 | response.reason = resp.reason 34 | 35 | if isinstance(req.url, bytes): 36 | response.url = req.url.decode("utf-8") 37 | 38 | else: 39 | response.url = req.url 40 | 41 | extract_cookies_to_jar(response.cookies, req, resp) 42 | response.request = req 43 | response.connection = self 44 | return response 45 | 46 | 47 | class CachedSession(): 48 | def __new__(self): 49 | s = Session() 50 | a = CachedHTTPAdapter(max_retries=3) 51 | s.mount("http://", a) 52 | s.mount("https://", a) 53 | return s 54 | -------------------------------------------------------------------------------- /foxford_downloader/requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.22.0 2 | beautifulsoup4==4.7.1 3 | more-itertools==7.1.0 4 | PyInquirer==1.0.3 5 | git+git://github.com/limitedeternity/pyppeteer@0.0.26#egg=pyppeteer 6 | async_lru==1.0.2 7 | --------------------------------------------------------------------------------