├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── _build ├── InnoSetup │ ├── installer_x64.iss │ └── installer_x86.iss ├── build.nuitka.linux ├── build.nuitka.win.bat ├── build.pyinstaller.osx ├── build.pyinstaller.win.bat ├── tvlinker.osx.spec ├── tvlinker.win32.spec ├── tvlinker.win64.spec └── version.py ├── _packaging └── debian │ ├── changelog │ ├── compat │ ├── control │ ├── rules │ ├── source │ ├── format │ └── options │ └── watch ├── data ├── desktop │ └── tvlinker.desktop └── icons │ ├── tvlinker.icns │ ├── tvlinker.ico │ ├── tvlinker.png │ └── tvlinker_setup.bmp ├── setup.py └── tvlinker ├── __init__.py ├── __main__.py ├── assets.py ├── assets ├── assets.qrc ├── fonts │ ├── opensans-bold.ttf │ ├── opensans-semibold.ttf │ └── opensans.ttf ├── images │ ├── cloud.png │ ├── cog.png │ ├── copy_icon.png │ ├── direct.png │ ├── down_arrow.png │ ├── download_icon.png │ ├── hosters │ │ ├── 4downfile.png │ │ ├── 4downfiles.png │ │ ├── ayefiles.png │ │ ├── cloudyfiles.png │ │ ├── dropapk.png │ │ ├── filefactory.png │ │ ├── go4up.png │ │ ├── multiup.png │ │ ├── nitroflare.png │ │ ├── openload.png │ │ ├── rapidgator.png │ │ ├── uploaded.png │ │ ├── uploadev.png │ │ ├── uploadgig.png │ │ ├── uploading.site.png │ │ └── uploadrocket.png │ ├── menu.png │ ├── menu_hover.png │ ├── minus.png │ ├── open_icon.png │ ├── plus.png │ ├── provider-scenerls.png │ ├── provider-tvrelease.png │ ├── realdebrid.png │ ├── refresh.png │ ├── refresh_hover.png │ ├── star_hover.png │ ├── star_off.png │ ├── star_on.png │ ├── thumbsup.png │ ├── tvlinker.png │ ├── up_arrow.png │ └── watermark.png ├── tvlinker.qss └── tvlinker_osx.qss ├── direct_download.py ├── downloader.py ├── filesize.py ├── hosters.py ├── jsfuck.py ├── notify.py ├── progress.py ├── pyload.py ├── settings.py ├── threads.py └── tvlinker.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Linux template 3 | *~ 4 | 5 | # temporary files which can be created if a process still has a handle open of a deleted file 6 | .fuse_hidden* 7 | 8 | # KDE directory preferences 9 | .directory 10 | 11 | # Linux trash folder which might appear on any partition or disk 12 | .Trash-* 13 | ### Python template 14 | # Byte-compiled / optimized / DLL files 15 | __pycache__/ 16 | *.py[cod] 17 | *$py.class 18 | 19 | # C extensions 20 | *.so 21 | 22 | # Distribution / packaging 23 | .Python 24 | env/ 25 | develop-eggs/ 26 | _build/build/ 27 | _build/dist/ 28 | build/ 29 | dist/ 30 | downloads/ 31 | eggs/ 32 | .eggs/ 33 | lib/ 34 | lib64/ 35 | parts/ 36 | sdist/ 37 | var/ 38 | *.egg-info/ 39 | .installed.cfg 40 | *.egg 41 | 42 | # PyInstaller 43 | # Usually these files are written by a python script from a template 44 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 45 | *.manifest 46 | 47 | # Installer logs 48 | pip-log.txt 49 | pip-delete-this-directory.txt 50 | 51 | # Unit test / coverage reports 52 | htmlcov/ 53 | .tox/ 54 | .coverage 55 | .coverage.* 56 | .cache 57 | nosetests.xml 58 | coverage.xml 59 | *,cover 60 | .hypothesis/ 61 | 62 | # Translations 63 | *.mo 64 | *.pot 65 | 66 | # Django stuff: 67 | *.log 68 | local_settings.py 69 | 70 | # Flask stuff: 71 | instance/ 72 | .webassets-cache 73 | 74 | # Scrapy stuff: 75 | .scrapy 76 | 77 | # Sphinx documentation 78 | docs/_build/ 79 | 80 | # PyBuilder 81 | target/ 82 | 83 | # IPython Notebook 84 | .ipynb_checkpoints 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # celery beat schedule file 90 | celerybeat-schedule 91 | 92 | # dotenv 93 | .env 94 | 95 | # virtualenv 96 | venv/ 97 | ENV/ 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | .idea/ 106 | build/dist/ 107 | 108 | # Personal configuration secrets 109 | tvlinker.ini.secret 110 | tvlinker.BAK.py 111 | .vscode 112 | 113 | # OS X annoyances 114 | .DS_Store 115 | 116 | # Arch packaging 117 | _packaging/archlinux/pkg 118 | _packaging/archlinux/src 119 | _packaging/archlinux/*.tar.gz 120 | _packaging/archlinux/*.pkg.* 121 | 122 | _build/InnoSetup/Output/* 123 | /tvlinker/assets/fonts/ 124 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | os: linux 3 | language: python 4 | python: 5 | - 3.5 6 | notifications: 7 | email: false 8 | install: 9 | - python3 setup.py bdist_wheel 10 | script: echo "Less is more.." 11 | # deploy: 12 | # provider: pypi 13 | # distributions: sdist bdist_wheel 14 | # user: ozmartian 15 | # password: 16 | # secure: JkN0BAAiLAGJewde9xW3eqjctSeOf4rqz7UxJKAfRVeE//QH3UEpQBDBXjQ0U63uIvLf+gjnhwfSv2gdQ7hBBI8QJFp3tAtTnDalZnziJzjL5sdFJypoWGFlDGGgXUvdE1J+GRzEd7nWZf3+g5wfNLkYlSLMmoMsGkGXlvepUxAnEFgocfZDexWBTO5jcgyUt3AiEMA+zpWluT2BHs5yqny7sAGVoJU99FdcTa2eonRU+0o2dDsiY5sRX8eLz9ioWxky+2mFC1uSDNpHEFLuftZPx8pE5XBW5LPZjloLLDtq31IEYcPY3y+GbfZ9ZiP0Dkr13/fVk7bPWglsivhvQRdoKp+PmfBUOQ8ocWUCJcCJpNQ3QJSgI9z/bSD0q5BI7Vbi97R/TlgXit1eGYGUDkKvB4hs+RFR+dvxDgvc/kNz8zCo/8Ptq1GK6PLzRGu+qpHJUBHRf3bCOP4orOGnJ8/im+st+mCzGSaKRHSFajTbRyAvZZljXaH9a/WXhDUOo0eyFlFKpf7lgEgsbTd/glEre4hrpRNtZgVbNRjm3feCXnJJ1NxTe9qc488Ul7EX3Z/Vd5Q0W43d/uUl3AcWeC72KAzztXBAmezZYKAPFSZcWTsK+I9i01m8Xuh5e9Qt8ERZfhT1TabWNcsEXHs0M1lEnNdxzPzaeV1op0IYNok= 17 | # on: 18 | # branch: master 19 | # tags: true 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## NOTE: Doesn't work anymore due to CloudFlare changes and python-cloudscraper dependency not supporting it no more. R.I.P. 2 | 3 | [![Build Status](https://travis-ci.org/ozmartian/tvlinker.svg?branch=master)](https://travis-ci.org/ozmartian/tvlinker) 4 | 5 | ![TVLinker](http://tvlinker.ozmartians.com/images/header-banner.png) 6 | [![Latest Release](http://tvlinker.ozmartians.com/images/button-latest-release.png)](https://github.com/ozmartian/tvlinker/releases/latest) 7 | 8 | ## Arch Linux AUR 9 | 10 | Just use your favourite AUR helper script/app and look for AUR package named 'tvlinker-git'. Example: 11 | 12 | yay -S tvlinker-git 13 | 14 | ## Ubuntu/Mint/Debian and all other Ubuntu derivatives 15 | 16 | Users can install the latest release via: 17 | 18 | ppa:ozmartian/apps 19 | 20 | If you are new to PPAs then just issue the following commands in a terminal: 21 | 22 | sudo add-apt-repository ppa:ozmartian/apps 23 | sudo apt-get update 24 | sudo apt-get install tvlinker 25 | 26 | ## What is TVLinker? 27 | 28 | PyQt5 based desktop widget for scraping and downloading TV shows from Scene-RLS.com 29 | 30 | **Recently updated for Scene-RLS.com due to tv-release.net closing down** 31 | 32 | You get link scraping together with integration to the real-debrid link unrestrictor 33 | service via their API (NOTE: a valid real-debrid account is required for link unrestricting 34 | to work). 35 | 36 | Supports a number of download managers across platforms: 37 | 38 | Built-in (window/linux) 39 | Aria2 RPC Daemon (windows/linux) 40 | Internet Download Manager (windows) 41 | KGet (linux) 42 | Persepolis (windows/linux) 43 | pyload (windows/linux) 44 | -------------------------------------------------------------------------------- /_build/InnoSetup/installer_x64.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Script Studio Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | [Setup] 5 | ; NOTE: The value of AppId uniquely identifies this application. 6 | ; Do not use the same AppId value in installers for other applications. 7 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 8 | AppId={{481211DA-28E0-4AA6-B316-C03482701DD7} 9 | AppName=TVLinker 10 | AppVersion=4.0.5 11 | AppVerName=TVLinker 12 | AppPublisher=Pete Alexandrou 13 | AppPublisherURL=http://tvlinker.ozmartians.com 14 | DefaultDirName={pf}\TVLinker 15 | DefaultGroupName=TVLinker 16 | OutputBaseFilename=TVLinker-4.0.5-setup-x64 17 | SetupIconFile=C:\DEV\tvlinker\data\icons\tvlinker.ico 18 | UninstallDisplayIcon={app}\tvlinker.exe 19 | Compression=lzma2 20 | SolidCompression=yes 21 | ShowLanguageDialog=no 22 | VersionInfoVersion=4.0.5 23 | VersionInfoCompany=ozmartians.com 24 | VersionInfoCopyright=(c) 2017 Pete Alexandrou 25 | VersionInfoProductName=TVLinker x64 26 | VersionInfoProductVersion=4.0.5 27 | ArchitecturesAllowed=x64 28 | ArchitecturesInstallIn64BitMode=x64 29 | 30 | [InstallDelete] 31 | Type: filesandordirs; Name: "{app}" 32 | 33 | [Languages] 34 | Name: "english"; MessagesFile: "compiler:Default.isl" 35 | 36 | [Tasks] 37 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}" 38 | 39 | [Files] 40 | Source: "C:\DEV\tvlinker\_build\dist\tvlinker.exe"; DestDir: "{app}" 41 | Source: "C:\DEV\vidcutter\data\icons\uninstall.ico"; DestDir: "{app}" 42 | 43 | [Icons] 44 | Name: "{group}\TVLinker"; Filename: "{app}\tvlinker.exe" 45 | Name: "{userdesktop}\TVLinker"; Filename: "{app}\tvlinker.exe"; Tasks: desktopicon 46 | Name: "{group}\{cm:UninstallProgram, TVLinker}"; Filename: "{uninstallexe}"; IconFilename: "{app}\uninstall.ico" 47 | 48 | [Run] 49 | Filename: "{app}\tvlinker.exe"; Flags: nowait postinstall skipifsilent 64bit; Description: "{cm:LaunchProgram,TVLinker}" -------------------------------------------------------------------------------- /_build/InnoSetup/installer_x86.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Script Studio Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | [Setup] 5 | ; NOTE: The value of AppId uniquely identifies this application. 6 | ; Do not use the same AppId value in installers for other applications. 7 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 8 | AppId={{481211DA-28E0-4AA6-B316-C03482701DD7} 9 | AppName=TVLinker 10 | AppVersion=4.0.5 11 | AppVerName=TVLinker 12 | AppPublisher=Pete Alexandrou 13 | AppPublisherURL=http://tvlinker.ozmartians.com 14 | DefaultDirName={pf}\TVLinker 15 | DefaultGroupName=TVLinker 16 | OutputBaseFilename=TVLinker-4.0.5-setup-x86 17 | SetupIconFile=C:\DEV\tvlinker\data\icons\tvlinker.ico 18 | UninstallDisplayIcon={app}\tvlinker.exe 19 | Compression=lzma2 20 | SolidCompression=yes 21 | ShowLanguageDialog=no 22 | VersionInfoVersion=4.0.5 23 | VersionInfoCompany=ozmartians.com 24 | VersionInfoCopyright=(c) 2017 Pete Alexandrou 25 | VersionInfoProductName=TVLinker x86 26 | VersionInfoProductVersion=4.0.5 27 | 28 | [InstallDelete] 29 | Type: filesandordirs; Name: "{app}" 30 | 31 | [Languages] 32 | Name: "english"; MessagesFile: "compiler:Default.isl" 33 | 34 | [Tasks] 35 | Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}" 36 | 37 | [Files] 38 | Source: "C:\DEV\tvlinker\_build\dist\tvlinker.exe"; DestDir: "{app}" 39 | Source: "C:\DEV\vidcutter\data\icons\uninstall.ico"; DestDir: "{app}" 40 | 41 | [Icons] 42 | Name: "{group}\TVLinker"; Filename: "{app}\tvlinker.exe" 43 | Name: "{userdesktop}\TVLinker"; Filename: "{app}\tvlinker.exe"; Tasks: desktopicon 44 | Name: "{group}\{cm:UninstallProgram, TVLinker}"; Filename: "{uninstallexe}"; IconFilename: "{app}\uninstall.ico" 45 | 46 | [Run] 47 | Filename: "{app}\tvlinker.exe"; Flags: nowait postinstall skipifsilent 32bit; Description: "{cm:LaunchProgram,TVLinker}" -------------------------------------------------------------------------------- /_build/build.nuitka.linux: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd ../tvlinker 4 | nuitka --recurse-all --remove-output tvlinker.py 5 | mv tvlinker.exe tvlinker 6 | -------------------------------------------------------------------------------- /_build/build.nuitka.win.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | cd .. 4 | nuitka --recurse-all --remove-output --windows-disable-console --windows-icon=assets\images\tvlinker.ico tvlinker.py 5 | -------------------------------------------------------------------------------- /_build/build.pyinstaller.osx: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf build dist 4 | python3 -m PyInstaller --clean tvlinker.osx.spec 5 | -------------------------------------------------------------------------------- /_build/build.pyinstaller.win.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | REM ......................setup variables...................... 4 | 5 | if [%1]==[] ( 6 | SET ARCH=64 7 | ) else ( 8 | SET ARCH=%1 9 | ) 10 | 11 | if ["%ARCH%"]==["64"] ( SET BINARCH=x64 ) 12 | if ["%ARCH%"]==["32"] ( SET BINARCH=x86 ) 13 | 14 | REM ......................get latest version number...................... 15 | 16 | for /f "delims=" %%a in ('python version.py') do @set VERSION=%%a 17 | 18 | REM ......................cleanup previous build scraps...................... 19 | 20 | rd /s /q build 21 | rd /s /q dist 22 | 23 | REM ......................run pyinstaller...................... 24 | 25 | pyinstaller --clean tvlinker.win%ARCH%.spec 26 | 27 | REM ......................add metadata to built Windows binary...................... 28 | 29 | verpatch dist\tvlinker.exe /va %VERSION%.0 /pv %VERSION%.0 /s desc "TVLinker" /s name "TVLinker" /s copyright "(c) 2017 Pete Alexandrou" /s product "TVLinker %BINARCH%" /s company "ozmartians.com" 30 | 31 | REM ......................call Inno Setup installer build script...................... 32 | 33 | cd InnoSetup 34 | "C:\Program Files (x86)\Inno Setup 5\iscc.exe" installer_%BINARCH%.iss 35 | cd .. 36 | -------------------------------------------------------------------------------- /_build/tvlinker.osx.spec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- mode: python -*- 3 | 4 | import os 5 | import sys 6 | import PyQt5 7 | 8 | block_cipher = None 9 | 10 | 11 | a = Analysis(['../tvlinker/__main__.py'], 12 | pathex=[ 13 | os.path.join(sys.modules['PyQt5'].__path__[0], 'Qt', 'bin'), 14 | '..' 15 | ], 16 | binaries=[], 17 | datas=[ 18 | ('../tvlinker/tvlinker.ini', '.'), 19 | ('../tvlinker/__init__.py', '.') 20 | ], 21 | hiddenimports=[], 22 | hookspath=[], 23 | runtime_hooks=[], 24 | excludes=[], 25 | win_no_prefer_redirects=False, 26 | win_private_assemblies=False, 27 | cipher=block_cipher) 28 | pyz = PYZ(a.pure, a.zipped_data, 29 | cipher=block_cipher) 30 | exe = EXE(pyz, 31 | a.scripts, 32 | exclude_binaries=True, 33 | name='TVLinker', 34 | debug=False, 35 | strip=False, 36 | upx=True, 37 | console=False , icon='../data/icons/tvlinker.icns') 38 | coll = COLLECT(exe, 39 | a.binaries, 40 | a.zipfiles, 41 | a.datas, 42 | strip=True, 43 | upx=False, 44 | name='TVLinker') 45 | app = BUNDLE(coll, 46 | name='TVLinker.app', 47 | icon='../data/icons/tvlinker.icns', 48 | bundle_identifier='com.ozmartians.tvlinker') 49 | -------------------------------------------------------------------------------- /_build/tvlinker.win32.spec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- mode: python -*- 3 | 4 | import os 5 | import sys 6 | import PyQt5 7 | 8 | block_cipher = None 9 | 10 | a = Analysis(['..\\tvlinker\\__main__.py'], 11 | pathex=[ 12 | os.path.join(sys.modules['PyQt5'].__path__[0], 'Qt', 'bin'), 13 | 'C:\\Program Files (x86)\\Windows Kits\\10\Redist\\ucrt\\DLLs\\x86', 14 | '..' 15 | ], 16 | binaries=[], 17 | datas=[ 18 | ('..\\tvlinker\\tvlinker.ini', '.'), 19 | ('..\\tvlinker\\__init__.py', '.') 20 | ], 21 | hiddenimports=[], 22 | hookspath=[], 23 | runtime_hooks=[], 24 | excludes=[], 25 | win_no_prefer_redirects=False, 26 | win_private_assemblies=False, 27 | cipher=block_cipher) 28 | pyz = PYZ(a.pure, a.zipped_data, 29 | cipher=block_cipher) 30 | exe = EXE(pyz, 31 | a.scripts, 32 | a.binaries, 33 | a.zipfiles, 34 | a.datas, 35 | name='tvlinker', 36 | debug=False, 37 | strip=False, 38 | upx=False, 39 | console=False , icon='..\\data\\icons\\tvlinker.ico') 40 | -------------------------------------------------------------------------------- /_build/tvlinker.win64.spec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- mode: python -*- 3 | 4 | import os 5 | import sys 6 | import PyQt5 7 | 8 | block_cipher = None 9 | 10 | a = Analysis(['..\\tvlinker\\__main__.py'], 11 | pathex=[ 12 | os.path.join(sys.modules['PyQt5'].__path__[0], 'Qt', 'bin'), 13 | 'C:\\Program Files (x86)\\Windows Kits\\10\Redist\\ucrt\\DLLs\\x64', 14 | '..' 15 | ], 16 | binaries=[], 17 | datas=[ 18 | ('..\\tvlinker\\tvlinker.ini', '.'), 19 | ('..\\tvlinker\\__init__.py', '.') 20 | ], 21 | hiddenimports=[], 22 | hookspath=[], 23 | runtime_hooks=[], 24 | excludes=[], 25 | win_no_prefer_redirects=False, 26 | win_private_assemblies=False, 27 | cipher=block_cipher) 28 | pyz = PYZ(a.pure, a.zipped_data, 29 | cipher=block_cipher) 30 | exe = EXE(pyz, 31 | a.scripts, 32 | a.binaries, 33 | a.zipfiles, 34 | a.datas, 35 | name='tvlinker', 36 | debug=False, 37 | strip=False, 38 | upx=False, 39 | console=False , icon='..\\data\\icons\\tvlinker.ico') 40 | -------------------------------------------------------------------------------- /_build/version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from codecs import open 5 | from os import path 6 | from re import match, sub 7 | 8 | with open(path.join(path.abspath(path.dirname(__file__)), 9 | '../tvlinker/__init__.py'), encoding='utf-8') as initfile: 10 | for line in initfile.readlines(): 11 | m = match('__version__ *= *[\'](.*)[\']', line) 12 | if m: 13 | print(sub('rc(.*)$', '', m.group(1))) 14 | -------------------------------------------------------------------------------- /_packaging/debian/changelog: -------------------------------------------------------------------------------- 1 | tvlinker (4.2.0-0) stable; urgency=medium 2 | * ShadowSocks + v2ray local proxy support 3 | * numerous fixes + enhancements 4 | 5 | tvlinker (4.0.0-0) stable; urgency=medium 6 | * massive overhaul of scraping code to use Scene-RLS.com as the TV 7 | show source website due to tv-release.net closing down. 8 | * supports cracking through CloudFlare.net bot-checker 9 | * multithreading rewrite for faster scraping/downloading + most 10 | stability, less crashes 11 | 12 | tvlinker (3.9.0-0) stable; urgency=low 13 | * added support for new hoster uploadgig 14 | * fixed download pages being changed after app launch 15 | * fixed crash when dowloading after starting first download already 16 | * download speed count added to built-in downloader stats 17 | 18 | tvlinker (3.8.5-0) unstable; urgency=low 19 | * numerous fixes 20 | * requests lib now used for most HTTP related code 21 | * aria2 RPC daemon fix 22 | * dbus notifications for Linux platform instead of ugly QMessageBox 23 | 24 | tvlinker (3.6.5-0) unstable; urgency=low 25 | * patch update 26 | 27 | tvlinker (3.6.0-0) unstable; urgency=low 28 | * kget + persepolis download managers bugfix; should now be working 100% 29 | * fixes / updates due to HTML changes upstram at tv-release.pw that was 30 | breaking the scraper code 31 | * new hosters added (go4up & multiup) 32 | * improvments to error handling + shell execution 33 | -------------------------------------------------------------------------------- /_packaging/debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /_packaging/debian/control: -------------------------------------------------------------------------------- 1 | Source: tvlinker 2 | Maintainer: Pete Alexandrou 3 | Section: net 4 | Priority: optional 5 | Build-Depends: dh-python, python3-setuptools, python3-all, debhelper (>= 9) 6 | Standards-Version: 3.9.8 7 | Homepage: http://tvlinker.ozmartians.com 8 | 9 | Package: tvlinker 10 | Architecture: all 11 | Depends: ${misc:Depends}, ${python3:Depends}, python3-pyqt5, python3-bs4, python3-lxml, 12 | python3-requests, nodejs 13 | Enhances: python3-socks, shadowsocks-qt5, aria2, kget 14 | Description: TV show link scraper + downloader for Scene-RLS.com with a modern PyQt5 GUI 15 | PyQt5 based desktop widget for scraping and downloading TV shows from Scene-RLS.com 16 | You get link scraping together with integration to the real-debrid link unrestrictor 17 | service via their API (NOTE: a valid real-debrid account is required for link unrestricting 18 | to work). 19 | . 20 | Supports a number of download managers across platforms: 21 | . 22 | Built-in (window/linux) 23 | Aria2 (windows/linux) 24 | Internet Download Manager (windows) 25 | KGet (linux) 26 | Persepolis (windows/linux) 27 | pyload (windows/linux) 28 | . 29 | -------------------------------------------------------------------------------- /_packaging/debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | # This file was automatically generated by stdeb 0.8.5 at 4 | # Fri, 06 Jan 2017 01:15:42 +0800 5 | export PYBUILD_NAME=tvlinker 6 | export DEB_BUILD_OPTIONS=nocheck 7 | %: 8 | dh $@ --with python3 --buildsystem=pybuild 9 | 10 | -------------------------------------------------------------------------------- /_packaging/debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /_packaging/debian/source/options: -------------------------------------------------------------------------------- 1 | extend-diff-ignore="^[^/]+.egg-info/" 2 | -------------------------------------------------------------------------------- /_packaging/debian/watch: -------------------------------------------------------------------------------- 1 | # please also check http://pypi.debian.net/tvlinker/watch 2 | version=3 3 | opts=uversionmangle=s/(rc|a|b|c)/~$1/ \ 4 | http://pypi.debian.net/tvlinker/tvlinker-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) -------------------------------------------------------------------------------- /data/desktop/tvlinker.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=TVLinker 3 | GenericName=TV link scraper + downloader 4 | X-GNOME-FullName=TVLinker 5 | Comment=TV link scraper and downloader 6 | Exec=tvlinker 7 | Terminal=false 8 | Type=Application 9 | Icon=tvlinker 10 | Categories=Network;FileTransfer;Qt;KDE; 11 | Keywords=television;downloader;internet;qt;python;pyqt5; 12 | MimeType=application/x-tvlinker; 13 | StartupNotify=true 14 | -------------------------------------------------------------------------------- /data/icons/tvlinker.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/data/icons/tvlinker.icns -------------------------------------------------------------------------------- /data/icons/tvlinker.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/data/icons/tvlinker.ico -------------------------------------------------------------------------------- /data/icons/tvlinker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/data/icons/tvlinker.png -------------------------------------------------------------------------------- /data/icons/tvlinker_setup.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/data/icons/tvlinker_setup.bmp -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | from codecs import open 6 | from os import path 7 | from re import match 8 | 9 | from setuptools import setup 10 | 11 | 12 | def get_value(varname, filename='tvlinker/__init__.py'): 13 | with open(path.join(here, filename), encoding='utf-8') as initfile: 14 | for line in initfile.readlines(): 15 | m = match('__%s__ *= *[\'](.*)[\']' % varname, line) 16 | if m: 17 | return m.group(1) 18 | 19 | 20 | def get_description(filename='README.md'): 21 | with open(path.join(here, filename), encoding='utf-8') as f: 22 | file = list(f) 23 | desc = '' 24 | for item in file[8: len(file)]: 25 | desc += item 26 | return desc 27 | 28 | 29 | def get_install_requires(): 30 | if packager == 'pypi': 31 | return ['PyQt5', 'beautifulsoup4', 'lxml', 'requests', 'cloudscraper'] 32 | else: 33 | return [] 34 | 35 | 36 | def get_extras_require(): 37 | if packager == 'pypi': 38 | return ['requests[socks]'] 39 | else: 40 | return [] 41 | 42 | 43 | def get_data_files(): 44 | files = [] 45 | if sys.platform.startswith('linux'): 46 | return [ 47 | ('/usr/share/pixmaps', ['data/icons/tvlinker.png']), 48 | ('/usr/share/applications', ['data/desktop/tvlinker.desktop']) 49 | ] 50 | return files 51 | 52 | 53 | here = path.abspath(path.dirname(__file__)) 54 | packager = get_value('packager') 55 | 56 | setup( 57 | name='tvlinker', 58 | version=get_value('version'), 59 | author='Pete Alexandrou', 60 | author_email='pete@ozmartians.com', 61 | description='TV show link scraper + downloader for Scene-RLS.com w/ debrid link unrestricting and support for a number of download managers across all platforms + ShadowSocks & v2ray local proxies.', 62 | long_description=get_description(), 63 | url='https://tvlinker.ozmartians.com', 64 | license='GPLv3+', 65 | packages=['tvlinker'], 66 | setup_requires=['setuptools'], 67 | install_requires=[], # get_install_requires(), 68 | # extras_require=get_extras_require(), 69 | package_data={'tvlinker': ['README.md', 'LICENSE', 'tvlinker/tvlinker.ini']}, 70 | data_files=get_data_files(), 71 | entry_points={'gui_scripts': ['tvlinker = tvlinker.__main__:main']}, 72 | keywords='tvlinker scraping Scene-RLS real-debrid filesharing internet tv-shows', 73 | classifiers=[ 74 | 'Development Status :: 4 - Beta', 75 | 'Environment :: X11 Applications :: Qt', 76 | 'Intended Audience :: End Users/Desktop', 77 | 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', 78 | 'Natural Language :: English', 79 | 'Operating System :: OS Independent', 80 | 'Topic :: Communications :: File Sharing', 81 | 'Programming Language :: Python :: 3 :: Only' 82 | ] 83 | ) 84 | -------------------------------------------------------------------------------- /tvlinker/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import sys 6 | 7 | 8 | __version__ = '4.5.0' 9 | __packager__ = 'arch' # (pypi, arch, deb, rpm) 10 | 11 | sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) 12 | -------------------------------------------------------------------------------- /tvlinker/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import inspect 5 | import os 6 | import platform 7 | import re 8 | import sys 9 | from datetime import datetime 10 | from enum import Enum 11 | from signal import SIGINT, SIGTERM, SIG_DFL, signal 12 | 13 | from PyQt5.QtCore import (QEvent, QFile, QFileInfo, QModelIndex, QProcess, QSettings, QSize, QStandardPaths, 14 | QTextStream, QThread, QUrl, Qt, pyqtSlot) 15 | from PyQt5.QtGui import QCloseEvent, QDesktopServices, QFont, QFontDatabase, QIcon, QKeyEvent, QPixmap 16 | from PyQt5.QtWidgets import (QAction, QApplication, QComboBox, QFileDialog, QGroupBox, QHBoxLayout, QHeaderView, 17 | QLabel, QLineEdit, QMenu, QMessageBox, QProgressBar, QProxyStyle, QPushButton, 18 | QSizePolicy, QStyle, QStyleFactory, QStyleHintReturn, QStyleOption, QTableWidget, 19 | QTableWidgetItem, QVBoxLayout, QWidget, qApp) 20 | 21 | from tvlinker.direct_download import DirectDownload 22 | from tvlinker.hosters import HosterLinks 23 | from tvlinker.progress import TaskbarProgress 24 | from tvlinker.pyload import PyloadConnection 25 | from tvlinker.settings import Settings 26 | from tvlinker.threads import Aria2Thread, DownloadThread, HostersThread, RealDebridThread, ScrapeWorker 27 | import tvlinker.assets as assets 28 | import sip 29 | 30 | 31 | if sys.platform == 'win32': 32 | # noinspection PyUnresolvedReferences 33 | from PyQt5.QtWinExtras import QWinTaskbarButton 34 | 35 | if sys.platform.startswith('linux'): 36 | import tvlinker.notify as notify 37 | 38 | signal(SIGINT, SIG_DFL) 39 | signal(SIGTERM, SIG_DFL) 40 | 41 | 42 | class OverrideStyle(QProxyStyle): 43 | def styleHint(self, hint, option: QStyleOption=0, widget: QWidget=0, returnData: QStyleHintReturn=0) -> int: 44 | if hint in { 45 | QStyle.SH_UnderlineShortcut, 46 | QStyle.SH_DialogButtons_DefaultButton, 47 | QStyle.SH_DialogButtonBox_ButtonsHaveIcons 48 | }: 49 | return 0 50 | return QProxyStyle.styleHint(self, hint, option, widget, returnData) 51 | 52 | 53 | class TVLinkerTable(QTableWidget): 54 | def __init__(self, rows: int, cols: int, parent=None): 55 | super(TVLinkerTable, self).__init__(rows, cols, parent) 56 | self.setMouseTracking(True) 57 | self.setEditTriggers(QTableWidget.NoEditTriggers) 58 | self.verticalHeader().hide() 59 | self.setAlternatingRowColors(True) 60 | self.setSelectionMode(QTableWidget.SingleSelection) 61 | self.setSelectionBehavior(QTableWidget.SelectRows) 62 | self.setHorizontalHeaderLabels(('DATE', 'URL', 'DESCRIPTION', 'SIZE')) 63 | self.horizontalHeader().setMinimumSectionSize(100) 64 | self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 65 | self.sortByColumn(0, Qt.DescendingOrder) 66 | self.setColumnHidden(1, True) 67 | self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents) 68 | self.horizontalHeader().setSectionResizeMode(2, QHeaderView.Stretch) 69 | _savestyle = self.style() 70 | self.setStyle(QStyleFactory.create('Fusion')) 71 | # self.horizontalHeader().setStyle(_savestyle) 72 | self.verticalScrollBar().setStyle(_savestyle) 73 | self.setFocus() 74 | 75 | 76 | class TVLinker(QWidget): 77 | def __init__(self, settings: QSettings, parent=None): 78 | super(TVLinker, self).__init__(parent) 79 | self.firstrun = True 80 | self.rows, self.cols = 0, 0 81 | self.parent = parent 82 | self.settings = settings 83 | self.taskbar = TaskbarProgress(self) 84 | self.init_styles() 85 | self.init_settings() 86 | self.init_icons() 87 | if sys.platform.startswith('linux'): 88 | notify.init(qApp.applicationName()) 89 | layout = QVBoxLayout() 90 | layout.setSpacing(0) 91 | layout.setContentsMargins(15, 15, 15, 0) 92 | form_groupbox = QGroupBox(self, objectName='mainForm') 93 | form_groupbox.setLayout(self.init_form()) 94 | self.table = TVLinkerTable(0, 4, self) 95 | self.table.doubleClicked.connect(self.show_hosters) 96 | layout.addWidget(form_groupbox) 97 | layout.addWidget(self.table) 98 | layout.addLayout(self.init_metabar()) 99 | self.setLayout(layout) 100 | qApp.setWindowIcon(self.icon_app) 101 | self.resize(FixedSettings.windowSize) 102 | self.show() 103 | self.start_scraping() 104 | self.firstrun = False 105 | 106 | class ProcError(Enum): 107 | FAILED_TO_START = 0 108 | CRASHED = 1 109 | TIMED_OUT = 2 110 | READ_ERROR = 3 111 | WRITE_ERROR = 4 112 | UNKNOWN_ERROR = 5 113 | 114 | class NotifyIcon(Enum): 115 | SUCCESS = ':assets/images/tvlinker.png' 116 | DEFAULT = ':assets/images/tvlinker.png' 117 | 118 | def init_threads(self, threadtype: str = 'scrape') -> None: 119 | if threadtype == 'scrape': 120 | if hasattr(self, 'scrapeThread'): 121 | if not sip.isdeleted(self.scrapeThread) and self.scrapeThread.isRunning(): 122 | self.scrapeThread.terminate() 123 | del self.scrapeWorker 124 | del self.scrapeThread 125 | self.scrapeThread = QThread(self) 126 | self.scrapeWorker = ScrapeWorker(self.source_url, self.user_agent, self.dl_pagecount) 127 | self.scrapeThread.started.connect(self.show_progress) 128 | self.scrapeThread.started.connect(self.scrapeWorker.begin) 129 | self.scrapeWorker.moveToThread(self.scrapeThread) 130 | self.scrapeWorker.addRow.connect(self.add_row) 131 | self.scrapeWorker.workFinished.connect(self.scrape_finished) 132 | self.scrapeWorker.workFinished.connect(self.scrapeWorker.deleteLater, Qt.DirectConnection) 133 | self.scrapeWorker.workFinished.connect(self.scrapeThread.quit, Qt.DirectConnection) 134 | self.scrapeThread.finished.connect(self.scrapeThread.deleteLater, Qt.DirectConnection) 135 | elif threadtype == 'unrestrict': 136 | pass 137 | 138 | @staticmethod 139 | def load_stylesheet(qssfile: str) -> None: 140 | if QFileInfo(qssfile).exists(): 141 | qss = QFile(qssfile) 142 | qss.open(QFile.ReadOnly | QFile.Text) 143 | qApp.setStyleSheet(QTextStream(qss).readAll()) 144 | 145 | def init_styles(self) -> None: 146 | if sys.platform == 'darwin': 147 | qss_stylesheet = self.get_path('%s_osx.qss' % qApp.applicationName().lower()) 148 | else: 149 | qss_stylesheet = self.get_path('%s.qss' % qApp.applicationName().lower()) 150 | TVLinker.load_stylesheet(qss_stylesheet) 151 | QFontDatabase.addApplicationFont(':assets/fonts/opensans.ttf') 152 | QFontDatabase.addApplicationFont(':assets/fonts/opensans-bold.ttf') 153 | QFontDatabase.addApplicationFont(':assets/fonts/opensans-semibold.ttf') 154 | qApp.setFont(QFont('Open Sans', 12 if sys.platform == 'darwin' else 10)) 155 | 156 | def init_icons(self) -> None: 157 | self.icon_app = QIcon(self.get_path('images/%s.png' % qApp.applicationName().lower())) 158 | self.icon_faves_off = QIcon(':assets/images/star_off.png') 159 | self.icon_faves_on = QIcon(':assets/images/star_on.png') 160 | self.icon_refresh = QIcon(':assets/images/refresh.png') 161 | self.icon_menu = QIcon(':assets/images/menu.png') 162 | self.icon_settings = QIcon(':assets/images/cog.png') 163 | self.icon_updates = QIcon(':assets/images/cloud.png') 164 | 165 | def init_settings(self) -> None: 166 | self.provider = 'Scene-RLS' 167 | self.select_provider(0) 168 | self.user_agent = self.settings.value('user_agent') 169 | self.dl_pagecount = self.settings.value('dl_pagecount', 20, int) 170 | self.dl_pagelinks = FixedSettings.linksPerPage 171 | self.realdebrid_api_token = self.settings.value('realdebrid_apitoken') 172 | self.realdebrid_api_proxy = self.settings.value('realdebrid_apiproxy') 173 | self.download_manager = self.settings.value('download_manager') 174 | self.persepolis_cmd = self.settings.value('persepolis_cmd') 175 | self.pyload_host = self.settings.value('pyload_host') 176 | self.pyload_username = self.settings.value('pyload_username') 177 | self.pyload_password = self.settings.value('pyload_password') 178 | self.idm_exe_path = self.settings.value('idm_exe_path') 179 | self.kget_cmd = self.settings.value('kget_cmd') 180 | self.favorites = self.settings.value('favorites') 181 | 182 | def init_form(self) -> QHBoxLayout: 183 | self.search_field = QLineEdit(self, clearButtonEnabled=True, placeholderText='Enter search criteria') 184 | self.search_field.setObjectName('searchInput') 185 | self.search_field.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) 186 | # self.search_field.setFocus() 187 | self.search_field.textChanged.connect(self.clear_filters) 188 | self.search_field.returnPressed.connect(lambda: self.filter_table(self.search_field.text())) 189 | self.favorites_button = QPushButton(parent=self, flat=True, cursor=Qt.PointingHandCursor, 190 | objectName='favesButton', toolTip='Favorites', checkable=True, 191 | toggled=self.filter_faves, 192 | checked=self.settings.value('faves_filter', False, bool)) 193 | self.refresh_button = QPushButton(parent=self, flat=True, cursor=Qt.PointingHandCursor, 194 | objectName='refreshButton', toolTip='Refresh [F5]', clicked=self.start_scraping) 195 | self.dlpages_field = QComboBox(self, toolTip='Pages', editable=False, cursor=Qt.PointingHandCursor) 196 | self.dlpages_field.addItems(('10', '20', '30', '40', '50', '60', '70', '80')) 197 | self.dlpages_field.setCurrentIndex(self.dlpages_field.findText(str(self.dl_pagecount), Qt.MatchFixedString)) 198 | self.dlpages_field.currentIndexChanged.connect(self.update_pagecount) 199 | self.settings_button = QPushButton(parent=self, flat=True, toolTip='Menu', 200 | objectName='menuButton', cursor=Qt.PointingHandCursor) 201 | self.settings_button.setMenu(self.settings_menu()) 202 | layout = QHBoxLayout(spacing=10) 203 | # providerCombo = QComboBox(self, toolTip='Provider', editable=False, cursor=Qt.PointingHandCursor) 204 | # providerCombo.setObjectName('providercombo') 205 | # providerCombo.addItem(QIcon(':assets/images/provider-scenerls.png'), '') 206 | # providerCombo.addItem(QIcon(':assets/images/provider-tvrelease.png'), '') 207 | # providerCombo.setIconSize(QSize(146, 36)) 208 | # providerCombo.setMinimumSize(QSize(160, 40)) 209 | # providerCombo.setStyleSheet(''' 210 | # QComboBox, QComboBox::drop-down { background-color: transparent; border: none; margin: 5px; } 211 | # QComboBox::down-arrow { image: url(:assets/images/down_arrow.png); } 212 | # QComboBox QAbstractItemView { selection-background-color: #DDDDE4; } 213 | # ''') 214 | # providerCombo.currentIndexChanged.connect(self.select_provider) 215 | layout.addWidget(QLabel(pixmap=QPixmap(':assets/images/provider-scenerls.png'))) 216 | layout.addWidget(self.search_field) 217 | layout.addWidget(self.favorites_button) 218 | layout.addWidget(self.refresh_button) 219 | layout.addWidget(QLabel('Pages:')) 220 | layout.addWidget(self.dlpages_field) 221 | layout.addWidget(self.settings_button) 222 | return layout 223 | 224 | @pyqtSlot(int) 225 | def select_provider(self, index: int): 226 | if index == 0: 227 | self.provider = 'Scene-RLS' 228 | self.source_url = 'http://scene-rls.net/releases/index.php?p={0}&cat=TV%20Shows' 229 | elif index == 1: 230 | self.provider = 'TV-Release' 231 | self.source_url = 'http://tv-release.pw/?cat=TV' 232 | self.setWindowTitle('%s :: %s' % (qApp.applicationName(), self.provider)) 233 | 234 | def settings_menu(self) -> QMenu: 235 | settings_action = QAction(self.icon_settings, 'Settings', self, triggered=self.show_settings) 236 | updates_action = QAction(self.icon_updates, 'Check for updates', self, triggered=self.check_update) 237 | aboutqt_action = QAction('About Qt', self, triggered=qApp.aboutQt) 238 | about_action = QAction('About %s' % qApp.applicationName(), self, triggered=self.about_app) 239 | menu = QMenu() 240 | menu.addAction(settings_action) 241 | menu.addAction(updates_action) 242 | menu.addSeparator() 243 | menu.addAction(aboutqt_action) 244 | menu.addAction(about_action) 245 | return menu 246 | 247 | def init_metabar(self) -> QHBoxLayout: 248 | self.meta_template = '%i / %i' 249 | self.progress = QProgressBar(parent=self, minimum=0, maximum=(self.dl_pagecount * self.dl_pagelinks), 250 | visible=False) 251 | self.taskbar.setProgress(0.0, True) 252 | if sys.platform == 'win32': 253 | self.win_taskbar_button = QWinTaskbarButton(self) 254 | 255 | self.meta_label = QLabel(textFormat=Qt.RichText, alignment=Qt.AlignRight, objectName='totals') 256 | self.update_metabar() 257 | layout = QHBoxLayout() 258 | layout.setContentsMargins(10, 5, 10, 10) 259 | layout.addWidget(self.progress, Qt.AlignLeft) 260 | layout.addWidget(self.meta_label, Qt.AlignRight) 261 | return layout 262 | 263 | @pyqtSlot() 264 | def check_update(self) -> None: 265 | QDesktopServices.openUrl(QUrl(FixedSettings.latest_release_url)) 266 | 267 | @pyqtSlot() 268 | def show_settings(self) -> None: 269 | settings_win = Settings(self, self.settings) 270 | settings_win.exec_() 271 | 272 | def update_metabar(self) -> bool: 273 | rowcount = self.table.rowCount() 274 | self.meta_label.setText(self.meta_template % (rowcount, self.dl_pagecount * self.dl_pagelinks)) 275 | self.progress.setValue(rowcount) 276 | self.taskbar.setProgress(rowcount / self.progress.maximum()) 277 | if sys.platform == 'win32': 278 | self.win_taskbar_button.progress().setValue(self.progress.value()) 279 | return True 280 | 281 | def start_scraping(self) -> None: 282 | if hasattr(self, 'scrapeThread'): 283 | if not sip.isdeleted(self.scrapeThread) and self.scrapeThread.isRunning(): 284 | self.scrapeThread.terminate() 285 | del self.scrapeWorker 286 | del self.scrapeThread 287 | self.init_threads('scrape') 288 | self.rows = 0 289 | self.table.clearContents() 290 | self.table.setRowCount(0) 291 | self.table.setSortingEnabled(False) 292 | self.update_metabar() 293 | self.scrapeThread.start() 294 | 295 | @pyqtSlot() 296 | def about_app(self) -> None: 297 | about_html = ''' 301 |

%s

302 |

303 | Version: %s 304 | (%s) 305 |

306 |

307 | Copyright © %s Pete Alexandrou 308 |
309 | Web: %s 310 |

311 |

312 | This program is free software; you can redistribute it and/or 313 | modify it under the terms of the GNU General Public License 314 | as published by the Free Software Foundation; either version 2 315 | of the License, or (at your option) any later version. 316 |

''' % (qApp.applicationName(), qApp.applicationVersion(), platform.architecture()[0], 317 | datetime.now().year, qApp.organizationDomain(), qApp.organizationDomain()) 318 | QMessageBox.about(self, 'About %s' % qApp.applicationName(), about_html) 319 | 320 | @pyqtSlot(int) 321 | def update_pagecount(self, index: int) -> None: 322 | self.dl_pagecount = int(self.dlpages_field.itemText(index)) 323 | self.scrapeWorker.maxpages = self.dl_pagecount 324 | self.progress.setMaximum(self.dl_pagecount * self.dl_pagelinks) 325 | self.settings.setValue('dl_pagecount', self.dl_pagecount) 326 | if sys.platform == 'win32': 327 | self.win_taskbar_button.progress().setMaximum(self.dl_pagecount * self.dl_pagelinks) 328 | if self.scrapeThread.isRunning(): 329 | self.scrapeThread.requestInterruption() 330 | self.start_scraping() 331 | 332 | @pyqtSlot() 333 | def show_progress(self): 334 | self.progress.show() 335 | self.taskbar.setProgress(0.0, True) 336 | if sys.platform == 'win32': 337 | self.win_taskbar_button.setWindow(self.windowHandle()) 338 | self.win_taskbar_button.progress().setRange(0, self.dl_pagecount * self.dl_pagelinks) 339 | self.win_taskbar_button.progress().setVisible(True) 340 | self.win_taskbar_button.progress().setValue(self.progress.value()) 341 | 342 | @pyqtSlot() 343 | def scrape_finished(self) -> None: 344 | self.progress.hide() 345 | self.taskbar.setProgress(0.0, False) 346 | if sys.platform == 'win32': 347 | self.win_taskbar_button.progress().setVisible(False) 348 | self.table.setSortingEnabled(True) 349 | self.filter_table(text='') 350 | 351 | @pyqtSlot(list) 352 | def add_row(self, row: list) -> None: 353 | if self.scrapeThread.isInterruptionRequested(): 354 | self.scrapeThread.terminate() 355 | else: 356 | self.cols = 0 357 | self.table.setRowCount(self.rows + 1) 358 | if self.table.cursor() != Qt.PointingHandCursor: 359 | self.table.setCursor(Qt.PointingHandCursor) 360 | for item in row: 361 | table_item = QTableWidgetItem(item) 362 | # table_item.setToolTip('%s\n\nDouble-click to view hoster links.' % row[1]) 363 | table_item.setFont(QFont('Open Sans', weight=QFont.Normal)) 364 | if self.cols == 2: 365 | if sys.platform == 'win32': 366 | table_item.setFont(QFont('Open Sans Semibold', pointSize=10)) 367 | elif sys.platform == 'darwin': 368 | table_item.setFont(QFont('Open Sans Bold', weight=QFont.Bold)) 369 | else: 370 | table_item.setFont(QFont('Open Sans', weight=QFont.DemiBold, pointSize=10)) 371 | table_item.setText(' ' + table_item.text()) 372 | elif self.cols in (0, 3): 373 | table_item.setTextAlignment(Qt.AlignCenter) 374 | self.table.setItem(self.rows, self.cols, table_item) 375 | self.update_metabar() 376 | self.cols += 1 377 | self.rows += 1 378 | 379 | @pyqtSlot(list) 380 | def add_hosters(self, links: list) -> None: 381 | self.hosters_win.show_hosters(links) 382 | 383 | @pyqtSlot(QModelIndex) 384 | def show_hosters(self, index: QModelIndex) -> None: 385 | QDesktopServices.openUrl(QUrl('http://scene-rls.net/releases/' + self.table.item(self.table.currentRow(), 1).text())) 386 | # qApp.setOverrideCursor(Qt.BusyCursor) 387 | # self.hosters_win = HosterLinks(self) 388 | # self.hosters_win.downloadLink.connect(self.download_link) 389 | # self.hosters_win.copyLink.connect(self.copy_download_link) 390 | # self.links = HostersThread(self.table.item(self.table.currentRow(), 1).text(), self.user_agent) 391 | # self.links.setHosters.connect(self.add_hosters) 392 | # self.links.noLinks.connect(self.no_links) 393 | # self.links.start() 394 | 395 | @pyqtSlot() 396 | def no_links(self) -> None: 397 | self.hosters_win.loading_progress.cancel() 398 | self.hosters_win.close() 399 | QMessageBox.warning(self, 'No Links Available', 'No links are available yet for the chosen TV show. ' + 400 | 'This is most likely due to the files still being uploaded. This is normal if the ' + 401 | 'link was published 30-45 mins ago.\n\nPlease check back again in 10-15 minutes.') 402 | 403 | @pyqtSlot(bool) 404 | def filter_faves(self, checked: bool) -> None: 405 | self.settings.setValue('faves_filter', checked) 406 | # if hasattr(self, 'scrapeWorker') and (sip.isdeleted(self.scrapeWorker) or self.scrapeWorker.complete): 407 | if not self.firstrun: 408 | self.filter_table() 409 | 410 | @pyqtSlot(str) 411 | @pyqtSlot() 412 | def filter_table(self, text: str='') -> None: 413 | filters = [] 414 | if self.favorites_button.isChecked(): 415 | filters = self.favorites 416 | self.table.sortItems(2, Qt.AscendingOrder) 417 | else: 418 | self.table.sortItems(0,Qt.DescendingOrder) 419 | if len(text): 420 | filters.append(text) 421 | if not len(filters) or not hasattr(self, 'valid_rows'): 422 | self.valid_rows = [] 423 | for search_term in filters: 424 | for item in self.table.findItems(search_term, Qt.MatchContains): 425 | self.valid_rows.append(item.row()) 426 | for row in range(0, self.table.rowCount()): 427 | if not len(filters): 428 | self.table.showRow(row) 429 | else: 430 | if row not in self.valid_rows: 431 | self.table.hideRow(row) 432 | else: 433 | self.table.showRow(row) 434 | 435 | @pyqtSlot() 436 | def clear_filters(self): 437 | if not len(self.search_field.text()): 438 | self.filter_table('') 439 | 440 | @pyqtSlot(bool) 441 | def aria2_confirmation(self, success: bool) -> None: 442 | qApp.restoreOverrideCursor() 443 | if success: 444 | if sys.platform.startswith('linux'): 445 | self.notify(title=qApp.applicationName(), msg='Your download link has been unrestricted and now ' + 446 | 'queued in Aria2 RPC Daemon', icon=self.NotifyIcon.SUCCESS) 447 | else: 448 | QMessageBox.information(self, qApp.applicationName(), 449 | 'Download link has been queued in Aria2.', QMessageBox.Ok) 450 | else: 451 | QMessageBox.critical(self, 'Aria2 RPC Daemon', 452 | 'Could not connect to Aria2 RPC Daemon. ' + 453 | 'Check your %s settings and try again.' % qApp.applicationName(), QMessageBox.Ok) 454 | 455 | @pyqtSlot(str) 456 | def download_link(self, link: str) -> None: 457 | if len(self.realdebrid_api_token) > 0 and 'real-debrid.com' not in link \ 458 | and 'rdeb.io' not in link: 459 | qApp.setOverrideCursor(Qt.BusyCursor) 460 | self.unrestrict_link(link, True) 461 | else: 462 | if self.download_manager == 'aria2': 463 | self.aria2 = Aria2Thread(settings=self.settings, link_url=link) 464 | self.aria2.aria2Confirmation.connect(self.aria2_confirmation) 465 | self.aria2.start() 466 | self.hosters_win.close() 467 | elif self.download_manager == 'pyload': 468 | self.pyload_conn = PyloadConnection(self.pyload_host, self.pyload_username, self.pyload_password) 469 | pid = self.pyload_conn.addPackage(name='TVLinker', links=[link]) 470 | qApp.restoreOverrideCursor() 471 | self.hosters_win.close() 472 | if sys.platform.startswith('linux'): 473 | self.notify(title='Download added to %s' % self.download_manager, icon=self.NotifyIcon.SUCCESS) 474 | else: 475 | QMessageBox.information(self, self.download_manager, 'Your link has been queued in %s.' 476 | % self.download_manager, QMessageBox.Ok) 477 | # open_pyload = msgbox.addButton('Open pyLoad', QMessageBox.AcceptRole) 478 | # open_pyload.clicked.connect(self.open_pyload) 479 | elif self.download_manager in ('kget', 'persepolis'): 480 | provider = self.kget_cmd if self.download_manager == 'kget' else self.persepolis_cmd 481 | cmd = '{0} "{1}"'.format(provider, link) 482 | if self.cmdexec(cmd): 483 | qApp.restoreOverrideCursor() 484 | self.hosters_win.close() 485 | if sys.platform.startswith('linux'): 486 | self.notify(title='Download added to %s' % self.download_manager, icon=self.NotifyIcon.SUCCESS) 487 | else: 488 | QMessageBox.information(self, self.download_manager, 'Your link has been queued in %s.' 489 | % self.download_manager, QMessageBox.Ok) 490 | elif self.download_manager == 'idm': 491 | cmd = '"%s" /n /d "%s"' % (self.idm_exe_path, link) 492 | if self.cmdexec(cmd): 493 | qApp.restoreOverrideCursor() 494 | self.hosters_win.close() 495 | QMessageBox.information(self, 'Internet Download Manager', 'Your link has been queued in IDM.') 496 | else: 497 | print('IDM QProcess error = %s' % self.ProcError(self.idm.error()).name) 498 | qApp.restoreOverrideCursor() 499 | self.hosters_win.close() 500 | QMessageBox.critical(self, 'Internet Download Manager', 501 | '

Could not connect to your local IDM application instance. ' + 502 | 'Please check your settings and ensure the IDM executable path is correct ' + 503 | 'according to your installation.

Error Code: %s

' 504 | % self.ProcError(self.idm.error()).name, QMessageBox.Ok) 505 | else: 506 | dlpath, _ = QFileDialog.getSaveFileName(self, 'Save File', link.split('/')[-1]) 507 | if dlpath != '': 508 | self.directdl_win = DirectDownload(parent=self) 509 | self.directdl = DownloadThread(link_url=link, dl_path=dlpath) 510 | self.directdl.dlComplete.connect(self.directdl_win.download_complete) 511 | if sys.platform.startswith('linux'): 512 | self.directdl.dlComplete.connect(lambda: self.notify(qApp.applicationName(), 513 | 'Download complete', 514 | self.NotifyIcon.SUCCESS)) 515 | else: 516 | self.directdl.dlComplete.connect(lambda: QMessageBox.information(self, qApp.applicationName(), 517 | 'Download complete', 518 | QMessageBox.Ok)) 519 | self.directdl.dlProgressTxt.connect(self.directdl_win.update_progress_label) 520 | self.directdl.dlProgress.connect(self.directdl_win.update_progress) 521 | self.directdl_win.cancelDownload.connect(self.cancel_download) 522 | self.directdl.start() 523 | self.hosters_win.close() 524 | 525 | def _init_notification_icons(self): 526 | for icon in self.NotifyIcon: 527 | icon_file = QPixmap(icon.value, 'PNG') 528 | icon_file.save(os.path.join(FixedSettings.config_path, os.path.basename(icon.value)), 'PNG', 100) 529 | 530 | def notify(self, title: str, msg: str = '', icon: Enum = None, urgency: int = 1) -> bool: 531 | icon_path = icon.value if icon is not None else self.NotifyIcon.DEFAULT.value 532 | icon_path = os.path.join(FixedSettings.config_path, os.path.basename(icon_path)) 533 | if not os.path.exists(icon_path): 534 | self._init_notification_icons() 535 | notification = notify.Notification(title, msg, icon_path) 536 | notification.set_urgency(urgency) 537 | return notification.show() 538 | 539 | def cmdexec(self, cmd: str) -> bool: 540 | self.proc = QProcess() 541 | self.proc.setProcessChannelMode(QProcess.MergedChannels) 542 | if hasattr(self.proc, 'errorOccurred'): 543 | self.proc.errorOccurred.connect(lambda error: print('Process error = %s' % self.ProcError(error).name)) 544 | if self.proc.state() == QProcess.NotRunning: 545 | self.proc.start(cmd) 546 | self.proc.waitForFinished(-1) 547 | rc = self.proc.exitStatus() == QProcess.NormalExit and self.proc.exitCode() == 0 548 | self.proc.deleteLater() 549 | return rc 550 | return False 551 | 552 | @pyqtSlot() 553 | def cancel_download(self) -> None: 554 | self.directdl.cancel_download = True 555 | self.directdl.quit() 556 | self.directdl.deleteLater() 557 | 558 | def open_pyload(self) -> None: 559 | QDesktopServices.openUrl(QUrl(self.pyload_config.host)) 560 | 561 | @pyqtSlot(str) 562 | def copy_download_link(self, link: str) -> None: 563 | if len(self.realdebrid_api_token) > 0 and 'real-debrid.com' not in link \ 564 | and 'rdeb.io' not in link: 565 | qApp.setOverrideCursor(Qt.BusyCursor) 566 | self.unrestrict_link(link, False) 567 | else: 568 | clip = qApp.clipboard() 569 | clip.setText(link) 570 | self.hosters_win.close() 571 | qApp.restoreOverrideCursor() 572 | 573 | def unrestrict_link(self, link: str, download: bool = True) -> None: 574 | caller = inspect.stack()[1].function 575 | self.realdebrid = RealDebridThread(settings=self.settings, api_url=FixedSettings.realdebrid_api_url, 576 | link_url=link, action=RealDebridThread.RealDebridAction.UNRESTRICT_LINK) 577 | self.realdebrid.errorMsg.connect(self.error_handler) 578 | if download: 579 | self.realdebrid.unrestrictedLink.connect(self.download_link) 580 | else: 581 | self.realdebrid.unrestrictedLink.connect(self.copy_download_link) 582 | self.realdebrid.start() 583 | 584 | def closeEvent(self, event: QCloseEvent) -> None: 585 | if hasattr(self, 'scrapeThread'): 586 | if not sip.isdeleted(self.scrapeThread) and self.scrapeThread.isRunning(): 587 | self.scrapeThread.requestInterruption() 588 | self.scrapeThread.quit() 589 | qApp.quit() 590 | 591 | def error_handler(self, props: list) -> None: 592 | qApp.restoreOverrideCursor() 593 | QMessageBox.critical(self, props[0], props[1], QMessageBox.Ok) 594 | 595 | @staticmethod 596 | def get_path(path: str = None, override: bool = False) -> str: 597 | if override: 598 | if getattr(sys, 'frozen', False): 599 | return os.path.join(sys._MEIPASS, path) 600 | return os.path.join(QFileInfo(__file__).absolutePath(), path) 601 | return ':assets/%s' % path 602 | 603 | @staticmethod 604 | def get_version(filename: str = '__init__.py') -> str: 605 | with open(TVLinker.get_path(filename, override=True), 'r') as initfile: 606 | for line in initfile.readlines(): 607 | m = re.match('__version__ *= *[\'](.*)[\']', line) 608 | if m: 609 | return m.group(1) 610 | 611 | def keyPressEvent(self, event: QKeyEvent) -> None: 612 | if event.key() == Qt.Key_F5: 613 | self.start_scraping() 614 | return 615 | if event.key() in {Qt.Key_Q, Qt.Key_W} and event.modifiers() == Qt.ControlModifier: 616 | qApp.quit() 617 | return 618 | 619 | class FixedSettings: 620 | applicationName = 'TVLinker' 621 | applicationVersion = TVLinker.get_version() 622 | organizationDomain = 'https://tvlinker.ozmartians.com' 623 | windowSize = QSize(1000, 785) 624 | linksPerPage = 20 625 | latest_release_url = 'https://github.com/ozmartian/tvlinker/releases/latest' 626 | realdebrid_api_url = 'https://api.real-debrid.com/rest/1.0' 627 | config_path = None 628 | 629 | @staticmethod 630 | def get_app_settings() -> QSettings: 631 | FixedSettings.config_path = QStandardPaths.writableLocation(QStandardPaths.AppConfigLocation) 632 | settings_ini = os.path.join(FixedSettings.config_path, '%s.ini' % FixedSettings.applicationName.lower()) 633 | if not os.path.exists(settings_ini): 634 | os.makedirs(FixedSettings.config_path, exist_ok=True) 635 | QFile.copy(':%s.ini' % FixedSettings.applicationName.lower(), settings_ini) 636 | QFile.setPermissions(settings_ini, QFile.ReadOwner | QFile.WriteOwner) 637 | return QSettings(settings_ini, QSettings.IniFormat) 638 | 639 | 640 | def main(): 641 | app = QApplication(sys.argv) 642 | app.setStyle(OverrideStyle()) 643 | app.setApplicationName(FixedSettings.applicationName) 644 | app.setOrganizationDomain(FixedSettings.organizationDomain) 645 | app.setApplicationVersion(FixedSettings.applicationVersion) 646 | app.setQuitOnLastWindowClosed(True) 647 | tvlinker = TVLinker(FixedSettings.get_app_settings()) 648 | sys.exit(app.exec_()) 649 | 650 | 651 | if __name__ == '__main__': 652 | main() 653 | -------------------------------------------------------------------------------- /tvlinker/assets/assets.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | tvlinker.ini 4 | assets/fonts/opensans.ttf 5 | assets/fonts/opensans-bold.ttf 6 | assets/fonts/opensans-semibold.ttf 7 | assets/images/cloud.png 8 | assets/images/cog.png 9 | assets/images/copy_icon.png 10 | assets/images/direct.png 11 | assets/images/down_arrow.png 12 | assets/images/download_icon.png 13 | assets/images/hosters/4downfile.png 14 | assets/images/hosters/4downfiles.png 15 | assets/images/hosters/ayefiles.png 16 | assets/images/hosters/cloudyfiles.png 17 | assets/images/hosters/dropapk.png 18 | assets/images/hosters/filefactory.png 19 | assets/images/hosters/go4up.png 20 | assets/images/hosters/multiup.png 21 | assets/images/hosters/nitroflare.png 22 | assets/images/hosters/openload.png 23 | assets/images/hosters/rapidgator.png 24 | assets/images/hosters/uploaded.png 25 | assets/images/hosters/uploadev.png 26 | assets/images/hosters/uploadgig.png 27 | assets/images/hosters/uploadrocket.png 28 | assets/images/hosters/uploading.site.png 29 | assets/images/menu.png 30 | assets/images/menu_hover.png 31 | assets/images/minus.png 32 | assets/images/open_icon.png 33 | assets/images/plus.png 34 | assets/images/provider-scenerls.png 35 | assets/images/provider-tvrelease.png 36 | assets/images/realdebrid.png 37 | assets/images/refresh.png 38 | assets/images/refresh_hover.png 39 | assets/images/star_hover.png 40 | assets/images/star_off.png 41 | assets/images/star_on.png 42 | assets/images/thumbsup.png 43 | assets/images/tvlinker.png 44 | assets/images/up_arrow.png 45 | assets/images/watermark.png 46 | assets/tvlinker.qss 47 | assets/tvlinker_osx.qss 48 | 49 | 50 | -------------------------------------------------------------------------------- /tvlinker/assets/fonts/opensans-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/fonts/opensans-bold.ttf -------------------------------------------------------------------------------- /tvlinker/assets/fonts/opensans-semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/fonts/opensans-semibold.ttf -------------------------------------------------------------------------------- /tvlinker/assets/fonts/opensans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/fonts/opensans.ttf -------------------------------------------------------------------------------- /tvlinker/assets/images/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/cloud.png -------------------------------------------------------------------------------- /tvlinker/assets/images/cog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/cog.png -------------------------------------------------------------------------------- /tvlinker/assets/images/copy_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/copy_icon.png -------------------------------------------------------------------------------- /tvlinker/assets/images/direct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/direct.png -------------------------------------------------------------------------------- /tvlinker/assets/images/down_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/down_arrow.png -------------------------------------------------------------------------------- /tvlinker/assets/images/download_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/download_icon.png -------------------------------------------------------------------------------- /tvlinker/assets/images/hosters/4downfile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/hosters/4downfile.png -------------------------------------------------------------------------------- /tvlinker/assets/images/hosters/4downfiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/hosters/4downfiles.png -------------------------------------------------------------------------------- /tvlinker/assets/images/hosters/ayefiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/hosters/ayefiles.png -------------------------------------------------------------------------------- /tvlinker/assets/images/hosters/cloudyfiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/hosters/cloudyfiles.png -------------------------------------------------------------------------------- /tvlinker/assets/images/hosters/dropapk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/hosters/dropapk.png -------------------------------------------------------------------------------- /tvlinker/assets/images/hosters/filefactory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/hosters/filefactory.png -------------------------------------------------------------------------------- /tvlinker/assets/images/hosters/go4up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/hosters/go4up.png -------------------------------------------------------------------------------- /tvlinker/assets/images/hosters/multiup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/hosters/multiup.png -------------------------------------------------------------------------------- /tvlinker/assets/images/hosters/nitroflare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/hosters/nitroflare.png -------------------------------------------------------------------------------- /tvlinker/assets/images/hosters/openload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/hosters/openload.png -------------------------------------------------------------------------------- /tvlinker/assets/images/hosters/rapidgator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/hosters/rapidgator.png -------------------------------------------------------------------------------- /tvlinker/assets/images/hosters/uploaded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/hosters/uploaded.png -------------------------------------------------------------------------------- /tvlinker/assets/images/hosters/uploadev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/hosters/uploadev.png -------------------------------------------------------------------------------- /tvlinker/assets/images/hosters/uploadgig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/hosters/uploadgig.png -------------------------------------------------------------------------------- /tvlinker/assets/images/hosters/uploading.site.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/hosters/uploading.site.png -------------------------------------------------------------------------------- /tvlinker/assets/images/hosters/uploadrocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/hosters/uploadrocket.png -------------------------------------------------------------------------------- /tvlinker/assets/images/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/menu.png -------------------------------------------------------------------------------- /tvlinker/assets/images/menu_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/menu_hover.png -------------------------------------------------------------------------------- /tvlinker/assets/images/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/minus.png -------------------------------------------------------------------------------- /tvlinker/assets/images/open_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/open_icon.png -------------------------------------------------------------------------------- /tvlinker/assets/images/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/plus.png -------------------------------------------------------------------------------- /tvlinker/assets/images/provider-scenerls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/provider-scenerls.png -------------------------------------------------------------------------------- /tvlinker/assets/images/provider-tvrelease.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/provider-tvrelease.png -------------------------------------------------------------------------------- /tvlinker/assets/images/realdebrid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/realdebrid.png -------------------------------------------------------------------------------- /tvlinker/assets/images/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/refresh.png -------------------------------------------------------------------------------- /tvlinker/assets/images/refresh_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/refresh_hover.png -------------------------------------------------------------------------------- /tvlinker/assets/images/star_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/star_hover.png -------------------------------------------------------------------------------- /tvlinker/assets/images/star_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/star_off.png -------------------------------------------------------------------------------- /tvlinker/assets/images/star_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/star_on.png -------------------------------------------------------------------------------- /tvlinker/assets/images/thumbsup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/thumbsup.png -------------------------------------------------------------------------------- /tvlinker/assets/images/tvlinker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/tvlinker.png -------------------------------------------------------------------------------- /tvlinker/assets/images/up_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/up_arrow.png -------------------------------------------------------------------------------- /tvlinker/assets/images/watermark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozmartian/tvlinker/fad16cada2b7d579c193d2c5109409d2a2e4151d/tvlinker/assets/images/watermark.png -------------------------------------------------------------------------------- /tvlinker/assets/tvlinker.qss: -------------------------------------------------------------------------------- 1 | QHeaderView::section { 2 | padding: 3px; 3 | font-family: "Open Sans", sans-serif; 4 | font-weight: bold; 5 | } 6 | 7 | QTableView { 8 | color: #444; 9 | border: 1px solid #ADAACC; 10 | margin-top: 0; 11 | margin-bottom: 0; 12 | background: #FFF url(:assets/images/watermark.png) no-repeat center center; 13 | background-attachment: fixed; 14 | alternate-background-color: rgba(0, 0, 0, 0.05); 15 | } 16 | 17 | QGroupBox { 18 | border: 1px solid #BABABA; 19 | margin: 0; 20 | padding: 6px; 21 | } 22 | 23 | QGroupBox#mainForm { 24 | border: 1px solid #ADAACC; 25 | border-bottom: none; 26 | background: rgba(152, 150, 171, 0.64); 27 | } 28 | 29 | QLabel#pages { 30 | color: #555; 31 | font-size: 7pt; 32 | font-weight: 500; 33 | } 34 | 35 | QLabel#totals { 36 | color: #6A687D; 37 | } 38 | 39 | QLineEdit:read-only { 40 | background-color: #EAEAEA; 41 | } 42 | 43 | QDialogButtonBox { 44 | dialogbuttonbox-buttons-have-icons: 0; 45 | } 46 | 47 | QDialog#hosters { 48 | background-color: #EFEFEF; 49 | } 50 | 51 | #mainForm QPushButton { 52 | border: none; 53 | outline: none; 54 | margin: 10px; 55 | } 56 | 57 | #mainForm QPushButton#favesButton { 58 | border-image: url(:assets/images/star_off.png); 59 | width: 30px; 60 | height: 30px; 61 | } 62 | 63 | #mainForm QPushButton:hover#favesButton { 64 | border-image: url(:assets/images/star_hover.png); 65 | } 66 | 67 | #mainForm QPushButton:checked#favesButton { 68 | border-image: url(:assets/images/star_on.png); 69 | } 70 | 71 | #mainForm QPushButton#refreshButton { 72 | border-image: url(:assets/images/refresh.png); 73 | width: 22px; 74 | height: 21px; 75 | } 76 | 77 | #mainForm QPushButton:hover#refreshButton { 78 | border-image: url(:assets/images/refresh_hover.png); 79 | } 80 | 81 | #mainForm QPushButton#menuButton { 82 | border-image: url(:assets/images/menu.png); 83 | width: 18px; 84 | height: 18px; 85 | } 86 | 87 | #mainForm QPushButton:hover#menuButton { 88 | border-image: url(:assets/images/menu_hover.png); 89 | } 90 | 91 | #mainForm QPushButton::menu-indicator#menuButton { 92 | left: -1000px; 93 | } 94 | -------------------------------------------------------------------------------- /tvlinker/assets/tvlinker_osx.qss: -------------------------------------------------------------------------------- 1 | QHeaderView::section { 2 | padding: 4px; 3 | font-family: "Open Sans", sans-serif; 4 | font-weight: bold; 5 | } 6 | 7 | QTableView { 8 | color: #444; 9 | border: 1px solid #ADAACC; 10 | margin-top: 0; 11 | margin-bottom: 0; 12 | background: #FFF url(:assets/images/watermark.png) no-repeat center center; 13 | background-attachment: fixed; 14 | alternate-background-color: rgba(0, 0, 0, 0.05); 15 | } 16 | 17 | QGroupBox { 18 | border: 1px solid #BABABA; 19 | margin: 0; 20 | padding: 6px; 21 | } 22 | 23 | QGroupBox#mainForm { 24 | border: 1px solid #ADAACC; 25 | border-bottom: none; 26 | background: rgba(152, 150, 171, 0.64); 27 | } 28 | 29 | QLabel#pages { 30 | color: #555; 31 | font-size: 9pt; 32 | font-weight: 500; 33 | } 34 | 35 | QLabel#totals { 36 | color: #6A687D; 37 | } 38 | 39 | QLineEdit:read-only { 40 | background-color: #EAEAEA; 41 | } 42 | 43 | QDialogButtonBox { 44 | dialogbuttonbox-buttons-have-icons: 0; 45 | } 46 | 47 | #mainForm QPushButton { 48 | border: none; 49 | outline: none; 50 | margin: 10px; 51 | } 52 | 53 | #hosters QPushButton { 54 | border: none; 55 | outline: none; 56 | background-color: transparent; 57 | margin: 0; 58 | padding: 0; 59 | } 60 | 61 | #mainForm QPushButton#favesButton { 62 | border-image: url(:assets/images/star_off.png); 63 | width: 30px; 64 | height: 30px; 65 | } 66 | 67 | #mainForm QPushButton:hover#favesButton { 68 | border-image: url(:assets/images/star_hover.png); 69 | } 70 | 71 | #mainForm QPushButton:checked#favesButton { 72 | border-image: url(:assets/images/star_on.png); 73 | } 74 | 75 | #mainForm QPushButton#refreshButton { 76 | border-image: url(:assets/images/refresh.png); 77 | width: 22px; 78 | height: 21px; 79 | } 80 | 81 | #mainForm QPushButton:hover#refreshButton { 82 | border-image: url(:assets/images/refresh_hover.png); 83 | } 84 | 85 | #mainForm QPushButton#menuButton { 86 | border-image: url(:assets/images/menu.png); 87 | width: 18px; 88 | height: 18px; 89 | } 90 | 91 | #mainForm QPushButton:hover#menuButton { 92 | border-image: url(:assets/images/menu_hover.png); 93 | } 94 | 95 | #mainForm QPushButton::menu-indicator#menuButton { 96 | left: -1000px; 97 | } 98 | 99 | -------------------------------------------------------------------------------- /tvlinker/direct_download.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt 5 | from PyQt5.QtGui import QCloseEvent 6 | from PyQt5.QtWidgets import QDialog, QVBoxLayout, QLabel, QProgressBar, qApp, QMessageBox 7 | 8 | 9 | class DirectDownload(QDialog): 10 | cancelDownload = pyqtSignal() 11 | 12 | def __init__(self, parent, f=Qt.WindowCloseButtonHint): 13 | super(DirectDownload, self).__init__(parent, f) 14 | self.parent = parent 15 | self.setWindowTitle('Download Progress') 16 | self.setWindowModality(Qt.ApplicationModal) 17 | self.setMinimumWidth(485) 18 | self.setContentsMargins(20, 20, 20, 20) 19 | layout = QVBoxLayout() 20 | self.progress_label = QLabel(alignment=Qt.AlignCenter) 21 | self.progress = QProgressBar(self.parent) 22 | self.progress.setMinimum(0) 23 | self.progress.setMaximum(100) 24 | layout.addWidget(self.progress_label) 25 | layout.addWidget(self.progress) 26 | self.setLayout(layout) 27 | 28 | @pyqtSlot(int) 29 | def update_progress(self, progress: int) -> None: 30 | self.progress.setValue(progress) 31 | 32 | @pyqtSlot(str) 33 | def update_progress_label(self, progress_txt: str) -> None: 34 | if not self.isVisible(): 35 | self.show() 36 | self.progress_label.setText(progress_txt) 37 | 38 | @pyqtSlot() 39 | def download_complete(self) -> None: 40 | qApp.restoreOverrideCursor() 41 | self.close() 42 | 43 | def closeEvent(self, event: QCloseEvent) -> None: 44 | qApp.restoreOverrideCursor() 45 | self.cancelDownload.emit() 46 | super(DirectDownload, self).closeEvent(event) 47 | -------------------------------------------------------------------------------- /tvlinker/downloader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import platform 6 | import shlex 7 | import sys 8 | from distutils.spawn import find_executable 9 | 10 | from PyQt5.QtCore import QFileInfo, QProcess, QSize, pyqtSlot 11 | from PyQt5.QtWidgets import QDialog, QMessageBox, QTextEdit, QVBoxLayout, qApp 12 | 13 | 14 | class Downloader(QDialog): 15 | dltool_cmd = 'aria2c' 16 | dltool_args = '-x 12 -d {dl_path} {dl_link}' 17 | 18 | def __init__(self, link_url: str, dl_path: str, parent=None): 19 | super(Downloader, self).__init__(parent) 20 | self.parent = parent 21 | self.dltool_cmd = find_executable(self.download_cmd) 22 | self.download_link = link_url 23 | self.download_path = dl_path 24 | if self.dltool_cmd.strip(): 25 | self.dltool_args = self.dltool_args.format(dl_path=self.download_path, dl_link=self.download_link) 26 | self.console = QTextEdit(self.parent) 27 | self.console.setWindowTitle('%s Downloader' % qApp.applicationName()) 28 | self.proc = QProcess(self.parent) 29 | layout = QVBoxLayout() 30 | layout.addWidget(self.console) 31 | self.setLayout(layout) 32 | self.setFixedSize(QSize(400, 300)) 33 | else: 34 | QMessageBox.critical(self.parent, 'DOWNLOADER ERROR', '

The aria2c executable binary could not ' + 35 | 'be found in your installation folders. The binary comes packaged with this ' + 36 | 'application so it is likely that it was accidentally deleted via human ' + 37 | 'intervntion or incorrect file permissions are preventing access to it.

' + 38 | '

You may either download and install aria2 manually yourself, ensuring ' + 39 | 'its installation location is globally accessible via PATH environmnt variables or ' + 40 | 'simply reinstall this application again. If the issue is not resolved then try ' + 41 | 'to download the application again incase the orignal you installed already was ' + 42 | 'corrupted/broken.', buttons=QMessageBox.Close) 43 | 44 | def __del__(self) -> None: 45 | self.proc.terminate() 46 | if not self.proc.waitForFinished(10000): 47 | self.proc.kill() 48 | 49 | @staticmethod 50 | def get_machine_code() -> str: 51 | mcode = '' 52 | if sys.platform == 'darwin': 53 | mcode = 'macOS' 54 | elif sys.platform == 'win32' and platform.machine().endswith('86'): 55 | mcode = 'win32' 56 | elif sys.platform == 'win32' and platform.machine().endswith('64'): 57 | mcode = 'win64' 58 | elif sys.platform.startswith('linux') and platform.machine().endswith('86'): 59 | mcode = 'linux32' 60 | elif sys.platform.startswith('linux') and platform.machine().endswith('64'): 61 | mcode = 'linux64' 62 | return mcode 63 | 64 | @staticmethod 65 | def setup_aria() -> bool: 66 | aria_zip = Downloader.aria_clients()[Downloader.get_machine_code()]['bin_archive'] 67 | aria_install = Downloader.aria_clients()[Downloader.get_machine_code()]['target_path'] 68 | if os.path.exists(aria_zip): 69 | with ZipFile(aria_zip) as archive: 70 | target_path, target_file = os.path.split(aria_install) 71 | extracted_path = archive.extract(target_file, path=target_path) 72 | if extracted_path == aria_install and os.path.exists(extracted_path): 73 | if sys.platform != 'win32': 74 | os.chmod(extracted_path, 0o755) 75 | return True 76 | return False 77 | 78 | def init_proc(self) -> None: 79 | self.proc.setProcessChannelMode(QProcess.MergedChannels) 80 | self.proc.readyRead.connect(self.console_output) 81 | self.proc.setProgram(self.aria2_cmd) 82 | self.proc.setArguments(shlex.split(self.aria2_args)) 83 | 84 | def start(self) -> None: 85 | self.init_proc() 86 | self.show() 87 | self.proc.start() 88 | 89 | @pyqtSlot() 90 | def console_output(self) -> None: 91 | self.console.append(str(self.proc.readAllStandardOutput())) 92 | 93 | @pyqtSlot(QProcess.ProcessError) 94 | def cmd_error(self, error: QProcess.ProcessError) -> None: 95 | if error != QProcess.Crashed: 96 | QMessageBox.critical(self.parent, 'Error calling an external process', 97 | self.proc.errorString(), buttons=QMessageBox.Close) 98 | 99 | @staticmethod 100 | def get_path(path: str) -> str: 101 | prefix = sys._MEIPASS if getattr(sys, 'frozen', False) else QFileInfo(__file__).absolutePath() 102 | return os.path.join(prefix, path) 103 | -------------------------------------------------------------------------------- /tvlinker/filesize.py: -------------------------------------------------------------------------------- 1 | 2 | traditional = [ 3 | (1024 ** 5, 'P'), 4 | (1024 ** 4, 'T'), 5 | (1024 ** 3, 'G'), 6 | (1024 ** 2, 'M'), 7 | (1024 ** 1, 'K'), 8 | (1024 ** 0, 'B'), 9 | ] 10 | 11 | alternative = [ 12 | (1024 ** 5, ' PB'), 13 | (1024 ** 4, ' TB'), 14 | (1024 ** 3, ' GB'), 15 | (1024 ** 2, ' MB'), 16 | (1024 ** 1, ' KB'), 17 | (1024 ** 0, (' byte', ' bytes')), 18 | ] 19 | 20 | verbose = [ 21 | (1024 ** 5, (' petabyte', ' petabytes')), 22 | (1024 ** 4, (' terabyte', ' terabytes')), 23 | (1024 ** 3, (' gigabyte', ' gigabytes')), 24 | (1024 ** 2, (' megabyte', ' megabytes')), 25 | (1024 ** 1, (' kilobyte', ' kilobytes')), 26 | (1024 ** 0, (' byte', ' bytes')), 27 | ] 28 | 29 | iec = [ 30 | (1024 ** 5, 'Pi'), 31 | (1024 ** 4, 'Ti'), 32 | (1024 ** 3, 'Gi'), 33 | (1024 ** 2, 'Mi'), 34 | (1024 ** 1, 'Ki'), 35 | (1024 ** 0, ''), 36 | ] 37 | 38 | si = [ 39 | (1000 ** 5, 'P'), 40 | (1000 ** 4, 'T'), 41 | (1000 ** 3, 'G'), 42 | (1000 ** 2, 'M'), 43 | (1000 ** 1, 'K'), 44 | (1000 ** 0, 'B'), 45 | ] 46 | 47 | 48 | 49 | def size(bytes, system=traditional): 50 | """Human-readable file size. 51 | 52 | Using the traditional system, where a factor of 1024 is used:: 53 | 54 | >>> size(10) 55 | '10B' 56 | >>> size(100) 57 | '100B' 58 | >>> size(1000) 59 | '1000B' 60 | >>> size(2000) 61 | '1K' 62 | >>> size(10000) 63 | '9K' 64 | >>> size(20000) 65 | '19K' 66 | >>> size(100000) 67 | '97K' 68 | >>> size(200000) 69 | '195K' 70 | >>> size(1000000) 71 | '976K' 72 | >>> size(2000000) 73 | '1M' 74 | 75 | Using the SI system, with a factor 1000:: 76 | 77 | >>> size(10, system=si) 78 | '10B' 79 | >>> size(100, system=si) 80 | '100B' 81 | >>> size(1000, system=si) 82 | '1K' 83 | >>> size(2000, system=si) 84 | '2K' 85 | >>> size(10000, system=si) 86 | '10K' 87 | >>> size(20000, system=si) 88 | '20K' 89 | >>> size(100000, system=si) 90 | '100K' 91 | >>> size(200000, system=si) 92 | '200K' 93 | >>> size(1000000, system=si) 94 | '1M' 95 | >>> size(2000000, system=si) 96 | '2M' 97 | 98 | """ 99 | for factor, suffix in system: 100 | if bytes >= factor: 101 | break 102 | amount = int(bytes/factor) 103 | if isinstance(suffix, tuple): 104 | singular, multiple = suffix 105 | if amount == 1: 106 | suffix = singular 107 | else: 108 | suffix = multiple 109 | return str(amount) + suffix 110 | 111 | -------------------------------------------------------------------------------- /tvlinker/hosters.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | 6 | from bs4 import BeautifulSoup, SoupStrainer, Tag 7 | 8 | from PyQt5.QtCore import QSize, QUrl, Qt, pyqtSignal, pyqtSlot 9 | from PyQt5.QtGui import QCloseEvent, QColor, QDesktopServices, QIcon, QPalette 10 | from PyQt5.QtWidgets import (QDialog, QGraphicsDropShadowEffect, QHBoxLayout, QLabel, QMenu, QProgressBar, 11 | QProgressDialog, QPushButton, QScrollArea, QSizePolicy, QStyleFactory, 12 | QVBoxLayout, QWidget, qApp) 13 | 14 | 15 | class HosterLinks(QDialog): 16 | downloadLink = pyqtSignal(str) 17 | copyLink = pyqtSignal(str) 18 | 19 | def __init__(self, parent, f=Qt.WindowCloseButtonHint): 20 | super(HosterLinks, self).__init__(parent, f) 21 | self.parent = parent 22 | self.setObjectName('hosters') 23 | self.loading_progress = HosterProgress('Retrieving hoster links...', 0, 0, self.parent, 24 | Qt.WindowCloseButtonHint) 25 | self.layout = QVBoxLayout() 26 | self.layout.setSpacing(15) 27 | self.setLayout(self.layout) 28 | self.setWindowTitle('Hoster Links') 29 | self.setWindowModality(Qt.ApplicationModal) 30 | self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) 31 | 32 | def show_hosters(self, links: list) -> None: 33 | self.links = links 34 | self.loading_progress.cancel() 35 | hosterswidget_layout = QVBoxLayout() 36 | for tag in self.links: 37 | title_label = QLabel(HosterLinks.bs_tag_to_string(tag.find_previous('p')), self) 38 | title_label.setTextInteractionFlags(Qt.TextBrowserInteraction) 39 | title_label.setCursor(Qt.IBeamCursor) 40 | title_label.setOpenExternalLinks(True) 41 | title_label.setAlignment(Qt.AlignCenter) 42 | title_label.setStyleSheet('QLabel { margin: 0; color: #444; font-size: 14px; padding: 8px; ' + 43 | 'border: 1px solid #C0C0C0; background-color: #FEFEFE; }') 44 | title_layout = QHBoxLayout() 45 | title_layout.setContentsMargins(0, 0, 0, 0) 46 | title_layout.setSpacing(6) 47 | title_layout.addStretch(1) 48 | bs = BeautifulSoup(HosterLinks.bs_tag_to_string(tag), 'lxml', parse_only=SoupStrainer('a')) 49 | for anchor in bs: 50 | link = anchor['href'] 51 | hoster_name = HosterLinks.get_hoster_name(link) 52 | menu = QMenu(self) 53 | menu.setCursor(Qt.PointingHandCursor) 54 | menu.addAction(' COPY LINK', lambda dl=link: self.copy_link(dl), 0) 55 | menu.addAction(' OPEN LINK', lambda dl=link: self.open_link(dl), 0) 56 | menu.addAction(' DOWNLOAD', lambda dl=link: self.download_link(dl), 0) 57 | shadow = QGraphicsDropShadowEffect() 58 | shadow.setColor(Qt.gray) 59 | shadow.setBlurRadius(10) 60 | shadow.setOffset(2, 2) 61 | hoster_btn = QPushButton(self) 62 | hoster_btn.setStyle(QStyleFactory.create('Fusion')) 63 | hoster_btn.setGraphicsEffect(shadow) 64 | hoster_btn.setDefault(False) 65 | hoster_btn.setAutoDefault(False) 66 | hoster_btn.setToolTip(hoster_name) 67 | hoster_btn.setCursor(Qt.PointingHandCursor) 68 | hoster_btn.setIcon(QIcon(self.parent.get_path('images/hosters/%s.png' % hoster_name))) 69 | hoster_btn.setIconSize(QSize(100, 21)) 70 | hoster_btn.setStyleSheet(''' 71 | QPushButton { 72 | background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, 73 | stop: 0 #FEFEFE, stop: 1 #FAFAFA); 74 | padding: 6px 0; 75 | border-radius: 0; 76 | border: 1px solid #B9B9B9; 77 | } 78 | QPushButton:hover { 79 | background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, 80 | stop: 0 #B9B9B9, stop: 1 #DADBDE); 81 | } 82 | QPushButton:pressed { 83 | border: 1px inset #B9B9B9; 84 | background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, 85 | stop: 0 #DADBDE, stop: 1 #B9B9B9); 86 | } 87 | QPushButton::menu-indicator { left: -2000px; } 88 | ''') 89 | menu.setFixedWidth(118) 90 | menu.setStyleSheet(''' 91 | QMenu { 92 | border-radius: 0; 93 | border: 1px solid #C0C2C3; 94 | background-color: #FAFAFA; 95 | color: #4C4C4C; 96 | } 97 | QMenu::item { text-align: center; } 98 | QMenu::item:selected, QMenu::item:hover { background-color: #6A687D; color: #FFF; }''') 99 | hoster_btn.setMenu(menu) 100 | title_layout.addWidget(hoster_btn) 101 | title_layout.addStretch(1) 102 | hoster_layout = QVBoxLayout() 103 | hoster_layout.addWidget(title_label) 104 | hoster_layout.addLayout(title_layout) 105 | hosterswidget_layout.addLayout(hoster_layout) 106 | hosterswidget_layout.addSpacing(15) 107 | 108 | stretch_layout = QHBoxLayout() 109 | stretch_layout.addStretch(1) 110 | stretch_layout.addLayout(hosterswidget_layout) 111 | stretch_layout.addStretch(1) 112 | hosters_widget = QWidget(self) 113 | hosters_widget.setLayout(stretch_layout) 114 | scrollarea = QScrollArea(self) 115 | scrollarea.setFrameShape(QScrollArea.NoFrame) 116 | scrollarea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 117 | scrollarea.setWidget(hosters_widget) 118 | scrollarea.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) 119 | scrollarea.setMinimumWidth(hosters_widget.geometry().width() + 20) 120 | self.layout.addWidget(scrollarea) 121 | w = scrollarea.width() + 15 122 | h = hosters_widget.geometry().height() + 10 123 | self.setFixedSize(w if w <= 810 else 810, h if h <= 750 else 750) 124 | self.show() 125 | qApp.restoreOverrideCursor() 126 | 127 | @staticmethod 128 | def bs_tag_to_string(bstag: Tag) -> str: 129 | return ''.join(str(item) for item in bstag.contents) 130 | 131 | @staticmethod 132 | def get_hoster_name(link: str) -> str: 133 | name = QUrl(link).host().replace('www.', '').replace('.com', '').replace('.net', '') \ 134 | .replace('.org', '').replace('.co', '').replace('.to', '').replace('.download', '') 135 | if name == 'businessnewscurrent.online': 136 | name = 'cloudyfiles' 137 | elif name == 'drop': 138 | name = 'dropapk' 139 | elif name == 'nitro': 140 | name = 'nitroflare' 141 | elif name == 'ul': 142 | name = 'uploaded' 143 | return name 144 | 145 | @pyqtSlot(str) 146 | def copy_link(self, link: str) -> None: 147 | self.copyLink.emit(link) 148 | 149 | @pyqtSlot(str) 150 | def open_link(self, link: str) -> None: 151 | QDesktopServices.openUrl(QUrl(link)) 152 | self.close() 153 | 154 | @pyqtSlot(str) 155 | def download_link(self, link: str) -> None: 156 | self.downloadLink.emit(link) 157 | 158 | def closeEvent(self, event: QCloseEvent) -> None: 159 | self.loading_progress.cancel() 160 | self.deleteLater() 161 | qApp.restoreOverrideCursor() 162 | super(QDialog, self).closeEvent(event) 163 | 164 | 165 | class HosterProgress(QProgressDialog): 166 | def __init__(self, label: str, minval: int, maxval: int, parent: QWidget, flags: Qt.WindowFlags): 167 | super(HosterProgress, self).__init__(label, None, minval, maxval, parent, flags) 168 | self.setWindowTitle('Hoster Links') 169 | self.setMinimumWidth(485) 170 | self.setWindowModality(Qt.ApplicationModal) 171 | self.setLabelText(label) 172 | bar = QProgressBar(self) 173 | bar.setRange(minval, maxval) 174 | bar.setValue(minval) 175 | bar.setStyle(QStyleFactory.create('Fusion')) 176 | p = bar.palette() 177 | p.setColor(QPalette.Highlight, QColor(100, 44, 104, 185)) 178 | bar.setPalette(p) 179 | self.setBar(bar) 180 | self.show() 181 | -------------------------------------------------------------------------------- /tvlinker/jsfuck.py: -------------------------------------------------------------------------------- 1 | MAPPING = { 2 | 'a': '(false+"")[1]', 3 | 'b': '([]["entries"]()+"")[2]', 4 | 'c': '([]["fill"]+"")[3]', 5 | 'd': '(undefined+"")[2]', 6 | 'e': '(true+"")[3]', 7 | 'f': '(false+"")[0]', 8 | 'g': '(false+[0]+String)[20]', 9 | 'h': '(+(101))["to"+String["name"]](21)[1]', 10 | 'i': '([false]+undefined)[10]', 11 | 'j': '([]["entries"]()+"")[3]', 12 | 'k': '(+(20))["to"+String["name"]](21)', 13 | 'l': '(false+"")[2]', 14 | 'm': '(Number+"")[11]', 15 | 'n': '(undefined+"")[1]', 16 | 'o': '(true+[]["fill"])[10]', 17 | 'p': '(+(211))["to"+String["name"]](31)[1]', 18 | 'q': '(+(212))["to"+String["name"]](31)[1]', 19 | 'r': '(true+"")[1]', 20 | 's': '(false+"")[3]', 21 | 't': '(true+"")[0]', 22 | 'u': '(undefined+"")[0]', 23 | 'v': '(+(31))["to"+String["name"]](32)', 24 | 'w': '(+(32))["to"+String["name"]](33)', 25 | 'x': '(+(101))["to"+String["name"]](34)[1]', 26 | 'y': '(NaN+[Infinity])[10]', 27 | 'z': '(+(35))["to"+String["name"]](36)', 28 | 'A': '(+[]+Array)[10]', 29 | 'B': '(+[]+Boolean)[10]', 30 | 'C': 'Function("return escape")()(("")["italics"]())[2]', 31 | 'D': 'Function("return escape")()([]["fill"])["slice"]("-1")', 32 | 'E': '(RegExp+"")[12]', 33 | 'F': '(+[]+Function)[10]', 34 | 'G': '(false+Function("return Date")()())[30]', 35 | 'I': '(Infinity+"")[0]', 36 | 'M': '(true+Function("return Date")()())[30]', 37 | 'N': '(NaN+"")[0]', 38 | 'O': '(NaN+Function("return{}")())[11]', 39 | 'R': '(+[]+RegExp)[10]', 40 | 'S': '(+[]+String)[10]', 41 | 'T': '(NaN+Function("return Date")()())[30]', 42 | 'U': '(NaN+Function("return{}")()["to"+String["name"]]["call"]())[11]', 43 | ' ': '(NaN+[]["fill"])[11]', 44 | '"': '("")["fontcolor"]()[12]', 45 | '%': 'Function("return escape")()([]["fill"])[21]', 46 | '&': '("")["link"](0+")[10]', 47 | '(': '(undefined+[]["fill"])[22]', 48 | ')': '([0]+false+[]["fill"])[20]', 49 | '+': '(+(+!+[]+(!+[]+[])[!+[]+!+[]+!+[]]+[+!+[]]+[+[]]+[+[]])+[])[2]', 50 | ',': '([]["slice"]["call"](false+"")+"")[1]', 51 | '-': '(+(.+[0000000001])+"")[2]', 52 | '.': '(+(+!+[]+[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+[!+[]+!+[]]+[+[]])+[])[+!+[]]', 53 | '/': '(false+[0])["italics"]()[10]', 54 | ':': '(RegExp()+"")[3]', 55 | ';': '("")["link"](")[14]', 56 | '<': '("")["italics"]()[0]', 57 | '=': '("")["fontcolor"]()[11]', 58 | '>': '("")["italics"]()[2]', 59 | '?': '(RegExp()+"")[2]', 60 | '[': '([]["entries"]()+"")[0]', 61 | ']': '([]["entries"]()+"")[22]', 62 | '{': '(true+[]["fill"])[20]', 63 | '}': '([]["fill"]+"")["slice"]("-1")' 64 | } 65 | 66 | SIMPLE = { 67 | 'false': '![]', 68 | 'true': '!![]', 69 | 'undefined': '[][[]]', 70 | 'NaN': '+[![]]', 71 | 'Infinity': '+(+!+[]+(!+[]+[])[!+[]+!+[]+!+[]]+[+!+[]]+[+[]]+[+[]]+[+[]])' # +"1e1000" 72 | } 73 | 74 | CONSTRUCTORS = { 75 | 'Array': '[]', 76 | 'Number': '(+[])', 77 | 'String': '([]+[])', 78 | 'Boolean': '(![])', 79 | 'Function': '[]["fill"]', 80 | 'RegExp': 'Function("return/"+false+"/")()' 81 | } 82 | 83 | def jsunfuck(jsfuckString): 84 | 85 | for key in sorted(MAPPING, key=lambda k: len(MAPPING[k]), reverse=True): 86 | if MAPPING.get(key) in jsfuckString: 87 | jsfuckString = jsfuckString.replace(MAPPING.get(key), '"{}"'.format(key)) 88 | 89 | for key in sorted(SIMPLE, key=lambda k: len(SIMPLE[k]), reverse=True): 90 | if SIMPLE.get(key) in jsfuckString: 91 | jsfuckString = jsfuckString.replace(SIMPLE.get(key), '{}'.format(key)) 92 | 93 | #for key in sorted(CONSTRUCTORS, key=lambda k: len(CONSTRUCTORS[k]), reverse=True): 94 | # if CONSTRUCTORS.get(key) in jsfuckString: 95 | # jsfuckString = jsfuckString.replace(CONSTRUCTORS.get(key), '{}'.format(key)) 96 | 97 | return jsfuckString 98 | -------------------------------------------------------------------------------- /tvlinker/notify.py: -------------------------------------------------------------------------------- 1 | """This is a pure-python replacement for notify-python, using python-dbus 2 | to communicate with the notifications server directly. It's compatible with 3 | Python 2 and 3, and its callbacks can work with Gtk 3 or Qt 4 applications. 4 | 5 | To use it, first call ``notify2.init('app name')``, then create and show notifications:: 6 | 7 | n = notify2.Notification("Summary", 8 | "Some body text", 9 | "notification-message-im" # Icon name 10 | ) 11 | n.show() 12 | 13 | To see more of what's possible, refer to docstrings of methods and objects. 14 | 15 | Based on the notifications spec at: 16 | http://developer.gnome.org/notification-spec/ 17 | 18 | Porting applications from pynotify 19 | ---------------------------------- 20 | 21 | There are a few differences from pynotify you should be aware of: 22 | 23 | - If you need callbacks from notifications, notify2 must know about your event 24 | loop. The simplest way is to pass 'glib' or 'qt' as the ``mainloop`` parameter 25 | to ``init``. 26 | - The methods ``attach_to_widget`` and ``attach_to_status_icon`` are not 27 | implemented. You can calculate the location you want the notification to 28 | appear and call ``Notification``. 29 | - ``set_property`` and ``get_property`` are not implemented. The summary, body 30 | and icon are accessible as attributes of a ``Notification`` instance. 31 | - Various methods that pynotify Notification instances got from gobject do not 32 | exist, or only implement part of the functionality. 33 | 34 | Several pynotify functions, especially getters and setters, are only supported 35 | for compatibility. You are encouraged to use more direct, Pythonic alternatives. 36 | """ 37 | 38 | import dbus 39 | 40 | # Constants 41 | EXPIRES_DEFAULT = -1 42 | EXPIRES_NEVER = 0 43 | 44 | URGENCY_LOW = 0 45 | URGENCY_NORMAL = 1 46 | URGENCY_CRITICAL = 2 47 | urgency_levels = [URGENCY_LOW, URGENCY_NORMAL, URGENCY_CRITICAL] 48 | 49 | # Initialise the module (following pynotify's API) ----------------------------- 50 | 51 | initted = False 52 | appname = "" 53 | _have_mainloop = False 54 | 55 | 56 | class UninittedError(RuntimeError): 57 | pass 58 | 59 | 60 | class UninittedDbusObj(object): 61 | def __getattr__(self, name): 62 | raise UninittedError("You must call notify2.init() before using the " 63 | "notification features.") 64 | 65 | 66 | dbus_iface = UninittedDbusObj() 67 | 68 | 69 | def init(app_name, mainloop=None): 70 | """Initialise the Dbus connection. 71 | 72 | To get callbacks from notifications, DBus must be integrated with a mainloop. 73 | There are three ways to achieve this: 74 | 75 | - Set a default mainloop (dbus.set_default_main_loop) before calling init() 76 | - Pass the mainloop parameter as a string 'glib' or 'qt' to integrate with 77 | those mainloops. (N.B. passing 'qt' currently makes that the default dbus 78 | mainloop, because that's the only way it seems to work.) 79 | - Pass the mainloop parameter a DBus compatible mainloop instance, such as 80 | dbus.mainloop.glib.DBusGMainLoop(). 81 | 82 | If you only want to display notifications, without receiving information 83 | back from them, you can safely omit mainloop. 84 | """ 85 | global appname, initted, dbus_iface, _have_mainloop 86 | 87 | if mainloop == 'glib': 88 | from dbus.mainloop.glib import DBusGMainLoop 89 | mainloop = DBusGMainLoop() 90 | elif mainloop == 'qt': 91 | from dbus.mainloop.qt import DBusQtMainLoop 92 | # For some reason, this only works if we make it the default mainloop 93 | # for dbus. That might make life tricky for anyone trying to juggle two 94 | # event loops, but I can't see any way round it. 95 | mainloop = DBusQtMainLoop(set_as_default=True) 96 | 97 | bus = dbus.SessionBus(mainloop=mainloop) 98 | 99 | dbus_obj = bus.get_object('org.freedesktop.Notifications', 100 | '/org/freedesktop/Notifications') 101 | dbus_iface = dbus.Interface(dbus_obj, 102 | dbus_interface='org.freedesktop.Notifications') 103 | appname = app_name 104 | initted = True 105 | 106 | if mainloop or dbus.get_default_main_loop(): 107 | _have_mainloop = True 108 | dbus_iface.connect_to_signal('ActionInvoked', _action_callback) 109 | dbus_iface.connect_to_signal('NotificationClosed', _closed_callback) 110 | 111 | return True 112 | 113 | 114 | def is_initted(): 115 | """Has init() been called? Only exists for compatibility with pynotify. 116 | """ 117 | return initted 118 | 119 | 120 | def get_app_name(): 121 | """Return appname. Only exists for compatibility with pynotify. 122 | """ 123 | return appname 124 | 125 | 126 | def uninit(): 127 | """Undo what init() does.""" 128 | global initted, dbus_iface, _have_mainloop 129 | initted = False 130 | _have_mainloop = False 131 | dbus_iface = UninittedDbusObj() 132 | 133 | 134 | # Retrieve basic server information -------------------------------------------- 135 | 136 | def get_server_caps(): 137 | """Get a list of server capabilities. 138 | """ 139 | return [str(x) for x in dbus_iface.GetCapabilities()] 140 | 141 | 142 | def get_server_info(): 143 | """Get basic information about the server. 144 | """ 145 | res = dbus_iface.GetServerInformation() 146 | return {'name': str(res[0]), 147 | 'vendor': str(res[1]), 148 | 'version': str(res[2]), 149 | 'spec-version': str(res[3]), 150 | } 151 | 152 | 153 | # Action callbacks ------------------------------------------------------------- 154 | 155 | notifications_registry = {} 156 | 157 | 158 | def _action_callback(nid, action): 159 | nid, action = int(nid), str(action) 160 | n = notifications_registry[nid] 161 | n._action_callback(action) 162 | 163 | 164 | def _closed_callback(nid, reason): 165 | nid, reason = int(nid), int(reason) 166 | n = notifications_registry[nid] 167 | n._closed_callback(n) 168 | del notifications_registry[nid] 169 | 170 | 171 | def no_op(*args): 172 | """No-op function for callbacks. 173 | """ 174 | pass 175 | 176 | 177 | # Controlling notifications ---------------------------------------------------- 178 | 179 | class Notification(object): 180 | id = 0 181 | timeout = -1 # -1 = server default settings 182 | _closed_callback = no_op 183 | 184 | def __init__(self, summary, message='', icon=''): 185 | self.summary = summary 186 | self.message = message 187 | self.icon = icon 188 | self.hints = {} 189 | self.actions = {} 190 | self.data = {} # Any data the user wants to attach 191 | 192 | def show(self): 193 | """Ask the server to show the notification. 194 | """ 195 | nid = dbus_iface.Notify(appname, # app_name (spec names) 196 | self.id, # replaces_id 197 | self.icon, # app_icon 198 | self.summary, # summary 199 | self.message, # body 200 | self._make_actions_array(), # actions 201 | self.hints, # hints 202 | self.timeout, # expire_timeout 203 | ) 204 | 205 | self.id = int(nid) 206 | 207 | if _have_mainloop: 208 | notifications_registry[self.id] = self 209 | return True 210 | 211 | def update(self, summary, message="", icon=None): 212 | """Replace the summary and body of the notification, and optionally its 213 | icon. You should call show() again after this to display the updated 214 | notification. 215 | """ 216 | self.summary = summary 217 | self.message = message 218 | if icon is not None: 219 | self.icon = icon 220 | 221 | def close(self): 222 | """Ask the server to close this notification. 223 | """ 224 | if self.id != 0: 225 | dbus_iface.CloseNotification(self.id) 226 | 227 | def set_hint(self, key, value): 228 | """n.set_hint(key, value) <--> n.hints[key] = value 229 | 230 | Only exists for compatibility with pynotify. 231 | """ 232 | self.hints[key] = value 233 | 234 | set_hint_string = set_hint_int32 = set_hint_double = set_hint 235 | 236 | def set_hint_byte(self, key, value): 237 | """Set a hint with a dbus byte value. The input value can be an 238 | integer or a bytes string of length 1. 239 | """ 240 | self.hints[key] = dbus.Byte(value) 241 | 242 | def set_urgency(self, level): 243 | """Set the urgency level to one of URGENCY_LOW, URGENCY_NORMAL or 244 | URGENCY_CRITICAL. 245 | """ 246 | if level not in urgency_levels: 247 | raise ValueError("Unknown urgency level specified", level) 248 | self.set_hint_byte("urgency", level) 249 | 250 | def set_category(self, category): 251 | """Set the 'category' hint for this notification. 252 | """ 253 | self.hints['category'] = category 254 | 255 | def set_timeout(self, timeout): 256 | """Set the display duration in milliseconds, or one of the special 257 | values EXPIRES_DEFAULT or EXPIRES_NEVER. 258 | 259 | Only exists for compatibility with pynotify; you can simply set:: 260 | 261 | n.timeout = 5000 262 | """ 263 | if not isinstance(timeout, int): 264 | raise TypeError("timeout value was not int", timeout) 265 | self.timeout = timeout 266 | 267 | def get_timeout(self): 268 | """Return the timeout value for this notification. 269 | 270 | Only exists for compatibility with pynotify; you can inspect the 271 | timeout attribute directly. 272 | """ 273 | return self.timeout 274 | 275 | def add_action(self, action, label, callback, user_data=None): 276 | """Add an action to the notification (if the server supports it). 277 | 278 | action : str 279 | A brief key. 280 | label : str 281 | The text displayed on the action button 282 | callback : callable 283 | A function taking at 2-3 parameters: the Notification object, the 284 | action key and (if specified) the user_data. 285 | user_data : 286 | An extra argument to pass to the callback. 287 | """ 288 | self.actions[action] = (label, callback, user_data) 289 | 290 | def _make_actions_array(self): 291 | """Make the actions array to send over DBus. 292 | """ 293 | arr = [] 294 | for action, (label, callback, user_data) in self.actions.items(): 295 | arr.append(action) 296 | arr.append(label) 297 | return arr 298 | 299 | def _action_callback(self, action): 300 | """Called when the user selects an action on the notification, to 301 | dispatch it to the relevant user-specified callback. 302 | """ 303 | try: 304 | label, callback, user_data = self.actions[action] 305 | except KeyError: 306 | return 307 | 308 | if user_data is None: 309 | callback(self, action) 310 | else: 311 | callback(self, action, user_data) 312 | 313 | def connect(self, event, callback): 314 | """Set the callback for the notification closing; the only valid value 315 | for event is 'closed'. The API is compatible with pynotify. 316 | """ 317 | if event != 'closed': 318 | raise ValueError("'closed' is the only valid value for event", event) 319 | self._closed_callback = callback 320 | 321 | def set_data(self, key, value): 322 | """n.set_data(key, value) <--> n.data[key] = value 323 | 324 | Only exists for compatibility with pynotify. 325 | """ 326 | self.data[key] = value 327 | 328 | def get_data(self, key): 329 | """n.get_data(key) <--> n.data[key] 330 | 331 | Only exists for compatibility with pynotify. 332 | """ 333 | return self.data[key] 334 | 335 | def set_icon_from_pixbuf(self, icon): 336 | """Set a custom icon from a GdkPixbuf. 337 | """ 338 | struct = ( 339 | icon.get_width(), 340 | icon.get_height(), 341 | icon.get_rowstride(), 342 | icon.get_has_alpha(), 343 | icon.get_bits_per_sample(), 344 | icon.get_n_channels(), 345 | dbus.ByteArray(icon.get_pixels()) 346 | ) 347 | self.hints['icon_data'] = struct 348 | 349 | def set_location(self, x, y): 350 | """Set the notification location as (x, y), if the server supports it. 351 | """ 352 | if (not isinstance(x, int)) or (not isinstance(y, int)): 353 | raise TypeError("x and y must both be ints", (x, y)) 354 | self.hints['x'] = x 355 | self.hints['y'] = y 356 | -------------------------------------------------------------------------------- /tvlinker/progress.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from PyQt5.QtCore import pyqtSlot 5 | from PyQt5.QtDBus import QDBusConnection, QDBusMessage 6 | from PyQt5.QtWidgets import qApp, QWidget 7 | 8 | 9 | class TaskbarProgress(QWidget): 10 | def __init__(self, parent=None): 11 | super(TaskbarProgress, self).__init__(parent) 12 | self._desktopfile = 'application://{}.desktop'.format(qApp.applicationName().lower()) 13 | self._sessionbus = QDBusConnection.sessionBus() 14 | self.clear() 15 | 16 | @pyqtSlot() 17 | def clear(self): 18 | self.setProgress(0.0, False) 19 | 20 | @pyqtSlot(float, bool) 21 | def setProgress(self, value: float, visible: bool=True): 22 | signal = QDBusMessage.createSignal('/com/canonical/unity/launcherentry/337963624', 23 | 'com.canonical.Unity.LauncherEntry', 'Update') 24 | message = signal << self._desktopfile << {'progress-visible': visible, 'progress': value} 25 | self._sessionbus.send(message) 26 | -------------------------------------------------------------------------------- /tvlinker/pyload.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import json 5 | from urllib.parse import urlencode, urljoin 6 | from urllib.request import urlopen 7 | 8 | 9 | class PyloadConnection: 10 | def __init__(self, host, username, password): 11 | self.url_base = urljoin('%s' % host, 'api/') 12 | self.session = self._call('login', {'username': username, 'password': password}, False) 13 | 14 | def _call(self, name, args={}, encode=True): 15 | url = urljoin(self.url_base, name) 16 | if encode: 17 | data = {} 18 | for key, value in args.items(): 19 | data[key] = json.dumps(value) 20 | else: 21 | data = args 22 | if hasattr(self, 'session'): 23 | data['session'] = self.session 24 | post = urlencode(data).encode('utf-8') 25 | return json.loads(urlopen(url, post).read().decode('utf-8')) 26 | 27 | def __getattr__(self, name): 28 | def wrapper(**kargs): 29 | return self._call(name, kargs) 30 | 31 | return wrapper 32 | -------------------------------------------------------------------------------- /tvlinker/settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | 6 | from PyQt5.QtCore import pyqtSlot, QSettings, QSize, Qt 7 | from PyQt5.QtGui import QCloseEvent, QIcon, QKeyEvent, QPixmap 8 | from PyQt5.QtWidgets import (QAbstractItemView, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFormLayout, 9 | QGroupBox, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, QPushButton, 10 | QSizePolicy, QStackedWidget, QTabWidget, QVBoxLayout, QWidget, qApp) 11 | 12 | 13 | class Settings(QDialog): 14 | def __init__(self, parent, settings: QSettings, f=Qt.WindowCloseButtonHint): 15 | super(Settings, self).__init__(parent, f) 16 | self.parent = parent 17 | self.settings = settings 18 | self.setWindowModality(Qt.ApplicationModal) 19 | self.tab_general = GeneralTab(self.settings) 20 | self.tab_favorites = FavoritesTab(self.settings) 21 | tabs = QTabWidget() 22 | tabs.addTab(self.tab_general, 'General') 23 | tabs.addTab(self.tab_favorites, 'Favorites') 24 | button_box = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel, Qt.Horizontal, self) 25 | button_box.accepted.connect(self.save_settings) 26 | button_box.rejected.connect(self.close) 27 | layout = QVBoxLayout() 28 | layout.addWidget(tabs) 29 | layout.addWidget(button_box) 30 | self.setLayout(layout) 31 | self.setWindowTitle('%s Settings' % qApp.applicationName()) 32 | self.setWindowIcon(self.parent.icon_settings) 33 | 34 | def save_settings(self) -> None: 35 | self.tab_general.save() 36 | self.tab_favorites.save() 37 | self.parent.init_settings() 38 | self.close() 39 | 40 | def keyPressEvent(self, event: QKeyEvent) -> None: 41 | if event.key() in (Qt.Key_Enter, Qt.Key_Return): 42 | return 43 | super(Settings, self).keyPressEvent(event) 44 | 45 | def closeEvent(self, event: QCloseEvent) -> None: 46 | self.tab_general.deleteLater() 47 | self.tab_favorites.deleteLater() 48 | self.deleteLater() 49 | event.accept() 50 | 51 | 52 | class GeneralTab(QWidget): 53 | def __init__(self, settings: QSettings): 54 | super(GeneralTab, self).__init__() 55 | self.settings = settings 56 | 57 | realdebrid_logo = QLabel(pixmap=QPixmap(':assets/images/realdebrid.png')) 58 | self.realdebridtoken_lineEdit = QLineEdit(self, text=self.settings.value('realdebrid_apitoken')) 59 | self.realdebridtoken_lineEdit.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed) 60 | self.realdebridtoken_lineEdit.setMinimumWidth(250) 61 | apitoken_link = 'get your API token here' 63 | realdebrid_apitoken_link = QLabel(text=apitoken_link, textFormat=Qt.RichText, openExternalLinks=True) 64 | realdebrid_formLayout = QFormLayout(labelAlignment=Qt.AlignRight) 65 | realdebrid_formLayout.addRow('API Token:', self.realdebridtoken_lineEdit) 66 | if len(self.realdebridtoken_lineEdit.text()): 67 | self.realdebridproxy_checkBox = QCheckBox('Use ShadowSocks proxy for API connections', self) 68 | self.realdebridproxy_checkBox.setChecked(self.settings.value('realdebrid_apiproxy', False, bool)) 69 | self.realdebridproxy_checkBox.setCursor(Qt.PointingHandCursor) 70 | realdebrid_formLayout.addRow('', self.realdebridproxy_checkBox) 71 | else: 72 | realdebrid_formLayout.addRow('', realdebrid_apitoken_link) 73 | realdebrid_layout = QHBoxLayout() 74 | realdebrid_layout.addWidget(realdebrid_logo) 75 | realdebrid_layout.addSpacing(15) 76 | realdebrid_layout.addLayout(realdebrid_formLayout) 77 | realdebrid_group = QGroupBox() 78 | realdebrid_group.setLayout(realdebrid_layout) 79 | 80 | self.dlmanager_comboBox = QComboBox(self, editable=False, cursor=Qt.PointingHandCursor) 81 | self.dlmanager_comboBox.setAutoFillBackground(True) 82 | self.dlmanager_comboBox.addItems(('built-in', 'aria2')) 83 | if sys.platform == 'win32': 84 | self.dlmanager_comboBox.addItem('IDM') 85 | if sys.platform.startswith('linux'): 86 | self.dlmanager_comboBox.addItem('KGet') 87 | self.dlmanager_comboBox.addItems(('Persepolis', 'pyLoad')) 88 | self.dlmanager_comboBox.setCurrentIndex(self.dlmanager_comboBox.findText( 89 | str(self.settings.value('download_manager')), Qt.MatchFixedString)) 90 | 91 | # self.dlpagecount_comboBox = QComboBox(self, toolTip='Default Page Count', editable=False, 92 | # cursor=Qt.PointingHandCursor) 93 | # self.dlpagecount_comboBox.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 94 | # self.dlpagecount_comboBox.addItems(('10', '20', '30', '40', '50')) 95 | # self.dlpagecount_comboBox.setCurrentIndex(self.dlpagecount_comboBox.findText( 96 | # str(self.settings.value('dl_pagecount')), Qt.MatchFixedString)) 97 | 98 | minchars = 20 99 | 100 | self.dlmanager_comboBox.setMinimumContentsLength(minchars) 101 | # self.dlpagecount_comboBox.setMinimumContentsLength(minchars) 102 | 103 | general_formlayout = QFormLayout(labelAlignment=Qt.AlignRight) 104 | general_formlayout.addRow('Download Manager:', self.dlmanager_comboBox) 105 | general_layout = QHBoxLayout() 106 | general_layout.addStretch(1) 107 | general_layout.addLayout(general_formlayout) 108 | general_layout.addStretch(1) 109 | 110 | directdl_label = QLabel('No settings for built-in downloader') 111 | directdl_label.setStyleSheet('font-weight:300; text-align:center;') 112 | directdl_label.setAlignment(Qt.AlignCenter) 113 | 114 | self.aria2rpchost_lineEdit = QLineEdit(self, text=self.settings.value('aria2_rpc_host')) 115 | self.aria2rpchost_lineEdit.setPlaceholderText('http://localhost') 116 | self.aria2rpchost_lineEdit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) 117 | self.aria2rpcport_lineEdit = QLineEdit(self, text=self.settings.value('aria2_rpc_port')) 118 | self.aria2rpcport_lineEdit.setPlaceholderText('6800') 119 | self.aria2rpcport_lineEdit.setFixedWidth(100) 120 | self.aria2rpcsecret_lineEdit = QLineEdit(self, text=self.settings.value('aria2_rpc_secret')) 121 | self.aria2rpcsecret_lineEdit.setFixedWidth(100) 122 | self.aria2rpcsecret_lineEdit.setEchoMode(QLineEdit.PasswordEchoOnEdit) 123 | self.aria2rpcuser_lineEdit = QLineEdit(self, text=self.settings.value('aria2_rpc_username')) 124 | self.aria2rpcuser_lineEdit.setFixedWidth(150) 125 | self.aria2rpcpass_lineEdit = QLineEdit(self, text=self.settings.value('aria2_rpc_password')) 126 | self.aria2rpcpass_lineEdit.setFixedWidth(150) 127 | self.aria2rpcpass_lineEdit.setEchoMode(QLineEdit.PasswordEchoOnEdit) 128 | aria2_formLayout = QFormLayout(labelAlignment=Qt.AlignRight) 129 | aria2_formLayout.addRow('RPC Host:', self.aria2rpchost_lineEdit) 130 | aria2_formLayout_left = QFormLayout(labelAlignment=Qt.AlignRight) 131 | aria2_formLayout_left.addRow('RPC Port:', self.aria2rpcport_lineEdit) 132 | aria2_formLayout_left.addRow('RPC Secret:', self.aria2rpcsecret_lineEdit) 133 | aria2_formLayout_right = QFormLayout(labelAlignment=Qt.AlignRight) 134 | aria2_formLayout_right.addRow('RPC Username:', self.aria2rpcuser_lineEdit) 135 | aria2_formLayout_right.addRow('RPC Password:', self.aria2rpcpass_lineEdit) 136 | aria2_formLayout_hbox = QHBoxLayout() 137 | aria2_formLayout_hbox.addStretch(1) 138 | aria2_formLayout_hbox.addLayout(aria2_formLayout_left) 139 | aria2_formLayout_hbox.addStretch(1) 140 | aria2_formLayout_hbox.addLayout(aria2_formLayout_right) 141 | aria2_formLayout_hbox.addStretch(1) 142 | aria2_formLayout.addRow(aria2_formLayout_hbox) 143 | aria2_settings = QWidget(self) 144 | aria2_settings.setLayout(aria2_formLayout) 145 | 146 | self.dlmanagersettings_stack = QStackedWidget() 147 | self.dlmanagersettings_stack.addWidget(directdl_label) 148 | self.dlmanagersettings_stack.addWidget(aria2_settings) 149 | 150 | self.persepoliscmd_lineEdit = QLineEdit(self, text=self.settings.value('persepolis_cmd')) 151 | self.persepoliscmd_lineEdit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) 152 | persepolis_formLayout = QFormLayout(labelAlignment=Qt.AlignRight) 153 | persepolis_formLayout.addRow('persepolis command:', self.persepoliscmd_lineEdit) 154 | persepolis_settings = QWidget(self) 155 | persepolis_settings.setLayout(persepolis_formLayout) 156 | 157 | self.pyloadhost_lineEdit = QLineEdit(self, text=self.settings.value('pyload_host')) 158 | self.pyloadhost_lineEdit.setPlaceholderText('http://localhost:8000') 159 | self.pyloadhost_lineEdit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) 160 | self.pyloaduser_lineEdit = QLineEdit(self, text=self.settings.value('pyload_username')) 161 | self.pyloaduser_lineEdit.setFixedWidth(200) 162 | self.pyloadpass_lineEdit = QLineEdit(self, text=self.settings.value('pyload_password')) 163 | self.pyloadpass_lineEdit.setFixedWidth(200) 164 | pyload_formLayout = QFormLayout(labelAlignment=Qt.AlignRight) 165 | pyload_formLayout.addRow('pyLoad Host:', self.pyloadhost_lineEdit) 166 | pyload_formLayout.addRow('pyLoad Username:', self.pyloaduser_lineEdit) 167 | pyload_formLayout.addRow('pyLoad Password:', self.pyloadpass_lineEdit) 168 | pyload_settings = QWidget(self) 169 | pyload_settings.setLayout(pyload_formLayout) 170 | 171 | if sys.platform == 'win32': 172 | self.idmexepath_lineEdit = QLineEdit(self, text=self.settings.value('idm_exe_path')) 173 | self.idmexepath_lineEdit.setPlaceholderText('C:\Program Files (x86)\Internet Download Manager\IDMan.exe') 174 | self.idmexepath_lineEdit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) 175 | idm_formLayout = QFormLayout(labelAlignment=Qt.AlignRight) 176 | idm_formLayout.addRow('IDM EXE Path:', self.idmexepath_lineEdit) 177 | idm_settings = QWidget(self) 178 | idm_settings.setLayout(idm_formLayout) 179 | self.dlmanagersettings_stack.addWidget(idm_settings) 180 | 181 | if sys.platform.startswith('linux'): 182 | self.kgetcmd_lineEdit = QLineEdit(self, text=self.settings.value('kget_cmd')) 183 | self.kgetcmd_lineEdit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) 184 | kget_formLayout = QFormLayout(labelAlignment=Qt.AlignRight) 185 | kget_formLayout.addRow('kget command:', self.kgetcmd_lineEdit) 186 | kget_settings = QWidget(self) 187 | kget_settings.setLayout(kget_formLayout) 188 | self.dlmanagersettings_stack.addWidget(kget_settings) 189 | 190 | self.dlmanagersettings_stack.addWidget(persepolis_settings) 191 | self.dlmanagersettings_stack.addWidget(pyload_settings) 192 | 193 | self.dlmanagersettings_stack.setCurrentIndex(self.dlmanager_comboBox.currentIndex()) 194 | self.dlmanager_comboBox.currentIndexChanged.connect(self.dlmanagersettings_stack.setCurrentIndex) 195 | 196 | general_formlayout.addRow(self.dlmanagersettings_stack) 197 | general_group = QGroupBox() 198 | general_group.setLayout(general_layout) 199 | 200 | tab_layout = QVBoxLayout() 201 | tab_layout.addStretch(1) 202 | tab_layout.addWidget(realdebrid_group) 203 | tab_layout.addWidget(general_group) 204 | tab_layout.addStretch(1) 205 | 206 | self.setLayout(tab_layout) 207 | 208 | def save(self) -> None: 209 | # self.settings.setValue('dl_pagecount', self.dlpagecount_comboBox.currentText()) 210 | self.settings.setValue('realdebrid_apitoken', self.realdebridtoken_lineEdit.text()) 211 | if hasattr(self, 'realdebridproxy_checkBox'): 212 | self.settings.setValue('realdebrid_apiproxy', self.realdebridproxy_checkBox.isChecked()) 213 | self.settings.setValue('download_manager', self.dlmanager_comboBox.currentText().lower()) 214 | if self.dlmanager_comboBox.currentText() == 'aria2': 215 | self.settings.setValue('aria2_rpc_host', self.aria2rpchost_lineEdit.text()) 216 | self.settings.setValue('aria2_rpc_port', self.aria2rpcport_lineEdit.text()) 217 | self.settings.setValue('aria2_rpc_secret', self.aria2rpcsecret_lineEdit.text()) 218 | self.settings.setValue('aria2_rpc_username', self.aria2rpcuser_lineEdit.text()) 219 | self.settings.setValue('aria2_rpc_password', self.aria2rpcpass_lineEdit.text()) 220 | elif self.dlmanager_comboBox.currentText() == 'pyLoad': 221 | self.settings.setValue('pyload_host', self.pyloadhost_lineEdit.text()) 222 | self.settings.setValue('pyload_username', self.pyloaduser_lineEdit.text()) 223 | self.settings.setValue('pyload_password', self.pyloadpass_lineEdit.text()) 224 | elif self.dlmanager_comboBox.currentText() == 'IDM': 225 | self.settings.setValue('idm_exe_path', self.idmexepath_lineEdit.text()) 226 | elif self.dlmanager_comboBox.currentText() == 'KGet': 227 | self.settings.setValue('kget_cmd', self.kgetcmd_lineEdit.text()) 228 | elif self.dlmanager_comboBox.currentText() == 'Persepolis': 229 | self.settings.setValue('persepolis_cmd', self.persepoliscmd_lineEdit.text()) 230 | 231 | 232 | class FavoritesTab(QWidget): 233 | def __init__(self, settings: QSettings): 234 | super(FavoritesTab, self).__init__() 235 | self.settings = settings 236 | faves_formLayout = QFormLayout(labelAlignment=Qt.AlignRight) 237 | self.faves_lineEdit = QLineEdit(self) 238 | self.faves_lineEdit.returnPressed.connect(self.add_item) 239 | self.faves_lineEdit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) 240 | faves_addItemButton = QPushButton(parent=self, flat=False, cursor=Qt.PointingHandCursor, text='Add', 241 | icon=QIcon(':assets/images/plus.png'), toolTip='Add item', 242 | clicked=self.add_item) 243 | faves_addItemButton.setIconSize(QSize(12, 12)) 244 | faves_deleteItemButton = QPushButton(parent=self, flat=False, cursor=Qt.PointingHandCursor, text='Delete', 245 | icon=QIcon(':assets/images/minus.png'), toolTip='Delete selected item', 246 | clicked=self.delete_items) 247 | faves_deleteItemButton.setIconSize(QSize(12, 12)) 248 | faves_buttonLayout = QHBoxLayout() 249 | faves_buttonLayout.addWidget(faves_addItemButton) 250 | faves_buttonLayout.addWidget(faves_deleteItemButton) 251 | faves_formLayout.addRow('Item Label:', self.faves_lineEdit) 252 | faves_formLayout.addRow(faves_buttonLayout) 253 | faves_formLayout.addRow(self.get_notes()) 254 | self.faves_listWidget = QListWidget(self) 255 | self.faves_listWidget.setSelectionMode(QAbstractItemView.ExtendedSelection) 256 | self.faves_listWidget.setSortingEnabled(True) 257 | self.add_items(self.settings.value('favorites', '')) 258 | 259 | tab_layout = QHBoxLayout() 260 | tab_layout.addLayout(faves_formLayout) 261 | tab_layout.addWidget(self.faves_listWidget) 262 | 263 | self.setLayout(tab_layout) 264 | 265 | def add_items(self, items: list) -> None: 266 | for item in items: 267 | listitem = QListWidgetItem(item, self.faves_listWidget) 268 | listitem.setFlags(listitem.flags() | Qt.ItemIsEditable) 269 | self.faves_listWidget.sortItems(Qt.AscendingOrder) 270 | 271 | @pyqtSlot() 272 | def delete_items(self) -> None: 273 | for item in self.faves_listWidget.selectedItems(): 274 | deleted_item = self.faves_listWidget.takeItem(self.faves_listWidget.row(item)) 275 | del deleted_item 276 | 277 | @pyqtSlot() 278 | def add_item(self) -> None: 279 | if len(self.faves_lineEdit.text()): 280 | item = QListWidgetItem(self.faves_lineEdit.text(), self.faves_listWidget) 281 | item.setFlags(item.flags() | Qt.ItemIsEditable) 282 | self.faves_listWidget.sortItems(order=Qt.AscendingOrder) 283 | self.faves_lineEdit.clear() 284 | 285 | def get_notes(self) -> QLabel: 286 | content = QLabel() 287 | content.setStyleSheet('margin:10px; border:1px solid #BABABA; padding:10px; color:#666;') 288 | content.setTextFormat(Qt.RichText) 289 | content.setWordWrap(True) 290 | content.setText('''Labels from this list will be used to filter links. Filtering is NOT case-sensitive. 291 |

Example:

    the simpsons
292 |     south park''') 293 | return content 294 | 295 | def save(self) -> None: 296 | if self.faves_listWidget.count(): 297 | faves = [] 298 | for row in range(0, self.faves_listWidget.count()): 299 | faves.append(self.faves_listWidget.item(row).text()) 300 | self.settings.setValue('favorites', faves) 301 | -------------------------------------------------------------------------------- /tvlinker/threads.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import sys 6 | import time 7 | 8 | from datetime import datetime, timedelta 9 | from tzlocal import get_localzone 10 | 11 | import pytz 12 | import requests 13 | 14 | from PyQt5.QtCore import QObject, QSettings, QThread, pyqtSignal, pyqtSlot 15 | from PyQt5.QtWidgets import QMessageBox, qApp 16 | from bs4 import BeautifulSoup 17 | from requests.exceptions import HTTPError 18 | 19 | import cloudscraper 20 | 21 | from tvlinker.filesize import alternative, size 22 | 23 | try: 24 | # noinspection PyPackageRequirements 25 | import simplejson as json 26 | except ImportError: 27 | import json 28 | 29 | 30 | class ShadowSocks: 31 | config = { 32 | 'ssocks': { 33 | 'procs': ['ss-qt5', 'sslocal'], 34 | 'proxies': { 35 | 'http': 'socks5://127.0.0.1:1080', 36 | 'https': 'socks5://127.0.0.1:1080' 37 | }, 38 | }, 39 | 'v2ray': { 40 | 'procs': ['v2ray'], 41 | 'proxies': { 42 | 'http': 'socks5://127.0.0.1:10808', 43 | 'https': 'socks5://127.0.0.1:10808' 44 | } 45 | } 46 | } 47 | 48 | @staticmethod 49 | def detect() -> str: 50 | if sys.platform.startswith('linux'): 51 | ptypes = ShadowSocks.config.keys() 52 | ps = os.popen('ps -Af').read() 53 | for ptype in ptypes: 54 | procs = ShadowSocks.config[ptype]['procs'] 55 | for p in procs: 56 | if ps.count(p): 57 | return ptype 58 | return None 59 | 60 | @staticmethod 61 | def proxies() -> dict: 62 | proxy_type = ShadowSocks.detect() 63 | return ShadowSocks.config[proxy_type]['proxies'] if proxy_type is not None else {} 64 | 65 | 66 | class ScrapeWorker(QObject): 67 | addRow = pyqtSignal(list) 68 | workFinished = pyqtSignal() 69 | 70 | def __init__(self, source_url: str, useragent: str, maxpages: int): 71 | super(ScrapeWorker, self).__init__() 72 | self.maxpages = maxpages 73 | self.source_url = source_url 74 | self.user_agent = useragent 75 | self.scraper = cloudscraper.create_scraper() 76 | self.scraper.proxies = ShadowSocks.proxies() 77 | self.tz_format = '%b %d %Y %H:%M' 78 | self.tz_local = get_localzone() 79 | self.complete = False 80 | 81 | def scrape(self, pagenum: int) -> None: 82 | try: 83 | url = self.source_url.format(pagenum + 1) 84 | req = self.scraper.get(url) 85 | bs = BeautifulSoup(req.text, 'lxml') 86 | posts = bs('div', class_='post') 87 | for post in posts: 88 | dt_utc = datetime.strptime(post.find('div', class_='p-c p-c-time').get_text().strip(), self.tz_format) 89 | # TODO: fix hardcoded DST adjustment 90 | dt_local = dt_utc.replace(tzinfo=pytz.utc).astimezone(self.tz_local) - timedelta(hours=2) 91 | dlsize = post.find('h2').get_text().strip() 92 | table_row = [ 93 | dt_local.strftime(self.tz_format), 94 | post.find('a', class_='p-title').get('href').strip(), 95 | post.find('a', class_='p-title').get_text().strip(), 96 | dlsize[dlsize.rfind('(') + 1:len(dlsize) - 1] 97 | ] 98 | self.addRow.emit(table_row) 99 | except HTTPError: 100 | sys.stderr.write(sys.exc_info()[0]) 101 | # noinspection PyTypeChecker 102 | QMessageBox.critical(None, 'ERROR NOTIFICATION', sys.exc_info()[0]) 103 | # self.exit() 104 | 105 | @pyqtSlot() 106 | def begin(self): 107 | for page in range(self.maxpages): 108 | if QThread.currentThread().isInterruptionRequested(): 109 | return 110 | self.scrape(page) 111 | self.complete = True 112 | self.workFinished.emit() 113 | 114 | 115 | class HostersThread(QThread): 116 | setHosters = pyqtSignal(list) 117 | noLinks = pyqtSignal() 118 | 119 | def __init__(self, link_url: str, useragent: str): 120 | QThread.__init__(self) 121 | self.link_url = link_url 122 | self.user_agent = useragent 123 | self.scraper = cloudscraper.create_scraper() 124 | self.scraper.proxies = ShadowSocks.proxies() 125 | 126 | def __del__(self) -> None: 127 | self.wait() 128 | 129 | def get_hoster_links(self) -> None: 130 | try: 131 | req = self.scraper.get(self.link_url) 132 | bs = BeautifulSoup(req.text, 'lxml') 133 | links = bs.select('div.post h2[style="text-align: center;"]') 134 | self.setHosters.emit(links) 135 | except HTTPError: 136 | print(sys.exc_info()[0]) 137 | # noinspection PyTypeChecker 138 | QMessageBox.critical(None, 'ERROR NOTIFICATION', sys.exc_info()[0]) 139 | QThread.currentThread().quit() 140 | except IndexError: 141 | self.noLinks.emit() 142 | QThread.currentThread().quit() 143 | 144 | def run(self) -> None: 145 | self.get_hoster_links() 146 | 147 | 148 | class RealDebridThread(QThread): 149 | unrestrictedLink = pyqtSignal(str) 150 | supportedHosts = pyqtSignal(dict) 151 | hostStatus = pyqtSignal(dict) 152 | errorMsg = pyqtSignal(list) 153 | 154 | class RealDebridAction: 155 | UNRESTRICT_LINK = 0, 156 | SUPPORTED_HOSTS = 1, 157 | HOST_STATUS = 2 158 | 159 | def __init__(self, 160 | settings: QSettings, 161 | api_url: str, 162 | link_url: str, 163 | action: RealDebridAction = RealDebridAction.UNRESTRICT_LINK, 164 | check_host: str = None): 165 | QThread.__init__(self) 166 | self.api_url = api_url 167 | self.api_token = settings.value('realdebrid_apitoken') 168 | self.api_proxy = settings.value('realdebrid_apiproxy', False, bool) 169 | self.link_url = link_url 170 | self.action = action 171 | self.check_host = check_host 172 | self.proxies = ShadowSocks.proxies() if self.api_proxy else {} 173 | 174 | def __del__(self): 175 | self.wait() 176 | 177 | def post(self, endpoint: str, payload: object = None) -> dict: 178 | try: 179 | res = requests.post('{0}{1}?auth_token={2}'.format(self.api_url, endpoint, self.api_token), 180 | data=payload, proxies=self.proxies) 181 | return res.json() 182 | except HTTPError: 183 | print(sys.exc_info()) 184 | self.errorMsg.emit([ 185 | 'ERROR NOTIFICATION', 186 | '

Real-Debrid API Error

' 187 | 'A problem occurred whilst communicating with Real-Debrid. Please check your ' 188 | 'Internet connection.

' 189 | 'ERROR LOG:
(Error Code %s) %s
%s' % 190 | (qApp.applicationName(), HTTPError.code, HTTPError.reason) 191 | ]) 192 | # self.exit() 193 | 194 | def unrestrict_link(self) -> None: 195 | jsonres = self.post(endpoint='/unrestrict/link', payload={'link': self.link_url}) 196 | if 'download' in jsonres.keys(): 197 | self.unrestrictedLink.emit(jsonres['download']) 198 | else: 199 | self.errorMsg.emit([ 200 | 'REALDEBRID ERROR', 201 | '

Could not unrestrict link

The hoster is most likely ' 202 | 'down, please try again later.

{}'.format(jsonres) 203 | ]) 204 | 205 | def supported_hosts(self) -> None: 206 | jsonres = self.post(endpoint='/hosts') 207 | self.supportedHosts.emit(jsonres) 208 | 209 | # def host_status(self, host: str) -> None: 210 | # jsonres = self.post(endpoint='/hosts/status') 211 | # self.hostStatus.emit(jsonres) 212 | 213 | def run(self) -> None: 214 | if self.action == RealDebridThread.RealDebridAction.UNRESTRICT_LINK: 215 | self.unrestrict_link() 216 | elif self.action == RealDebridThread.RealDebridAction.SUPPORTED_HOSTS: 217 | self.supported_hosts() 218 | # elif self.action == RealDebridThread.HOST_STATUS: 219 | # self.host_status(self.check_host) 220 | 221 | 222 | class Aria2Thread(QThread): 223 | aria2Confirmation = pyqtSignal(bool) 224 | 225 | def __init__(self, settings: QSettings, link_url: str): 226 | QThread.__init__(self) 227 | self.rpc_host = settings.value('aria2_rpc_host') 228 | self.rpc_port = settings.value('aria2_rpc_port') 229 | self.rpc_secret = settings.value('aria2_rpc_secret') 230 | self.rpc_username = settings.value('aria2_rpc_username') 231 | self.rpc_password = settings.value('aria2_rpc_password') 232 | self.link_url = link_url 233 | 234 | def __del__(self) -> None: 235 | self.wait() 236 | 237 | def add_uri(self) -> None: 238 | user, passwd = '', '' 239 | if len(self.rpc_username) > 0 and len(self.rpc_password) > 0: 240 | user = self.rpc_username 241 | passwd = self.rpc_password 242 | elif len(self.rpc_secret) > 0: 243 | user = 'token' 244 | passwd = self.rpc_secret 245 | aria2_endpoint = '%s:%s/jsonrpc' % (self.rpc_host, self.rpc_port) 246 | headers = {'Content-Type': 'application/json'} 247 | payload = json.dumps( 248 | { 249 | 'jsonrpc': '2.0', 250 | 'id': 1, 251 | 'method': 'aria2.addUri', 252 | 'params': ['%s:%s' % (user, passwd), [self.link_url]] 253 | }, 254 | sort_keys=False).encode('utf-8') 255 | try: 256 | from urllib.parse import urlencode 257 | from urllib.request import Request, urlopen 258 | req = Request(aria2_endpoint, headers=headers, data=payload) 259 | res = urlopen(req).read().decode('utf-8') 260 | jsonres = json.loads(res) 261 | # res = requests.post(aria2_endpoint, headers=headers, data=payload) 262 | # jsonres = res.json() 263 | self.aria2Confirmation.emit('result' in jsonres.keys()) 264 | except HTTPError: 265 | print(sys.exc_info()) 266 | # noinspection PyTypeChecker 267 | QMessageBox.critical(None, 'ERROR NOTIFICATION', sys.exc_info(), QMessageBox.Ok) 268 | self.aria2Confirmation.emit(False) 269 | # self.exit() 270 | 271 | def run(self) -> None: 272 | self.add_uri() 273 | 274 | 275 | class DownloadThread(QThread): 276 | dlComplete = pyqtSignal() 277 | dlProgress = pyqtSignal(int) 278 | dlProgressTxt = pyqtSignal(str) 279 | 280 | def __init__(self, link_url: str, dl_path: str): 281 | QThread.__init__(self) 282 | self.download_link = link_url 283 | self.download_path = dl_path 284 | self.cancel_download = False 285 | self.proxies = ShadowSocks.proxies() 286 | 287 | def __del__(self) -> None: 288 | self.wait() 289 | 290 | def download_file(self) -> None: 291 | req = requests.get(self.download_link, stream=True, proxies=self.proxies) 292 | filesize = int(req.headers['Content-Length']) 293 | filename = os.path.basename(self.download_path) 294 | downloadedChunk = 0 295 | blockSize = 8192 296 | start = time.clock() 297 | with open(self.download_path, 'wb') as f: 298 | for chunk in req.iter_content(chunk_size=blockSize): 299 | if self.cancel_download or not chunk: 300 | req.close() 301 | break 302 | f.write(chunk) 303 | downloadedChunk += len(chunk) 304 | progress = float(downloadedChunk) / filesize 305 | self.dlProgress.emit(progress * 100) 306 | dlspeed = downloadedChunk // (time.clock() - start) / 1000 307 | progressTxt = 'Downloading {0}:
{1} of {3} [{2:.2%}] [{4} kbps]' \ 308 | .format(filename, downloadedChunk, progress, size(filesize, system=alternative), dlspeed) 309 | self.dlProgressTxt.emit(progressTxt) 310 | self.dlComplete.emit() 311 | 312 | def run(self) -> None: 313 | self.download_file() 314 | -------------------------------------------------------------------------------- /tvlinker/tvlinker.ini: -------------------------------------------------------------------------------- 1 | [General] 2 | aria2_rpc_host=http://localhost 3 | aria2_rpc_password= 4 | aria2_rpc_port=6800 5 | aria2_rpc_secret= 6 | aria2_rpc_username= 7 | dl_pagecount=10 8 | download_manager=built-in 9 | favorites= 10 | faves_filter=false 11 | idm_exe_path=C:\\Program Files (x86)\\Internet Download Manager\\IDMan.exe 12 | kget_cmd=/usr/bin/kget --hideMainWindow 13 | persepolis_cmd=/usr/bin/persepolis --tray --link 14 | pyload_host=http://localhost:8000 15 | pyload_password= 16 | pyload_username= 17 | realdebrid_apitoken= 18 | realdebrid_apiproxy=true 19 | user_agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.61 Safari/537.36" 20 | --------------------------------------------------------------------------------