Repository: persepolisdm/persepolis
Branch: master
Commit: 109e95261fa2
Files: 124
Total size: 2.9 MB
Directory structure:
gitextract_m5nz38oz/
├── .github/
│ ├── ISSUE_TEMPLATE
│ └── contributing.md
├── .gitignore
├── .pep8
├── .travis.yml
├── .tx/
│ └── config
├── LICENSE
├── README.md
├── check_dependencies.py
├── clear.py
├── man/
│ ├── meson.build
│ └── persepolis.1
├── meson.build
├── persepolis/
│ ├── .pep8
│ ├── Persepolis Download Manager.py
│ ├── __init__.py
│ ├── __main__.py
│ ├── constants/
│ │ ├── Browser.py
│ │ ├── Os.py
│ │ ├── Version.py
│ │ └── __init__.py
│ ├── gui/
│ │ ├── .pep8
│ │ ├── __init__.py
│ │ ├── about_ui.py
│ │ ├── addlink_ui.py
│ │ ├── after_download_ui.py
│ │ ├── customized_widgets.py
│ │ ├── log_window_ui.py
│ │ ├── mainwindow_ui.py
│ │ ├── progress_ui.py
│ │ ├── resources.py
│ │ ├── setting_ui.py
│ │ ├── text_queue_ui.py
│ │ └── video_finder_progress_ui.py
│ ├── meson.build
│ ├── persepolis.py
│ └── scripts/
│ ├── .pep8
│ ├── __init__.py
│ ├── about.py
│ ├── addlink.py
│ ├── after_download.py
│ ├── browser_integration.py
│ ├── browser_plugin_queue.py
│ ├── bubble.py
│ ├── check_proxy.py
│ ├── compatibility.py
│ ├── data_base.py
│ ├── download_link.py
│ ├── error_window.py
│ ├── initialization.py
│ ├── log_window.py
│ ├── logger.py
│ ├── mainwindow.py
│ ├── newopen.py
│ ├── osCommands.py
│ ├── persepolis.py
│ ├── persepolis_lib_prime.py
│ ├── play.py
│ ├── progress.py
│ ├── properties.py
│ ├── queue.py
│ ├── queue_prime.py
│ ├── setting.py
│ ├── shutdown.py
│ ├── spider.py
│ ├── startup.py
│ ├── text_queue.py
│ ├── useful_tools.py
│ ├── video_finder.py
│ ├── video_finder_addlink.py
│ ├── video_finder_progress.py
│ └── ytdlp_downloader.py
├── requirements.md
├── requirements.txt
├── resources/
│ ├── PersepolisBI.py
│ ├── dark_style.qss
│ ├── light_style.qss
│ ├── locales/
│ │ ├── ui.qm
│ │ ├── ui.ts
│ │ ├── ui_ar.qm
│ │ ├── ui_ar.ts
│ │ ├── ui_de.qm
│ │ ├── ui_de.ts
│ │ ├── ui_es_ES.qm
│ │ ├── ui_es_ES.ts
│ │ ├── ui_fa_IR.qm
│ │ ├── ui_fa_IR.ts
│ │ ├── ui_fr_FR.qm
│ │ ├── ui_fr_FR.ts
│ │ ├── ui_hu.qm
│ │ ├── ui_hu.ts
│ │ ├── ui_ko.qm
│ │ ├── ui_ko.ts
│ │ ├── ui_nl_NL.qm
│ │ ├── ui_nl_NL.ts
│ │ ├── ui_pl_PL.qm
│ │ ├── ui_pl_PL.ts
│ │ ├── ui_pt.qm
│ │ ├── ui_pt.ts
│ │ ├── ui_pt_BR.qm
│ │ ├── ui_pt_BR.ts
│ │ ├── ui_ru.qm
│ │ ├── ui_ru.ts
│ │ ├── ui_sv.qm
│ │ ├── ui_sv.ts
│ │ ├── ui_tr.qm
│ │ ├── ui_tr.ts
│ │ ├── ui_tr_TR.qm
│ │ ├── ui_tr_TR.ts
│ │ ├── ui_zh_CN.qm
│ │ ├── ui_zh_CN.ts
│ │ ├── ui_zh_TW.qm
│ │ └── ui_zh_TW.ts
│ ├── meson.build
│ ├── resources.qrc
│ ├── resources_generator.sh
│ ├── translation_files.pro
│ └── translators.txt
├── test/
│ ├── .pep8
│ └── test.py
├── uninstall.py
└── xdg/
├── com.github.persepolisdm.persepolis.appdata.xml
├── com.github.persepolisdm.persepolis.desktop.in
└── meson.build
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/ISSUE_TEMPLATE
================================================
**System Details:**
* Operating System: ?
* Distro(For GNU/Linux and BSD users): ?
* Desktop Environment(For GNU/Linux and BSD users): ?
* Persepolis Version: ?
* How do you install Persepolis? (Repositories, source, ...)
* _Please attach log files if crash or paste error message_
**Issue Description and steps to reproduce:**
#### Write in English language please, Thanks :)
#### Feel free to edit or delete lines in this template if it is necessary.
#### Please report only one Issue (Bug or feature request or ...) in one issue! and open new Issue for another one :)
================================================
FILE: .github/contributing.md
================================================
Please read our [contribution instruction](https://github.com/persepolisdm/persepolis/wiki/Home-en#contributers-guide).
================================================
FILE: .gitignore
================================================
__pycache__/
*.pyc
.env
*.orig
root/
build/
builddir/
persepolis.egg-info/
man/persepolis.1.gz
.idea/
.mypy_cache
persepolis_wenv/
test/ffmpeg
resources/ffmpeg
dist/
*.spec
venv_dir/
test/ffmpeg.exe
Persepolis\ Download\ Manager.build/
Persepolis\ Download\ Manager.dist/
Persepolis\ Download\ Manager.onefile-build/
================================================
FILE: .pep8
================================================
[pycodestyle]
max_line_length = 120
ignore = E501,E722,W503
================================================
FILE: .travis.yml
================================================
language: python
dist: bionic
sudo: required
python:
- "3.6"
virtualenv:
system_site_packages: true
addons:
apt:
update: true
packages:
python3
aria2
sound-theme-freedesktop
libnotify-bin
libqt5svg5
python3-pyqt5.qtsvg
python3-setuptools
python3-pip
python3-pyqt5
pulseaudio
python3-psutil
ffmpeg
before_install:
- sudo pip3 install youtube-dl requests setproctitle
install: true
script:
- sudo python3 setup.py install
- persepolis --version
after_success:
- pip3 install transifex-client==0.12.5
- sudo echo $'[https://www.transifex.com]\napi_hostname = https://api.transifex.com\nhostname = https://www.transifex.com\nusername = '"$TRANSIFEX_USER"$'\npassword = '"$TRANSIFEX_PASSWORD"$'\n' > ~/.transifexrc
- tx push -s
================================================
FILE: .tx/config
================================================
[main]
host = https://www.transifex.com
[persepolis-translations.ui]
file_filter = resources/locales/ui_.ts
minimum_perc = 0
source_file = resources/locales/ui.ts
source_lang = en
type = QT
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{one line to give the program's name and a brief idea of what it does.}
Copyright (C) {year} {name of author}
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
{project} Copyright (C) {year} {fullname}
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
.
================================================
FILE: README.md
================================================
Persepolis Download Manager
[](https://github.com/persepolisdm/persepolis/releases) [](https://github.com/persepolisdm/persepolis) [](https://github.com/persepolisdm/persepolis/blob/master/LICENSE) [](https://travis-ci.org/persepolisdm/persepolis) [](https://github.com/persepolisdm/persepolis/commits/master) [](https://github.com/persepolisdm/persepolis/graphs/contributors) [](https://github.com/persepolisdm/persepolis/commits/master) [](https://aur.archlinux.org/packages/persepolis-git) [](https://twitter.com/persepolisdm)
> **Content**
> - [About](https://github.com/persepolisdm/persepolis#about)
> - [How to install PersepolisDM](https://github.com/persepolisdm/persepolis/wiki)
> - [FAQ](https://github.com/persepolisdm/persepolis#faq)
> - [Screenshots](https://github.com/persepolisdm/persepolis#screenshots)
> - [Credits](https://github.com/persepolisdm/persepolis#credits)
### **About**
Persepolis is a download manager written in Python. Persepolis is a sample of free and open-source software. It's developed for GNU/Linux distributions, BSDs, macOS, and Microsoft Windows.
### **Features**
- Multi-segment downloading (64 connections)
- Scheduling downloads
- Download queuing
- Downloading videos from Youtube and ...
- and many more!
Demonstration clip:
[](https://www.youtube.com/watch?v=QHdMShFgzhQ)
### How to install PersepolisDM?
> Please checkout the Persepolis [wiki](https://github.com/persepolisdm/persepolis/wiki)
### **FAQ**
> **Where does the name come from?**
> > [From Wikipedia:](https://en.wikipedia.org/wiki/Persepolis) Persepolis (Greek: Περσέπολις Persépolis; "the Persian city") or Parsa (Old Persian: 𐎱𐎠𐎼𐎿 Pārsa; "Persia"),
also known as Takht-e-Jamshid (Persian: تخت جمشيد Taxt e Jamšid; "Throne of Jamshid"),
was the ceremonial capital of the Achaemenid Empire (ca. 550–330 BC).
Persepolis is situated 60 km northeast of the city of Shiraz in Fars Province, Iran. The earliest remains of Persepolis date back to 515 BC.
It exemplifies the Achaemenid style of architecture. UNESCO declared the ruins of Persepolis a World Heritage Site in 1979.
> **How can I install Persepolis?**
> > Read [our wiki](https://github.com/persepolisdm/persepolis/wiki) or check the [Persepolis website](https://persepolisdm.github.io) for more information.
### **Screenshots**
|GNU/Linux|BSD|
|:---:|:---:|
|||
|Mac OSX|Windows|
|:---:|:---:|
|||
### Credits
**Alireza Amirsamimi:** Persepolis lead developer and manager
[GitHub](https://github.com/alireza-amirsamimi) | [E-mail](mailto:alireza.amirsamimi@gmail.com)
**Mohammadreza Abdollahzadeh:** Arch Linux and BSD support
[GitHub](https://github.com/morealaz) | [E-mail](mailto:morealaz@gmail.com)
**Mostafa Asadi:** Ubuntu, Debian and Windows support
[GitHub](https://github.com/mostafaasadi) | [E-mail](mailto:mostafaasadi73@gmail.com)
**Sadegh Alirezaie:** Persepolis website support
[GitHub](https://github.com/Alirezaies) | [E-mail](mailto:alirezaie@sadegh.org)
**Jafar Akhondali:** Browsers add-ons support
[GitHub](https://github.com/JafarAkhondali) | [E-mail](mailto:jafar.akhondali@yahoo.com)
**Kia Hamedi:** Mac OSX support
[GitHub](https://github.com/Kiahamedi) | [E-mail](mailto:me@kiahamedi.com)
**H Rostami:** UI translation, Fedora & OpenSuse support
[GitHub](https://github.com/hayyan71) | [E-mail](mailto:hayyan71@yahoo.com)
**Ehsan Titish:** Mac OSX support
[GitHub](https://github.com/Maders) | [E-mail](mailto:me@maders.ir)
**MohammadAmin Vahedinia:** Mac OSX support
[GitHub](https://github.com/Mr0Null) | [E-mail](mailto:persepolisdm@vahedinia.me)
[Persepolis website (En)](https://persepolisdm.github.io/) | [Persepolis website (Fa)](https://persepolisdm.github.io/fa) | [Twitter](https://twitter.com/persepolisdm) | [Telegram Channel](https://telegram.me/persepolisdm)
### Acknowledgments:
[YT-DLP project](https://github.com/yt-dlp/yt-dlp)
[FFmpeg project](https://github.com/FFmpeg/FFmpeg)
[PySide project](https://wiki.qt.io/Qt_for_Python)
[Python Requests project](https://github.com/psf/requests)
---
_Are there any mistakes in README.md? Report it in the [issue tracker](https://github.com/persepolisdm/persepolis/issues) or correct it by yourself._
================================================
FILE: check_dependencies.py
================================================
#!/usr/bin/env python3
# coding: utf-8
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import os
import platform
# finding os platform
os_type = platform.system()
# Checking dependencies!
not_installed = ''
# PyQt5 or PySide6
try:
import PySide6
print('python3-pyside6 is found')
pyside6_is_installed = True
except ImportError:
pyside6_is_installed = False
if not(pyside6_is_installed):
try:
import PyQt5
print('python3-pyqt5 is found')
except ImportError:
print('Error : python3-pyqt5 or pyside6 must be installed!')
not_installed = not_installed + '(PyQt5 or PySide6) '
# python3-requests
try:
import requests
print('python3-requests is found!')
except ImportError:
print('Error : requests is not installed!')
not_installed = not_installed + 'python3-requests, '
# python3-urllib3
try:
import urllib3
print('python3-urllib3 is found!')
except ImportError:
print('Error : urllib3 is not installed!')
not_installed = not_installed + 'python3-urllib3, '
# python3-setproctitle
try:
import setproctitle
print('python3-setproctitle is found!')
except ImportError:
print("Warning: setproctitle is not installed!")
not_installed = not_installed + 'python3-setproctitle, '
# python3-PySocks
try:
import socks
print('python3-pysocks is found!')
except ImportError:
print("Warning: python3-pysocks is not installed!")
not_installed = not_installed + 'python3-pysocks, '
# psutil
try:
import psutil
print('python3-psutil is found!')
except ImportError:
print("Warning: python3-psutil is not installed!")
not_installed = not_installed + 'psutil, '
# yt_dlp
try:
import yt_dlp
print('yt-dlp is found')
except ImportError:
print('Warning: yt-dlp is not installed!')
not_installed = not_installed + 'yt-dlp, '
# ffmpeg
answer = os.system('ffmpeg -version 1>/dev/null')
if answer != 0:
print("Warning: ffmpeg not installed!")
not_installed = not_installed + 'ffmpeg, '
else:
print('ffmpeg is found!')
if os_type == 'Linux':
try:
from dasbus.connection import SessionMessageBus
print('python3-dasbus is found!')
except ImportError:
print('python3-dasbus is not installed!')
not_installed = not_installed + 'python3-dasbus,'
if not_installed != '':
print('########################')
print('####### WARNING ########')
print('########################')
print('Some dependencies are not installed .It causes some problems for persepolis! : \n')
print(not_installed + '\n\n')
print('Read this link for more information: \n')
print('https://github.com/persepolisdm/persepolis/wiki/git-installation-instruction\n\n')
================================================
FILE: clear.py
================================================
#!/usr/bin/env python3
# coding: utf-8
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import os
import warnings
import sys
import platform
import shutil
from persepolis.constants import OS
# finding os platform
os_type = platform.system()
if os_type == 'Linux' or os_type == 'FreeBSD' or os_type == 'OpenBSD':
print(os_type + " detected!")
else:
print('This script is only work for GNU/Linux or BSD!')
sys.exit(1)
# finding current directory
cwd = os.path.abspath(__file__)
setup_dir = os.path.dirname(cwd)
# clearing __pycache__
src_pycache = os.path.join(setup_dir, 'persepolis', '__pycache__')
gui_pycache = os.path.join(setup_dir, 'persepolis', 'gui', '__pycache__')
scripts_pycache = os.path.join(setup_dir, 'persepolis', 'scripts', '__pycache__')
constants_pycache = os.path.join(setup_dir, 'persepolis', 'constants', '__pycache__')
for folder in [src_pycache, gui_pycache, scripts_pycache, constants_pycache]:
if os.path.isdir(folder):
shutil.rmtree(folder)
print(str(folder)
+ ' is removed!')
uid = os.getuid()
if uid != 0:
print('Run this script as root\n\
if you want to clean unwanted files that created by setup tools')
sys.exit(1)
# finding current directory
cwd = os.path.abspath(__file__)
setup_dir = os.path.dirname(cwd)
# clearing __pycache__
src_pycache = os.path.join(setup_dir, 'persepolis', '__pycache__')
gui_pycache = os.path.join(setup_dir, 'persepolis', 'gui', '__pycache__')
scripts_pycache = os.path.join(setup_dir, 'persepolis', 'scripts', '__pycache__')
constants_pycache = os.path.join(setup_dir, 'persepolis', 'constants', '__pycache__')
for folder in [src_pycache, gui_pycache, scripts_pycache, constants_pycache]:
if os.path.isdir(folder):
shutil.rmtree(folder)
print(str(folder)
+ ' is removed!')
# clear unwanted files!
for folder in ['build', 'dist', 'root', 'persepolis.egg-info']:
if os.path.isdir(folder):
shutil.rmtree(folder)
print(str(folder)
+ ' is removed!')
man_page = 'man/persepolis.1.gz'
if os.path.isfile(man_page):
os.remove('man/persepolis.1.gz')
================================================
FILE: man/meson.build
================================================
# creating man page file
persepolis_man_page='persepolis.1'
install_man(persepolis_man_page)
================================================
FILE: man/persepolis.1
================================================
.TH "Persepolis" "1" "July 14, 2025" "5.2.0" "persepolis"
.SH NAME
persepolis \- Persepolis Download Manager
.
.nr rst2man-indent-level 0
.
.de1 rstReportMargin
\\$1 \\n[an-margin]
level \\n[rst2man-indent-level]
level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
-
\\n[rst2man-indent0]
\\n[rst2man-indent1]
\\n[rst2man-indent2]
..
.de1 INDENT
.\" .rstReportMargin pre:
. RS \\$1
. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
. nr rst2man-indent-level +1
.\" .rstReportMargin post:
..
.de UNINDENT
. RE
.\" indent \\n[an-margin]
.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
.nr rst2man-indent-level -1
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
.\" Man page generated from reStructuredText.
.
.SH SYNOPSIS
.sp
\fBpersepolis\fP --link [] []
.SH DESCRIPTION
.sp
Persepolis Download Manager is a feature-full utility for easier and faster downloads with aria2.
It support HTTP(S) & FTP.Persepolis configures aria2 automatically
for maximum speed and it has a GUI for the ease of use.
You can adjust start time and end time for downloads.Persepolis can shutdown
system after download. You can integrate persepolis with your browser(read persepolis wiki on github page).
.SH OPTIONS
.SS Basic Options
.INDENT 0.0
.TP
.B \--link
Your Download link.(Use "" for links)
$ persepolis --link "https://www.google.com/images/srpr/logo11w.png"
.UNINDENT
.INDENT 0.0
.TP
.B \--referrer
Set an http referrer (Referer). This affects all http/https downloads. If * is given, the download URI is also used as the referrer.
.UNINDENT
.INDENT 0.0
.TP
.B \--load-cookies
Set cookies file path.
.UNINDENT
.INDENT 0.0
.TP
.B \--agent
Set user agent for HTTP(S) downloads. Default: aria2/$VERSION, $VERSION is replaced by package version.
.UNINDENT
.INDENT 0.0
.TP
.B \--headers
Append HEADER to HTTP request header.
.UNINDENT
.INDENT 0.0
.TP
.B \--name
The file name of the downloaded file.
.UNINDENT
.INDENT 0.0
.TP
.B \--default
Restore default settings.
.UNINDENT
.INDENT 0.0
.TP
.B \--clear
Clear download list and user setting!
.UNINDENT
.INDENT 0.0
.TP
.B \--tray
Persepolis starts in tray icon. It's useful when you want to put persepolis in system's startup.
.UNINDENT
.INDENT 0.0
.TP
.B \--version
Showing persepolis version.
.UNINDENT
.INDENT 0.0
.TP
.B \--help
Persepolis help.
.UNINDENT
.INDENT 0.0
.TP
.SH MORE HELP
for configuration with firefox flashgot please see README.md file on github
https://github.com/persepolisdm/persepolis
.SH SEE ALSO
aria2c(1)
.SH AUTHOR
.B AliReza AmirSamimi
.UNINDENT
.INDENT 0.0
.TP
.B Github page
https://github.com/persepolisdm/persepolis
.UNINDENT
.INDENT 0.0
.TP
.B PersepolisDM Telegram Channel
https://telegram.me/persepolisdm
================================================
FILE: meson.build
================================================
project('persepolis',
version: '5.2.0',
meson_version: '>=0.61.2')
python = import('python')
python3 = python.find_installation('python3')
if not python3.found()
error('No valid python3 installation found!')
endif
# run dependency check script
meson.add_install_script('check_dependencies.py')
prefix = get_option('prefix')
bindir = join_paths(prefix, get_option('bindir'))
datadir = join_paths(prefix, get_option('datadir'))
pythondir = join_paths(prefix, python3.get_path('purelib'))
pkgdatadir = join_paths(datadir, meson.project_name())
pkgappid = 'com.github.persepolisdm.persepolis'
appdatadir = join_paths(datadir, 'metainfo')
desktopdir = join_paths(datadir, 'applications')
persepolisdir = python3.get_install_dir(subdir: 'persepolis')
icondir = join_paths(datadir, 'icons', 'hicolor', 'scalable', 'apps')
subdir('persepolis')
subdir('man')
subdir('xdg')
subdir('resources')
================================================
FILE: persepolis/.pep8
================================================
[pycodestyle]
max_line_length = 120
================================================
FILE: persepolis/Persepolis Download Manager.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
# this file is created for building persepolis with pyinstaller.
from persepolis.scripts import persepolis
persepolis.main()
================================================
FILE: persepolis/__init__.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
# import sys
# sys.path.insert(1, '.')
================================================
FILE: persepolis/__main__.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
from persepolis.scripts import persepolis
persepolis.main()
================================================
FILE: persepolis/constants/Browser.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
class BROWSER:
CHROME = 'chrome'
CHROMIUM = 'chromium'
OPERA = 'opera'
VIVALDI = 'vivaldi'
FIREFOX = 'firefox'
BRAVE = 'brave'
LIBREWOLF = 'librewolf'
CHROME_FAMILY = [CHROME, CHROMIUM, VIVALDI, OPERA, BRAVE]
FIREFOX_FAMILY = [FIREFOX, LIBREWOLF]
LIST = [CHROME, CHROMIUM, OPERA, VIVALDI, FIREFOX, BRAVE]
================================================
FILE: persepolis/constants/Os.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
class OS:
LINUX = 'Linux'
WINDOWS = 'Windows'
FREE_BSD = 'FreeBSD'
OPEN_BSD = 'OpenBSD'
OSX = DARWIN = 'Darwin'
BSD_FAMILY = [FREE_BSD, OPEN_BSD]
UNIX_LIKE = [FREE_BSD, OPEN_BSD, LINUX]
LIST = [LINUX, WINDOWS, FREE_BSD, OPEN_BSD, OSX]
================================================
FILE: persepolis/constants/Version.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
class VERSION:
version_str = '5.2.0'
================================================
FILE: persepolis/constants/__init__.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
# import sys
# sys.path.insert(1, '.')
from .Os import OS
from .Browser import BROWSER
from .Version import VERSION
================================================
FILE: persepolis/gui/.pep8
================================================
[pycodestyle]
max_line_length = 120
================================================
FILE: persepolis/gui/__init__.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
# import sys
# sys.path.insert(1, '.')
================================================
FILE: persepolis/gui/about_ui.py
================================================
# -*- coding: utf-8 -*-
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
try:
from PySide6.QtWidgets import QWidget, QTabWidget, QHBoxLayout, QVBoxLayout, QLabel, QTextEdit, QPushButton
from PySide6.QtCore import Qt, QSize, QTranslator, QCoreApplication, QLocale
from PySide6.QtGui import QIcon, QFont
pyside6_is_installed = True
except ImportError:
from PyQt5.QtWidgets import QWidget, QTabWidget, QHBoxLayout, QVBoxLayout, QLabel, QTextEdit, QPushButton
from PyQt5.QtCore import Qt, QSize, QTranslator, QCoreApplication, QLocale
from PyQt5.QtGui import QIcon, QFont
pyside6_is_installed = False
from persepolis.gui import resources
from persepolis.constants import VERSION
if pyside6_is_installed is True:
try:
from PySide6 import QtSvgWidgets
qtsvg_available = True
except ImportError:
qtsvg_available = False
else:
try:
from PyQt5 import QtSvg as QtSvgWidgets
qtsvg_available = True
except ImportError:
qtsvg_available = False
class AboutWindow_Ui(QWidget):
def __init__(self, persepolis_setting):
super().__init__()
self.persepolis_setting = persepolis_setting
# add support for other languages
locale = str(self.persepolis_setting.value('settings/locale'))
QLocale.setDefault(QLocale(locale))
self.translator = QTranslator()
if self.translator.load(':/translations/locales/ui_' + locale, 'ts'):
QCoreApplication.installTranslator(self.translator)
# set ui direction
ui_direction = self.persepolis_setting.value('ui_direction')
if ui_direction == 'rtl':
self.setLayoutDirection(Qt.RightToLeft)
elif ui_direction in 'ltr':
self.setLayoutDirection(Qt.LeftToRight)
icons = ':/' + \
str(self.persepolis_setting.value('settings/icons')) + '/'
self.setMinimumSize(QSize(545, 375))
self.setWindowIcon(QIcon.fromTheme('persepolis', QIcon(':/com.github.persepolisdm.persepolis.svg')))
verticalLayout = QVBoxLayout(self)
self.about_tabWidget = QTabWidget(self)
# about tab
self.about_tab = QWidget(self)
about_tab_horizontalLayout = QHBoxLayout(self.about_tab)
about_tab_verticalLayout = QVBoxLayout()
# persepolis icon
if qtsvg_available:
persepolis_icon_verticalLayout = QVBoxLayout()
self.persepolis_icon = QtSvgWidgets.QSvgWidget(':/com.github.persepolisdm.persepolis.svg')
self.persepolis_icon.setFixedSize(QSize(64, 64))
persepolis_icon_verticalLayout.addWidget(self.persepolis_icon)
persepolis_icon_verticalLayout.addStretch(1)
about_tab_horizontalLayout.addLayout(persepolis_icon_verticalLayout)
self.title_label = QLabel(self.about_tab)
font = QFont()
font.setBold(True)
font.setWeight(QFont.Weight.Bold)
self.title_label.setFont(font)
self.title_label.setAlignment(Qt.AlignCenter)
about_tab_verticalLayout.addWidget(self.title_label)
self.version_label = QLabel(self.about_tab)
self.version_label.setAlignment(Qt.AlignCenter)
self.version_label.setFont(font)
about_tab_verticalLayout.addWidget(self.version_label)
self.site2_label = QLabel(self.about_tab)
self.site2_label.setTextFormat(Qt.RichText)
self.site2_label.setAlignment(Qt.AlignCenter)
self.site2_label.setOpenExternalLinks(True)
self.site2_label.setTextInteractionFlags(
Qt.TextBrowserInteraction)
about_tab_verticalLayout.addWidget(self.site2_label)
self.telegram_label = QLabel(self.about_tab)
self.telegram_label.setTextFormat(Qt.RichText)
self.telegram_label.setAlignment(Qt.AlignCenter)
self.telegram_label.setOpenExternalLinks(True)
self.telegram_label.setTextInteractionFlags(
Qt.TextBrowserInteraction)
about_tab_verticalLayout.addWidget(self.telegram_label)
self.twitter_label = QLabel(self.about_tab)
self.twitter_label.setTextFormat(Qt.RichText)
self.twitter_label.setAlignment(Qt.AlignCenter)
self.twitter_label.setOpenExternalLinks(True)
self.twitter_label.setTextInteractionFlags(
Qt.TextBrowserInteraction)
about_tab_verticalLayout.addWidget(self.twitter_label)
about_tab_verticalLayout.addStretch(1)
about_tab_horizontalLayout.addLayout(about_tab_verticalLayout)
# developers_tab
# developers
self.developers_tab = QWidget(self)
developers_verticalLayout = QVBoxLayout(self.developers_tab)
self.developers_title_label = QLabel(self.developers_tab)
font.setBold(True)
font.setWeight(QFont.Weight.Bold)
self.developers_title_label.setFont(font)
self.developers_title_label.setAlignment(Qt.AlignCenter)
developers_verticalLayout.addWidget(self.developers_title_label)
self.name_label = QLabel(self.developers_tab)
self.name_label.setAlignment(Qt.AlignCenter)
developers_verticalLayout.addWidget(self.name_label)
# contributors
self.contributors_thank_label = QLabel(self.developers_tab)
self.contributors_thank_label.setFont(font)
self.contributors_thank_label.setAlignment(Qt.AlignCenter)
developers_verticalLayout.addWidget(self.contributors_thank_label)
self.contributors_link_label = QLabel(self.developers_tab)
self.contributors_link_label.setTextFormat(Qt.RichText)
self.contributors_link_label.setAlignment(Qt.AlignCenter)
self.contributors_link_label.setOpenExternalLinks(True)
self.contributors_link_label.setTextInteractionFlags(
Qt.TextBrowserInteraction)
developers_verticalLayout.addWidget(self.contributors_link_label)
developers_verticalLayout.addStretch(1)
# translators tab
self.translators_tab = QWidget(self)
translators_tab_verticalLayout = QVBoxLayout(self.translators_tab)
# translators
self.translators_textEdit = QTextEdit(self.translators_tab)
self.translators_textEdit.setReadOnly(True)
translators_tab_verticalLayout.addWidget(self.translators_textEdit)
# License tab
self.license_tab = QWidget(self)
license_tab_verticalLayout = QVBoxLayout(self.license_tab)
self.license_text = QTextEdit(self.license_tab)
self.license_text.setReadOnly(True)
license_tab_verticalLayout.addWidget(self.license_text)
verticalLayout.addWidget(self.about_tabWidget)
# Acknowledgments tab
self.acknowledgments_tab = QWidget(self)
acknowledgments_verticalLayout = QVBoxLayout(self.acknowledgments_tab)
self.acknowledgments_title_label = QLabel(self.acknowledgments_tab)
font.setBold(True)
font.setWeight(QFont.Weight.Bold)
self.acknowledgments_title_label.setFont(font)
self.acknowledgments_title_label.setAlignment(Qt.AlignCenter)
acknowledgments_verticalLayout.addWidget(self.acknowledgments_title_label)
self.acknowledgments_ffmpeg_label = QLabel(self.acknowledgments_tab)
self.acknowledgments_ffmpeg_label.setAlignment(Qt.AlignCenter)
self.acknowledgments_ytdlp_label = QLabel(self.acknowledgments_tab)
self.acknowledgments_ytdlp_label.setAlignment(Qt.AlignCenter)
self.acknowledgments_pyton_requests_label = QLabel(self.acknowledgments_tab)
self.acknowledgments_pyton_requests_label.setAlignment(Qt.AlignCenter)
self.acknowledgments_pyside_label = QLabel(self.acknowledgments_tab)
self.acknowledgments_pyside_label.setAlignment(Qt.AlignCenter)
self.acknowledgments_ffmpeg_label.setFont(font)
self.acknowledgments_ytdlp_label.setFont(font)
self.acknowledgments_pyside_label.setFont(font)
self.acknowledgments_pyton_requests_label.setFont(font)
acknowledgments_verticalLayout.addWidget(self.acknowledgments_ytdlp_label)
acknowledgments_verticalLayout.addWidget(self.acknowledgments_ffmpeg_label)
acknowledgments_verticalLayout.addWidget(self.acknowledgments_pyton_requests_label)
acknowledgments_verticalLayout.addWidget(self.acknowledgments_pyside_label)
acknowledgments_verticalLayout.addStretch(1)
# buttons
button_horizontalLayout = QHBoxLayout()
button_horizontalLayout.addStretch(1)
self.pushButton = QPushButton(self)
self.pushButton.setIcon(QIcon(icons + 'ok'))
self.pushButton.clicked.connect(self.close)
button_horizontalLayout.addWidget(self.pushButton)
verticalLayout.addLayout(button_horizontalLayout)
self.setWindowTitle(QCoreApplication.translate("about_ui_tr", "About Persepolis"))
# about_tab
self.title_label.setText(QCoreApplication.translate("about_ui_tr", "Persepolis Download Manager"))
self.version_label.setText(QCoreApplication.translate("about_ui_tr", "Version " + VERSION.version_str,
"TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!"))
self.site2_label.setText(QCoreApplication.translate("about_ui_tr",
"https://persepolisdm.github.io",
"TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!"))
self.telegram_label.setText(QCoreApplication.translate("about_ui_tr",
"https://telegram.me/persepolisdm",
"TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!"))
self.twitter_label.setText(QCoreApplication.translate("about_ui_tr",
"https://twitter.com/persepolisdm",
"TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!"))
# developers_tab
self.developers_title_label.setText(QCoreApplication.translate('about_ui_tr', 'Developers'))
self.name_label.setText(QCoreApplication.translate("about_ui_tr",
"\nAliReza AmirSamimi\nMohammadreza Abdollahzadeh\nSadegh Alirezaie\nMostafa Asadi\nJafar Akhondali\nKia Hamedi\nH.Rostami\nEhsan Titish\nMohammadAmin Vahedinia",
"TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!"))
self.contributors_thank_label.setText(QCoreApplication.translate('about_ui_tr', 'Special thanks to:'))
self.contributors_link_label.setText(
"our contributors")
# Acknowledgments
self.acknowledgments_title_label.setText(QCoreApplication.translate('about_ui_tr', 'Acknowledgments:'))
self.acknowledgments_ytdlp_label.setText(QCoreApplication.translate("about_ui_tr",
"YT-DLP project",
"TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!"))
self.acknowledgments_ffmpeg_label.setText(QCoreApplication.translate("about_ui_tr",
"FFmpeg project",
"TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!"))
self.acknowledgments_pyside_label.setText(QCoreApplication.translate("about_ui_tr",
"Pyside project",
"TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!"))
self.acknowledgments_pyton_requests_label.setText(QCoreApplication.translate("about_ui_tr",
"Requests project",
"TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!"))
# License
self.license_text.setPlainText("""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see http://www.gnu.org/licenses/.
""")
# tabs
self.about_tabWidget.addTab(self.about_tab, QCoreApplication.translate("about_ui_tr", "About Persepolis"))
self.about_tabWidget.addTab(self.developers_tab, QCoreApplication.translate("about_ui_tr", "Developers"))
self.about_tabWidget.addTab(self.acknowledgments_tab, QCoreApplication.translate("about_ui_tr", "Acknowledgments"))
self.about_tabWidget.addTab(self.translators_tab, QCoreApplication.translate("about_ui_tr", "Translators"))
self.about_tabWidget.addTab(self.license_tab, QCoreApplication.translate("about_ui_tr", "License"))
# button
self.pushButton.setText(QCoreApplication.translate("about_ui_tr", "OK"))
================================================
FILE: persepolis/gui/addlink_ui.py
================================================
# -*- coding: utf-8 -*-
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
try:
from PySide6.QtWidgets import QTabWidget, QPushButton, QComboBox, QSpinBox, QVBoxLayout, QHBoxLayout, QLabel, QWidget, QGridLayout, QCheckBox, QFrame, QLineEdit, QRadioButton
from PySide6.QtCore import Qt, QTranslator, QCoreApplication, QLocale
from PySide6 import QtCore
from PySide6.QtGui import QIcon
except ImportError:
from PyQt5.QtWidgets import QTabWidget, QPushButton, QComboBox, QSpinBox, QVBoxLayout, QHBoxLayout, QLabel, QWidget, QGridLayout, QCheckBox, QFrame, QLineEdit, QRadioButton
from PyQt5.QtCore import Qt, QTranslator, QCoreApplication, QLocale
from PyQt5 import QtCore
from PyQt5.QtGui import QIcon
from persepolis.gui import resources
from persepolis.gui.customized_widgets import MyQDateTimeEdit
class AddLinkWindow_Ui(QWidget):
def __init__(self, persepolis_setting):
super().__init__()
self.persepolis_setting = persepolis_setting
# add support for other languages
locale = str(self.persepolis_setting.value('settings/locale'))
QLocale.setDefault(QLocale(locale))
self.translator = QTranslator()
if self.translator.load(':/translations/locales/ui_' + locale, 'ts'):
QCoreApplication.installTranslator(self.translator)
# set ui direction
ui_direction = self.persepolis_setting.value('ui_direction')
if ui_direction == 'rtl':
self.setLayoutDirection(Qt.RightToLeft)
elif ui_direction in 'ltr':
self.setLayoutDirection(Qt.LeftToRight)
# get icons name
icons = ':/' + \
str(self.persepolis_setting.value('settings/icons')) + '/'
self.setMinimumSize(QtCore.QSize(520, 425))
self.setWindowIcon(QIcon.fromTheme('persepolis', QIcon(':/com.github.persepolisdm.persepolis.svg')))
# main layout
window_verticalLayout = QVBoxLayout()
# add link tab widget
self.add_link_tabWidget = QTabWidget(self)
window_verticalLayout.addWidget(self.add_link_tabWidget)
# link tab
self.link_tab = QWidget()
link_tab_verticalLayout = QVBoxLayout(self.link_tab)
link_tab_verticalLayout.setContentsMargins(21, 21, 21, 81)
self.link_frame = QFrame(self.link_tab)
self.link_frame.setFrameShape(QFrame.StyledPanel)
self.link_frame.setFrameShadow(QFrame.Raised)
horizontalLayout_2 = QHBoxLayout(self.link_frame)
self.link_verticalLayout = QVBoxLayout()
# link ->
self.link_horizontalLayout = QHBoxLayout()
self.link_label = QLabel(self.link_frame)
self.link_horizontalLayout.addWidget(self.link_label)
self.link_lineEdit = QLineEdit(self.link_frame)
self.link_horizontalLayout.addWidget(self.link_lineEdit)
self.link_verticalLayout.addLayout(self.link_horizontalLayout)
horizontalLayout_2.addLayout(self.link_verticalLayout)
link_tab_verticalLayout.addWidget(self.link_frame)
# add change_name field ->
self.change_name_horizontalLayout = QHBoxLayout()
self.change_name_checkBox = QCheckBox(self.link_frame)
self.change_name_horizontalLayout.addWidget(self.change_name_checkBox)
self.change_name_lineEdit = QLineEdit(self.link_frame)
self.change_name_horizontalLayout.addWidget(self.change_name_lineEdit)
self.link_verticalLayout.addLayout(self.change_name_horizontalLayout)
# add_category ->
queue_horizontalLayout = QHBoxLayout()
self.queue_frame = QFrame(self)
self.queue_frame.setFrameShape(QFrame.StyledPanel)
self.queue_frame.setFrameShadow(QFrame.Raised)
add_queue_horizontalLayout = QHBoxLayout(self.queue_frame)
self.add_queue_label = QLabel(self.queue_frame)
add_queue_horizontalLayout.addWidget(self.add_queue_label)
self.add_queue_comboBox = QComboBox(self.queue_frame)
add_queue_horizontalLayout.addWidget(self.add_queue_comboBox)
queue_horizontalLayout.addWidget(self.queue_frame)
queue_horizontalLayout.addStretch(1)
self.size_label = QLabel(self)
queue_horizontalLayout.addWidget(self.size_label)
link_tab_verticalLayout.addLayout(queue_horizontalLayout)
link_tab_verticalLayout.addStretch(1)
self.add_link_tabWidget.addTab(self.link_tab, '')
# proxy tab
self.proxy_tab = QWidget(self)
proxy_verticalLayout = QVBoxLayout(self.proxy_tab)
proxy_verticalLayout.setContentsMargins(21, 21, 21, 171)
proxy_horizontalLayout = QHBoxLayout()
self.proxy_checkBox = QCheckBox(self.proxy_tab)
self.detect_proxy_pushButton = QPushButton(self.proxy_tab)
self.detect_proxy_label = QLabel(self.proxy_tab)
proxy_horizontalLayout.addWidget(self.proxy_checkBox)
proxy_horizontalLayout.addWidget(self.detect_proxy_label)
proxy_horizontalLayout.addWidget(self.detect_proxy_pushButton)
proxy_verticalLayout.addLayout(proxy_horizontalLayout)
self.proxy_frame = QFrame(self.proxy_tab)
self.proxy_frame.setFrameShape(QFrame.StyledPanel)
self.proxy_frame.setFrameShadow(QFrame.Raised)
gridLayout = QGridLayout(self.proxy_frame)
self.ip_label = QLabel(self.proxy_frame)
gridLayout.addWidget(self.ip_label, 0, 0, 1, 1)
self.ip_lineEdit = QLineEdit(self.proxy_frame)
self.ip_lineEdit.setInputMethodHints(QtCore.Qt.ImhNone)
gridLayout.addWidget(self.ip_lineEdit, 0, 1, 1, 1)
self.port_label = QLabel(self.proxy_frame)
gridLayout.addWidget(self.port_label, 0, 2, 1, 1)
self.port_spinBox = QSpinBox(self.proxy_frame)
self.port_spinBox.setMaximum(65535)
self.port_spinBox.setSingleStep(1)
gridLayout.addWidget(self.port_spinBox, 0, 3, 1, 1)
self.proxy_user_label = QLabel(self.proxy_frame)
gridLayout.addWidget(self.proxy_user_label, 2, 0, 1, 1)
self.proxy_user_lineEdit = QLineEdit(self.proxy_frame)
gridLayout.addWidget(self.proxy_user_lineEdit, 2, 1, 1, 1)
self.proxy_pass_label = QLabel(self.proxy_frame)
gridLayout.addWidget(self.proxy_pass_label, 2, 2, 1, 1)
self.proxy_pass_lineEdit = QLineEdit(self.proxy_frame)
self.proxy_pass_lineEdit.setEchoMode(QLineEdit.Password)
gridLayout.addWidget(self.proxy_pass_lineEdit, 2, 3, 1, 1)
proxy_verticalLayout.addWidget(self.proxy_frame)
# type of proxy
self.http_radioButton = QRadioButton(self.proxy_frame)
gridLayout.addWidget(self.http_radioButton, 4, 0, 1, 1)
self.https_radioButton = QRadioButton(self.proxy_frame)
gridLayout.addWidget(self.https_radioButton, 5, 0, 1, 1)
# hide this widget until https support
self.https_radioButton.hide()
self.socks5_radioButton = QRadioButton(self.proxy_frame)
gridLayout.addWidget(self.socks5_radioButton, 6, 0, 1, 1)
proxy_verticalLayout.addStretch(1)
self.add_link_tabWidget.addTab(self.proxy_tab, '')
# more options tab
self.more_options_tab = QWidget(self)
more_options_tab_verticalLayout = QVBoxLayout(self.more_options_tab)
# download Username & Password ->
download_horizontalLayout = QHBoxLayout()
download_horizontalLayout.setContentsMargins(-1, 10, -1, -1)
download_verticalLayout = QVBoxLayout()
self.download_checkBox = QCheckBox(self.more_options_tab)
download_verticalLayout.addWidget(self.download_checkBox)
self.download_frame = QFrame(self.more_options_tab)
self.download_frame.setFrameShape(QFrame.StyledPanel)
self.download_frame.setFrameShadow(QFrame.Raised)
gridLayout_2 = QGridLayout(self.download_frame)
self.download_user_lineEdit = QLineEdit(self.download_frame)
gridLayout_2.addWidget(self.download_user_lineEdit, 0, 1, 1, 1)
self.download_user_label = QLabel(self.download_frame)
gridLayout_2.addWidget(self.download_user_label, 0, 0, 1, 1)
self.download_pass_label = QLabel(self.download_frame)
gridLayout_2.addWidget(self.download_pass_label, 1, 0, 1, 1)
self.download_pass_lineEdit = QLineEdit(self.download_frame)
self.download_pass_lineEdit.setEchoMode(QLineEdit.Password)
gridLayout_2.addWidget(self.download_pass_lineEdit, 1, 1, 1, 1)
download_verticalLayout.addWidget(self.download_frame)
download_horizontalLayout.addLayout(download_verticalLayout)
# select folder ->
self.folder_frame = QFrame(self.more_options_tab)
self.folder_frame.setFrameShape(QFrame.StyledPanel)
self.folder_frame.setFrameShadow(QFrame.Raised)
gridLayout_3 = QGridLayout(self.folder_frame)
self.download_folder_lineEdit = QLineEdit(self.folder_frame)
gridLayout_3.addWidget(self.download_folder_lineEdit, 2, 0, 1, 1)
self.folder_pushButton = QPushButton(self.folder_frame)
gridLayout_3.addWidget(self.folder_pushButton, 3, 0, 1, 1)
self.folder_pushButton.setIcon(QIcon(icons + 'folder'))
self.folder_checkBox = QCheckBox(self.folder_frame)
gridLayout_3.addWidget(self.folder_checkBox)
self.folder_label = QLabel(self.folder_frame)
self.folder_label.setAlignment(QtCore.Qt.AlignCenter)
gridLayout_3.addWidget(self.folder_label, 1, 0, 1, 1)
download_horizontalLayout.addWidget(self.folder_frame)
more_options_tab_verticalLayout.addLayout(download_horizontalLayout)
# start time ->
time_limit_horizontalLayout = QHBoxLayout()
time_limit_horizontalLayout.setContentsMargins(-1, 10, -1, -1)
start_verticalLayout = QVBoxLayout()
self.start_checkBox = QCheckBox(self.more_options_tab)
start_verticalLayout.addWidget(self.start_checkBox)
self.start_frame = QFrame(self.more_options_tab)
self.start_frame.setFrameShape(QFrame.StyledPanel)
self.start_frame.setFrameShadow(QFrame.Raised)
horizontalLayout_5 = QHBoxLayout(self.start_frame)
self.start_time_qDataTimeEdit = MyQDateTimeEdit(self.start_frame)
self.start_time_qDataTimeEdit.setDisplayFormat('H:mm')
horizontalLayout_5.addWidget(self.start_time_qDataTimeEdit)
start_verticalLayout.addWidget(self.start_frame)
time_limit_horizontalLayout.addLayout(start_verticalLayout)
# end time ->
end_verticalLayout = QVBoxLayout()
self.end_checkBox = QCheckBox(self.more_options_tab)
end_verticalLayout.addWidget(self.end_checkBox)
self.end_frame = QFrame(self.more_options_tab)
self.end_frame.setFrameShape(QFrame.StyledPanel)
self.end_frame.setFrameShadow(QFrame.Raised)
horizontalLayout_6 = QHBoxLayout(self.end_frame)
self.end_time_qDateTimeEdit = MyQDateTimeEdit(self.end_frame)
self.end_time_qDateTimeEdit.setDisplayFormat('H:mm')
horizontalLayout_6.addWidget(self.end_time_qDateTimeEdit)
end_verticalLayout.addWidget(self.end_frame)
time_limit_horizontalLayout.addLayout(end_verticalLayout)
# limit Speed ->
limit_verticalLayout = QVBoxLayout()
self.limit_frame = QFrame(self.more_options_tab)
self.limit_frame.setFrameShape(QFrame.StyledPanel)
self.limit_frame.setFrameShadow(QFrame.Raised)
verticalLayout_4 = QVBoxLayout(self.limit_frame)
limit_verticalLayout.addWidget(self.limit_frame)
time_limit_horizontalLayout.addLayout(limit_verticalLayout)
more_options_tab_verticalLayout.addLayout(time_limit_horizontalLayout)
# number of connections ->
connections_horizontalLayout = QHBoxLayout()
connections_horizontalLayout.setContentsMargins(-1, 10, -1, -1)
self.connections_frame = QFrame(self.more_options_tab)
self.connections_frame.setFrameShape(QFrame.StyledPanel)
self.connections_frame.setFrameShadow(QFrame.Raised)
self.connections_label = QLabel(self.connections_frame)
verticalLayout_4.addWidget(self.connections_label)
self.connections_spinBox = QSpinBox(self.connections_frame)
self.connections_spinBox.setMinimum(1)
self.connections_spinBox.setMaximum(64)
self.connections_spinBox.setProperty("value", 64)
verticalLayout_4.addWidget(self.connections_spinBox)
connections_horizontalLayout.addWidget(self.connections_frame)
connections_horizontalLayout.addStretch(1)
more_options_tab_verticalLayout.addLayout(connections_horizontalLayout)
more_options_tab_verticalLayout.addStretch(1)
self.add_link_tabWidget.addTab(self.more_options_tab, '')
# advance options
self.advance_options_tab = QWidget(self)
advance_options_tab_verticalLayout = QVBoxLayout(self.advance_options_tab)
# referer
referer_horizontalLayout = QHBoxLayout()
self.referer_label = QLabel(self.advance_options_tab)
referer_horizontalLayout.addWidget(self.referer_label)
self.referer_lineEdit = QLineEdit(self.advance_options_tab)
referer_horizontalLayout.addWidget(self.referer_lineEdit)
advance_options_tab_verticalLayout.addLayout(referer_horizontalLayout)
# header
header_horizontalLayout = QHBoxLayout()
self.header_label = QLabel(self.advance_options_tab)
header_horizontalLayout.addWidget(self.header_label)
self.header_lineEdit = QLineEdit(self.advance_options_tab)
header_horizontalLayout.addWidget(self.header_lineEdit)
advance_options_tab_verticalLayout.addLayout(header_horizontalLayout)
# user_agent
user_agent_horizontalLayout = QHBoxLayout()
self.user_agent_label = QLabel(self.advance_options_tab)
user_agent_horizontalLayout.addWidget(self.user_agent_label)
self.user_agent_lineEdit = QLineEdit(self.advance_options_tab)
user_agent_horizontalLayout.addWidget(self.user_agent_lineEdit)
advance_options_tab_verticalLayout.addLayout(user_agent_horizontalLayout)
# load_cookies
load_cookies_horizontalLayout = QHBoxLayout()
self.load_cookies_label = QLabel(self.advance_options_tab)
load_cookies_horizontalLayout.addWidget(self.load_cookies_label)
self.load_cookies_lineEdit = QLineEdit(self.advance_options_tab)
load_cookies_horizontalLayout.addWidget(self.load_cookies_lineEdit)
advance_options_tab_verticalLayout.addLayout(load_cookies_horizontalLayout)
advance_options_tab_verticalLayout.addStretch(1)
self.add_link_tabWidget.addTab(self.advance_options_tab, '')
# ok cancel download_later buttons ->
buttons_horizontalLayout = QHBoxLayout()
buttons_horizontalLayout.addStretch(1)
self.download_later_pushButton = QPushButton(self)
self.download_later_pushButton.setIcon(QIcon(icons + 'stop'))
self.cancel_pushButton = QPushButton(self)
self.cancel_pushButton.setIcon(QIcon(icons + 'remove'))
self.ok_pushButton = QPushButton(self)
self.ok_pushButton.setIcon(QIcon(icons + 'ok'))
buttons_horizontalLayout.addWidget(self.download_later_pushButton)
buttons_horizontalLayout.addWidget(self.cancel_pushButton)
buttons_horizontalLayout.addWidget(self.ok_pushButton)
window_verticalLayout.addLayout(buttons_horizontalLayout)
self.setLayout(window_verticalLayout)
# labels ->
self.setWindowTitle(QCoreApplication.translate("addlink_ui_tr", "Add Download Link"))
self.link_label.setText(QCoreApplication.translate("addlink_ui_tr", "Download link: "))
self.add_queue_label.setText(QCoreApplication.translate("addlink_ui_tr", "Add to category: "))
self.change_name_checkBox.setText(QCoreApplication.translate("addlink_ui_tr", "Change file name: "))
self.detect_proxy_pushButton.setText(QCoreApplication.translate("addlink_ui_tr", "Detect System Proxy Settings"))
self.proxy_checkBox.setText(QCoreApplication.translate("addlink_ui_tr", "Proxy"))
self.proxy_pass_label.setText(QCoreApplication.translate("addlink_ui_tr", "Proxy password: "))
self.ip_label.setText(QCoreApplication.translate("addlink_ui_tr", "IP: "))
self.proxy_user_label.setText(QCoreApplication.translate("addlink_ui_tr", "Proxy username: "))
self.port_label.setText(QCoreApplication.translate("addlink_ui_tr", "Port:"))
self.http_radioButton.setText(QCoreApplication.translate("addlink_ui_tr", "HTTP"))
self.https_radioButton.setText(QCoreApplication.translate("addlink_ui_tr", "HTTPS"))
self.socks5_radioButton.setText(QCoreApplication.translate("addlink_ui_tr", "SOCKS5"))
self.download_checkBox.setText(QCoreApplication.translate("addlink_ui_tr", "Download username and password"))
self.download_user_label.setText(QCoreApplication.translate("addlink_ui_tr", "Download username: "))
self.download_pass_label.setText(QCoreApplication.translate("addlink_ui_tr", "Download password: "))
self.folder_pushButton.setText(QCoreApplication.translate("addlink_ui_tr", "Change Download Folder"))
self.folder_checkBox.setText(QCoreApplication.translate("addlink_ui_tr", "Remember this path"))
self.folder_label.setText(QCoreApplication.translate("addlink_ui_tr", "Download Folder: "))
self.start_checkBox.setText(QCoreApplication.translate("addlink_ui_tr", "Start time"))
self.end_checkBox.setText(QCoreApplication.translate("addlink_ui_tr", "End time"))
self.connections_label.setText(QCoreApplication.translate("addlink_ui_tr", "Number of connections:"))
self.cancel_pushButton.setText(QCoreApplication.translate("addlink_ui_tr", "Cancel"))
self.ok_pushButton.setText(QCoreApplication.translate("addlink_ui_tr", "OK"))
self.download_later_pushButton.setText(QCoreApplication.translate("addlink_ui_tr", "Download Later"))
self.add_link_tabWidget.setTabText(self.add_link_tabWidget.indexOf(
self.link_tab), QCoreApplication.translate("addlink_ui_tr", "Link"))
self.add_link_tabWidget.setTabText(self.add_link_tabWidget.indexOf(
self.proxy_tab), QCoreApplication.translate("addlink_ui_tr", "Proxy"))
self.add_link_tabWidget.setTabText(self.add_link_tabWidget.indexOf(
self.more_options_tab), QCoreApplication.translate("addlink_ui_tr", "More Options"))
self.add_link_tabWidget.setTabText(self.add_link_tabWidget.indexOf(
self.advance_options_tab), QCoreApplication.translate("addlink_ui_tr", "Advanced Options"))
self.referer_label.setText(QCoreApplication.translate("addlink_ui_tr", 'Referrer: '))
self.header_label.setText(QCoreApplication.translate("addlink_ui_tr", 'Header: '))
self.load_cookies_label.setText(QCoreApplication.translate("addlink_ui_tr", 'Load cookies: '))
self.user_agent_label.setText(QCoreApplication.translate("addlink_ui_tr", 'User agent: '))
================================================
FILE: persepolis/gui/after_download_ui.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
try:
from PySide6.QtWidgets import QCheckBox, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QLineEdit
from PySide6.QtCore import Qt, QTranslator, QCoreApplication, QLocale
from PySide6.QtGui import QIcon
except ImportError:
from PyQt5.QtWidgets import QCheckBox, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QLineEdit
from PyQt5.QtCore import Qt, QTranslator, QCoreApplication, QLocale
from PyQt5.QtGui import QIcon
from persepolis.gui import resources
class AfterDownloadWindow_Ui(QWidget):
def __init__(self, persepolis_setting):
super().__init__()
self.persepolis_setting = persepolis_setting
# add support for other languages
locale = str(self.persepolis_setting.value('settings/locale'))
QLocale.setDefault(QLocale(locale))
self.translator = QTranslator()
if self.translator.load(':/translations/locales/ui_' + locale, 'ts'):
QCoreApplication.installTranslator(self.translator)
# set ui direction
ui_direction = self.persepolis_setting.value('ui_direction')
if ui_direction == 'rtl':
self.setLayoutDirection(Qt.RightToLeft)
elif ui_direction in 'ltr':
self.setLayoutDirection(Qt.LeftToRight)
icons = ':/' + str(self.persepolis_setting.value('settings/icons')) + '/'
self.setWindowIcon(QIcon.fromTheme('persepolis', QIcon(':/com.github.persepolisdm.persepolis.svg')))
self.setWindowTitle(QCoreApplication.translate("after_download_ui_tr", "Persepolis Download Manager"))
# complete_label
window_verticalLayout = QVBoxLayout()
window_verticalLayout.setContentsMargins(21, 21, 21, 21)
self.complete_label = QLabel()
window_verticalLayout.addWidget(self.complete_label)
# file_name_label
self.file_name_label = QLabel()
window_verticalLayout.addWidget(self.file_name_label)
# size_label
self.size_label = QLabel()
window_verticalLayout.addWidget(self.size_label)
# link
self.link_label = QLabel()
window_verticalLayout.addWidget(self.link_label)
self.link_lineEdit = QLineEdit()
window_verticalLayout.addWidget(self.link_lineEdit)
# save_as
self.save_as_label = QLabel()
window_verticalLayout.addWidget(self.save_as_label)
self.save_as_lineEdit = QLineEdit()
window_verticalLayout.addWidget(self.save_as_lineEdit)
# open_pushButtun
button_horizontalLayout = QHBoxLayout()
button_horizontalLayout.setContentsMargins(10, 10, 10, 10)
button_horizontalLayout.addStretch(1)
self.open_pushButtun = QPushButton()
self.open_pushButtun.setIcon(QIcon(icons + 'file'))
button_horizontalLayout.addWidget(self.open_pushButtun)
# open_folder_pushButtun
self.open_folder_pushButtun = QPushButton()
self.open_folder_pushButtun.setIcon(QIcon(icons + 'folder'))
button_horizontalLayout.addWidget(self.open_folder_pushButtun)
# ok_pushButton
self.ok_pushButton = QPushButton()
self.ok_pushButton.setIcon(QIcon(icons + 'ok'))
button_horizontalLayout.addWidget(self.ok_pushButton)
window_verticalLayout.addLayout(button_horizontalLayout)
# dont_show_checkBox
self.dont_show_checkBox = QCheckBox()
window_verticalLayout.addWidget(self.dont_show_checkBox)
window_verticalLayout.addStretch(1)
self.setLayout(window_verticalLayout)
# labels
self.open_pushButtun.setText(QCoreApplication.translate("after_download_ui_tr", " Open File "))
self.open_folder_pushButtun.setText(QCoreApplication.translate("after_download_ui_tr", "Open Download Folder"))
self.ok_pushButton.setText(QCoreApplication.translate("after_download_ui_tr", " OK "))
self.dont_show_checkBox.setText(QCoreApplication.translate(
"after_download_ui_tr", "Don't show this message again."))
self.complete_label.setText(QCoreApplication.translate("after_download_ui_tr", "Download Completed!"))
self.save_as_label.setText(QCoreApplication.translate("after_download_ui_tr", "Save as: "))
self.link_label.setText(QCoreApplication.translate("after_download_ui_tr", "Link: "))
================================================
FILE: persepolis/gui/customized_widgets.py
================================================
# -*- coding: utf-8 -*-
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
try:
from PySide6.QtWidgets import QDateTimeEdit
from PySide6.QtCore import QSettings, Qt
except ImportError:
from PyQt5.QtWidgets import QDateTimeEdit
from PyQt5.QtCore import QSettings, Qt
# import persepolis_setting
persepolis_setting = QSettings('persepolis_download_manager', 'persepolis')
# check ui_direction RTL or LTR
ui_direction = persepolis_setting.value('ui_direction')
class MyQDateTimeEdit(QDateTimeEdit):
def __init__(self, parent=None):
super().__init__(parent)
# change ui direction from rtl to ltr
if ui_direction == 'rtl':
self.setLayoutDirection(Qt.LeftToRight)
================================================
FILE: persepolis/gui/log_window_ui.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
try:
from PySide6.QtWidgets import QWidget, QTextEdit, QVBoxLayout, QHBoxLayout, QPushButton, QTabWidget
from PySide6.QtCore import Qt, QTranslator, QCoreApplication, QLocale
from PySide6.QtGui import QIcon
from PySide6 import QtCore
except ImportError:
from PyQt5.QtWidgets import QWidget, QTextEdit, QVBoxLayout, QHBoxLayout, QPushButton, QTabWidget
from PyQt5.QtCore import Qt, QTranslator, QCoreApplication, QLocale
from PyQt5.QtGui import QIcon
from PyQt5 import QtCore
from persepolis.gui import resources
class LogWindow_Ui(QWidget):
def __init__(self, persepolis_setting):
super().__init__()
self.persepolis_setting = persepolis_setting
# add support for other languages
locale = str(self.persepolis_setting.value('settings/locale'))
QLocale.setDefault(QLocale(locale))
self.translator = QTranslator()
if self.translator.load(':/translations/locales/ui_' + locale, 'ts'):
QCoreApplication.installTranslator(self.translator)
# set ui direction
ui_direction = self.persepolis_setting.value('ui_direction')
if ui_direction == 'rtl':
self.setLayoutDirection(Qt.RightToLeft)
elif ui_direction in 'ltr':
self.setLayoutDirection(Qt.LeftToRight)
icons = ':/' + \
str(self.persepolis_setting.value('settings/icons')) + '/'
# finding windows_size
self.setMinimumSize(QtCore.QSize(620, 300))
self.setWindowIcon(QIcon.fromTheme('persepolis', QIcon(':/com.github.persepolisdm.persepolis.svg')))
verticalLayout = QVBoxLayout(self)
horizontalLayout = QHBoxLayout()
horizontalLayout.addStretch(1)
# Define tabwidget
self.log_tabWidget = QTabWidget(self)
verticalLayout.addWidget(self.log_tabWidget)
# Initialization tab
self.initialization_tab = QWidget()
initialization_tab_verticalLayout = QVBoxLayout(self.initialization_tab)
# text_edit
self.initialization_text_edit = QTextEdit(self.initialization_tab)
self.initialization_text_edit.setReadOnly(True)
initialization_tab_verticalLayout.addWidget(self.initialization_text_edit)
# downloads tab
self.downloads_tab = QWidget()
downloads_tab_verticalLayout = QVBoxLayout(self.downloads_tab)
# text_edit
self.downloads_text_edit = QTextEdit(self.downloads_tab)
self.downloads_text_edit.setReadOnly(True)
downloads_tab_verticalLayout.addWidget(self.downloads_text_edit)
# errors tab
self.errors_tab = QWidget()
errors_tab_verticalLayout = QVBoxLayout(self.errors_tab)
# text_edit
self.errors_text_edit = QTextEdit(self.errors_tab)
self.errors_text_edit.setReadOnly(True)
errors_tab_verticalLayout.addWidget(self.errors_text_edit)
self.log_tabWidget.addTab(self.initialization_tab, '')
self.log_tabWidget.addTab(self.downloads_tab, '')
self.log_tabWidget.addTab(self.errors_tab, '')
# clear_log_pushButton
self.clear_log_pushButton = QPushButton(self)
horizontalLayout.addWidget(self.clear_log_pushButton)
# refresh_log_pushButton
self.refresh_log_pushButton = QPushButton(self)
self.refresh_log_pushButton.setIcon(QIcon(icons + 'refresh'))
horizontalLayout.addWidget(self.refresh_log_pushButton)
# report_pushButton
self.report_pushButton = QPushButton(self)
self.report_pushButton.setIcon(QIcon(icons + 'about'))
horizontalLayout.addWidget(self.report_pushButton)
self.copy_log_pushButton = QPushButton(self)
# copy_log_pushButton
self.copy_log_pushButton.setIcon(QIcon(icons + 'clipboard'))
horizontalLayout.addWidget(self.copy_log_pushButton)
# close_pushButton
self.close_pushButton = QPushButton(self)
self.close_pushButton.setIcon(QIcon(icons + 'remove'))
horizontalLayout.addWidget(self.close_pushButton)
verticalLayout.addLayout(horizontalLayout)
# set labels
self.setWindowTitle(QCoreApplication.translate("log_window_ui_tr", 'Persepolis Log'))
self.close_pushButton.setText(QCoreApplication.translate("log_window_ui_tr", 'Close'))
self.copy_log_pushButton.setText(QCoreApplication.translate("log_window_ui_tr", 'Copy Selected to Clipboard'))
self.report_pushButton.setText(QCoreApplication.translate("log_window_ui_tr", "Report Issue"))
self.refresh_log_pushButton.setText(QCoreApplication.translate("log_window_ui_tr", 'Refresh Log Messages'))
self.clear_log_pushButton.setText(QCoreApplication.translate("log_window_ui_tr", 'Clear Log Messages'))
self.log_tabWidget.setTabText(self.log_tabWidget.indexOf(
self.initialization_tab), QCoreApplication.translate("log_window_ui_tr", "Initialization and information"))
self.log_tabWidget.setTabText(self.log_tabWidget.indexOf(
self.downloads_tab), QCoreApplication.translate("log_window_ui_tr", "Downloads"))
self.log_tabWidget.setTabText(self.log_tabWidget.indexOf(
self.errors_tab), QCoreApplication.translate("log_window_ui_tr", "Errors and warnings"))
================================================
FILE: persepolis/gui/mainwindow_ui.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
try:
from PySide6.QtWidgets import QDial, QHeaderView, QPushButton, QComboBox, QMenu, QTreeView, QSplitter, QHBoxLayout, QVBoxLayout, QTableWidgetItem, QAbstractItemView, QToolBar, QMenuBar, QStatusBar, QTableWidget, QMainWindow, QWidget, QFrame, QCheckBox, QLabel
from PySide6.QtGui import QShortcut, QAction, QCursor, QIcon, QStandardItemModel
from PySide6.QtCore import QCoreApplication, QRect, Qt, QTranslator, QLocale
except ImportError:
from PyQt5.QtWidgets import QDial, QHeaderView, QShortcut, QPushButton, QComboBox, QTreeView, QSplitter, QHBoxLayout, QVBoxLayout, QMenu, QTableWidgetItem, QAbstractItemView, QToolBar, QMenuBar, QStatusBar, QTableWidget, QAction, QMainWindow, QWidget, QFrame, QCheckBox, QLabel
from PyQt5.QtGui import QCursor, QIcon, QStandardItemModel
from PyQt5.QtCore import QCoreApplication, QRect, Qt, QTranslator, QLocale
from persepolis.gui import resources
from persepolis.gui.customized_widgets import MyQDateTimeEdit
# align center for items in download table
class QTableWidgetItem(QTableWidgetItem):
def __init__(self, input):
super().__init__(input)
self.setTextAlignment(0x0004 | 0x0080)
class MenuWidget(QPushButton):
def __init__(self, parent):
super().__init__()
self.parent = parent
icons = ':/' + \
str(self.parent.persepolis_setting.value('settings/icons')) + '/'
# add support for other languages
locale = str(self.parent.persepolis_setting.value('settings/locale'))
QLocale.setDefault(QLocale(locale))
self.translator = QTranslator()
if self.translator.load(':/translations/locales/ui_' + locale, 'ts'):
QCoreApplication.installTranslator(self.translator)
# set ui direction
ui_direction = self.parent.persepolis_setting.value('ui_direction')
if ui_direction == 'rtl':
self.setLayoutDirection(Qt.RightToLeft)
elif ui_direction in 'ltr':
self.setLayoutDirection(Qt.LeftToRight)
# creating context menu
self.menubar = QMenu(self)
self.setMenu(self.menubar)
self.setIcon(QIcon(icons + 'menu'))
self.setStyleSheet("""QPushButton{border: none; background-color: transparent; padding: 0px}""")
fileMenu = self.menubar.addMenu(QCoreApplication.translate("mainwindow_ui_tr", 'File'))
editMenu = self.menubar.addMenu(QCoreApplication.translate("mainwindow_ui_tr", 'Edit'))
viewMenu = self.menubar.addMenu(QCoreApplication.translate("mainwindow_ui_tr", 'View'))
downloadMenu = self.menubar.addMenu(QCoreApplication.translate("mainwindow_ui_tr", 'Download'))
queueMenu = self.menubar.addMenu(QCoreApplication.translate("mainwindow_ui_tr", 'Queue'))
videoFinderMenu = self.menubar.addMenu(QCoreApplication.translate("mainwindow_ui_tr", 'Video Finder'))
helpMenu = self.menubar.addMenu(QCoreApplication.translate("mainwindow_ui_tr", 'Help'))
sortMenu = viewMenu.addMenu(QCoreApplication.translate("mainwindow_ui_tr", 'Sort by'))
videoFinderMenu.addAction(self.parent.videoFinderAddLinkAction)
downloadMenu.addAction(self.parent.stopAllAction)
sortMenu.addAction(self.parent.sort_file_name_Action)
sortMenu.addAction(self.parent.sort_file_size_Action)
sortMenu.addAction(self.parent.sort_first_try_date_Action)
sortMenu.addAction(self.parent.sort_last_try_date_Action)
sortMenu.addAction(self.parent.sort_download_status_Action)
viewMenu.addAction(self.parent.trayAction)
viewMenu.addAction(self.parent.showMenuBarAction)
viewMenu.addAction(self.parent.showSidePanelAction)
viewMenu.addAction(self.parent.minimizeAction)
fileMenu.addAction(self.parent.addlinkAction)
fileMenu.addAction(self.parent.addtextfileAction)
fileMenu.addAction(self.parent.addFromClipboardAction)
downloadMenu.addAction(self.parent.resumeAction)
downloadMenu.addAction(self.parent.pauseAction)
downloadMenu.addAction(self.parent.stopAction)
downloadMenu.addAction(self.parent.propertiesAction)
downloadMenu.addAction(self.parent.progressAction)
fileMenu.addAction(self.parent.openFileAction)
fileMenu.addAction(self.parent.openDownloadFolderAction)
fileMenu.addAction(self.parent.openDefaultDownloadFolderAction)
fileMenu.addAction(self.parent.exitAction)
editMenu.addAction(self.parent.clearAction)
editMenu.addAction(self.parent.removeSelectedAction)
editMenu.addAction(self.parent.deleteSelectedAction)
queueMenu.addAction(self.parent.createQueueAction)
queueMenu.addAction(self.parent.removeQueueAction)
queueMenu.addAction(self.parent.startQueueAction)
queueMenu.addAction(self.parent.stopQueueAction)
queueMenu.addAction(self.parent.moveUpSelectedAction)
queueMenu.addAction(self.parent.moveDownSelectedAction)
editMenu.addAction(self.parent.preferencesAction)
helpMenu.addAction(self.parent.aboutAction)
helpMenu.addAction(self.parent.issueAction)
helpMenu.addAction(self.parent.logAction)
helpMenu.addAction(self.parent.helpAction)
# viewMenu submenus
# DownloadTableWidget Class adds QMenu to QTableWidget Class
class DownloadTableWidget(QTableWidget):
def __init__(self, parent):
super().__init__()
# set ui direction
ui_direction = parent.persepolis_setting.value('ui_direction')
if ui_direction == 'rtl':
self.setLayoutDirection(Qt.RightToLeft)
elif ui_direction in 'ltr':
self.setLayoutDirection(Qt.LeftToRight)
# creating context menu
self.tablewidget_menu = QMenu(self)
self.sendMenu = self.tablewidget_menu.addMenu('')
# don't wrap items
self.setWordWrap(False)
def contextMenuEvent(self, event):
self.tablewidget_menu.popup(QCursor.pos())
# CategoryTreeView Class adds QMenu to QTreeView
class CategoryTreeView(QTreeView):
def __init__(self, parent):
super().__init__()
# set ui direction
ui_direction = parent.persepolis_setting.value('ui_direction')
if ui_direction == 'rtl':
self.setLayoutDirection(Qt.RightToLeft)
elif ui_direction in 'ltr':
self.setLayoutDirection(Qt.LeftToRight)
# creating context menu
self.category_tree_menu = QMenu(self)
# connecting activation event
self.activated.connect(parent.categoryTreeSelected)
self.pressed.connect(parent.categoryTreeSelected)
def contextMenuEvent(self, event):
self.category_tree_menu.popup(QCursor.pos())
class MainWindow_Ui(QMainWindow):
def __init__(self, persepolis_setting):
super().__init__()
# MainWindow
self.persepolis_setting = persepolis_setting
# add support for other languages
locale = str(self.persepolis_setting.value('settings/locale'))
QLocale.setDefault(QLocale(locale))
self.translator = QTranslator()
if self.translator.load(':/translations/locales/ui_' + locale, 'ts'):
QCoreApplication.installTranslator(self.translator)
# set ui direction
ui_direction = self.persepolis_setting.value('ui_direction')
if ui_direction == 'rtl':
self.setLayoutDirection(Qt.RightToLeft)
elif ui_direction in 'ltr':
self.setLayoutDirection(Qt.LeftToRight)
icons = ':/' + \
str(self.persepolis_setting.value('settings/icons')) + '/'
self.setWindowTitle(QCoreApplication.translate("mainwindow_ui_tr", "Persepolis Download Manager"))
self.setWindowIcon(QIcon.fromTheme('persepolis', QIcon(':/com.github.persepolisdm.persepolis.svg')))
self.centralwidget = QWidget(self)
self.verticalLayout = QVBoxLayout(self.centralwidget)
# enable drag and drop
self.setAcceptDrops(True)
# frame
self.frame = QFrame(self.centralwidget)
# download_table_horizontalLayout
download_table_horizontalLayout = QHBoxLayout()
horizontal_splitter = QSplitter(Qt.Horizontal)
vertical_splitter = QSplitter(Qt.Vertical)
# category_tree
self.category_tree_qwidget = QWidget(self)
category_tree_verticalLayout = QVBoxLayout()
self.category_tree = CategoryTreeView(self)
category_tree_verticalLayout.addWidget(self.category_tree)
self.category_tree_model = QStandardItemModel()
self.category_tree.setModel(self.category_tree_model)
category_table_header = [QCoreApplication.translate("mainwindow_ui_tr", 'Category')]
self.category_tree_model.setHorizontalHeaderLabels(
category_table_header)
self.category_tree.header().setStretchLastSection(True)
self.category_tree.header().setDefaultAlignment(Qt.AlignCenter)
# queue_panel
self.queue_panel_widget = QWidget(self)
queue_panel_verticalLayout_main = QVBoxLayout(self.queue_panel_widget)
# queue_panel_show_button
self.queue_panel_show_button = QPushButton(self)
queue_panel_verticalLayout_main.addWidget(self.queue_panel_show_button)
# queue_panel_widget_frame
self.queue_panel_widget_frame = QFrame(self)
self.queue_panel_widget_frame.setFrameShape(QFrame.StyledPanel)
self.queue_panel_widget_frame.setFrameShadow(QFrame.Raised)
queue_panel_verticalLayout_main.addWidget(
self.queue_panel_widget_frame)
queue_panel_verticalLayout = QVBoxLayout(self.queue_panel_widget_frame)
queue_panel_verticalLayout_main.setContentsMargins(50, -1, 50, -1)
# start_end_frame
self.start_end_frame = QFrame(self)
# start time
start_verticalLayout = QVBoxLayout(self.start_end_frame)
self.start_checkBox = QCheckBox(self)
start_verticalLayout.addWidget(self.start_checkBox)
self.start_frame = QFrame(self)
self.start_frame.setFrameShape(QFrame.StyledPanel)
self.start_frame.setFrameShadow(QFrame.Raised)
start_frame_verticalLayout = QVBoxLayout(self.start_frame)
self.start_time_qDataTimeEdit = MyQDateTimeEdit(self.start_frame)
self.start_time_qDataTimeEdit.setDisplayFormat('H:mm')
start_frame_verticalLayout.addWidget(self.start_time_qDataTimeEdit)
start_verticalLayout.addWidget(self.start_frame)
# end time
self.end_checkBox = QCheckBox(self)
start_verticalLayout.addWidget(self.end_checkBox)
self.end_frame = QFrame(self)
self.end_frame.setFrameShape(QFrame.StyledPanel)
self.end_frame.setFrameShadow(QFrame.Raised)
end_frame_verticalLayout = QVBoxLayout(self.end_frame)
self.end_time_qDateTimeEdit = MyQDateTimeEdit(self.end_frame)
self.end_time_qDateTimeEdit.setDisplayFormat('H:mm')
end_frame_verticalLayout.addWidget(self.end_time_qDateTimeEdit)
start_verticalLayout.addWidget(self.end_frame)
self.reverse_checkBox = QCheckBox(self)
start_verticalLayout.addWidget(self.reverse_checkBox)
queue_panel_verticalLayout.addWidget(self.start_end_frame)
# limit_after_frame
self.limit_after_frame = QFrame(self)
# limit_checkBox
limit_verticalLayout = QVBoxLayout(self.limit_after_frame)
# limit_frame
self.limit_frame = QFrame(self)
self.limit_frame.setFrameShape(QFrame.StyledPanel)
self.limit_frame.setFrameShadow(QFrame.Raised)
limit_verticalLayout.addWidget(self.limit_frame)
limit_frame_verticalLayout = QVBoxLayout(self.limit_frame)
# limit_dial and limit_label
self.limit_dial = QDial(self.limit_frame)
self.limit_dial.setNotchesVisible(True)
self.limit_dial.setMaximum(10)
self.limit_dial.setMinimum(0)
limit_frame_verticalLayout.addWidget(self.limit_dial)
self.limit_label = QLabel(self.limit_frame)
limit_frame_verticalLayout.addWidget(self.limit_label)
# after_checkBox
self.after_checkBox = QCheckBox(self)
limit_verticalLayout.addWidget(self.after_checkBox)
# after_frame
self.after_frame = QFrame(self)
self.after_frame.setFrameShape(QFrame.StyledPanel)
self.after_frame.setFrameShadow(QFrame.Raised)
limit_verticalLayout.addWidget(self.after_frame)
after_frame_verticalLayout = QVBoxLayout(self.after_frame)
# after_comboBox
self.after_comboBox = QComboBox(self)
self.after_comboBox.addItem("")
after_frame_verticalLayout.addWidget(self.after_comboBox)
# after_pushButton
self.after_pushButton = QPushButton(self)
after_frame_verticalLayout.addWidget(self.after_pushButton)
queue_panel_verticalLayout.addWidget(self.limit_after_frame)
category_tree_verticalLayout.addWidget(self.queue_panel_widget)
# keep_awake_checkBox
self.keep_awake_checkBox = QCheckBox(self)
queue_panel_verticalLayout.addWidget(self.keep_awake_checkBox)
self.category_tree_qwidget.setLayout(category_tree_verticalLayout)
horizontal_splitter.addWidget(self.category_tree_qwidget)
# download table widget
self.download_table_content_widget = QWidget(self)
download_table_content_widget_verticalLayout = QVBoxLayout(
self.download_table_content_widget)
# download_table
self.download_table = DownloadTableWidget(self)
vertical_splitter.addWidget(self.download_table)
horizontal_splitter.addWidget(self.download_table_content_widget)
self.download_table.setColumnCount(13)
self.download_table.setSelectionBehavior(QAbstractItemView.SelectRows)
self.download_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.download_table.verticalHeader().hide()
# hide column of GID and column of link.
self.download_table.setColumnHidden(8, True)
self.download_table.setColumnHidden(9, True)
download_table_header = [QCoreApplication.translate("mainwindow_ui_tr", 'File Name'), QCoreApplication.translate("mainwindow_ui_tr", 'Status'), QCoreApplication.translate("mainwindow_ui_tr", 'Size'), QCoreApplication.translate("mainwindow_ui_tr", 'Downloaded'), QCoreApplication.translate("mainwindow_ui_tr", 'Percentage'), QCoreApplication.translate("mainwindow_ui_tr", 'Connections'),
QCoreApplication.translate("mainwindow_ui_tr", 'Transfer Rate'), QCoreApplication.translate("mainwindow_ui_tr", 'Estimated Time Left'), 'Gid', QCoreApplication.translate("mainwindow_ui_tr", 'Link'), QCoreApplication.translate("mainwindow_ui_tr", 'First Try Date'), QCoreApplication.translate("mainwindow_ui_tr", 'Last Try Date'), QCoreApplication.translate("mainwindow_ui_tr", 'Category')]
self.download_table.setHorizontalHeaderLabels(download_table_header)
# fixing the size of download_table when window is Maximized!
self.download_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
self.download_table.horizontalHeader().setStretchLastSection(True)
horizontal_splitter.setStretchFactor(0, 3) # category_tree width
horizontal_splitter.setStretchFactor(1, 10) # ratio of tables's width
# video_finder_widget
self.video_finder_widget = QWidget(self)
video_finder_horizontalLayout = QHBoxLayout(self.video_finder_widget)
self.muxing_pushButton = QPushButton(self)
self.muxing_pushButton.setIcon(QIcon(icons + 'video_finder'))
video_finder_horizontalLayout.addWidget(self.muxing_pushButton)
video_finder_horizontalLayout.addSpacing(20)
video_audio_verticalLayout = QVBoxLayout()
self.video_label = QLabel(self)
video_audio_verticalLayout.addWidget(self.video_label)
self.audio_label = QLabel(self)
video_audio_verticalLayout.addWidget(self.audio_label)
video_finder_horizontalLayout.addLayout(video_audio_verticalLayout)
status_muxing_verticalLayout = QVBoxLayout()
self.video_finder_status_label = QLabel(self)
status_muxing_verticalLayout.addWidget(self.video_finder_status_label)
self.muxing_status_label = QLabel(self)
status_muxing_verticalLayout.addWidget(self.muxing_status_label)
video_finder_horizontalLayout.addLayout(status_muxing_verticalLayout)
vertical_splitter.addWidget(self.video_finder_widget)
download_table_content_widget_verticalLayout.addWidget(vertical_splitter)
download_table_horizontalLayout.addWidget(horizontal_splitter)
self.frame.setLayout(download_table_horizontalLayout)
self.verticalLayout.addWidget(self.frame)
self.setCentralWidget(self.centralwidget)
# menubar
self.menubar = QMenuBar(self)
self.menubar.setGeometry(QRect(0, 0, 600, 24))
self.setMenuBar(self.menubar)
fileMenu = self.menubar.addMenu(QCoreApplication.translate("mainwindow_ui_tr", '&File'))
editMenu = self.menubar.addMenu(QCoreApplication.translate("mainwindow_ui_tr", '&Edit'))
viewMenu = self.menubar.addMenu(QCoreApplication.translate("mainwindow_ui_tr", '&View'))
downloadMenu = self.menubar.addMenu(QCoreApplication.translate("mainwindow_ui_tr", '&Download'))
queueMenu = self.menubar.addMenu(QCoreApplication.translate("mainwindow_ui_tr", '&Queue'))
videoFinderMenu = self.menubar.addMenu(QCoreApplication.translate("mainwindow_ui_tr", 'V&ideo Finder'))
helpMenu = self.menubar.addMenu(QCoreApplication.translate("mainwindow_ui_tr", '&Help'))
# viewMenu submenus
sortMenu = viewMenu.addMenu(QCoreApplication.translate("mainwindow_ui_tr", 'Sort by'))
# statusbar
self.statusbar = QStatusBar(self)
self.setStatusBar(self.statusbar)
self.statusbar.showMessage(QCoreApplication.translate("mainwindow_ui_tr", "Persepolis Download Manager"))
# toolBar
self.toolBar2 = QToolBar(self)
self.addToolBar(Qt.TopToolBarArea, self.toolBar2)
# self.toolBar2.setWindowTitle(QCoreApplication.translate("mainwindow_ui_tr", 'Menu'))
self.toolBar2.setFloatable(False)
self.toolBar2.setMovable(False)
self.toolBar = QToolBar(self)
self.addToolBar(Qt.TopToolBarArea, self.toolBar)
# self.toolBar.setWindowTitle(QCoreApplication.translate("mainwindow_ui_tr", 'Toolbar'))
self.toolBar.setFloatable(False)
self.toolBar.setMovable(False)
# toolBar and menubar and actions
self.persepolis_setting.beginGroup('settings/shortcuts')
# videoFinderAddLinkAction
self.videoFinderAddLinkAction = QAction(QIcon(icons + 'video_finder'), QCoreApplication.translate("mainwindow_ui_tr", 'Find Video Links...'), self, statusTip=QCoreApplication.translate("mainwindow_ui_tr", 'Download video or audio from Youtube, Vimeo, etc.'),
triggered=self.showVideoFinderAddLinkWindow)
self.videoFinderAddLinkAction_shortcut = QShortcut(self.persepolis_setting.value(
'video_finder_shortcut'), self, self.showVideoFinderAddLinkWindow)
videoFinderMenu.addAction(self.videoFinderAddLinkAction)
# stopAllAction
self.stopAllAction = QAction(QIcon(icons + 'stop_all'), QCoreApplication.translate("mainwindow_ui_tr", 'Stop All Active Downloads'),
self, statusTip='Stop All Active Downloads', triggered=self.stopAllDownloads)
downloadMenu.addAction(self.stopAllAction)
# sort_file_name_Action
self.sort_file_name_Action = QAction(
QCoreApplication.translate("mainwindow_ui_tr", 'File Name'), self, triggered=self.sortByName)
sortMenu.addAction(self.sort_file_name_Action)
# sort_file_size_Action
self.sort_file_size_Action = QAction(
QCoreApplication.translate("mainwindow_ui_tr", 'File Size'), self, triggered=self.sortBySize)
sortMenu.addAction(self.sort_file_size_Action)
# sort_first_try_date_Action
self.sort_first_try_date_Action = QAction(
QCoreApplication.translate("mainwindow_ui_tr", 'First Try Date'), self, triggered=self.sortByFirstTry)
sortMenu.addAction(self.sort_first_try_date_Action)
# sort_last_try_date_Action
self.sort_last_try_date_Action = QAction(
QCoreApplication.translate("mainwindow_ui_tr", 'Last Try Date'), self, triggered=self.sortByLastTry)
sortMenu.addAction(self.sort_last_try_date_Action)
# sort_download_status_Action
self.sort_download_status_Action = QAction(
QCoreApplication.translate("mainwindow_ui_tr", 'Download Status'), self, triggered=self.sortByStatus)
sortMenu.addAction(self.sort_download_status_Action)
# trayAction
self.trayAction = QAction(QCoreApplication.translate("mainwindow_ui_tr", 'Show System Tray Icon'), self,
statusTip=QCoreApplication.translate("mainwindow_ui_tr", "Show/Hide system tray icon"), triggered=self.showTray)
self.trayAction.setCheckable(True)
viewMenu.addAction(self.trayAction)
# showMenuBarAction
self.showMenuBarAction = QAction(
QCoreApplication.translate("mainwindow_ui_tr", 'Show Menubar'), self, statusTip=QCoreApplication.translate("mainwindow_ui_tr", 'Show Menubar'), triggered=self.showMenuBar)
self.showMenuBarAction.setCheckable(True)
viewMenu.addAction(self.showMenuBarAction)
# showSidePanelAction
self.showSidePanelAction = QAction(
QCoreApplication.translate("mainwindow_ui_tr", 'Show Side Panel'), self, statusTip=QCoreApplication.translate("mainwindow_ui_tr", 'Show Side Panel'), triggered=self.showSidePanel)
self.showSidePanelAction.setCheckable(True)
viewMenu.addAction(self.showSidePanelAction)
# minimizeAction
self.minimizeAction = QAction(QIcon(icons + 'minimize'), QCoreApplication.translate("mainwindow_ui_tr", 'Minimize to System Tray'), self,
statusTip=QCoreApplication.translate("mainwindow_ui_tr", "Minimize to System Tray"), triggered=self.minMaxTray)
self.minimizeAction_shortcut = QShortcut(
self.persepolis_setting.value('hide_window_shortcut'), self, self.minMaxTray)
viewMenu.addAction(self.minimizeAction)
# addlinkAction
self.addlinkAction = QAction(QIcon(icons + 'add'), QCoreApplication.translate("mainwindow_ui_tr", 'Add New Download Link...'), self,
statusTip=QCoreApplication.translate("mainwindow_ui_tr", "Add New Download Link"), triggered=self.addLinkButtonPressed)
self.addlinkAction_shortcut = QShortcut(self.persepolis_setting.value(
'add_new_download_shortcut'), self, self.addLinkButtonPressed)
fileMenu.addAction(self.addlinkAction)
# importText
self.addtextfileAction = QAction(QIcon(icons + 'file'), QCoreApplication.translate("mainwindow_ui_tr", 'Import Links from Text File...'), self,
statusTip=QCoreApplication.translate("mainwindow_ui_tr", 'Create a text file and put links in it, line by line!'), triggered=self.importText)
self.addtextfileAction_shortcut = QShortcut(
self.persepolis_setting.value('import_text_shortcut'), self, self.importText)
fileMenu.addAction(self.addtextfileAction)
# importText From Clipboard
self.addFromClipboardAction = QAction(QIcon(icons + 'clipboard'), QCoreApplication.translate("mainwindow_ui_tr", 'Import Links from Clipboard...'), self,
statusTip=QCoreApplication.translate("mainwindow_ui_tr", 'Import Links From Clipboard'), triggered=self.importLinksFromClipboard)
fileMenu.addAction(self.addFromClipboardAction)
# resumeAction
self.resumeAction = QAction(QIcon(icons + 'play'), QCoreApplication.translate("mainwindow_ui_tr", 'Resume Download'), self,
statusTip=QCoreApplication.translate("mainwindow_ui_tr", "Resume Download"), triggered=self.resumeButtonPressed)
downloadMenu.addAction(self.resumeAction)
# pauseAction
self.pauseAction = QAction(QIcon(icons + 'pause'), QCoreApplication.translate("mainwindow_ui_tr", 'Pause Download'), self,
statusTip=QCoreApplication.translate("mainwindow_ui_tr", "Pause Download"), triggered=self.pauseButtonPressed)
downloadMenu.addAction(self.pauseAction)
# stopAction
self.stopAction = QAction(QIcon(icons + 'stop'), QCoreApplication.translate("mainwindow_ui_tr", 'Stop Download'), self,
statusTip=QCoreApplication.translate("mainwindow_ui_tr", "Stop/Cancel Download"), triggered=self.stopButtonPressed)
downloadMenu.addAction(self.stopAction)
# propertiesAction
self.propertiesAction = QAction(QIcon(icons + 'setting'), QCoreApplication.translate("mainwindow_ui_tr", 'Properties'), self,
statusTip=QCoreApplication.translate("mainwindow_ui_tr", "Properties"), triggered=self.propertiesButtonPressed)
downloadMenu.addAction(self.propertiesAction)
# progressAction
self.progressAction = QAction(QIcon(icons + 'window'), QCoreApplication.translate("mainwindow_ui_tr", 'Progress'), self,
statusTip=QCoreApplication.translate("mainwindow_ui_tr", "Progress"), triggered=self.progressButtonPressed)
downloadMenu.addAction(self.progressAction)
# openFileAction
self.openFileAction = QAction(QIcon(
icons + 'file'), QCoreApplication.translate("mainwindow_ui_tr", 'Open File...'), self, statusTip=QCoreApplication.translate("mainwindow_ui_tr", 'Open File...'), triggered=self.openFile)
fileMenu.addAction(self.openFileAction)
# openDownloadFolderAction
self.openDownloadFolderAction = QAction(QIcon(
icons + 'folder'), QCoreApplication.translate("mainwindow_ui_tr", 'Open Download Folder'), self, statusTip=QCoreApplication.translate("mainwindow_ui_tr", 'Open Download Folder'), triggered=self.openDownloadFolder)
fileMenu.addAction(self.openDownloadFolderAction)
# openDefaultDownloadFolderAction
self.openDefaultDownloadFolderAction = QAction(QIcon(
icons + 'folder'), QCoreApplication.translate("mainwindow_ui_tr", 'Open Default Download Folder'), self, statusTip=QCoreApplication.translate("mainwindow_ui_tr", 'Open Default Download Folder'), triggered=self.openDefaultDownloadFolder)
fileMenu.addAction(self.openDefaultDownloadFolderAction)
# exitAction
self.exitAction = QAction(QIcon(icons + 'exit'), QCoreApplication.translate("mainwindow_ui_tr", 'Exit'), self,
statusTip=QCoreApplication.translate("mainwindow_ui_tr", "Exit"), triggered=self.closeAction)
self.exitAction_shortcut = QShortcut(self.persepolis_setting.value('quit_shortcut'), self, self.closeAction)
fileMenu.addAction(self.exitAction)
# clearAction
self.clearAction = QAction(QIcon(icons + 'multi_remove'), QCoreApplication.translate("mainwindow_ui_tr", 'Clear Download List'),
self, statusTip=QCoreApplication.translate("mainwindow_ui_tr", 'Clear all items in download list'), triggered=self.clearDownloadList)
editMenu.addAction(self.clearAction)
# removeSelectedAction
self.removeSelectedAction = QAction(QIcon(icons + 'remove'), QCoreApplication.translate("mainwindow_ui_tr", 'Remove Selected Downloads from List'),
self, statusTip=QCoreApplication.translate("mainwindow_ui_tr", 'Remove Selected Downloads from List'), triggered=self.removeSelected)
self.removeSelectedAction_shortcut = QShortcut(
self.persepolis_setting.value('remove_shortcut'), self, self.removeSelected)
editMenu.addAction(self.removeSelectedAction)
self.removeSelectedAction.setEnabled(False)
# deleteSelectedAction
self.deleteSelectedAction = QAction(QIcon(icons + 'trash'), QCoreApplication.translate("mainwindow_ui_tr", 'Delete Selected Download Files'),
self, statusTip=QCoreApplication.translate("mainwindow_ui_tr", 'Delete Selected Download Files'), triggered=self.deleteSelected)
self.deleteSelectedAction_shortcut = QShortcut(
self.persepolis_setting.value('delete_shortcut'), self, self.deleteSelected)
editMenu.addAction(self.deleteSelectedAction)
self.deleteSelectedAction.setEnabled(False)
# moveSelectedDownloadsAction
self.moveSelectedDownloadsAction = QAction(QIcon(icons + 'folder'), QCoreApplication.translate("mainwindow_ui_tr", 'Move Selected Download Files to Another Folder...'),
self, statusTip=QCoreApplication.translate("mainwindow_ui_tr", 'Move Selected Download Files to Another Folder'), triggered=self.moveSelectedDownloads)
editMenu.addAction(self.moveSelectedDownloadsAction)
self.moveSelectedDownloadsAction.setEnabled(False)
# createQueueAction
self.createQueueAction = QAction(QIcon(icons + 'add_queue'), QCoreApplication.translate("mainwindow_ui_tr", 'Create New Queue...'),
self, statusTip=QCoreApplication.translate("mainwindow_ui_tr", 'Create new download queue'), triggered=self.createQueue)
queueMenu.addAction(self.createQueueAction)
# removeQueueAction
self.removeQueueAction = QAction(QIcon(icons + 'remove_queue'), QCoreApplication.translate("mainwindow_ui_tr", 'Remove Queue'),
self, statusTip=QCoreApplication.translate("mainwindow_ui_tr", 'Remove this queue'), triggered=self.removeQueue)
queueMenu.addAction(self.removeQueueAction)
# startQueueAction
self.startQueueAction = QAction(QIcon(
icons + 'start_queue'), QCoreApplication.translate("mainwindow_ui_tr", 'Start this queue'), self, statusTip=QCoreApplication.translate("mainwindow_ui_tr", 'Start Queue'), triggered=self.startQueue)
queueMenu.addAction(self.startQueueAction)
# stopQueueAction
self.stopQueueAction = QAction(QIcon(
icons + 'stop_queue'), QCoreApplication.translate("mainwindow_ui_tr", 'Stop this queue'), self, statusTip=QCoreApplication.translate("mainwindow_ui_tr", 'Stop Queue'), triggered=self.stopQueue)
queueMenu.addAction(self.stopQueueAction)
# moveUpSelectedAction
self.moveUpSelectedAction = QAction(QIcon(icons + 'multi_up'), QCoreApplication.translate("mainwindow_ui_tr", 'Move Selected Items Up'), self,
statusTip=QCoreApplication.translate("mainwindow_ui_tr", 'Move currently selected items up by one row'), triggered=self.moveUpSelected)
self.moveUpSelectedAction_shortcut = QShortcut(self.persepolis_setting.value(
'move_up_selection_shortcut'), self, self.moveUpSelected)
queueMenu.addAction(self.moveUpSelectedAction)
# moveDownSelectedAction
self.moveDownSelectedAction = QAction(QIcon(icons + 'multi_down'), QCoreApplication.translate("mainwindow_ui_tr", 'Move Selected Items Down'),
self, statusTip=QCoreApplication.translate("mainwindow_ui_tr", 'Move currently selected items down by one row'), triggered=self.moveDownSelected)
self.moveDownSelectedAction_shortcut = QShortcut(self.persepolis_setting.value(
'move_down_selection_shortcut'), self, self.moveDownSelected)
queueMenu.addAction(self.moveDownSelectedAction)
# preferencesAction
self.preferencesAction = QAction(QIcon(icons + 'preferences'), QCoreApplication.translate("mainwindow_ui_tr", 'Preferences'),
self, statusTip=QCoreApplication.translate("mainwindow_ui_tr", 'Preferences'), triggered=self.openPreferences, menuRole=QAction.MenuRole.PreferencesRole)
editMenu.addAction(self.preferencesAction)
# aboutAction
self.aboutAction = QAction(QIcon(
icons + 'about'), QCoreApplication.translate("mainwindow_ui_tr", 'About'), self, statusTip=QCoreApplication.translate("mainwindow_ui_tr", 'About'), triggered=self.openAbout, menuRole=QAction.MenuRole.AboutRole)
helpMenu.addAction(self.aboutAction)
# issueAction
self.issueAction = QAction(QIcon(icons + 'about'), QCoreApplication.translate("mainwindow_ui_tr", 'Report an Issue'),
self, statusTip=QCoreApplication.translate("mainwindow_ui_tr", 'Report an issue'), triggered=self.reportIssue)
helpMenu.addAction(self.issueAction)
# logAction
self.logAction = QAction(QIcon(icons + 'about'), QCoreApplication.translate("mainwindow_ui_tr", 'Show Log File'),
self, statusTip=QCoreApplication.translate("mainwindow_ui_tr", 'Help'), triggered=self.showLog)
helpMenu.addAction(self.logAction)
# helpAction
self.helpAction = QAction(QIcon(icons + 'about'), QCoreApplication.translate("mainwindow_ui_tr", 'Help'),
self, statusTip=QCoreApplication.translate("mainwindow_ui_tr", 'Help'), triggered=self.persepolisHelp)
helpMenu.addAction(self.helpAction)
self.persepolis_setting.endGroup()
self.qmenu = MenuWidget(self)
self.toolBar2.addWidget(self.qmenu)
# labels
self.queue_panel_show_button.setText(QCoreApplication.translate("mainwindow_ui_tr", "Hide Options"))
self.start_checkBox.setText(QCoreApplication.translate("mainwindow_ui_tr", "Start Time"))
self.end_checkBox.setText(QCoreApplication.translate("mainwindow_ui_tr", "End Time"))
self.reverse_checkBox.setText(QCoreApplication.translate(
"mainwindow_ui_tr", "Download bottom of\n the list first"))
self.after_checkBox.setText(QCoreApplication.translate("mainwindow_ui_tr", "After download"))
self.after_comboBox.setItemText(0, QCoreApplication.translate("mainwindow_ui_tr", "Shut Down"))
self.keep_awake_checkBox.setText(QCoreApplication.translate("mainwindow_ui_tr", "Keep System Awake!"))
self.keep_awake_checkBox.setToolTip(
QCoreApplication.translate("mainwindow_ui_tr", "
This option will prevent the system from going to sleep.\
It is necessary if your power manager is suspending the system automatically.
This \
option will prev\
ent the system f\
rom going to sle\
ep. I\
t is necessary i\
f your power man\
ager is suspendi\
ng the system au\
tomatically.
\
This option will\
prevent the sys\
tem from going t\
o sleep. \
It is necess\
ary if your powe\
r manager is sus\
pending the syst\
em automatically\
.
Th\
e program will a\
utomatically che\
ck the clipboard\
for copied link\
s. \
p>\
\x07\x00\x00\x00\x0dsetting_ui_\
tr\x01\x03\x00\x00\x00\x8a\x00<\x00h\x00t\x00m\
\x00l\x00>\x00<\x00h\x00e\x00a\x00d\x00/\
\x00>\x00<\x00b\x00o\x00d\x00y\x00>\x00<\
\x00p\x00>\xc7t\x00 \xae0\xb2\xa5\xc7@\x00 \
\xc6\xb4\xc6\x01\x00 \xcc\xb4\xc8\x1c\xc5\xd0\xc1\x1c\x00 \
\xc7\x91\xb3\xd9\xd5X\xc9\xc0\x00 \xc5J\xc7D\x00 \
\xc2\x18\x00 \xc7\x88\xc2\xb5\xb2\xc8\xb2\xe4\x00.\x00<\
\x00/\x00p\x00>\x00<\x00/\x00b\x00o\x00d\
\x00y\x00>\x00<\x00/\x00h\x00t\x00m\x00l\
\x00>\x08\x00\x00\x00\x00\x06\x00\x00\x00[
<\
p>This feature m\
ay not work in y\
our operating sy\
stem.
This option wil\
l prevent the sy\
stem from going \
to sleep. \
It is neces\
sary if your pow\
er manager is su\
spending the sys\
tem automaticall\
y.
This option \
avoids SSL/TLS h\
andshake failure\
. But use it at \
your own risk!\
p>\
\x07\x00\x00\x00\x0dsetting_ui_\
tr\x01\x03\x00\x00\x00\xa4\x00<\x00h\x00t\x00m\
\x00l\x00>\x00<\x00h\x00e\x00a\x00d\x00/\
\x00>\x00<\x00b\x00o\x00d\x00y\x00>\x00<\
\x00p\x00>kd\x90\x09\x98y\x5c\x06\x96;kb\
|\xfb~\xdf\x8f\xdbQewaw r\xb6`\x01\
0\x02Y\x82g\x9c`\xa8v\x84u5n\x90{\xa1\
t\x06\x8b\xbe\x7fnO\x1a\x81\xeaR\xa8c\x02\x8dw\
|\xfb~\xdf\xff\x0cR\x19kd\x8b\xbe\x7fnf/\
_\xc5\x97\x00v\x840\x02\x00<\x00/\x00p\x00>\
\x00<\x00/\x00b\x00o\x00d\x00y\x00>\x00<\
\x00/\x00h\x00t\x00m\x00l\x00>\x08\x00\x00\x00\
\x00\x06\x00\x00\x00\xba
This\
option will pre\
vent the system \
from going to sl\
eep. \
It is necessary \
if your power ma\
nager is suspend\
ing the system a\
utomatically. \
p>\
\x07\x00\x00\x00\x0dsetting_ui_\
tr\x01\x03\x00\x00\x00l\x00<\x00h\x00t\x00m\
\x00l\x00>\x00<\x00h\x00e\x00a\x00d\x00/\
\x00>\x00<\x00b\x00o\x00d\x00y\x00>\x00<\
\x00p\x00>O\x7fu(Y\x1a~\xbfz\x0bS\xef\
N\xe5c\xd0SGN\x0b\x8f}\x90\x1f^\xa60\x02\
\x00<\x00/\x00p\x00>\x00<\x00/\x00b\x00o\
\x00d\x00y\x00>\x00<\x00/\x00h\x00t\x00m\
\x00l\x00>\x08\x00\x00\x00\x00\x06\x00\x00\x00c
Using multip\
le connections c\
an help speed up\
your download.<\
/p>\x07\x00\x00\x00\x0dsetting_ui\
_tr\x01\x03\x00\x00\x00\x8e\x00<\x00h\x00t\x00\
m\x00l\x00>\x00<\x00h\x00e\x00a\x00d\x00\
/\x00>\x00<\x00b\x00o\x00d\x00y\x00>\x00\
<\x00p\x00>_SN\xcemO\x89\xc8Vhb\
i\x5cUS\xd1\x90\x01N\x0b\x8f}\x8b\xf7lBe\
\xf6\xff\x0cN\x0b\x8f}\x5c\x06_\x00Y\xcb\x80\x0cN\
\x0df>y:m\xfbR\xa0\x94\xfec\xa5z\x97S\
\xe30\x02\x00<\x00/\x00p\x00>\x00<\x00/\x00\
b\x00o\x00d\x00y\x00>\x00<\x00/\x00h\x00\
t\x00m\x00l\x00>\x08\x00\x00\x00\x00\x06\x00\x00\x00\
\xac
This opti\
on avoids SSL/TL\
S handshake fail\
ure. But use it \
at your own risk\
!
This \
option will prev\
ent the system f\
rom going to sle\
ep. I\
t is necessary i\
f your power man\
ager is suspendi\
ng the system au\
tomatically.
Using multiple connections can help speed up your download.
"))
self.connections_label.setText(QCoreApplication.translate("setting_ui_tr", "Number of connections: "))
self.connections_spinBox.setToolTip(
QCoreApplication.translate("setting_ui_tr", "
Using multiple connections can help speed up your download.
"))
self.chunk_size_label.setText(QCoreApplication.translate("setting_ui_tr", "Chunk size(KiB): "))
self.chunk_size_label.setToolTip(
QCoreApplication.translate("setting_ui_tr", "It is python requests library chunk size. Do not change this If you are not familiar with it."))
self.wait_queue_label.setText(QCoreApplication.translate(
"setting_ui_tr", 'Wait period between each download in queue:'))
self.dont_check_certificate_checkBox.setText(QCoreApplication.translate("setting_ui_tr", "Don't use certificate to verify the peers"))
self.dont_check_certificate_checkBox.setToolTip(
QCoreApplication.translate("setting_ui_tr", "
This option avoids SSL/TLS handshake failure. But use it at your own risk!
This feature may not work in your operating system.
"))
self.start_persepolis_if_browser_executed_checkBox.setText(
QCoreApplication.translate('setting_ui_tr', 'If browser is opened, start Persepolis in system tray'))
self.enable_system_tray_checkBox.setText(
QCoreApplication.translate("setting_ui_tr", "Enable system tray icon"))
self.after_download_checkBox.setText(
QCoreApplication.translate("setting_ui_tr", "Show download complete dialog when download is finished"))
self.show_menubar_checkbox.setText(QCoreApplication.translate("setting_ui_tr", "Show menubar"))
self.show_sidepanel_checkbox.setText(QCoreApplication.translate("setting_ui_tr", "Show side panel"))
self.show_progress_window_checkbox.setText(
QCoreApplication.translate("setting_ui_tr", "Show download progress window"))
self.startup_checkbox.setText(QCoreApplication.translate("setting_ui_tr", "Run Persepolis at startup"))
self.keep_awake_checkBox.setText(QCoreApplication.translate("setting_ui_tr", "Keep system awake!"))
self.keep_awake_checkBox.setToolTip(
QCoreApplication.translate("setting_ui_tr", "
This option will prevent the system from going to sleep.\
It is necessary if your power manager is suspending the system automatically.
"))
self.check_clipboard_checkBox.setText(QCoreApplication.translate("setting_ui_tr", "Check system clipboard for copied links"))
self.check_clipboard_checkBox.setToolTip(
QCoreApplication.translate("setting_ui_tr", "
The program will automatically check the clipboard for copied links.\
"))
self.dont_show_add_link_window_checkBox.setText(QCoreApplication.translate("setting_ui_tr", "Download requests from the browser will be executed immediately."))
self.dont_show_add_link_window_checkBox.setToolTip(
QCoreApplication.translate("setting_ui_tr", "
When a download request is sent from the browser extension, the download will start without showing the Add Link window.\
"))
self.setting_tabWidget.setTabText(
self.setting_tabWidget.indexOf(self.style_tab), QCoreApplication.translate("setting_ui_tr", "Preferences"))
# columns_tab
self.show_column_label.setText(QCoreApplication.translate("setting_ui_tr", 'Show these columns:'))
self.column0_checkBox.setText(QCoreApplication.translate("setting_ui_tr", 'File Name'))
self.column1_checkBox.setText(QCoreApplication.translate("setting_ui_tr", 'Status'))
self.column2_checkBox.setText(QCoreApplication.translate("setting_ui_tr", 'Size'))
self.column3_checkBox.setText(QCoreApplication.translate("setting_ui_tr", 'Downloaded'))
self.column4_checkBox.setText(QCoreApplication.translate("setting_ui_tr", 'Percentage'))
self.column5_checkBox.setText(QCoreApplication.translate("setting_ui_tr", 'Connections'))
self.column6_checkBox.setText(QCoreApplication.translate("setting_ui_tr", 'Transfer Rate'))
self.column7_checkBox.setText(QCoreApplication.translate("setting_ui_tr", 'Estimated Time Left'))
self.column10_checkBox.setText(QCoreApplication.translate("setting_ui_tr", 'First Try Date'))
self.column11_checkBox.setText(QCoreApplication.translate("setting_ui_tr", 'Last Try Date'))
self.column12_checkBox.setText(QCoreApplication.translate("setting_ui_tr", 'Category'))
self.setting_tabWidget.setTabText(
self.setting_tabWidget.indexOf(self.columns_tab), QCoreApplication.translate("setting_ui_tr", "Columns Customization"))
# Video Finder options tab
self.setting_tabWidget.setTabText(self.setting_tabWidget.indexOf(
self.video_finder_tab), QCoreApplication.translate("setting_ui_tr", "Video Finder Options"))
self.max_links_label.setText(QCoreApplication.translate("setting_ui_tr", 'Maximum number of links to capture: '
'(If browser sends multiple video links at a time)'))
# browser_integration_tab
for key, checkbox in self.browser_checkboxes.items():
checkbox.setText(QCoreApplication.translate("setting_ui_tr", key.capitalize()))
# window buttons
self.defaults_pushButton.setText(QCoreApplication.translate("setting_ui_tr", "Defaults"))
self.cancel_pushButton.setText(QCoreApplication.translate("setting_ui_tr", "Cancel"))
self.ok_pushButton.setText(QCoreApplication.translate("setting_ui_tr", "OK"))
================================================
FILE: persepolis/gui/text_queue_ui.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
try:
from PySide6.QtWidgets import QHeaderView, QPushButton, QWidget, QTabWidget, QVBoxLayout, QTableWidget, QAbstractItemView, QLabel, QLineEdit, QHBoxLayout, QSpinBox, QComboBox, QFrame, QCheckBox, QGridLayout, QRadioButton
from PySide6.QtCore import Qt, QTranslator, QCoreApplication, QLocale
from PySide6.QtGui import QIcon
except ImportError:
from PyQt5.QtWidgets import QHeaderView, QPushButton, QWidget, QTabWidget, QVBoxLayout, QTableWidget, QAbstractItemView, QLabel, QLineEdit, QHBoxLayout, QSpinBox, QComboBox, QFrame, QCheckBox, QGridLayout, QRadioButton
from PyQt5.QtCore import Qt, QTranslator, QCoreApplication, QLocale
from PyQt5.QtGui import QIcon
from persepolis.gui import resources
class TextQueue_Ui(QWidget):
def __init__(self, persepolis_setting):
super().__init__()
self.persepolis_setting = persepolis_setting
icons = ':/' + \
str(self.persepolis_setting.value('settings/icons')) + '/'
# add support for other languages
locale = str(self.persepolis_setting.value('settings/locale'))
QLocale.setDefault(QLocale(locale))
self.translator = QTranslator()
if self.translator.load(':/translations/locales/ui_' + locale, 'ts'):
QCoreApplication.installTranslator(self.translator)
# set ui direction
ui_direction = self.persepolis_setting.value('ui_direction')
if ui_direction == 'rtl':
self.setLayoutDirection(Qt.RightToLeft)
elif ui_direction in 'ltr':
self.setLayoutDirection(Qt.LeftToRight)
self.setWindowIcon(QIcon.fromTheme('persepolis', QIcon(':/com.github.persepolisdm.persepolis.svg')))
window_verticalLayout = QVBoxLayout()
self.setLayout(window_verticalLayout)
# queue_tabWidget
self.queue_tabWidget = QTabWidget(self)
window_verticalLayout.addWidget(self.queue_tabWidget)
# links_tab
self.links_tab = QWidget()
links_tab_verticalLayout = QVBoxLayout(self.links_tab)
# link table
self.links_table = QTableWidget(self.links_tab)
self.links_table.setSizeAdjustPolicy(QAbstractItemView.AdjustToContents)
links_tab_verticalLayout.addWidget(self.links_table)
self.links_table.setSelectionBehavior(QAbstractItemView.SelectRows)
self.links_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.links_table.verticalHeader().hide()
self.links_table.setColumnCount(3)
links_table_header_labels = [
'File Name', 'Download Link', 'dictionary']
self.links_table.setHorizontalHeaderLabels(links_table_header_labels)
self.links_table.setColumnHidden(2, True)
self.links_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
self.links_table.horizontalHeader().setStretchLastSection(True)
# add_queue
add_queue_horizontalLayout = QHBoxLayout()
self.select_all_pushButton = QPushButton(self.links_tab)
add_queue_horizontalLayout.addWidget(self.select_all_pushButton)
self.deselect_all_pushButton = QPushButton(self.links_tab)
add_queue_horizontalLayout.addWidget(self.deselect_all_pushButton)
add_queue_horizontalLayout.addStretch(1)
self.add_queue_label = QLabel(self.links_tab)
add_queue_horizontalLayout.addWidget(self.add_queue_label)
self.add_queue_comboBox = QComboBox(self.links_tab)
add_queue_horizontalLayout.addWidget(self.add_queue_comboBox)
links_tab_verticalLayout.addLayout(add_queue_horizontalLayout)
links_tab_verticalLayout.addStretch(1)
self.queue_tabWidget.addTab(self.links_tab, "")
# options_tab
self.options_tab = QWidget()
options_tab_verticalLayout = QVBoxLayout(self.options_tab)
# proxy
proxy_verticalLayout = QVBoxLayout()
self.proxy_checkBox = QCheckBox(self.options_tab)
proxy_verticalLayout.addWidget(self.proxy_checkBox)
self.proxy_frame = QFrame(self.options_tab)
self.proxy_frame.setFrameShape(QFrame.StyledPanel)
self.proxy_frame.setFrameShadow(QFrame.Raised)
proxy_gridLayout = QGridLayout(self.proxy_frame)
self.ip_lineEdit = QLineEdit(self.proxy_frame)
self.ip_lineEdit.setInputMethodHints(Qt.ImhNone)
proxy_gridLayout.addWidget(self.ip_lineEdit, 0, 1, 1, 1)
self.proxy_pass_label = QLabel(self.proxy_frame)
proxy_gridLayout.addWidget(self.proxy_pass_label, 2, 2, 1, 1)
self.proxy_pass_lineEdit = QLineEdit(self.proxy_frame)
self.proxy_pass_lineEdit.setEchoMode(QLineEdit.Password)
proxy_gridLayout.addWidget(self.proxy_pass_lineEdit, 2, 3, 1, 1)
self.ip_label = QLabel(self.proxy_frame)
proxy_gridLayout.addWidget(self.ip_label, 0, 0, 1, 1)
self.proxy_user_lineEdit = QLineEdit(self.proxy_frame)
proxy_gridLayout.addWidget(self.proxy_user_lineEdit, 0, 3, 1, 1)
self.proxy_user_label = QLabel(self.proxy_frame)
proxy_gridLayout.addWidget(self.proxy_user_label, 0, 2, 1, 1)
self.port_label = QLabel(self.proxy_frame)
proxy_gridLayout.addWidget(self.port_label, 2, 0, 1, 1)
self.port_spinBox = QSpinBox(self.proxy_frame)
self.port_spinBox.setMaximum(9999)
self.port_spinBox.setSingleStep(1)
proxy_gridLayout.addWidget(self.port_spinBox, 2, 1, 1, 1)
proxy_verticalLayout.addWidget(self.proxy_frame)
# type of proxy
self.http_radioButton = QRadioButton(self.proxy_frame)
proxy_gridLayout.addWidget(self.http_radioButton, 4, 0, 1, 1)
self.https_radioButton = QRadioButton(self.proxy_frame)
proxy_gridLayout.addWidget(self.https_radioButton, 5, 0, 1, 1)
# hide this widget until https support
self.https_radioButton.hide()
self.socks5_radioButton = QRadioButton(self.proxy_frame)
proxy_gridLayout.addWidget(self.socks5_radioButton, 6, 0, 1, 1)
options_tab_verticalLayout.addLayout(proxy_verticalLayout)
# download Username & Password
download_horizontalLayout = QHBoxLayout()
download_horizontalLayout.setContentsMargins(-1, 10, -1, -1)
download_verticalLayout = QVBoxLayout()
self.download_checkBox = QCheckBox(self.options_tab)
download_verticalLayout.addWidget(self.download_checkBox)
self.download_frame = QFrame(self.options_tab)
self.download_frame.setFrameShape(QFrame.StyledPanel)
self.download_frame.setFrameShadow(QFrame.Raised)
download_gridLayout = QGridLayout(self.download_frame)
self.download_user_lineEdit = QLineEdit(self.download_frame)
download_gridLayout.addWidget(self.download_user_lineEdit, 0, 1, 1, 1)
self.download_user_label = QLabel(self.download_frame)
download_gridLayout.addWidget(self.download_user_label, 0, 0, 1, 1)
self.download_pass_label = QLabel(self.download_frame)
download_gridLayout.addWidget(self.download_pass_label, 1, 0, 1, 1)
self.download_pass_lineEdit = QLineEdit(self.download_frame)
self.download_pass_lineEdit.setEchoMode(QLineEdit.Password)
download_gridLayout.addWidget(self.download_pass_lineEdit, 1, 1, 1, 1)
download_verticalLayout.addWidget(self.download_frame)
download_horizontalLayout.addLayout(download_verticalLayout)
# select folder
self.folder_frame = QFrame(self.options_tab)
self.folder_frame.setFrameShape(QFrame.StyledPanel)
self.folder_frame.setFrameShadow(QFrame.Raised)
folder_gridLayout = QGridLayout(self.folder_frame)
self.download_folder_lineEdit = QLineEdit(self.folder_frame)
folder_gridLayout.addWidget(self.download_folder_lineEdit, 2, 0, 1, 1)
self.folder_pushButton = QPushButton(self.folder_frame)
folder_gridLayout.addWidget(self.folder_pushButton, 3, 0, 1, 1)
self.folder_pushButton.setIcon(QIcon(icons + 'folder'))
self.folder_checkBox = QCheckBox(self.folder_frame)
folder_gridLayout.addWidget(self.folder_checkBox)
self.folder_label = QLabel(self.folder_frame)
self.folder_label.setAlignment(Qt.AlignCenter)
folder_gridLayout.addWidget(self.folder_label, 1, 0, 1, 1)
download_horizontalLayout.addWidget(self.folder_frame)
options_tab_verticalLayout.addLayout(download_horizontalLayout)
self.queue_tabWidget.addTab(self.options_tab, '')
# limit Speed
limit_verticalLayout = QVBoxLayout()
self.limit_frame = QFrame(self.options_tab)
self.limit_frame.setFrameShape(QFrame.StyledPanel)
self.limit_frame.setFrameShadow(QFrame.Raised)
connections_verticaLayout = QVBoxLayout(self.limit_frame)
limit_verticalLayout.addWidget(self.limit_frame)
limit_connections_horizontalLayout = QHBoxLayout()
limit_connections_horizontalLayout.addLayout(limit_verticalLayout)
# number of connections
connections_horizontalLayout = QHBoxLayout()
connections_horizontalLayout.setContentsMargins(-1, 10, -1, -1)
self.connections_frame = QFrame(self.options_tab)
self.connections_frame.setFrameShape(QFrame.StyledPanel)
self.connections_frame.setFrameShadow(QFrame.Raised)
self.connections_label = QLabel(self.connections_frame)
connections_verticaLayout.addWidget(self.connections_label)
self.connections_spinBox = QSpinBox(self.connections_frame)
self.connections_spinBox.setMinimum(1)
self.connections_spinBox.setMaximum(64)
self.connections_spinBox.setProperty("value", 64)
connections_verticaLayout.addWidget(self.connections_spinBox)
connections_horizontalLayout.addWidget(self.connections_frame)
limit_connections_horizontalLayout.addLayout(connections_horizontalLayout)
options_tab_verticalLayout.addLayout(limit_connections_horizontalLayout)
options_tab_verticalLayout.addStretch(1)
# buttons
buttons_horizontalLayout = QHBoxLayout()
buttons_horizontalLayout.addStretch(1)
# ok_pushButton
self.ok_pushButton = QPushButton(self)
self.ok_pushButton.setIcon(QIcon(icons + 'ok'))
buttons_horizontalLayout.addWidget(self.ok_pushButton)
# cancel_pushButton
self.cancel_pushButton = QPushButton(self)
self.cancel_pushButton.setIcon(QIcon(icons + 'remove'))
buttons_horizontalLayout.addWidget(self.cancel_pushButton)
window_verticalLayout.addLayout(buttons_horizontalLayout)
# labels
self.setWindowTitle(QCoreApplication.translate("text_ui_tr", "Persepolis Download Manager"))
self.queue_tabWidget.setTabText(
self.queue_tabWidget.indexOf(self.links_tab), QCoreApplication.translate("text_ui_tr", 'Links'))
self.queue_tabWidget.setTabText(
self.queue_tabWidget.indexOf(self.options_tab), QCoreApplication.translate("text_ui_tr", 'Download Options'))
self.select_all_pushButton.setText(QCoreApplication.translate("text_ui_tr", 'Select All'))
self.deselect_all_pushButton.setText(QCoreApplication.translate("text_ui_tr", 'Deselect All'))
self.add_queue_label.setText(QCoreApplication.translate("text_ui_tr", 'Add to queue: '))
self.proxy_checkBox.setText(QCoreApplication.translate("text_ui_tr", 'Proxy'))
self.proxy_pass_label.setText(QCoreApplication.translate("text_ui_tr", "Proxy password: "))
self.ip_label.setText(QCoreApplication.translate("text_ui_tr", "IP:"))
self.proxy_user_label.setText(QCoreApplication.translate("text_ui_tr", "Proxy username: "))
self.port_label.setText(QCoreApplication.translate("text_ui_tr", "Port:"))
self.http_radioButton.setText(QCoreApplication.translate("addlink_ui_tr", "HTTP"))
self.https_radioButton.setText(QCoreApplication.translate("addlink_ui_tr", "HTTPS"))
self.socks5_radioButton.setText(QCoreApplication.translate("addlink_ui_tr", "SOCKS5"))
self.download_checkBox.setText(QCoreApplication.translate("text_ui_tr", "Download username and password"))
self.download_user_label.setText(QCoreApplication.translate("text_ui_tr", "Download username: "))
self.download_pass_label.setText(QCoreApplication.translate("text_ui_tr", "Download password: "))
self.folder_pushButton.setText(QCoreApplication.translate("text_ui_tr", "Change Download Folder"))
self.folder_checkBox.setText(QCoreApplication.translate("addlink_ui_tr", "Remember this path"))
self.folder_label.setText(QCoreApplication.translate("text_ui_tr", "Download folder: "))
self.connections_label.setText(QCoreApplication.translate("text_ui_tr", "Number of connections:"))
self.ok_pushButton.setText(QCoreApplication.translate("text_ui_tr", 'OK'))
self.cancel_pushButton.setText(QCoreApplication.translate("text_ui_tr", 'Cancel'))
================================================
FILE: persepolis/gui/video_finder_progress_ui.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
from persepolis.gui.progress_ui import ProgressWindow_Ui
try:
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel
from PySide6.QtCore import QCoreApplication
except ImportError:
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel
from PyQt5.QtCore import QCoreApplication
class VideoFinderProgressWindow_Ui(ProgressWindow_Ui):
def __init__(self, persepolis_setting, parent):
super().__init__(persepolis_setting, parent)
# status_tab
self.status_tab = QWidget()
status_tab_verticalLayout = QVBoxLayout(self.status_tab)
# video_status_label
self.video_status_label = QLabel(self.status_tab)
status_tab_verticalLayout.addWidget(self.video_status_label)
# audio_status_label
self.audio_status_label = QLabel(self.status_tab)
status_tab_verticalLayout.addWidget(self.audio_status_label)
# muxing_status_label
self.muxing_status_label = QLabel(self.status_tab)
status_tab_verticalLayout.addWidget(self.muxing_status_label)
self.progress_tabWidget.addTab(self.status_tab, "")
# set status_tab as default tab
self.progress_tabWidget.setCurrentIndex(2)
# hide limit speed
self.limit_frame.setVisible(False)
# hide pause_pushButton
self.pause_pushButton.setVisible(False)
# hide resume_pushButton
self.resume_pushButton.setVisible(False)
# labels
self.video_status_label.setText(QCoreApplication.translate(
"video_finder_progress_ui_tr", "Video file status: "))
self.audio_status_label.setText(QCoreApplication.translate(
"video_finder_progress_ui_tr", "Audio file status: "))
self.muxing_status_label.setText(QCoreApplication.translate(
"video_finder_progress_ui_tr", "Mixing status: "))
self.progress_tabWidget.setTabText(self.progress_tabWidget.indexOf(
self.status_tab), QCoreApplication.translate("setting_ui_tr", "Status"))
================================================
FILE: persepolis/meson.build
================================================
shell_sources = [
'__init__.py',
'__main__.py'
]
constants_sources = [
'constants/Browser.py',
'constants/__init__.py',
'constants/Os.py',
'constants/Version.py'
]
gui_sources = [
'gui/__init__.py',
'gui/about_ui.py',
'gui/after_download_ui.py',
'gui/mainwindow_ui.py',
'gui/progress_ui.py',
'gui/resources.py',
'gui/text_queue_ui.py',
'gui/addlink_ui.py',
'gui/customized_widgets.py',
'gui/log_window_ui.py',
'gui/setting_ui.py',
'gui/video_finder_progress_ui.py'
]
scripts_sources = [
'scripts/about.py',
'scripts/bubble.py',
'scripts/download_link.py',
'scripts/error_window.py',
'scripts/play.py',
'scripts/shutdown.py',
'scripts/useful_tools.py',
'scripts/addlink.py',
'scripts/check_proxy.py',
'scripts/initialization.py',
'scripts/mainwindow.py',
'scripts/progress.py',
'scripts/spider.py',
'scripts/video_finder_addlink.py',
'scripts/after_download.py',
'scripts/compatibility.py',
'scripts/__init__.py',
'scripts/newopen.py',
'scripts/properties.py',
'scripts/startup.py',
'scripts/video_finder_progress.py',
'scripts/browser_integration.py',
'scripts/data_base.py',
'scripts/logger.py',
'scripts/osCommands.py',
'scripts/queue.py',
'scripts/text_queue.py',
'scripts/browser_plugin_queue.py',
'scripts/persepolis_lib_prime.py',
'scripts/log_window.py',
'scripts/persepolis.py',
'scripts/setting.py',
'scripts/video_finder.py',
'scripts/ytdlp_downloader.py',
]
python3.install_sources(shell_sources, subdir: 'persepolis')
python3.install_sources(gui_sources, subdir: join_paths('persepolis', 'gui'))
python3.install_sources(scripts_sources, subdir: join_paths('persepolis', 'scripts'))
python3.install_sources(constants_sources, subdir: join_paths('persepolis', 'constants'))
python = import('python')
conf = configuration_data()
conf.set('PYTHON', python.find_installation('python3').full_path())
conf.set('pythonsitepackagedir', python3.get_install_dir())
configure_file(
input: 'persepolis.py',
output: 'persepolis',
configuration: conf,
install: true,
install_dir: get_option('bindir'),
install_mode: 'r-xr-xr-x'
)
================================================
FILE: persepolis/persepolis.py
================================================
#!@PYTHON@
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
# this file is created for running persepolis from /usr/bin/ or /usr/local/bin/.
import sys
pythonsitepackagedir = '@pythonsitepackagedir@'
if __name__ == '__main__':
sys.path.append(pythonsitepackagedir)
from persepolis.scripts import persepolis
persepolis.main()
================================================
FILE: persepolis/scripts/.pep8
================================================
[pycodestyle]
max_line_length = 120
================================================
FILE: persepolis/scripts/__init__.py
================================================
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
# import sys
# sys.path.insert(1, '.')
================================================
FILE: persepolis/scripts/about.py
================================================
# -*- coding: utf-8 -*-
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
try:
from PySide6.QtCore import Qt, QSize, QPoint, QFile, QIODevice, QTextStream
from PySide6.QtGui import QIcon
except ImportError:
from PyQt5.QtCore import Qt, QSize, QPoint, QFile, QIODevice, QTextStream
from PyQt5.QtGui import QIcon
from persepolis.gui.about_ui import AboutWindow_Ui
from persepolis.gui import resources
class AboutWindow(AboutWindow_Ui):
def __init__(self, persepolis_setting):
super().__init__(persepolis_setting)
self.persepolis_setting = persepolis_setting
# setting window size and position
size = self.persepolis_setting.value(
'AboutWindow/size', QSize(545, 375))
position = self.persepolis_setting.value(
'AboutWindow/position', QPoint(300, 300))
# read translators.txt files.
# this file contains all translators.
f = QFile(':/translators.txt')
f.open(QIODevice.ReadOnly | QFile.Text)
f_text = QTextStream(f).readAll()
f.close()
self.translators_textEdit.insertPlainText(f_text)
self.resize(size)
self.move(position)
def keyPressEvent(self, event):
if event.key() == Qt.Key_Escape:
self.close()
def changeIcon(self, icons):
icons = ':/' + str(icons) + '/'
self.pushButton.setIcon(QIcon(icons + 'ok'))
def closeEvent(self, event):
# saving window size and position
self.persepolis_setting.setValue('AboutWindow/size', self.size())
self.persepolis_setting.setValue('AboutWindow/position', self.pos())
self.persepolis_setting.sync()
event.accept()
================================================
FILE: persepolis/scripts/addlink.py
================================================
# -*- coding: utf-8 -*-
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
try:
from PySide6.QtWidgets import QApplication, QFileDialog
from PySide6.QtCore import Qt, QPoint, QSize, QDir, QThread, Signal
from PySide6.QtGui import QIcon
except ImportError:
from PyQt5.QtWidgets import QApplication, QFileDialog
from PyQt5.QtCore import Qt, QPoint, QSize, QDir, QThread
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import pyqtSignal as Signal
from persepolis.gui.addlink_ui import AddLinkWindow_Ui
from persepolis.scripts.check_proxy import getProxy
from persepolis.scripts import spider
from persepolis.scripts import logger
from functools import partial
import os
# find file name and file size
class AddLinkSpiderThread(QThread):
ADDLINKSPIDERSIGNAL = Signal(dict)
def __init__(self, add_link_dictionary):
QThread.__init__(self)
self.add_link_dictionary = add_link_dictionary
def run(self):
try:
# get file name and file size
file_name, file_size = spider.addLinkSpider(self.add_link_dictionary)
spider_dict = {'file_size': file_size, 'file_name': file_name}
# emit results
self.ADDLINKSPIDERSIGNAL.emit(spider_dict)
# write an ERROR in log, If spider couldn't find file_name or file_size.
if not (file_name):
logger.sendToLog(
"Spider couldn't find file name", "ERROR")
if not (file_size):
logger.sendToLog(
"Spider couldn't find file size", "ERROR")
except Exception as e:
logger.sendToLog(
"Spider couldn't find download information", "ERROR")
logger.sendToLog(
str(e), "ERROR")
class AddLinkWindow(AddLinkWindow_Ui):
def __init__(self, parent, callback, persepolis_setting, plugin_add_link_dictionary={}):
super().__init__(persepolis_setting)
self.callback = callback
self.plugin_add_link_dictionary = plugin_add_link_dictionary
self.persepolis_setting = persepolis_setting
self.parent = parent
# entry initialization
# read values from persepolis_setting
# connections
connections = int(
self.persepolis_setting.value('settings/connections'))
self.connections_spinBox.setValue(connections)
# download_path
download_path = str(
self.persepolis_setting.value('settings/download_path'))
self.download_folder_lineEdit.setText(download_path)
self.download_folder_lineEdit.setEnabled(False)
# enable ok button only if link_lineEdit is not empty!
# see linkLineChanged method.
self.ok_pushButton.setEnabled(False)
self.download_later_pushButton.setEnabled(False)
self.link_lineEdit.textChanged.connect(self.linkLineChanged)
# if browsers plugin didn't send any links
# then check clipboard for link!
if ('link' in self.plugin_add_link_dictionary.keys()):
# check plugin_add_link_dictionary for link!
# "link" key-value must be checked
self.link_lineEdit.setText(
str(self.plugin_add_link_dictionary['link']))
else:
# check clipboard
clipboard = QApplication.clipboard()
text = clipboard.text()
if (("tp:/" in text[2:6]) or ("tps:/" in text[2:7])):
self.link_lineEdit.setText(str(text))
# detect_proxy_pushButton
self.detect_proxy_pushButton.clicked.connect(
self.detectProxy)
# ip_lineEdit initialization ->
settings_ip = self.persepolis_setting.value(
'add_link_initialization/ip', None)
if (settings_ip):
self.ip_lineEdit.setText(str(settings_ip))
# proxy user lineEdit initialization ->
settings_proxy_user = self.persepolis_setting.value(
'add_link_initialization/proxy_user', None)
if (settings_proxy_user):
self.proxy_user_lineEdit.setText(str(settings_proxy_user))
# port_spinBox initialization ->
settings_port = self.persepolis_setting.value(
'add_link_initialization/port', 0)
self.port_spinBox.setValue(int(int(settings_port)))
# download UserName initialization ->
settings_download_user = self.persepolis_setting.value(
'add_link_initialization/download_user', None)
if (settings_download_user):
self.download_user_lineEdit.setText(str(settings_download_user))
# http or socks5 initialization
settings_proxy_type = self.persepolis_setting.value(
'add_link_initialization/proxy_type', None)
if settings_proxy_type == 'socks5':
self.socks5_radioButton.setChecked(True)
elif settings_proxy_type == 'https':
self.https_radioButton.setChecked(True)
else:
self.http_radioButton.setChecked(True)
# get categories name and add them to add_queue_comboBox
categories_list = self.parent.persepolis_db.categoriesList()
for queue in categories_list:
if queue != 'All Downloads':
self.add_queue_comboBox.addItem(queue)
self.add_queue_comboBox.setCurrentIndex(0)
# add_queue_comboBox event
self.add_queue_comboBox.currentIndexChanged.connect(self.queueChanged)
# connect folder_pushButton
self.folder_pushButton.clicked.connect(self.changeFolder)
# connect OK and cancel download_later button ->
self.cancel_pushButton.clicked.connect(self.close)
self.ok_pushButton.clicked.connect(partial(
self.okButtonPressed, download_later=False))
self.download_later_pushButton.clicked.connect(
partial(self.okButtonPressed, download_later=True))
# frames and checkBoxes ->
self.proxy_frame.setEnabled(False)
self.proxy_checkBox.toggled.connect(self.proxyFrame)
self.download_frame.setEnabled(False)
self.download_checkBox.toggled.connect(self.downloadFrame)
self.start_frame.setEnabled(False)
self.start_checkBox.toggled.connect(self.startFrame)
self.end_frame.setEnabled(False)
self.end_checkBox.toggled.connect(self.endFrame)
self.change_name_lineEdit.setEnabled(False)
self.change_name_checkBox.toggled.connect(self.changeName)
self.add_link_tabWidget.currentChanged.connect(self.currentTabChanged)
# set focus to ok button
self.ok_pushButton.setFocus()
# check plugin_add_link_dictionary for finding file name
# perhaps plugin sended file name in plugin_add_link_dictionary
# for finding file name "out" key must be checked
if ('out' in self.plugin_add_link_dictionary.keys()):
if self.plugin_add_link_dictionary['out']:
self.change_name_lineEdit.setText(
str(self.plugin_add_link_dictionary['out']))
self.change_name_checkBox.setChecked(True)
# get referer and header and user_agent and load_cookies in plugin_add_link_dictionary if exits.
if ('referer' in self.plugin_add_link_dictionary):
self.referer_lineEdit.setText(str(self.plugin_add_link_dictionary['referer']))
if ('header' in self.plugin_add_link_dictionary):
if str(self.plugin_add_link_dictionary['header']) != 'None':
self.header_lineEdit.setText(str(self.plugin_add_link_dictionary['header']))
if ('user_agent' in self.plugin_add_link_dictionary):
self.user_agent_lineEdit.setText(str(self.plugin_add_link_dictionary['user_agent']))
if ('load_cookies' in self.plugin_add_link_dictionary):
self.load_cookies_lineEdit.setText((self.plugin_add_link_dictionary['load_cookies']))
# set window size and position
size = self.persepolis_setting.value(
'AddLinkWindow/size', QSize(652, 480))
position = self.persepolis_setting.value(
'AddLinkWindow/position', QPoint(300, 300))
self.resize(size)
self.move(position)
# if user clicked on link_tab so send spider again
# perhaps proxy or user password , ... set!
def currentTabChanged(self, index):
if index == 0:
self.linkLineChanged(index)
# detect system proxy setting, and set ip_lineEdit and port_spinBox
def detectProxy(self, button):
# get system proxy information
system_proxy_dict = getProxy()
enable_proxy_frame = False
# ip
if 'http_proxy_ip' in system_proxy_dict.keys():
self.ip_lineEdit.setText(str(system_proxy_dict['http_proxy_ip']))
enable_proxy_frame = True
# port
if 'http_proxy_port' in system_proxy_dict.keys():
self.port_spinBox.setValue(int(system_proxy_dict['http_proxy_port']))
enable_proxy_frame = True
# enable proxy frame if http_proxy_ip or http_proxy_port is valid.
if enable_proxy_frame:
self.proxy_checkBox.setChecked(True)
self.detect_proxy_label.setText('')
else:
self.proxy_checkBox.setChecked(False)
self.detect_proxy_label.setText('No proxy detected!')
# active frames if checkBoxes are checked
def proxyFrame(self, checkBox):
if self.proxy_checkBox.isChecked() is True:
self.proxy_frame.setEnabled(True)
else:
self.proxy_frame.setEnabled(False)
def downloadFrame(self, checkBox):
if self.download_checkBox.isChecked() is True:
self.download_frame.setEnabled(True)
else:
self.download_frame.setEnabled(False)
def startFrame(self, checkBox):
if self.start_checkBox.isChecked() is True:
self.start_frame.setEnabled(True)
else:
self.start_frame.setEnabled(False)
def endFrame(self, checkBox):
if self.end_checkBox.isChecked() is True:
self.end_frame.setEnabled(True)
else:
self.end_frame.setEnabled(False)
def changeFolder(self, button):
# get download_path from lineEdit
download_path = self.download_folder_lineEdit.text()
# open select folder dialog
fname = QFileDialog.getExistingDirectory(
self, 'Select a directory', download_path)
if fname:
# Returns pathName with the '/' separators converted to separators that are appropriate for the underlying operating system.
# On Windows, toNativeSeparators("c:/winnt/system32") returns
# "c:\winnt\system32".
fname = QDir.toNativeSeparators(fname)
if os.path.isdir(fname):
self.download_folder_lineEdit.setText(fname)
# enable when link_lineEdit is not empty and find size of file.
def linkLineChanged(self, lineEdit):
if str(self.link_lineEdit.text()) == '':
self.ok_pushButton.setEnabled(False)
self.download_later_pushButton.setEnabled(False)
else: # find file size
# get proxy information
ip, port, proxy_user, proxy_passwd, proxy_type = self.getProxyInformation()
# get download username and password information
download_user, download_passwd = self.getUserPass()
# get additinal information
referer, header, user_agent, load_cookies = self.getAdditionalInformation()
dict = {'link': str(self.link_lineEdit.text()),
'ip': ip,
'port': port,
'proxy_user': proxy_user,
'proxy_passwd': proxy_passwd,
'proxy_type': proxy_type,
'download_user': download_user,
'download_passwd': download_passwd,
'referer': referer,
'header': header,
'user_agent': user_agent,
'load_cookies': load_cookies}
# spider is finding file size
new_spider = AddLinkSpiderThread(dict)
self.parent.threadPool.append(new_spider)
self.parent.threadPool[-1].start()
self.parent.threadPool[-1].ADDLINKSPIDERSIGNAL.connect(
partial(self.parent.addLinkSpiderCallBack, child=self))
self.ok_pushButton.setEnabled(True)
self.download_later_pushButton.setEnabled(True)
# enable change_name_lineEdit if change_name_checkBox is checked.
def changeName(self, checkBoxes):
if self.change_name_checkBox.isChecked() is True:
self.change_name_lineEdit.setEnabled(True)
else:
self.change_name_lineEdit.setEnabled(False)
def queueChanged(self, combo):
# if one of the queues selected by user , start time and end time must
# be deactivated
if self.add_queue_comboBox.currentIndex() != 0:
self.start_checkBox.setCheckState(Qt.Unchecked)
self.start_checkBox.setEnabled(False)
self.end_checkBox.setCheckState(Qt.Unchecked)
self.end_checkBox.setEnabled(False)
else:
self.start_checkBox.setEnabled(True)
self.end_checkBox.setEnabled(True)
# this method returns proxy information.
def getProxyInformation(self):
# http, https or socks5 proxy
if self.http_radioButton.isChecked() is True:
proxy_type = 'http'
elif self.https_radioButton.isChecked() is True:
proxy_type = 'https'
else:
proxy_type = 'socks5'
# get proxy information
if not (self.proxy_checkBox.isChecked()):
ip = None
port = None
proxy_user = None
proxy_passwd = None
proxy_type = None
else:
ip = self.ip_lineEdit.text()
if not (ip):
ip = None
port = self.port_spinBox.value()
if not (port):
port = None
proxy_user = self.proxy_user_lineEdit.text()
if not (proxy_user):
proxy_user = None
proxy_passwd = self.proxy_pass_lineEdit.text()
if not (proxy_passwd):
proxy_passwd = None
return ip, port, proxy_user, proxy_passwd, proxy_type
def getUserPass(self):
# get download username and password information
if not (self.download_checkBox.isChecked()):
download_user = None
download_passwd = None
else:
download_user = self.download_user_lineEdit.text()
if not (download_user):
download_user = None
download_passwd = self.download_pass_lineEdit.text()
if not (download_passwd):
download_passwd = None
return download_user, download_passwd
def getAdditionalInformation(self):
# referer
if self.referer_lineEdit.text() != '':
referer = self.referer_lineEdit.text()
else:
referer = None
# header
if self.header_lineEdit.text() != '':
header = self.header_lineEdit.text()
else:
header = None
# user_agent
if self.user_agent_lineEdit.text() != '':
user_agent = self.user_agent_lineEdit.text()
else:
user_agent = None
# load_cookies
if self.load_cookies_lineEdit.text() != '':
load_cookies = self.load_cookies_lineEdit.text()
else:
load_cookies = None
return referer, header, user_agent, load_cookies
def okButtonPressed(self, download_later, button=None):
# user submitted information by pressing ok_pushButton, so get information
# from AddLinkWindow and return them to the mainwindow with callback!
# write user's new inputs in persepolis_setting for next time :)
self.persepolis_setting.setValue(
'add_link_initialization/ip', self.ip_lineEdit.text())
self.persepolis_setting.setValue(
'add_link_initialization/port', self.port_spinBox.value())
self.persepolis_setting.setValue(
'add_link_initialization/proxy_user', self.proxy_user_lineEdit.text())
self.persepolis_setting.setValue(
'add_link_initialization/download_user', self.download_user_lineEdit.text())
# get proxy information
ip, port, proxy_user, proxy_passwd, proxy_type = self.getProxyInformation()
if proxy_type is not None:
self.persepolis_setting.setValue('add_link_initialization/proxy_type', proxy_type)
# get download username and password information
download_user, download_passwd = self.getUserPass()
# get start time for download if user set that.
if not (self.start_checkBox.isChecked()):
start_time = None
else:
start_time = self.start_time_qDataTimeEdit.text()
# get end time for download if user set that.
if not (self.end_checkBox.isChecked()):
end_time = None
else:
end_time = self.end_time_qDateTimeEdit.text()
# check that if user set new name for download file.
if self.change_name_checkBox.isChecked():
out = str(self.change_name_lineEdit.text())
self.plugin_add_link_dictionary['out'] = out
else:
out = None
# get download link
link = self.link_lineEdit.text()
# get number of connections
connections = self.connections_spinBox.value()
# get download_path
download_path = self.download_folder_lineEdit.text()
# get additinal information
referer, header, user_agent, load_cookies = self.getAdditionalInformation()
# save information in a dictionary(add_link_dictionary).
self.add_link_dictionary = {'referer': referer, 'header': header, 'user_agent': user_agent, 'load_cookies': load_cookies,
'out': out, 'start_time': start_time, 'end_time': end_time, 'link': link, 'ip': ip,
'port': port, 'proxy_user': proxy_user, 'proxy_passwd': proxy_passwd, 'proxy_type': proxy_type,
'download_user': download_user, 'download_passwd': download_passwd,
'connections': connections, 'limit_value': 10, 'download_path': download_path}
# get category of download
category = str(self.add_queue_comboBox.currentText())
del self.plugin_add_link_dictionary
# return information to mainwindow
self.callback(self.add_link_dictionary, download_later, category)
# close window
self.close()
# close window with ESC key
def keyPressEvent(self, event):
if event.key() == Qt.Key_Escape:
self.close()
# save size and position of window, when user closes the window.
def closeEvent(self, event):
self.persepolis_setting.setValue('AddLinkWindow/size', self.size())
self.persepolis_setting.setValue('AddLinkWindow/position', self.pos())
self.persepolis_setting.sync()
event.accept()
def changeIcon(self, icons):
icons = ':/' + str(icons) + '/'
self.folder_pushButton.setIcon(QIcon(icons + 'folder'))
self.download_later_pushButton.setIcon(QIcon(icons + 'stop'))
self.cancel_pushButton.setIcon(QIcon(icons + 'remove'))
self.ok_pushButton.setIcon(QIcon(icons + 'ok'))
================================================
FILE: persepolis/scripts/after_download.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
try:
from PySide6.QtCore import Qt, QSize, QPoint, QTranslator, QCoreApplication, QLocale
from PySide6.QtGui import QIcon
except ImportError:
from PyQt5.QtCore import Qt, QSize, QPoint, QTranslator, QCoreApplication, QLocale
from PyQt5.QtGui import QIcon
from persepolis.gui.after_download_ui import AfterDownloadWindow_Ui
from persepolis.scripts import osCommands
import os
import time
class AfterDownloadWindow(AfterDownloadWindow_Ui):
def __init__(self, parent, dict, persepolis_setting):
super().__init__(persepolis_setting)
self.persepolis_setting = persepolis_setting
self.dict = dict
self.parent = parent
# add support for other languages
locale = str(self.persepolis_setting.value('settings/locale'))
QLocale.setDefault(QLocale(locale))
self.translator = QTranslator()
if self.translator.load(':/translations/locales/ui_' + locale, 'ts'):
QCoreApplication.installTranslator(self.translator)
# connecting buttons
self.open_pushButtun.clicked.connect(self.openFile)
self.open_folder_pushButtun.clicked.connect(self.openFolder)
self.ok_pushButton.clicked.connect(self.okButtonPressed)
# labels
# find gid
gid = self.dict['gid']
# If file path not valid, Wait a little and try again.
# The file transfer and database update process may
# not be finished after the download is finished.
while True:
self.add_link_dict = self.parent.persepolis_db.searchGidInAddLinkTable(gid)
file_path = self.add_link_dict['download_path']
if os.path.isfile(file_path):
break
else:
# Wait a little and try again!
time.sleep(0.1)
# save_as
self.save_as_lineEdit.setText(file_path)
self.save_as_lineEdit.setToolTip(file_path)
# link
link = str(self.dict['link'])
self.link_lineEdit.setText(link)
self.link_lineEdit.setToolTip(link)
# file_name
window_title = str(self.dict['file_name'])
file_name = QCoreApplication.translate("after_download_src_ui_tr", "File name: ") + \
window_title
self.setWindowTitle(window_title)
self.file_name_label.setText(file_name)
# size
size = QCoreApplication.translate("after_download_src_ui_tr", "Size: ") + str(self.dict['size'])
self.size_label.setText(size)
# disable link_lineEdit and save_as_lineEdit
self.link_lineEdit.setEnabled(False)
self.save_as_lineEdit.setEnabled(False)
# set window size and position
size = self.persepolis_setting.value(
'AfterDownloadWindow/size', QSize(659, 300))
position = self.persepolis_setting.value(
'AfterDownloadWindow/position', QPoint(300, 300))
self.resize(size)
self.move(position)
def openFile(self):
# execute file
file_path = self.add_link_dict['download_path']
if os.path.isfile(file_path):
osCommands.xdgOpen(file_path)
# close window
self.close()
def openFolder(self):
# open download folder
download_path = self.add_link_dict['download_path']
# file_name = os.path.basename(file_path)
# file_path_split = file_path.split(file_name)
# del file_path_split[-1]
# download_path = file_name.join(file_path_split)
if os.path.isfile(download_path):
osCommands.xdgOpen(download_path, 'folder', 'file')
# close window
self.close()
def okButtonPressed(self):
if self.dont_show_checkBox.isChecked():
self.persepolis_setting.setValue('settings/after-dialog', 'no')
self.persepolis_setting.sync()
# close window
self.close()
# close window with ESC key
def keyPressEvent(self, event):
if event.key() == Qt.Key_Escape:
self.close()
def closeEvent(self, event):
# saving window size and position
self.persepolis_setting.setValue(
'AfterDownloadWindow/size', self.size())
self.persepolis_setting.setValue(
'AfterDownloadWindow/position', self.pos())
self.persepolis_setting.sync()
event.accept()
def changeIcon(self, icons):
icons = ':/' + str(icons) + '/'
self.ok_pushButton.setIcon(QIcon(icons + 'ok'))
self.open_folder_pushButtun.setIcon(QIcon(icons + 'folder'))
self.open_pushButtun.setIcon(QIcon(icons + 'file'))
================================================
FILE: persepolis/scripts/browser_integration.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from persepolis.scripts.useful_tools import determineConfigFolder, getExecPath, findExternalAppPath
from persepolis.scripts import osCommands
from persepolis.constants import OS, BROWSER
import os
import platform
import subprocess
import sys
if sys.platform == "win32":
import winreg
os_type = platform.system()
home_address = str(os.path.expanduser("~"))
# download manager config folder .
config_folder = determineConfigFolder()
def removeRegistryKey(browser):
logg_message = 'trying to remove registry kry for ' + str(browser) + ': '
if browser == BROWSER.CHROME or browser == BROWSER.CHROMIUM:
try:
# Open the parent key
with winreg.OpenKey(winreg.HKEY_CURRENT_USER,
r"SOFTWARE\Google\Chrome\NativeMessagingHosts",
0,
winreg.KEY_ALL_ACCESS) as parent_key:
# Delete the pdmchromewrapper key
winreg.DeleteKey(parent_key, "com.persepolis.pdmchromewrapper")
logg_message = logg_message + 'Key deleted successfully.'
except FileNotFoundError:
logg_message = logg_message + 'The specified key does not exist.'
except WindowsError as e:
logg_message = logg_message + f'An error occurred: {e}'
elif browser == BROWSER.FIREFOX:
try:
# Open the parent key
with winreg.OpenKey(winreg.HKEY_CURRENT_USER,
r"SOFTWARE\Mozilla\NativeMessagingHosts",
0,
winreg.KEY_ALL_ACCESS) as parent_key:
# Delete the pdmchromewrapper key
winreg.DeleteKey(parent_key, "com.persepolis.pdmchromewrapper")
logg_message = logg_message + 'Key deleted successfully.'
except FileNotFoundError:
logg_message = logg_message + 'The specified key does not exist.'
except WindowsError as e:
logg_message = logg_message + f'An error occurred: {e}'
else:
logg_message = logg_message + 'No need to remove registry key.'
return logg_message
def installIntermidiary(intermediary):
intermediary_done = None
exec_dictionary = getExecPath()
exec_path = exec_dictionary['modified_exec_file_path']
# create persepolis_run_shell(intermediry script) file for gnu/linux and BSD and Mac
# firefox and chromium and ... call persepolis with Native Messaging system.
# json file calls persepolis_run_shell file.
if os_type in OS.UNIX_LIKE or os_type == OS.OSX:
# find available shell
shell_list = ['/bin/bash', '/usr/local/bin/bash', '/bin/sh', '/usr/local/bin/sh', '/bin/ksh', '/bin/tcsh']
for shell in shell_list:
if os.path.isfile(shell):
# define shebang
shebang = '#!' + shell
break
persepolis_run_shell_contents = shebang + '\n' + exec_path + "\t$@"
f = open(intermediary, 'w')
f.writelines(persepolis_run_shell_contents)
f.close()
# make persepolis_run_shell executable
pipe_native = subprocess.Popen(['chmod', '+x', intermediary],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
shell=False)
if pipe_native.wait() == 0:
intermediary_done = True
else:
intermediary_done = False
else:
# for M.S.Windows
intermediary_done = True
return intermediary_done
def uninstallNativeMessageHost(browser, native_message_folder):
# native message host path
native_message_file = os.path.join(
native_message_folder, 'com.persepolis.pdmchromewrapper.json')
# remove file
answer = osCommands.remove(native_message_file)
if answer == 'ok':
logg_message = 'Native message file for ' + str(browser) + ' has been deleted!'
elif answer == 'cant':
logg_message = 'Persepolis can not delete native message host file for ' + str(browser) + '.'
return logg_message
def installNativeMessageHost(browser, native_message_folder, webextension_json_connector):
json_done = None
# native message host path
native_message_file = os.path.join(
native_message_folder, 'com.persepolis.pdmchromewrapper.json')
osCommands.makeDirs(native_message_folder)
# Write native message host file
f = open(native_message_file, 'w')
f.write(str(webextension_json_connector).replace("'", "\""))
f.close()
if os_type != OS.WINDOWS:
pipe_json = subprocess.Popen(['chmod', '+x', str(native_message_file)],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
shell=False)
if pipe_json.wait() == 0:
json_done = True
else:
json_done = False
else:
import winreg
# add the key to the windows registry
if browser in BROWSER.CHROME_FAMILY:
try:
# create pdmchromewrapper key under NativeMessagingHosts
winreg.CreateKey(winreg.HKEY_CURRENT_USER,
r"SOFTWARE\Google\Chrome\NativeMessagingHosts\com.persepolis.pdmchromewrapper")
# open a connection to pdmchromewrapper key
gintKey = winreg.OpenKey(
winreg.HKEY_CURRENT_USER, r"SOFTWARE\Google\Chrome\NativeMessagingHosts\com.persepolis.pdmchromewrapper", 0, winreg.KEY_ALL_ACCESS)
# set native_message_file as key value
winreg.SetValueEx(gintKey, '', 0, winreg.REG_SZ, native_message_file)
# close connection to pdmchromewrapper
winreg.CloseKey(gintKey)
json_done = True
except WindowsError:
json_done = False
elif browser == BROWSER.FIREFOX_FAMILY:
try:
# create pdmchromewrapper key under NativeMessagingHosts for firefox
winreg.CreateKey(winreg.HKEY_CURRENT_USER,
r"SOFTWARE\Mozilla\NativeMessagingHosts\com.persepolis.pdmchromewrapper")
# open a connection to pdmchromewrapper key for firefox
fintKey = winreg.OpenKey(
winreg.HKEY_CURRENT_USER, r"SOFTWARE\Mozilla\NativeMessagingHosts\com.persepolis.pdmchromewrapper", 0, winreg.KEY_ALL_ACCESS)
# set native_message_file as key value
winreg.SetValueEx(fintKey, '', 0, winreg.REG_SZ, native_message_file)
# close connection to pdmchromewrapper
winreg.CloseKey(fintKey)
json_done = True
except WindowsError:
json_done = False
return json_done
def nativeMessageHostFile(browser, intermediary):
# WebExtension native hosts file prototype
webextension_json_connector = {
"name": "com.persepolis.pdmchromewrapper",
"type": "stdio",
"path": str(intermediary),
"description": "Integrate Persepolis with %s using WebExtensions" % (browser)
}
# Add chrom* keys
if browser in BROWSER.CHROME_FAMILY:
webextension_json_connector["allowed_origins"] = ["chrome-extension://legimlagjjoghkoedakdjhocbeomojao/"]
# Add firefox keys
elif browser == BROWSER.FIREFOX_FAMILY:
webextension_json_connector["allowed_extensions"] = [
"com.persepolis.pdmchromewrapper@persepolisdm.github.io",
"com.persepolis.pdmchromewrapper.offline@persepolisdm.github.io"
]
return webextension_json_connector
def getIntermediaryPath():
if os_type in OS.UNIX_LIKE or os_type == OS.OSX:
intermediary = os.path.join(config_folder, 'persepolis_run_shell')
logg_message = str(intermediary)
elif os_type == OS.WINDOWS:
intermediary, logg_message = findExternalAppPath('PersepolisBI')
logg_message = "Persepolis intermediary path: " + logg_message
return intermediary, logg_message
def getNativeMessageFolder(browser):
if os_type in OS.UNIX_LIKE:
if browser == BROWSER.CHROMIUM:
return os.path.join(home_address, '.config/chromium/NativeMessagingHosts')
elif browser == BROWSER.CHROME:
return os.path.join(home_address, '.config/google-chrome/NativeMessagingHosts')
elif browser == BROWSER.FIREFOX:
return os.path.join(home_address, '.mozilla/native-messaging-hosts')
elif browser == BROWSER.VIVALDI:
return os.path.join(home_address, '.config/vivaldi/NativeMessagingHosts')
elif browser == BROWSER.OPERA:
return os.path.join(home_address, '.config/opera/NativeMessagingHosts')
elif browser == BROWSER.BRAVE:
return os.path.join(home_address, '.config/BraveSoftware/Brave-Browser/NativeMessagingHosts')
elif browser == BROWSER.LIBREWOLF:
return os.path.join(home_address, '.librewolf/native-messaging-hosts')
elif os_type == OS.OSX:
if browser == BROWSER.CHROMIUM:
return os.path.join(home_address, 'Library/Application Support/Chromium/NativeMessagingHosts')
elif browser == BROWSER.CHROME:
return os.path.join(home_address, 'Library/Application Support/Google/Chrome/NativeMessagingHosts')
elif browser == BROWSER.FIREFOX:
return os.path.join(home_address, 'Library/Application Support/Mozilla/NativeMessagingHosts')
elif browser == BROWSER.VIVALDI:
return os.path.join(home_address, 'Library/Application Support/Vivaldi/NativeMessagingHosts')
elif browser == BROWSER.OPERA:
return os.path.join(home_address, 'Library/Application Support/Opera/NativeMessagingHosts')
elif browser == BROWSER.BRAVE:
return os.path.join(home_address, 'Library/Application Support/BraveSoftware/Brave-Browser/NativeMessagingHosts')
elif browser == BROWSER.LIBREWOLF:
return os.path.join(home_address, 'Library/LibreWolf/NativeMessagingHosts')
elif os_type == OS.WINDOWS:
if browser in BROWSER.CHROME_FAMILY:
return os.path.join(home_address, 'AppData', 'Local', 'persepolis_download_manager', 'chrome')
elif browser in BROWSER.FIREFOX_FAMILY:
return os.path.join(home_address, 'AppData', 'Local', 'persepolis_download_manager', 'firefox')
else:
return None
def browserIntegration(browser):
native_message_folder = getNativeMessageFolder(browser)
intermediary, logg_message = getIntermediaryPath()
webextension_json_connector = nativeMessageHostFile(browser, intermediary)
json_done = installNativeMessageHost(browser, native_message_folder, webextension_json_connector)
intermediary_done = installIntermidiary(intermediary)
return json_done, intermediary_done, logg_message
def browserIsolation(browser):
native_message_folder = getNativeMessageFolder(browser)
logg_message = uninstallNativeMessageHost(browser, native_message_folder)
if os_type == OS.WINDOWS:
logg_message2 = removeRegistryKey(browser)
else:
logg_message2 = ''
return logg_message, logg_message2
================================================
FILE: persepolis/scripts/browser_plugin_queue.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
try:
from PySide6.QtCore import Qt, QPoint, QSize, QThread, Signal, QDir
from PySide6.QtWidgets import QTableWidgetItem, QFileDialog
from PySide6.QtGui import QIcon
except ImportError:
from PyQt5.QtCore import Qt, QPoint, QSize, QThread, QDir
from PyQt5.QtWidgets import QTableWidgetItem, QFileDialog
from PyQt5.QtCore import pyqtSignal as Signal
from PyQt5.QtGui import QIcon
from persepolis.gui.text_queue_ui import TextQueue_Ui
from persepolis.scripts import logger
from persepolis.scripts import spider
from functools import partial
from copy import deepcopy
import os
# This thread finds filename
class QueueSpiderThread(QThread):
QUEUESPIDERRETURNEDFILENAME = Signal(str)
def __init__(self, dict_):
QThread.__init__(self)
self.dict_ = dict_
def run(self):
try:
filename = spider.queueSpider(self.dict_)
if filename:
self.QUEUESPIDERRETURNEDFILENAME.emit(filename)
else:
logger.logObj.error(
"Spider couldn't find download information", exc_info=True)
except Exception as e:
logger.logObj.error(
"Spider couldn't find download information", exc_info=True)
logger.logObj.error(
str(e), exc_info=True)
class BrowserPluginQueue(TextQueue_Ui):
def __init__(self, parent, list_of_links, callback, persepolis_setting):
super().__init__(persepolis_setting)
self.persepolis_setting = persepolis_setting
self.callback = callback
self.parent = parent
self.list_of_links = list_of_links
global icons
icons = ':/' + \
str(self.persepolis_setting.value('settings/icons')) + '/'
self.list_of_links.reverse()
k = 1
for dict_ in self.list_of_links:
# add row to the links_table
self.links_table.insertRow(0)
# file_name
if 'out' in dict_.keys():
if dict_['out']:
file_name = dict_['out']
else:
file_name = '***'
else:
file_name = '***'
if file_name == '***':
# spider finds file name
new_spider = QueueSpiderThread(dict_)
self.parent.threadPool.append(new_spider)
self.parent.threadPool[-1].start()
self.parent.threadPool[-1].QUEUESPIDERRETURNEDFILENAME.connect(
partial(self.parent.queueSpiderCallBack, child=self, row_number=len(self.list_of_links) - k))
k = k + 1
item = QTableWidgetItem(file_name)
# add checkbox to the item
item.setFlags(Qt.ItemIsUserCheckable
| Qt.ItemIsEnabled)
item.setCheckState(Qt.Checked)
# insert file_name
self.links_table.setItem(0, 0, item)
# find link
link = dict_['link']
item = QTableWidgetItem(str(link))
# insert link
self.links_table.setItem(0, 1, item)
# get categories name and add them to add_queue_comboBox
categories_list = self.parent.persepolis_db.categoriesList()
for queue in categories_list:
if queue != 'All Downloads':
self.add_queue_comboBox.addItem(queue)
self.add_queue_comboBox.addItem(
QIcon(icons + 'add_queue'), 'Create new queue')
# entry initialization
global connections
connections = int(
self.persepolis_setting.value('settings/connections'))
global download_path
download_path = str(
self.persepolis_setting.value('settings/download_path'))
# initialization
self.connections_spinBox.setValue(connections)
self.download_folder_lineEdit.setText(download_path)
self.download_folder_lineEdit.setEnabled(False)
# ip_lineEdit initialization
settings_ip = self.persepolis_setting.value(
'add_link_initialization/ip', None)
if settings_ip:
self.ip_lineEdit.setText(str(settings_ip))
# proxy user lineEdit initialization
settings_proxy_user = self.persepolis_setting.value(
'add_link_initialization/proxy_user', None)
if settings_proxy_user:
self.proxy_user_lineEdit.setText(str(settings_proxy_user))
# port_spinBox initialization
settings_port = self.persepolis_setting.value(
'add_link_initialization/port', 0)
self.port_spinBox.setValue(int(int(settings_port)))
# download UserName initialization
settings_download_user = self.persepolis_setting.value(
'add_link_initialization/download_user', None)
if settings_download_user:
self.download_user_lineEdit.setText(str(settings_download_user))
# http or socks5 initialization
settings_proxy_type = self.persepolis_setting.value(
'add_link_initialization/proxy_type', None)
# default is http
if settings_proxy_type == 'socks5':
self.socks5_radioButton.setChecked(True)
elif settings_proxy_type == 'https':
self.https_radioButton.setChecked(True)
else:
self.http_radioButton.setChecked(True)
# connect folder_pushButton
self.folder_pushButton.clicked.connect(self.changeFolder)
# connect OK and cancel button
self.cancel_pushButton.clicked.connect(self.close)
self.ok_pushButton.clicked.connect(self.okButtonPressed)
# connect select_all_pushButton deselect_all_pushButton
self.select_all_pushButton.clicked.connect(self.selectAll)
self.deselect_all_pushButton.clicked.connect(self.deselectAll)
# frames and checkBoxes
self.proxy_frame.setEnabled(False)
self.proxy_checkBox.toggled.connect(self.proxyFrame)
self.download_frame.setEnabled(False)
self.download_checkBox.toggled.connect(self.downloadFrame)
self.queue_tabWidget.currentChanged.connect(self.currentTabChanged)
# set focus to ok button
self.ok_pushButton.setFocus()
# add_queue_comboBox event
self.add_queue_comboBox.currentIndexChanged.connect(self.queueChanged)
# set window size and position
size = self.persepolis_setting.value('TextQueue/size', QSize(700, 500))
position = self.persepolis_setting.value(
'TextQueue/position', QPoint(300, 300))
self.resize(size)
self.move(position)
# if user clicked on link_tab so send spider again
# perhaps proxy or user password , ... set!
def currentTabChanged(self, index):
if index == 0:
# get proxy information
ip, port, proxy_user, proxy_passwd, proxy_type = self.getProxyInformation()
# get download username and password information
download_user, download_passwd = self.getUserPass()
k = 1
for dict_ in self.list_of_links:
# file_name
if 'out' in dict_.keys():
if dict_['out']:
file_name = dict_['out']
else:
file_name = '***'
else:
file_name = '***'
if file_name == '***':
dict_['ip'] = ip
dict_['port'] = port
dict_['proxy_user'] = proxy_user
dict_['proxy_passwd'] = proxy_passwd
dict_['proxy_type'] = proxy_type
dict_['download_user'] = download_user
dict_['download_passwd'] = download_passwd
# spider finds file name
new_spider = QueueSpiderThread(dict_)
self.parent.threadPool.append(new_spider)
self.parent.threadPool[-1].start()
self.parent.threadPool[-1].QUEUESPIDERRETURNEDFILENAME.connect(
partial(self.parent.queueSpiderCallBack, child=self, row_number=len(self.list_of_links) - k))
k = k + 1
# this method selects all links in links_table
def selectAll(self, button):
for i in range(self.links_table.rowCount()):
item = self.links_table.item(i, 0)
item.setCheckState(Qt.Checked)
# this method unchecks all check boxes
def deselectAll(self, button):
for i in range(self.links_table.rowCount()):
item = self.links_table.item(i, 0)
item.setCheckState(Qt.Unchecked)
# this method is called, when user changes add_queue_comboBox
def queueChanged(self, combo):
if str(self.add_queue_comboBox.currentText()) == 'Create new queue':
# if user want to create new queue, then call createQueue method from mainwindow(parent)
new_queue = self.parent.createQueue(combo)
if new_queue:
# clear comboBox
self.add_queue_comboBox.clear()
# load queue list again!
queues_list = self.parent.persepolis_db.categoriesList()
for queue in queues_list:
if queue != 'All Downloads':
self.add_queue_comboBox.addItem(queue)
self.add_queue_comboBox.addItem(
QIcon(icons + 'add_queue'), 'Create new queue')
# finding index of new_queue and setting comboBox for it
index = self.add_queue_comboBox.findText(str(new_queue))
self.add_queue_comboBox.setCurrentIndex(index)
else:
self.add_queue_comboBox.setCurrentIndex(0)
# activate frames if checkBoxes checked
def proxyFrame(self, checkBox):
if self.proxy_checkBox.isChecked():
self.proxy_frame.setEnabled(True)
else:
self.proxy_frame.setEnabled(False)
def downloadFrame(self, checkBox):
if self.download_checkBox.isChecked():
self.download_frame.setEnabled(True)
else:
self.download_frame.setEnabled(False)
def changeFolder(self, button):
fname = QFileDialog.getExistingDirectory(
self, 'Select a directory', download_path)
if fname:
# Returns pathName with the '/' separators converted to separators that are appropriate for the underlying operating system.
# On Windows, toNativeSeparators("c:/winnt/system32") returns
# "c:\winnt\system32".
fname = QDir.toNativeSeparators(fname)
if os.path.isdir(fname):
self.download_folder_lineEdit.setText(fname)
# this method returns proxy information.
def getProxyInformation(self):
# http, https or socks5 proxy
if self.http_radioButton.isChecked() is True:
proxy_type = 'http'
elif self.https_radioButton.isChecked() is True:
proxy_type = 'https'
else:
proxy_type = 'socks5'
# get proxy information
if not (self.proxy_checkBox.isChecked()):
ip = None
port = None
proxy_user = None
proxy_passwd = None
proxy_type = None
else:
ip = self.ip_lineEdit.text()
if not (ip):
ip = None
port = self.port_spinBox.value()
if not (port):
port = None
proxy_user = self.proxy_user_lineEdit.text()
if not (proxy_user):
proxy_user = None
proxy_passwd = self.proxy_pass_lineEdit.text()
if not (proxy_passwd):
proxy_passwd = None
return ip, port, proxy_user, proxy_passwd, proxy_type
def getUserPass(self):
# get download username and password information
if not (self.download_checkBox.isChecked()):
download_user = None
download_passwd = None
else:
download_user = self.download_user_lineEdit.text()
if not (download_user):
download_user = None
download_passwd = self.download_pass_lineEdit.text()
if not (download_passwd):
download_passwd = None
return download_user, download_passwd
def okButtonPressed(self, button):
# write user's input data to init file
self.persepolis_setting.setValue(
'add_link_initialization/ip', self.ip_lineEdit.text())
self.persepolis_setting.setValue(
'add_link_initialization/port', self.port_spinBox.value())
self.persepolis_setting.setValue(
'add_link_initialization/proxy_user', self.proxy_user_lineEdit.text())
self.persepolis_setting.setValue(
'add_link_initialization/download_user', self.download_user_lineEdit.text())
# get proxy information
ip, port, proxy_user, proxy_passwd, proxy_type = self.getProxyInformation()
if proxy_type is not None:
self.persepolis_setting.setValue('add_link_initialization/proxy_type', proxy_type)
# get download username and password information
download_user, download_passwd = self.getUserPass()
category = str(self.add_queue_comboBox.currentText())
connections = self.connections_spinBox.value()
download_path = self.download_folder_lineEdit.text()
dict_ = {'out': None,
'start_time': None,
'end_time': None,
'link': None,
'ip': ip,
'port': port,
'proxy_user': proxy_user,
'proxy_passwd': proxy_passwd,
'proxy_type': proxy_type,
'download_user': download_user,
'download_passwd': download_passwd,
'connections': connections,
'limit_value': 10,
'download_path': download_path,
'referer': None,
'load_cookies': None,
'user_agent': None,
'header': None,
'after_download': None
}
# find checked links in links_table
self.list_of_links.reverse()
self.add_link_dictionary_list = []
i = 0
for row in range(self.links_table.rowCount()):
item = self.links_table.item(row, 0)
# if item is checked
if (item.checkState() == Qt.Checked):
# Create a copy from dict_ and add it to add_link_dictionary_list
self.add_link_dictionary_list.append(
deepcopy(dict_))
# get link and add it to dict_
link = self.links_table.item(row, 1).text()
self.add_link_dictionary_list[i]['link'] = str(link)
# add file name to the dict_
self.add_link_dictionary_list[i]['out'] = self.links_table.item(
row, 0).text()
input_dict = self.list_of_links[row]
keys_list = ['referer', 'header', 'user-agent', 'load_cookies']
for key in keys_list:
if key in input_dict:
self.add_link_dictionary_list[i][key] = dict_[key]
i = i + 1
# reverse list
self.add_link_dictionary_list.reverse()
# Create callback for mainwindow
self.callback(self.add_link_dictionary_list, category)
# close window
self.close()
# close window with ESC key
def keyPressEvent(self, event):
if event.key() == Qt.Key_Escape:
self.close()
def closeEvent(self, event):
self.persepolis_setting.setValue('TextQueue/size', self.size())
self.persepolis_setting.setValue('TextQueue/position', self.pos())
self.persepolis_setting.sync()
event.accept()
def changeIcon(self, icons):
icons = ':/' + str(icons) + '/'
self.folder_pushButton.setIcon(QIcon(icons + 'folder'))
self.ok_pushButton.setIcon(QIcon(icons + 'ok'))
self.cancel_pushButton.setIcon(QIcon(icons + 'remove'))
================================================
FILE: persepolis/scripts/bubble.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
from persepolis.scripts.play import playNotification
from persepolis.scripts.osCommands import makeDirs
from persepolis.gui import resources
from persepolis.constants import OS
import platform
import os
from persepolis.scripts import logger
from persepolis.scripts.download_link import DownloadSingleLink
try:
from PySide6.QtCore import QSettings
from PySide6.QtGui import QIcon
except ImportError:
from PyQt5.QtCore import QSettings
from PyQt5.QtGui import QIcon
# platform
os_type = platform.system()
global dasbus_is_installed
if os_type in OS.LINUX:
try:
from dasbus.connection import SessionMessageBus
dasbus_is_installed = True
except ImportError:
dasbus_is_installed = False
logger.sendToLog('python3-dasbus is not installed', 'ERROR')
# notifySend use notify-send program in user's system for sending notifications
# and use playNotification function in play.py file for playing sound
# notifications
# user home address
home_address = os.path.expanduser("~")
# When Persepolis is running on Linux, BSD, and MacOSX operating systems,
# the program will check for the presence of notification sound files,
# and if there are no files, it will download them from GitHub.
# The Windows operating system uses its own notification sounds,
# so there is no need for notification sound files in Windows.
def checkNotificationSounds():
if os_type in OS.UNIX_LIKE:
# for linux and bsd
# ~/.local/share/persepolis_download_manager/sounds
notification_directory = os.path.join(home_address, '.local/share/persepolis_download_manager/sounds')
elif os_type in OS.OSX:
# for mac
# ~/Library/Application Support//sounds/
notification_directory = os.path.join(home_address, 'Library/Application Support/persepolis_download_manager/sounds/')
else:
# windows
return True
notification_sounds = ['ok.wav', 'fail.wav', 'warning.wav', 'queue.wav']
file_availability = 0
for file in notification_sounds:
file_path = os.path.join(home_address, notification_directory, file)
if os.path.exists(file_path):
file_availability += 1
if file_availability == 4:
return True
else:
logger.sendToLog('Notification sounds are not available', 'ERROR')
return False
# this function downloads notification sounds from github for Linux, BSD and MacOSX
def createNotificationSounds(main_window):
# create a directory for sounds in:
if os_type in OS.UNIX_LIKE:
# for linux and bsd
# ~/.local/share/persepolis_download_manager/sounds
sounds_directory = os.path.join(home_address, '.local/share/persepolis_download_manager/sounds')
elif os_type in OS.OSX:
# for mac
# ~/Library/Application Support//sounds/
sounds_directory = os.path.join(home_address, 'Library/Application Support/persepolis_download_manager/sounds/')
# create directory
makeDirs(sounds_directory)
# file names
notification_sounds = ['ok.wav', 'fail.wav', 'warning.wav', 'queue.wav']
# download links
file_links = ['https://raw.githubusercontent.com/persepolisdm/persepolis/refs/heads/master/resources/notification%20sounds/ok.wav',
'https://raw.githubusercontent.com/persepolisdm/persepolis/refs/heads/master/resources/notification%20sounds/fail.wav',
'https://raw.githubusercontent.com/persepolisdm/persepolis/refs/heads/master/resources/notification%20sounds/warning.wav',
'https://raw.githubusercontent.com/persepolisdm/persepolis/refs/heads/master/resources/notification%20sounds/queue.wav']
# download notification sound files from github.
# The result will be written to the log.
for i in range(4):
new_download = DownloadSingleLink(file_links[i], os.path.join(sounds_directory, notification_sounds[i]))
main_window.threadPool.append(new_download)
main_window.threadPool[-1].start()
return True
def getSoundPath(name):
# create a directory for sounds in:
if os_type in OS.UNIX_LIKE:
# for linux and bsd
# ~/.local/share/persepolis_download_manager/sounds
sounds_directory = os.path.join(home_address, '.local/share/persepolis_download_manager/sounds')
elif os_type in OS.OSX:
# for mac
# ~/Library/Application Support//sounds/
sounds_directory = os.path.join(home_address, 'Library/Application Support/persepolis_download_manager/sounds/')
# Windows
else:
return None
file_name = name + '.wav'
file_path = os.path.join(sounds_directory, file_name)
# return file_path if it exists.
if os.path.exists(file_path):
return file_path
else:
return None
def notifySend(message1, message2, time, sound, parent=None):
file = getSoundPath(sound)
if file is not None:
playNotification(file)
# load settings
persepolis_setting = QSettings('persepolis_download_manager', 'persepolis')
enable_notification = persepolis_setting.value('settings/notification')
time = str(time)
message1 = str(message1)
message2 = str(message2)
# using Qt notification or Native system notification
if enable_notification == 'QT notification':
parent.system_tray_icon.showMessage(message1, message2, QIcon.fromTheme('persepolis-tray', QIcon(':/persepolis-tray.svg')), 10000)
else:
if os_type in OS.LINUX and dasbus_is_installed:
bus = SessionMessageBus()
proxy = bus.get_proxy(
"org.freedesktop.Notifications",
"/org/freedesktop/Notifications"
)
proxy.Notify(
"Persepolis", 0, QIcon.fromTheme('persepolis-tray', QIcon(':/persepolis-tray.svg')).name(), message1, message2, [], {}, 10000)
else:
parent.system_tray_icon.showMessage(message1, message2, QIcon.fromTheme('persepolis-tray', QIcon(':/persepolis-tray.svg')), 10000)
================================================
FILE: persepolis/scripts/check_proxy.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import re
import subprocess
import urllib
import os
from persepolis.scripts.useful_tools import osAndDesktopEnvironment
from persepolis.scripts import logger
from persepolis.constants import OS
# get proxy function
def getProxy():
socks_proxy = False
# find os and desktop environment
os_type, desktop = osAndDesktopEnvironment()
# destop == 'ubuntu:GNOME' to destop == 'GNOME'
try:
tmp = re.search(r'.*:(.*)', desktop)
if tmp is not None:
desktop = tmp.group(1)
except Exception:
pass
# write in log
platform = 'platform : ' + os_type
logger.sendToLog(platform, "INITIALIZATION")
proxy = {}
if os_type in OS.UNIX_LIKE:
if desktop is None:
desktop_env_type = 'Desktop Environment not detected!'
else:
desktop_env_type = 'Desktop environment: ' + str(desktop)
logger.sendToLog(desktop_env_type, "INITIALIZATION")
# check if it is KDE
if desktop == 'kde':
# creat empty list for proxies
proxysource = {}
# user home directory path
home_address = os.path.expanduser("~")
# get proxy file content
try:
plasma_proxy_config_file_path = os.path.join(
home_address,
'.config',
'kioslaverc'
)
except Exception:
logger.sendToLog('no proxy file detected', 'INITIALIZATION')
# check if proxy file exists
if os.path.isfile(plasma_proxy_config_file_path):
# read kde plasma proxy config file
try:
with open(plasma_proxy_config_file_path) as proxyfile:
for line in proxyfile:
name, var = line.partition("=")[::2]
proxysource[name.strip()] = str(var)
except (OSError, InterruptedError, ValueError):
logger.sendToLog('no proxy file detected', 'INITIALIZATION')
# check proxy enabled as manually
if proxysource['ProxyType'].split('\n')[0] == '1':
# get ftp proxy
try:
proxy['ftp_proxy_port'] = proxysource['ftpProxy'].split(' ')[1].replace("/", "").replace("\n", "")
proxy['ftp_proxy_ip'] = proxysource['ftpProxy'].split(' ')[0].split('//')[1]
except Exception:
logger.sendToLog('no manual ftp proxy detected', 'INITIALIZATION')
# get http proxy
try:
proxy['http_proxy_port'] = proxysource['httpProxy'].split(' ')[1].replace("/", "").replace("\n", "")
proxy['http_proxy_ip'] = proxysource['httpProxy'].split(' ')[0].split('//')[1]
except Exception:
logger.sendToLog('no manual http proxy detected', 'INITIALIZATION')
# get https proxy
try:
proxy['https_proxy_port'] = proxysource['httpsProxy'].split(
' ')[1].replace("/", "").replace("\n", "")
proxy['https_proxy_ip'] = proxysource['httpsProxy'].split(' ')[0].split('//')[1]
except Exception:
logger.sendToLog('no manual https proxy detected', 'INITIALIZATION')
# get socks proxy
try:
socks_proxy = proxysource['socksProxy'].split(' ')[0].split('//')[1]
except Exception:
socks_proxy = False
# proxy disabled
else:
logger.sendToLog('no manual proxy detected', 'INITIALIZATION')
# proxy file not exists
else:
logger.sendToLog('no proxy file detected', 'INITIALIZATION')
# Detect proxy from GNOME Desktop
elif desktop == 'gnome':
process = subprocess.run(['gsettings', 'get', 'org.gnome.system.proxy', 'mode'], stdout=subprocess.PIPE)
mode = re.search(r'manual', process.stdout.decode('utf-8'))
if mode is not None:
try:
process = subprocess.run(['gsettings', 'get', 'org.gnome.system.proxy.http', 'host'], stdout=subprocess.PIPE)
proxy['http_proxy_ip'] = re.search(r"\'([\w0-9\.]+)\'", process.stdout.decode('utf-8')).group(1)
process = subprocess.run(['gsettings', 'get', 'org.gnome.system.proxy.http', 'port'], stdout=subprocess.PIPE)
proxy['http_proxy_port'] = process.stdout.decode('utf-8')
except Exception:
logger.sendToLog('no http proxy detected', 'INITIALIZATION')
try:
process = subprocess.run(['gsettings', 'get', 'org.gnome.system.proxy.https', 'host'], stdout=subprocess.PIPE)
proxy['https_proxy_ip'] = re.search(r"\'([\w0-9\.]+)\'", process.stdout.decode('utf-8')).group(1)
process = subprocess.run(['gsettings', 'get', 'org.gnome.system.proxy.https', 'port'], stdout=subprocess.PIPE)
proxy['https_proxy_port'] = process.stdout.decode('utf-8')
except Exception:
logger.sendToLog('no https proxy detected', 'INITIALIZATION')
try:
process = subprocess.run(['gsettings', 'get', 'org.gnome.system.proxy.ftp', 'host'], stdout=subprocess.PIPE)
proxy['ftp_proxy_ip'] = re.search(r"\'([\w0-9\.]+)\'", process.stdout.decode('utf-8')).group(1)
process = subprocess.run(['gsettings', 'get', 'org.gnome.system.proxy.ftp', 'port'], stdout=subprocess.PIPE)
proxy['ftp_proxy_port'] = process.stdout.decode('utf-8')
except Exception:
logger.sendToLog('no ftp proxy detected', 'INITIALIZATION')
try:
process = subprocess.run(['gsettings', 'get', 'org.gnome.system.proxy.socks', 'host'], stdout=subprocess.PIPE)
# value = re.search(r"\'([\w0-9\.]+)\'", process.stdout.decode('utf-8')).group(1)
socks_proxy = True
except Exception:
socks_proxy = False
else:
logger.sendToLog('no manual proxy detected', 'INITIALIZATION')
# if it is windows,mac and other linux desktop
else:
# get proxies
proxysource = urllib.request.getproxies()
# get http proxy
try:
proxy['http_proxy_ip'] = proxysource['http'].split(':')[1].replace('//', '')
proxy['http_proxy_port'] = proxysource['http'].split(':')[2].replace("/", "").replace("\n", "")
except Exception:
logger.sendToLog('no http proxy detected', 'INITIALIZATION')
# get https proxy
try:
proxy['https_proxy_ip'] = proxysource['https'].split(':')[1].replace('//', '')
proxy['https_proxy_port'] = proxysource['https'].split(':')[2].replace("/", "").replace("\n", "")
except Exception:
logger.sendToLog('no https proxy detected', 'INITIALIZATION')
# get ftp proxy
try:
proxy['ftp_proxy_ip'] = proxysource['ftp'].split(':')[1].replace('//', '')
proxy['ftp_proxy_port'] = proxysource['ftp'].split(':')[2].replace("/", "").replace("\n", "")
except Exception:
logger.sendToLog('no ftp proxy detected', 'INITIALIZATION')
# get socks proxy
try:
# if it is unity
if desktop == 'unity7':
socks_proxy = proxysource['all'].split(':')[1].replace('//', '')
# if it is Mac OS
elif os_type == OS.OSX:
validKeys = ['SOCKSEnable']
# get proxies list using scutil command and parse it in tmp list
mac_tmp_proxies_list = {}
proxyList = subprocess.run(['scutil', '--proxy'], stdout=subprocess.PIPE)
for line in proxyList.stdout.decode('utf-8').split('\n'):
words = line.split()
if len(words) == 3 and words[0] in validKeys:
mac_tmp_proxies_list[words[0]] = words[2]
if mac_tmp_proxies_list['SOCKSEnable'] == '1':
socks_proxy = True
else:
socks_proxy = False
# others except KDE,Mac OS,gnome,unity7
else:
socks_proxy = proxysource['socks'].split(':')[1].replace('//', '')
except Exception:
socks_proxy = False
# check if just socks proxy exists
key_is_available = False
key_list = ['http_proxy_ip', 'https_proxy_ip', 'ftp_proxy_ip']
for key in key_list:
if key in proxy.keys():
key_is_available = True
if not key_is_available and socks_proxy:
# all print just for debugging
socks_message = "persepolis and aria2 don't support socks\n\
you must convert socks proxy to http proxy.\n\
Please read this for more help:\n\
https://github.com/persepolisdm/persepolis/wiki/Privoxy"
logger.sendToLog(socks_message, 'ERROR')
# return results
proxy_log_message = 'proxy: ' + str(proxy)
logger.sendToLog(proxy_log_message, 'INITIALIZATION')
return proxy
================================================
FILE: persepolis/scripts/compatibility.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
from persepolis.scripts.useful_tools import determineConfigFolder
from persepolis.scripts.osCommands import remove, removeDir
from persepolis.scripts.data_base import PersepolisDB
from persepolis.scripts.newopen import readList
import os
# config_folder
config_folder = determineConfigFolder()
download_info_folder = os.path.join(config_folder, "download_info")
# download_list_file contains GID of all downloads
download_list_file = os.path.join(config_folder, "download_list_file")
# download_list_file_active for active downloads
download_list_file_active = os.path.join(
config_folder, "download_list_file_active")
# queues_list contains queues name
queues_list_file = os.path.join(config_folder, 'queues_list')
# category_folder contains some file , and every files named with queues .
# every file contains gid of downloads for that queue
category_folder = os.path.join(config_folder, 'category_folder')
# queue_info_folder is contains queues information(start time,end
# time,limit speed , ...)
queue_info_folder = os.path.join(config_folder, "queue_info")
# single_downloads_list_file contains gid of non categorized downloads
single_downloads_list_file = os.path.join(category_folder, "Single Downloads")
# this script for compatibility between Version 2 and 3
def compatibility():
if os.path.isfile(queues_list_file):
persepolis_db = PersepolisDB()
# add categories to category_db_table in data_base
f = open(queues_list_file)
queues_list = f.readlines()
f.close()
# remove queues_list_file
remove(queues_list_file)
else:
return
category_list = ['All Downloads', 'Single Downloads']
for line in queues_list:
queue_name = line.strip()
category_list.append(queue_name)
for category in category_list:
gid_list = []
if category == 'All Downloads':
category_info_file = download_list_file
else:
category_info_file = os.path.join(category_folder, category)
f = open(category_info_file)
category_info_file_list = f.readlines()
f.close()
for item in category_info_file_list:
gid = item.strip()
gid_list.append(gid)
category_dict = {'category': category,
'start_time_enable': 'no',
'start_time': '0:0',
'end_time_enable': 'no',
'end_time': '0:0',
'reverse': 'no',
'limit_enable': 'no',
'limit_value': '0K',
'after_download': 'no',
'gid_list': str(gid_list)
}
# add category to data_base
if category == 'All Downloads' or category == 'Single Downloads':
persepolis_db.updateCategoryTable([category_dict])
else:
persepolis_db.insertInCategoryTable(category_dict)
# add items to download_db_table in data base
f_download_list_file = open(download_list_file)
download_list_file_lines = f_download_list_file.readlines()
f_download_list_file.close()
for line in download_list_file_lines:
gid = line.strip()
download_info_file = os.path.join(download_info_folder, gid)
download_info_file_list = readList(download_info_file)
add_link_dictionary = download_info_file_list[9]
dict = {'file_name': download_info_file_list[0],
'status': download_info_file_list[1],
'size': download_info_file_list[2],
'downloaded_size': download_info_file_list[3],
'percent': download_info_file_list[4],
'connections': download_info_file_list[5],
'rate': download_info_file_list[6],
'estimate_time_left': download_info_file_list[7],
'gid': download_info_file_list[8],
'link': add_link_dictionary['link'],
'first_try_date': download_info_file_list[10],
'last_try_date': download_info_file_list[11],
'category': download_info_file_list[12]}
add_link_dictionary['gid'] = download_info_file_list[8]
if 'user-agent' in add_link_dictionary.keys():
add_link_dictionary['user_agent'] = add_link_dictionary.pop('user-agent')
if 'load-cookies' in add_link_dictionary.keys():
add_link_dictionary['load_cookies'] = add_link_dictionary.pop('load-cookies')
add_link_dictionary['limit_value'] = 0
keys_list = ['gid',
'out',
'start_time',
'end_time',
'link',
'ip',
'port',
'proxy_user',
'proxy_passwd',
'download_user',
'download_passwd',
'connections',
'limit_value',
'download_path',
'referer',
'load_cookies',
'user_agent',
'header',
'after_download']
for key in keys_list:
# if a key is missed in dict,
# then add this key to the dict and assign None value for the key.
if key not in add_link_dictionary.keys():
add_link_dictionary[key] = None
# write information in data_base
persepolis_db.insertInDownloadTable([dict])
persepolis_db.insertInAddLinkTable([add_link_dictionary])
# close connections
persepolis_db.closeConnections()
# remove unwanted files and folders
for file in [download_list_file, download_list_file_active]:
remove(file)
for folder in [category_folder, queue_info_folder]:
removeDir(folder)
================================================
FILE: persepolis/scripts/data_base.py
================================================
# -*- coding: utf-8 -*-
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
from persepolis.scripts.useful_tools import determineConfigFolder
from time import sleep
import sqlite3
import random
import ast
import os
# download manager config folder .
config_folder = determineConfigFolder()
# persepolis tmp folder path
persepolis_tmp = os.path.join(config_folder, 'persepolis_tmp')
# This class manages TempDB
# TempDB contains gid of active downloads in every session.
class TempDB():
def __init__(self):
# temp_db saves in RAM
# temp_db_connection
self.temp_db_connection = sqlite3.connect(':memory:', check_same_thread=False)
# temp_db_cursor
self.temp_db_cursor = self.temp_db_connection.cursor()
# create a lock for data base
self.lock = False
# this method locks data base.
# this is pervent accessing data base simultaneously.
def lockCursor(self):
while self.lock:
rand_float = random.uniform(0, 0.5)
sleep(rand_float)
self.lock = True
# temp_db_table contains gid of active downloads.
def createTables(self):
# lock data base
self.lockCursor()
self.temp_db_cursor.execute("""CREATE TABLE IF NOT EXISTS single_db_table(
ID INTEGER,
gid TEXT PRIMARY KEY,
status TEXT,
shutdown TEXT
)""")
self.temp_db_cursor.execute("""CREATE TABLE IF NOT EXISTS queue_db_table(
ID INTEGER,
category TEXT PRIMARY KEY,
shutdown TEXT
)""")
self.temp_db_connection.commit()
self.lock = False
# insert new item in single_db_table
def insertInSingleTable(self, gid):
# lock data base
self.lockCursor()
self.temp_db_cursor.execute("""INSERT INTO single_db_table VALUES(
NULL,
'{}',
'active',
NULL)""".format(gid))
self.temp_db_connection.commit()
self.lock = False
# insert new item in queue_db_table
def insertInQueueTable(self, category):
# lock data base
self.lockCursor()
self.temp_db_cursor.execute("""INSERT INTO queue_db_table VALUES(
NULL,
'{}',
NULL)""".format(category))
self.temp_db_connection.commit()
self.lock = False
# this method updates single_db_table
def updateSingleTable(self, dict_):
# lock data base
self.lockCursor()
keys_list = ['gid',
'shutdown',
'status'
]
for key in keys_list:
# if a key is missed in dict_,
# then add this key to the dict_ and assign None value for the key.
if key not in dict_.keys():
dict_[key] = None
# update data base if value for the keys is not None
self.temp_db_cursor.execute("""UPDATE single_db_table SET shutdown = coalesce(:shutdown, shutdown),
status = coalesce(:status, status)
WHERE gid = :gid""", dict_)
self.temp_db_connection.commit()
self.lock = False
# this method updates queue_db_table
def updateQueueTable(self, dict_):
# lock data base
self.lockCursor()
keys_list = ['category',
'shutdown']
for key in keys_list:
# if a key is missed in dict_,
# then add this key to the dict_ and assign None value for the key.
if key not in dict_.keys():
dict_[key] = None
# update data base if value for the keys is not None
self.temp_db_cursor.execute("""UPDATE queue_db_table SET shutdown = coalesce(:shutdown, shutdown)
WHERE category = :category""", dict_)
self.temp_db_connection.commit()
self.lock = False
# this method returns gid of active downloads
def returnActiveGids(self):
# lock data base
self.lockCursor()
self.temp_db_cursor.execute("""SELECT gid FROM single_db_table WHERE status = 'active'""")
list_ = self.temp_db_cursor.fetchall()
self.lock = False
gid_list = []
for tuple_ in list_:
gid = tuple_[0]
gid_list.append(gid)
return gid_list
# this method returns shutdown value for specific gid
def returnGid(self, gid):
# lock data base
self.lockCursor()
self.temp_db_cursor.execute("""SELECT shutdown, status FROM single_db_table WHERE gid = '{}'""".format(gid))
list_ = self.temp_db_cursor.fetchall()
self.lock = False
tuple_ = list_[0]
dict_ = {'shutdown': str(tuple_[0]),
'status': tuple_[1]}
return dict_
# This method returns values of columns for specific category
def returnCategory(self, category):
# lock data base
self.lockCursor()
self.temp_db_cursor.execute("""SELECT shutdown FROM queue_db_table WHERE category = '{}'""".format(category))
list_ = self.temp_db_cursor.fetchall()
self.lock = False
tuple_ = list_[0]
dict_ = {'shutdown': tuple_[0]}
return dict_
def resetDataBase(self):
# lock data base
self.lockCursor()
# delete all items
self.temp_db_cursor.execute("""DELETE FROM single_db_table""")
self.temp_db_cursor.execute("""DELETE FROM queue_db_table""")
# release lock
self.lock = False
# close connections
def closeConnections(self):
# lock data base
self.lockCursor()
self.temp_db_cursor.close()
self.temp_db_connection.close()
self.lock = False
# plugins.db is store links, when browser plugins are send new links.
# This class is managing plugin.db
class PluginsDB():
def __init__(self):
# plugins.db file path
plugins_db_path = os.path.join(persepolis_tmp, 'plugins.db')
# plugins_db_connection
self.plugins_db_connection = sqlite3.connect(plugins_db_path, check_same_thread=False)
# plugins_db_cursor
self.plugins_db_cursor = self.plugins_db_connection.cursor()
# create a lock for data base
self.lock = False
# this method locks data base.
# this is pervent accessing data base simultaneously.
def lockCursor(self):
while self.lock:
rand_float = random.uniform(0, 0.5)
sleep(rand_float)
self.lock = True
# plugins_db_table contains links that sends by browser plugins.
def createTables(self):
# lock data base
self.lockCursor()
self.plugins_db_cursor.execute("""CREATE TABLE IF NOT EXISTS plugins_db_table(
ID INTEGER PRIMARY KEY,
link TEXT,
referer TEXT,
load_cookies TEXT,
user_agent TEXT,
header TEXT,
out TEXT,
status TEXT
)""")
self.plugins_db_connection.commit()
# release lock
self.lock = False
# insert new items in plugins_db_table
def insertInPluginsTable(self, list_):
# lock data base
self.lockCursor()
for dict_ in list_:
self.plugins_db_cursor.execute("""INSERT INTO plugins_db_table VALUES(
NULL,
:link,
:referer,
:load_cookies,
:user_agent,
:header,
:out,
'new'
)""", dict_)
self.plugins_db_connection.commit()
# release lock
self.lock = False
# this method returns all new links in plugins_db_table
def returnNewLinks(self):
# lock data base
self.lockCursor()
self.plugins_db_cursor.execute("""SELECT link, referer, load_cookies, user_agent, header, out
FROM plugins_db_table
WHERE status = 'new'""")
list_ = self.plugins_db_cursor.fetchall()
# chang all rows status to 'old'
self.plugins_db_cursor.execute("""UPDATE plugins_db_table SET status = 'old'
WHERE status = 'new'""")
# commit changes
self.plugins_db_connection.commit()
# release lock
self.lock = False
# create new_list
new_list = []
# put the information in tuple_s in dictionary format and add it to new_list
for tuple_ in list_:
dict_ = {'link': tuple_[0],
'referer': tuple_[1],
'load_cookies': tuple_[2],
'user_agent': tuple_[3],
'header': tuple_[4],
'out': tuple_[5]
}
new_list.append(dict_)
# return results in list format!
# every member of this list is a dictionary.
# every dictionary contains download information
return new_list
# delete old links from data base
def deleteOldLinks(self):
# lock data base
self.lockCursor()
self.plugins_db_cursor.execute("""DELETE FROM plugins_db_table WHERE status = 'old'""")
# commit changes
self.plugins_db_connection.commit()
# release lock
self.lock = False
# close connections
def closeConnections(self):
# lock data base
self.lockCursor()
self.plugins_db_cursor.close()
self.plugins_db_connection.close()
# release lock
self.lock = False
# persepolis main data base contains downloads information
# This class is managing persepolis.db
class PersepolisDB():
def __init__(self):
# persepolis.db file path
persepolis_db_path = os.path.join(config_folder, 'persepolis.db')
# persepolis_db_connection
self.persepolis_db_connection = sqlite3.connect(persepolis_db_path, check_same_thread=False)
# turn FOREIGN KEY Support on!
self.persepolis_db_connection.execute('pragma foreign_keys=ON')
# persepolis_db_cursor
self.persepolis_db_cursor = self.persepolis_db_connection.cursor()
# Create a lock for data base
self.lock = False
# this method locks data base.
# this is pervent accessing data base simultaneously.
def lockCursor(self):
while self.lock:
rand_float = random.uniform(0, 0.5)
sleep(rand_float)
self.lock = True
# queues_list contains name of categories and category settings
def createTables(self):
# lock data base
self.lockCursor()
# Create category_db_table and add 'All Downloads' and 'Single Downloads' to it
self.persepolis_db_cursor.execute("""CREATE TABLE IF NOT EXISTS category_db_table(
category TEXT PRIMARY KEY,
start_time_enable TEXT,
start_time TEXT,
end_time_enable TEXT,
end_time TEXT,
reverse TEXT,
limit_enable TEXT,
limit_value TEXT,
after_download TEXT,
gid_list TEXT
)""")
# download table contains download table download items information
self.persepolis_db_cursor.execute("""CREATE TABLE IF NOT EXISTS download_db_table(
file_name TEXT,
status TEXT,
size TEXT,
downloaded_size TEXT,
percent TEXT,
connections TEXT,
rate TEXT,
estimate_time_left TEXT,
gid TEXT PRIMARY KEY,
link TEXT,
first_try_date TEXT,
last_try_date TEXT,
category TEXT,
FOREIGN KEY(category) REFERENCES category_db_table(category)
ON UPDATE CASCADE
ON DELETE CASCADE
)""")
# addlink_db_table contains addlink window download information
self.persepolis_db_cursor.execute("""CREATE TABLE IF NOT EXISTS addlink_db_table(
ID INTEGER PRIMARY KEY,
gid TEXT,
out TEXT,
start_time TEXT,
end_time TEXT,
link TEXT,
ip TEXT,
port TEXT,
proxy_user TEXT,
proxy_passwd TEXT,
download_user TEXT,
download_passwd TEXT,
connections TEXT,
limit_value TEXT,
download_path TEXT,
referer TEXT,
load_cookies TEXT,
user_agent TEXT,
header TEXT,
after_download TEXT,
proxy_type TEXT,
FOREIGN KEY(gid) REFERENCES download_db_table(gid)
ON UPDATE CASCADE
ON DELETE CASCADE
)""")
# video_finder_db_table contains addlink window download information
self.persepolis_db_cursor.execute("""CREATE TABLE IF NOT EXISTS video_finder_db_table(
ID INTEGER PRIMARY KEY,
video_gid TEXT,
audio_gid TEXT,
video_completed TEXT,
audio_completed TEXT,
muxing_status TEXT,
checking TEXT,
download_path TEXT,
FOREIGN KEY(video_gid) REFERENCES download_db_table(gid)
ON DELETE CASCADE,
FOREIGN KEY(audio_gid) REFERENCES download_db_table(gid)
ON DELETE CASCADE
)""")
self.persepolis_db_connection.execute("""CREATE TABLE IF NOT EXISTS video_finder_db_table2(
ID INTEGER PRIMARY KEY,
gid TEXT,
download_status TEXT,
file_name TEXT,
eta TEXT,
download_speed_str TEXT,
downloaded_size REAL,
file_size REAL,
download_percent INT,
fragments TEXT,
error_message TEXT,
FOREIGN KEY(gid) REFERENCES download_db_table(gid)
ON DELETE CASCADE
ON UPDATE CASCADE
)""")
self.persepolis_db_connection.commit()
# job is done! open the lock
self.lock = False
# add 'All Downloads' and 'Single Downloads' to the category_db_table if they wasn't added.
answer = self.searchCategoryInCategoryTable('All Downloads')
if not (answer):
all_downloads_dict = {'category': 'All Downloads',
'start_time_enable': 'no',
'start_time': '0:0',
'end_time_enable': 'no',
'end_time': '0:0',
'reverse': 'no',
'limit_enable': 'no',
'limit_value': '0K',
'after_download': 'no',
'gid_list': '[]'
}
single_downloads_dict = {'category': 'Single Downloads',
'start_time_enable': 'no',
'start_time': '0:0',
'end_time_enable': 'no',
'end_time': '0:0',
'reverse': 'no',
'limit_enable': 'no',
'limit_value': '0K',
'after_download': 'no',
'gid_list': '[]'
}
self.insertInCategoryTable(all_downloads_dict)
self.insertInCategoryTable(single_downloads_dict)
# add default queue with the name 'Scheduled Downloads'
answer = self.searchCategoryInCategoryTable('Scheduled Downloads')
if not (answer):
scheduled_downloads_dict = {'category': 'Scheduled Downloads',
'start_time_enable': 'no',
'start_time': '0:0',
'end_time_enable': 'no',
'end_time': '0:0',
'reverse': 'no',
'limit_enable': 'no',
'limit_value': '0K',
'after_download': 'no',
'gid_list': '[]'
}
self.insertInCategoryTable(scheduled_downloads_dict)
# insert new category in category_db_table
def insertInCategoryTable(self, dict_):
# lock data base
self.lockCursor()
self.persepolis_db_cursor.execute("""INSERT INTO category_db_table VALUES(
:category,
:start_time_enable,
:start_time,
:end_time_enable,
:end_time,
:reverse,
:limit_enable,
:limit_value,
:after_download,
:gid_list
)""", dict_)
self.persepolis_db_connection.commit()
# job is done! open the lock
self.lock = False
# insert in to download_db_table in persepolis.db
def insertInDownloadTable(self, list_):
# lock data base
self.lockCursor()
for dict_ in list_:
self.persepolis_db_cursor.execute("""INSERT INTO download_db_table VALUES(
:file_name,
:status,
:size,
:downloaded_size,
:percent,
:connections,
:rate,
:estimate_time_left,
:gid,
:link,
:first_try_date,
:last_try_date,
:category
)""", dict_)
# commit changes
self.persepolis_db_connection.commit()
# job is done! open the lock
self.lock = False
if len(list_) != 0:
# item must be inserted to gid_list of 'All Downloads' and gid_list of category
# find download category and gid
category = dict_['category']
# get category_dict from data base
category_dict = self.searchCategoryInCategoryTable(category)
# get all_downloads_dict from data base
all_downloads_dict = self.searchCategoryInCategoryTable('All Downloads')
# get gid_list
category_gid_list = category_dict['gid_list']
all_downloads_gid_list = all_downloads_dict['gid_list']
for dict_ in list_:
gid = dict_['gid']
# add gid of item to gid_list
category_gid_list.append(gid)
all_downloads_gid_list.append(gid)
# update category_db_table
self.updateCategoryTable([all_downloads_dict])
self.updateCategoryTable([category_dict])
# insert in addlink table in persepolis.db
def insertInAddLinkTable(self, list_):
# lock data base
self.lockCursor()
for dict_ in list_:
# first column and after_download column is NULL
self.persepolis_db_cursor.execute("""INSERT INTO addlink_db_table VALUES(NULL,
:gid,
:out,
:start_time,
:end_time,
:link,
:ip,
:port,
:proxy_user,
:proxy_passwd,
:download_user,
:download_passwd,
:connections,
:limit_value,
:download_path,
:referer,
:load_cookies,
:user_agent,
:header,
NULL,
:proxy_type
)""", dict_)
self.persepolis_db_connection.commit()
# job is done! open the lock
self.lock = False
def insertInVideoFinderTable(self, list_):
# lock data base
self.lockCursor()
for dictionary in list_:
# first column is NULL
self.persepolis_db_cursor.execute("""INSERT INTO video_finder_db_table VALUES(NULL,
:video_gid,
:audio_gid,
:video_completed,
:audio_completed,
:muxing_status,
:checking,
:download_path
)""", dictionary)
self.persepolis_db_connection.commit()
# job is done! open the lock
self.lock = False
def searchGidInVideoFinderTable(self, gid):
# lock data base
self.lockCursor()
self.persepolis_db_cursor.execute(
"""SELECT * FROM video_finder_db_table WHERE audio_gid = '{}' OR video_gid = '{}'""".format(str(gid), str(gid)))
result_list = self.persepolis_db_cursor.fetchall()
# job is done
self.lock = False
if result_list:
tuple_ = result_list[0]
else:
return None
dictionary = {'video_gid': tuple_[1],
'audio_gid': tuple_[2],
'video_completed': tuple_[3],
'audio_completed': tuple_[4],
'muxing_status': tuple_[5],
'checking': tuple_[6],
'download_path': tuple_[7]}
# return the results
return dictionary
def insertInVideoFinderTable2(self, list_):
self.lockCursor()
for dictionary in list_:
self.persepolis_db_cursor.execute("""INSERT INTO video_finder_db_table2 VALUES(NULL,
:gid,
:download_status,
:file_name,
:eta,
:download_speed_str,
:downloaded_size,
:file_size,
:download_percent,
:fragments,
:error_message
)""", dictionary)
self.persepolis_db_connection.commit()
self.lock = False
def searchGidInVideoFinderTable2(self, gid):
# lock data base
self.lockCursor()
self.persepolis_db_cursor.execute(
"""SELECT * FROM video_finder_db_table2 WHERE gid = '{}'""".format(str(gid)))
result_list = self.persepolis_db_cursor.fetchall()
# job is done
self.lock = False
if result_list:
tuple_ = result_list[0]
else:
return None
dictionary = {'gid': tuple_[1],
'download_status': tuple_[2],
'file_name': tuple_[3],
'eta': tuple_[4],
'download_speed_str': tuple_[5],
'downloaded_size': tuple_[6],
'file_size': tuple_[7],
'download_percent': tuple_[8],
'fragments': tuple_[9],
'error_message': tuple_[10]}
# return the results
return dictionary
# return download information in download_db_table with special gid.
def searchGidInDownloadTable(self, gid):
# lock data base
self.lockCursor()
self.persepolis_db_cursor.execute("""SELECT * FROM download_db_table WHERE gid = '{}'""".format(str(gid)))
list_ = self.persepolis_db_cursor.fetchall()
# job is done! open the lock
self.lock = False
if list_:
tuple_ = list_[0]
else:
return None
dict_ = {'file_name': tuple_[0],
'status': tuple_[1],
'size': tuple_[2],
'downloaded_size': tuple_[3],
'percent': tuple_[4],
'connections': tuple_[5],
'rate': tuple_[6],
'estimate_time_left': tuple_[7],
'gid': tuple_[8],
'link': tuple_[9],
'first_try_date': tuple_[10],
'last_try_date': tuple_[11],
'category': tuple_[12]
}
# return results
return dict_
# return all items in download_db_table
# '*' for category, cause that method returns all items.
def returnItemsInDownloadTable(self, category=None):
# lock data base
self.lockCursor()
if category:
self.persepolis_db_cursor.execute(
"""SELECT * FROM download_db_table WHERE category = '{}'""".format(category))
else:
self.persepolis_db_cursor.execute("""SELECT * FROM download_db_table""")
rows = self.persepolis_db_cursor.fetchall()
# job is done! open the lock
self.lock = False
downloads_dict = {}
for tuple_ in rows:
# change format of tuple_ to dictionary
dict_ = {'file_name': tuple_[0],
'status': tuple_[1],
'size': tuple_[2],
'downloaded_size': tuple_[3],
'percent': tuple_[4],
'connections': tuple_[5],
'rate': tuple_[6],
'estimate_time_left': tuple_[7],
'gid': tuple_[8],
'link': tuple_[9],
'first_try_date': tuple_[10],
'last_try_date': tuple_[11],
'category': tuple_[12]
}
# add dict to the downloads_dict
# gid is key and dict_ is value
downloads_dict[tuple_[8]] = dict_
return downloads_dict
# this method checks existence of a link in addlink_db_table
def searchLinkInAddLinkTable(self, link):
# lock data base
self.lockCursor()
self.persepolis_db_cursor.execute("""SELECT * FROM addlink_db_table WHERE link = (?)""", (link,))
list_ = self.persepolis_db_cursor.fetchall()
# job is done! open the lock
self.lock = False
if list_:
return True
else:
return False
# return download information in addlink_db_table with special gid.
def searchGidInAddLinkTable(self, gid):
# lock data base
self.lockCursor()
self.persepolis_db_cursor.execute("""SELECT * FROM addlink_db_table WHERE gid = '{}'""".format(str(gid)))
list_ = self.persepolis_db_cursor.fetchall()
# job is done! open the lock
self.lock = False
if list_:
tuple_ = list_[0]
else:
return None
dict_ = {'gid': tuple_[1],
'out': tuple_[2],
'start_time': tuple_[3],
'end_time': tuple_[4],
'link': tuple_[5],
'ip': tuple_[6],
'port': tuple_[7],
'proxy_user': tuple_[8],
'proxy_passwd': tuple_[9],
'download_user': tuple_[10],
'download_passwd': tuple_[11],
'connections': tuple_[12],
'limit_value': tuple_[13],
'download_path': tuple_[14],
'referer': tuple_[15],
'load_cookies': tuple_[16],
'user_agent': tuple_[17],
'header': tuple_[18],
'after_download': tuple_[19],
'proxy_type': tuple_[20]
}
return dict_
# return items in addlink_db_table
# '*' for category, cause that method returns all items.
def returnItemsInAddLinkTable(self, category=None):
# lock data base
self.lockCursor()
if category:
self.persepolis_db_cursor.execute(
"""SELECT * FROM addlink_db_table WHERE category = '{}'""".format(category))
else:
self.persepolis_db_cursor.execute("""SELECT * FROM addlink_db_table""")
rows = self.persepolis_db_cursor.fetchall()
# job is done! open the lock
self.lock = False
addlink_dict = {}
for tuple_ in rows:
# change format of tuple_ to dictionary
dict_ = {'gid': tuple_[1],
'out': tuple_[2],
'start_time': tuple_[3],
'end_time': tuple_[4],
'link': tuple_[5],
'ip': tuple_[6],
'port': tuple_[7],
'proxy_user': tuple_[8],
'proxy_passwd': tuple_[9],
'download_user': tuple_[10],
'download_passwd': tuple_[11],
'connections': tuple_[12],
'limit_value': tuple_[13],
'download_path': tuple_[14],
'referer': tuple_[15],
'load_cookies': tuple_[16],
'user_agent': tuple_[17],
'header': tuple_[18],
'after_download': tuple_[19],
'proxy_type': tuple_[20]
}
# add dict_ to the addlink_dict
# gid as key and dict_ as value
addlink_dict[tuple_[1]] = dict_
return addlink_dict
# this method updates download_db_table
def updateDownloadTable(self, list_):
# lock data base
self.lockCursor()
keys_list = ['file_name',
'status',
'size',
'downloaded_size',
'percent',
'connections',
'rate',
'estimate_time_left',
'gid',
'link',
'first_try_date',
'last_try_date',
'category'
]
for dict_ in list_:
for key in keys_list:
# if a key is missed in dict_,
# then add this key to the dict_ and assign None value for the key.
if key not in dict_.keys():
dict_[key] = None
# update data base if value for the keys is not None
self.persepolis_db_cursor.execute("""UPDATE download_db_table SET file_name = coalesce(:file_name, file_name),
status = coalesce(:status, status),
size = coalesce(:size, size),
downloaded_size = coalesce(:downloaded_size, downloaded_size),
percent = coalesce(:percent, percent),
connections = coalesce(:connections, connections),
rate = coalesce(:rate, rate),
estimate_time_left = coalesce(:estimate_time_left, estimate_time_left),
link = coalesce(:link, link),
first_try_date = coalesce(:first_try_date, first_try_date),
last_try_date = coalesce(:last_try_date, last_try_date),
category = coalesce(:category, category)
WHERE gid = :gid""", dict_)
# commit the changes
self.persepolis_db_connection.commit()
# job is done! open the lock
self.lock = False
# this method updates category_db_table
def updateCategoryTable(self, list_):
# lock data base
self.lockCursor()
keys_list = ['category',
'start_time_enable',
'start_time',
'end_time_enable',
'end_time',
'reverse',
'limit_enable',
'limit_value',
'after_download',
'gid_list']
for dict_ in list_:
# format of gid_list is list and must be converted to string for sqlite3
if 'gid_list' in dict_.keys():
dict_['gid_list'] = str(dict_['gid_list'])
for key in keys_list:
# if a key is missed in dict_,
# then add this key to the dict_ and assign None value for the key.
if key not in dict_.keys():
dict_[key] = None
# update data base if value for the keys is not None
self.persepolis_db_cursor.execute("""UPDATE category_db_table SET start_time_enable = coalesce(:start_time_enable, start_time_enable),
start_time = coalesce(:start_time, start_time),
end_time_enable = coalesce(:end_time_enable, end_time_enable),
end_time = coalesce(:end_time, end_time),
reverse = coalesce(:reverse, reverse),
limit_enable = coalesce(:limit_enable, limit_enable),
limit_value = coalesce(:limit_value, limit_value),
after_download = coalesce(:after_download, after_download),
gid_list = coalesce(:gid_list, gid_list)
WHERE category = :category""", dict_)
# commit changes
self.persepolis_db_connection.commit()
# job is done! open the lock
self.lock = False
# this method updates addlink_db_table
def updateAddLinkTable(self, list_):
# lock data base
self.lockCursor()
keys_list = ['gid',
'out',
'start_time',
'end_time',
'link',
'ip',
'port',
'proxy_user',
'proxy_passwd',
'download_user',
'download_passwd',
'connections',
'limit_value',
'download_path',
'referer',
'load_cookies',
'user_agent',
'header',
'after_download',
'proxy_type']
for dict_ in list_:
update_query_set_statements_list = []
for key in keys_list:
if key in dict_.keys():
update_query_set_statements_list.append(f"{key} = :{key}")
update_query_set_statements = ' ,\n '.join(update_query_set_statements_list)
update_query = f"""UPDATE addlink_db_table SET
{update_query_set_statements}
WHERE gid = :gid
"""
if len(update_query_set_statements_list) > 0:
self.persepolis_db_cursor.execute(update_query, dict_)
# commit the changes!
self.persepolis_db_connection.commit()
# job is done! open the lock
self.lock = False
def updateVideoFinderTable(self, list_):
# lock data base
self.lockCursor()
keys_list = ['video_gid',
'audio_gid',
'video_completed',
'audio_completed',
'muxing_status',
'checking']
for dictionary in list_:
for key in keys_list:
# if a key is missed in dict_,
# then add this key to the dict_ and assign None value for the key.
if key not in dictionary.keys():
dictionary[key] = None
if dictionary['video_gid']:
# update data base if value for the keys is not None
self.persepolis_db_cursor.execute("""UPDATE video_finder_db_table SET video_completed = coalesce(:video_completed, video_completed),
audio_completed = coalesce(:audio_completed, audio_completed),
muxing_status = coalesce(:muxing_status, muxing_status),
checking = coalesce(:checking, checking),
download_path = coalesce(:download_path, download_path)
WHERE video_gid = :video_gid""", dictionary)
elif dictionary['audio_gid']:
# update data base if value for the keys is not None
self.persepolis_db_cursor.execute("""UPDATE video_finder_db_table SET video_completed = coalesce(:video_completed, video_completed),
audio_completed = coalesce(:audio_completed, audio_completed),
muxing_status = coalesce(:muxing_status, muxing_status),
checking = coalesce(:checking, checking),
download_path = coalesce(:download_path, download_path)
WHERE audio_gid = :audio_gid""", dictionary)
# commit the changes!
self.persepolis_db_connection.commit()
# job is done! open the lock
self.lock = False
def updateVideoFinderTable2(self, dict_):
# lock data base
self.lockCursor()
keys_list = ['gid',
'download_status',
'file_name',
'eta',
'download_speed_str',
'downloaded_size',
'file_size',
'download_percent',
'fragments',
'error_message']
for key in keys_list:
if key not in dict_.keys():
dict_[key] = None
self.persepolis_db_cursor.execute("""UPDATE video_finder_db_table2 SET download_status = coalesce(:download_status, download_status),
file_name = coalesce(:file_name, file_name),
eta = coalesce(:eta, eta),
download_speed_str = coalesce(:download_speed_str, download_speed_str),
downloaded_size = coalesce(:downloaded_size, downloaded_size),
file_size = coalesce(:file_size, file_size),
download_percent = coalesce(:download_percent, download_percent),
fragments = coalesce(:fragments, fragments),
error_message = coalesce(:error_message, error_message)
WHERE gid = :gid""", dict_)
self.persepolis_db_connection.commit()
self.lock = False
def setDefaultGidInAddlinkTable(self, gid, start_time=False, end_time=False, after_download=False):
# lock data base
self.lockCursor()
# change value of start_time and end_time and after_download for special gid to NULL value
if start_time:
self.persepolis_db_cursor.execute("""UPDATE addlink_db_table SET start_time = NULL
WHERE gid = '{}' """.format(gid))
if end_time:
self.persepolis_db_cursor.execute("""UPDATE addlink_db_table SET end_time = NULL
WHERE gid = '{}' """.format(gid))
if after_download:
self.persepolis_db_cursor.execute("""UPDATE addlink_db_table SET after_download = NULL
WHERE gid = '{}' """.format(gid))
self.persepolis_db_connection.commit()
# job is done! open the lock
self.lock = False
# return category information in category_db_table
def searchCategoryInCategoryTable(self, category):
# lock data base
self.lockCursor()
self.persepolis_db_cursor.execute(
"""SELECT * FROM category_db_table WHERE category = '{}'""".format(str(category)))
list_ = self.persepolis_db_cursor.fetchall()
# job is done! open the lock
self.lock = False
if list_:
tuple_ = list_[0]
else:
return None
# convert string to list
gid_list = ast.literal_eval(tuple_[9])
# create a dictionary from results
dict_ = {'category': tuple_[0],
'start_time_enable': tuple_[1],
'start_time': tuple_[2],
'end_time_enable': tuple_[3],
'end_time': tuple_[4],
'reverse': tuple_[5],
'limit_enable': tuple_[6],
'limit_value': tuple_[7],
'after_download': tuple_[8],
'gid_list': gid_list
}
# return dictionary
return dict_
# return categories name
def categoriesList(self):
# lock data base
self.lockCursor()
self.persepolis_db_cursor.execute("""SELECT category FROM category_db_table ORDER BY ROWID""")
rows = self.persepolis_db_cursor.fetchall()
# create a list from categories name
queues_list = []
for tuple_ in rows:
queues_list.append(tuple_[0])
# job is done! open the lock
self.lock = False
# return the list
return queues_list
def setDBTablesToDefaultValue(self):
# lock data base
self.lockCursor()
# change start_time_enable , end_time_enable , reverse ,
# limit_enable , after_download value to default value !
self.persepolis_db_cursor.execute("""UPDATE category_db_table SET start_time_enable = 'no', end_time_enable = 'no',
reverse = 'no', limit_enable = 'no', after_download = 'no'""")
# change checking value to no in video_finder_db_table
self.persepolis_db_cursor.execute("""UPDATE video_finder_db_table SET checking = 'no'""")
# change status of download to 'stopped' if status isn't 'complete' or 'error'
self.persepolis_db_cursor.execute("""UPDATE download_db_table SET status = 'stopped'
WHERE status NOT IN ('complete', 'error')""")
# change start_time and end_time and
# after_download value to None in addlink_db_table!
self.persepolis_db_cursor.execute("""UPDATE addlink_db_table SET start_time = NULL,
end_time = NULL,
after_download = NULL
""")
self.persepolis_db_connection.commit()
# job is done! open the lock
self.lock = False
def findActiveDownloads(self, category=None):
# lock data base
self.lockCursor()
# find download items is download_db_table with status = "downloading" or "waiting" or paused or scheduled
if category:
self.persepolis_db_cursor.execute("""SELECT gid FROM download_db_table WHERE (category = '{}') AND (status = 'downloading' OR status = 'waiting'
OR status = 'scheduled' OR status = 'paused' OR status = 'creating download file')""".format(str(category)))
else:
self.persepolis_db_cursor.execute("""SELECT gid FROM download_db_table WHERE (status = 'downloading' OR status = 'waiting'
OR status = 'scheduled' OR status = 'paused' OR status = 'creating download file')""")
# create a list for returning answer
result = self.persepolis_db_cursor.fetchall()
gid_list = []
for result_tuple in result:
gid_list.append(result_tuple[0])
# job is done! open the lock
self.lock = False
return gid_list
# this method returns items with 'downloading' or 'waiting' or 'creating download file' status
def returnDownloadingItems(self):
# lock data base
self.lockCursor()
# find download items is download_db_table with status = "downloading" or "waiting" or paused or scheduled
self.persepolis_db_cursor.execute(
"""SELECT gid FROM download_db_table WHERE (status = 'downloading' OR status = 'waiting' OR status = 'creating download file')""")
# create a list for returning answer
result = self.persepolis_db_cursor.fetchall()
gid_list = []
for result_tuple in result:
gid_list.append(result_tuple[0])
# job is done! open the lock
self.lock = False
return gid_list
# this method returns items with 'paused' status.
def returnPausedItems(self):
# lock data base
self.lockCursor()
# find download items is download_db_table with status = "downloading" or "waiting" or paused or scheduled
self.persepolis_db_cursor.execute("""SELECT gid FROM download_db_table WHERE (status = 'paused')""")
# create a list for returning answer
result = self.persepolis_db_cursor.fetchall()
gid_list = []
for result_tuple in result:
gid_list.append(result_tuple[0])
# job is done! open the lock
self.lock = False
return gid_list
# return all video_gids and audio_gids in video_finder_db_table
def returnVideoFinderGids(self):
# lock data base
self.lockCursor()
self.persepolis_db_cursor.execute("""SELECT video_gid, audio_gid FROM video_finder_db_table""")
# create a list for result
result = self.persepolis_db_cursor.fetchall()
# job is done! open the lock
self.lock = False
gid_list = []
video_gid_list = []
audio_gid_list = []
for result_tuple in result:
gid_list.append(result_tuple[0])
video_gid_list.append(result_tuple[0])
gid_list.append(result_tuple[1])
audio_gid_list.append(result_tuple[1])
# job is done
return gid_list, video_gid_list, audio_gid_list
# This method deletes a category from category_db_table
def deleteCategory(self, category):
# delete gids of this category from gid_list of 'All Downloads'
category_dict = self.searchCategoryInCategoryTable(category)
all_downloads_dict = self.searchCategoryInCategoryTable('All Downloads')
# get gid_list
category_gid_list = category_dict['gid_list']
all_downloads_gid_list = all_downloads_dict['gid_list']
for gid in category_gid_list:
# delete item from all_downloads_gid_list
all_downloads_gid_list.remove(gid)
# update category_db_table
self.updateCategoryTable([all_downloads_dict])
# delete category from data_base
# lock data base
self.lockCursor()
self.persepolis_db_cursor.execute(
"""DELETE FROM category_db_table WHERE category = '{}'""".format(str(category)))
# commit changes
self.persepolis_db_connection.commit()
# job is done! open the lock
self.lock = False
# this method deletes all items in data_base
def resetDataBase(self):
# update gid_list in categories with empty gid_list
all_downloads_dict = {'category': 'All Downloads', 'gid_list': []}
single_downloads_dict = {'category': 'Single Downloads', 'gid_list': []}
scheduled_downloads_dict = {'category': 'Scheduled Downloads', 'gid_list': []}
self.updateCategoryTable([all_downloads_dict, single_downloads_dict, scheduled_downloads_dict])
# lock data base
self.lockCursor()
# delete all items in category_db_table, except 'All Downloads' and 'Single Downloads'
self.persepolis_db_cursor.execute(
"""DELETE FROM category_db_table WHERE category NOT IN ('All Downloads', 'Single Downloads', 'Scheduled Downloads')""")
self.persepolis_db_cursor.execute("""DELETE FROM download_db_table""")
self.persepolis_db_cursor.execute("""DELETE FROM addlink_db_table""")
# commit
self.persepolis_db_connection.commit()
# release lock
self.lock = False
# This method deletes a download item from download_db_table
def deleteItemInDownloadTable(self, gid, category):
# lock data base
self.lockCursor()
self.persepolis_db_cursor.execute("""DELETE FROM download_db_table WHERE gid = '{}'""".format(str(gid)))
# commit changes
self.persepolis_db_connection.commit()
# job is done! open the lock
self.lock = False
# delete item from gid_list in category and All Downloads
for category_name in category, 'All Downloads':
category_dict = self.searchCategoryInCategoryTable(category_name)
# get gid_list
gid_list = category_dict['gid_list']
# delete item
if gid in gid_list:
gid_list.remove(gid)
# if gid is in video_finder_db_table, both of video_gid and audio_gid must be deleted from gid_list
video_finder_dictionary = self.searchGidInVideoFinderTable(gid)
if video_finder_dictionary:
video_gid = video_finder_dictionary['video_gid']
audio_gid = video_finder_dictionary['audio_gid']
if gid == video_gid:
gid_list.remove(audio_gid)
else:
gid_list.remove(video_gid)
# update category_db_table
self.updateCategoryTable([category_dict])
# this method replaces:
# GB >> GiB
# MB >> MiB
# KB >> KiB
# Read this link for more information:
# https://en.wikipedia.org/wiki/Orders_of_magnitude_(data)
def correctDataBase(self):
# lock data base
self.lockCursor()
for units in [['KB', 'KiB'], ['MB', 'MiB'], ['GB', 'GiB']]:
dict_ = {'old_unit': units[0],
'new_unit': units[1]}
self.persepolis_db_cursor.execute("""UPDATE download_db_table
SET size = replace(size, :old_unit, :new_unit)""", dict_)
self.persepolis_db_cursor.execute("""UPDATE download_db_table
SET rate = replace(rate, :old_unit, :new_unit)""", dict_)
self.persepolis_db_cursor.execute("""UPDATE download_db_table
SET downloaded_size = replace(downloaded_size, :old_unit, :new_unit)""", dict_)
self.persepolis_db_connection.commit()
# job is done! open the lock
self.lock = False
def correctDataBaseForVersion410(self):
# lock data base
self.lockCursor()
# set all proxy_type to http
# first check for proxy_type's column is exist or not.
try:
self.persepolis_db_cursor.execute("""SELECT proxy_type FROM addlink_db_table""")
except sqlite3.OperationalError:
# create proxy_type column in addlink_db_table
self.persepolis_db_cursor.execute("""ALTER TABLE addlink_db_table
ADD proxy_type NULL""")
# set "http" value for pervious downloads that use proxy
self.persepolis_db_cursor.execute("""UPDATE addlink_db_table
SET proxy_type = 'http' WHERE ip IS NOT NULL""")
self.persepolis_db_connection.commit()
# job is done! open the lock
self.lock = False
# close connections
def correctDataBaseForVersion411(self):
# lock data base
self.lockCursor()
# find gid of all unfinished downloads in download_db_table
self.persepolis_db_cursor.execute("""SELECT gid FROM download_db_table WHERE status IS NOT 'complete'""")
result = self.persepolis_db_cursor.fetchall()
gid_list = []
for result_tuple in result:
gid_list.append(result_tuple[0])
# correct download path
for gid in gid_list:
# find download_path
self.persepolis_db_cursor.execute("""SELECT download_path FROM addlink_db_table WHERE gid = '{}'""".format(str(gid)))
tuple_ = self.persepolis_db_cursor.fetchone()
download_path = tuple_[0]
import platform
os_type = platform.system()
home_address = os.path.expanduser("~")
try:
if os.lstat(download_path).st_dev == os.lstat(home_address).st_dev:
if os_type != 'Windows':
download_path_temp = os.path.join(home_address, '.persepolis')
else:
download_path_temp = os.path.join(
home_address, 'AppData', 'Local', 'persepolis')
else:
from persepolis.scripts.osCommands import findMountPoint
# Find mount point
mount_point = findMountPoint(download_path)
# find download_path_temp
if os_type == 'Windows':
download_path_temp = os.path.join(mount_point, 'persepolis')
else:
download_path_temp = os.path.join(mount_point, '.persepolis')
# set download_path_temp as download_path
self.persepolis_db_cursor.execute("""UPDATE addlink_db_table SET download_path = '{}'
WHERE gid = '{}' """.format(download_path_temp, gid))
self.persepolis_db_connection.commit()
except Exception:
pass
# job is done! open the lock
self.lock = False
def closeConnections(self):
# lock data base
self.lockCursor()
self.persepolis_db_cursor.close()
self.persepolis_db_connection.close()
# job is done! open the lock
self.lock = False
================================================
FILE: persepolis/scripts/download_link.py
================================================
# -*- coding: utf-8 -*-
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
import requests
try:
from PySide6.QtCore import QThread, Signal
except ImportError:
from PyQt5.QtCore import QThread
from PyQt5.QtCore import pyqtSignal as Signal
from persepolis.scripts import logger
# this thread starts download.
class DownloadLink(QThread):
def __init__(self, gid, download_session, main_window):
QThread.__init__(self)
self.gid = gid
self.download_session = download_session
self.main_window = main_window
def run(self):
# add gid of download to the active gids in temp_db
# or update data base , if it was existed before
try:
self.main_window.temp_db.insertInSingleTable(self.gid)
except Exception:
# release lock
self.main_window.temp_db.lock = False
dictionary = {'gid': self.gid, 'status': 'active'}
self.main_window.temp_db.updateSingleTable(dictionary)
self.download_session.start()
class DownloadSingleLink(QThread):
DOWNLOADSTATUSSIGNAL = Signal(bool)
def __init__(self, download_link, file_path):
QThread.__init__(self)
self.download_link = download_link
self.file_path = file_path
def run(self):
try:
# download link
response = requests.get(self.download_link)
# write it to file
with open(self.file_path, 'wb') as f:
f.write(response.content)
if response.ok:
log_message = 'Download complete! ' + str(self.file_path)
logger.sendToLog(log_message, "INFO")
self.DOWNLOADSTATUSSIGNAL.emit(True)
except Exception as e:
error_message = 'Download was unsuccessful:\n' + str(self.file_path) + '\n' + str(e)
logger.sendToLog(error_message, "ERROR")
self.DOWNLOADSTATUSSIGNAL.emit(False)
================================================
FILE: persepolis/scripts/error_window.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
try:
from PySide6.QtWidgets import QWidget, QTextEdit, QVBoxLayout, QHBoxLayout, QPushButton, QLabel
from PySide6.QtGui import QIcon
from PySide6.QtCore import Qt, QSize, QSettings
except ImportError:
from PyQt5.QtWidgets import QWidget, QTextEdit, QVBoxLayout, QHBoxLayout, QPushButton, QLabel
from PyQt5.QtCore import Qt, QSize, QSettings
from PyQt5.QtGui import QIcon
from persepolis.scripts.data_base import PersepolisDB
from persepolis.scripts import osCommands
from persepolis.gui import resources
class ErrorWindow(QWidget):
def __init__(self, text):
super().__init__()
# finding windows_size
self.setMinimumSize(QSize(363, 300))
self.setWindowIcon(QIcon.fromTheme('persepolis', QIcon(':/com.github.persepolisdm.persepolis.svg')))
self.setWindowTitle('Persepolis Download Manager')
verticalLayout = QVBoxLayout(self)
horizontalLayout = QHBoxLayout()
horizontalLayout.addStretch(1)
self.text_edit = QTextEdit(self)
self.text_edit.setReadOnly(True)
self.text_edit.insertPlainText(text)
verticalLayout.addWidget(self.text_edit)
self.label2 = QLabel(self)
self.label2.setText('Reseting persepolis may solving problem.\nDo not panic!If you add your download links again,\npersepolis will resume your downloads\nPlease copy this error message and press "Report Issue" button\nand open a new issue in Github for it.\nWe answer you as soon as possible. \nreporting this issue help us to improve persepolis.\nThank you!')
verticalLayout.addWidget(self.label2)
self.report_pushButton = QPushButton(self)
self.report_pushButton.setText("Report Issue")
horizontalLayout.addWidget(self.report_pushButton)
self.reset_persepolis_pushButton = QPushButton(self)
self.reset_persepolis_pushButton.clicked.connect(
self.resetPushButtonPressed)
self.reset_persepolis_pushButton.setText('Reset Persepolis')
horizontalLayout.addWidget(self.reset_persepolis_pushButton)
self.close_pushButton = QPushButton(self)
self.close_pushButton.setText('close')
horizontalLayout.addWidget(self.close_pushButton)
verticalLayout.addLayout(horizontalLayout)
self.report_pushButton.clicked.connect(self.reportPushButtonPressed)
self.close_pushButton.clicked.connect(self.closePushButtonPressed)
def reportPushButtonPressed(self, button):
osCommands.xdgOpen('https://github.com/persepolisdm/persepolis/issues')
# close window with ESC key
def keyPressEvent(self, event):
if event.key() == Qt.Key_Escape:
self.close()
def closePushButtonPressed(self, button):
self.close()
def resetPushButtonPressed(self, button):
# create an object for PersepolisDB
persepolis_db = PersepolisDB()
# Reset data base
persepolis_db.resetDataBase()
# close connections
persepolis_db.closeConnections()
# Reset persepolis_setting
persepolis_setting = QSettings('persepolis_download_manager', 'persepolis')
persepolis_setting.clear()
persepolis_setting.sync()
================================================
FILE: persepolis/scripts/initialization.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
# THIS FILE CONTAINING SOME VARIABLES , ... THAT USING FOR INITIALIZING PERSEPOLIS
from persepolis.scripts.data_base import PersepolisDB, PluginsDB
from persepolis.scripts import logger
from persepolis.scripts.useful_tools import determineConfigFolder, returnDefaultSettings, osAndDesktopEnvironment, getExecPath
from persepolis.scripts.browser_integration import browserIntegration
from persepolis.scripts import osCommands
from persepolis.constants import OS
import time
import os
import sys
try:
from PySide6.QtCore import QSettings
except ImportError:
from PyQt5.QtCore import QSettings
# initialization
# download manager config folder .
config_folder = determineConfigFolder()
# persepolis tmp folder path
persepolis_tmp = os.path.join(config_folder, 'persepolis_tmp')
# create folders
for folder in [config_folder, persepolis_tmp]:
osCommands.makeDirs(folder)
# persepolisdm.log file contains persepolis log.
# refresh logs!
# log files address
initialization_log_file = os.path.join(str(config_folder), 'initialization_log_file.log')
downloads_log_file = os.path.join(str(config_folder), 'downloads_log_file.log')
errors_log_file = os.path.join(str(config_folder), 'errors_log_file.log')
log_files_list = [initialization_log_file, downloads_log_file, errors_log_file]
# get current time
current_time = time.strftime('%Y/%m/%d %H:%M:%S')
# find number of lines in log_file.
for log_file in log_files_list:
with open(log_file) as f:
lines = sum(1 for _ in f)
# if number of lines in log_file is more than 300, then keep last 200 lines in log_file.
if lines < 300:
f = open(log_file, 'a')
f.writelines('===================================================\n' + 'Persepolis Download Manager, ' + current_time + '\n')
f.close()
else:
# keep last 200 lines
line_num = lines - 200
f = open(log_file, 'r')
f_lines = f.readlines()
f.close()
line_counter = 1
f = open(log_file, 'w')
for line in f_lines:
if line_counter > line_num:
f.writelines(str(line))
line_counter = line_counter + 1
f.close()
f = open(log_file, 'a')
f.writelines('Persepolis Download Manager, ' + current_time + '\n')
f.close()
# create an object for PersepolisDB
persepolis_db = PersepolisDB()
# create tables
persepolis_db.createTables()
# close connections
persepolis_db.closeConnections()
# create an object for PluginsDB
plugins_db = PluginsDB()
# create tables
plugins_db.createTables()
# delete old links
plugins_db.deleteOldLinks()
# close connections
plugins_db.closeConnections()
# import persepolis_setting
# persepolis is using QSettings for saving windows size and windows
# position and program settings.
persepolis_setting = QSettings('persepolis_download_manager', 'persepolis')
persepolis_setting.beginGroup('settings')
default_setting_dict = returnDefaultSettings()
# this loop is checking values in persepolis_setting . if value is not
# valid then value replaced by default_setting_dict value
for key in default_setting_dict.keys():
setting_value = persepolis_setting.value(key, default_setting_dict[key])
persepolis_setting.setValue(key, setting_value)
# set default dwonload path
if not (os.path.exists(persepolis_setting.value('download_path'))):
persepolis_setting.setValue('download_path', default_setting_dict['download_path'])
persepolis_setting.sync()
# Create downloads folder and subfolders.
# download sub folders if they did not existed.
download_path = persepolis_setting.value('download_path')
folder_list = [download_path]
# add subfolders to folder_list if user checked subfolders check box in setting window.
if persepolis_setting.value('subfolder') == 'yes':
for folder in ['Audios', 'Videos', 'Others', 'Documents', 'Compressed']:
folder_list.append(os.path.join(download_path, folder))
# create folders in folder_list
for folder in folder_list:
osCommands.makeDirs(folder)
persepolis_setting.endGroup()
# Browser integration for Firefox and chromium and google chrome
persepolis_setting.beginGroup('settings/native_messaging')
for browser in ['chrome', 'chromium', 'opera', 'vivaldi', 'firefox', 'brave', 'librewolf']:
if persepolis_setting.value(browser) == 'true':
json_done, intermediary_done, logg_message2 = browserIntegration(browser)
logg_message = browser
if json_done is True:
logg_message = logg_message + ': ' + 'Json file is created successfully.\n'
else:
logg_message = logg_message + ': ' + 'Json ERROR!\n'
if intermediary_done is True:
logg_message = logg_message + 'persepolis intermediary file is created successfully.\n'
elif intermediary_done is False:
logg_message = logg_message + ': ' + 'persepolis executer file ERROR!\n'
logger.sendToLog(logg_message, 'INITIALIZATION')
logger.sendToLog(logg_message2, 'INITIALIZATION')
persepolis_setting.endGroup()
# get locale and set ui direction
locale = str(persepolis_setting.value('settings/locale'))
# right to left languages
rtl_locale_list = ['fa_IR', 'ar']
# left to right languages
ltr_locale_list = ['en_US', 'zh_CN', 'fr_FR', 'pl_PL', 'nl_NL', 'pt_BR', 'es_ES', 'hu', 'tr', 'tr_TR']
if locale in rtl_locale_list:
persepolis_setting.setValue('ui_direction', 'rtl')
else:
persepolis_setting.setValue('ui_direction', 'ltr')
# check the existance of .desktop and icons file for
# Linux and BSD bundle and create it if it's necessary.
# check if persepolis run as bundle first
# find os platform
os_type, desktop_env = osAndDesktopEnvironment()
if os_type in OS.UNIX_LIKE:
exec_dictionary = getExecPath()
is_bundle = exec_dictionary['bundle']
if is_bundle:
# user home address
home_address = os.path.expanduser("~")
bundle_path = os.path.dirname(sys.executable)
# check existance of .desktop file
dot_desktop_path = os.path.join(home_address, '.local/share/applications/com.github.persepolisdm.persepolis.desktop')
icon_desktop_path = os.path.join(home_address, '.local/share/icons/hicolor/scalable/apps/com.github.persepolisdm.persepolis.svg')
icon_tray_path = os.path.join(home_address, '.local/share/icons/hicolor/scalable/apps/persepolis-tray.svg')
local_share_file_path = [dot_desktop_path,
icon_desktop_path,
icon_tray_path]
dot_desktop_bundle = os.path.join(bundle_path, 'com.github.persepolisdm.persepolis.desktop.in')
icon_desktop_bundle = os.path.join(bundle_path, 'com.github.persepolisdm.persepolis.svg')
icon_tray_bundle = os.path.join(bundle_path, 'persepolis-tray.svg')
in_bundle_file_path = [dot_desktop_bundle,
icon_desktop_bundle,
icon_tray_bundle]
for i in range(3):
# rewrite .desktop file every time
if not (os.path.exists(local_share_file_path[i])) or i == 0:
osCommands.copyFile(in_bundle_file_path[i], local_share_file_path[i])
# rewrite Exec path in .desktop file.
# perhaps user changed the place of the bundle.
# get exec path
# modify lines
# Because of space in file name and file path,
# Exec path in .desktop file must be in "".
exec_path = '"' + exec_dictionary['modified_exec_file_path'] + '"'
# get .desktop content
with open(dot_desktop_path, 'r') as file:
content = file.read()
# replace @persepolisbin@ by new exec path
new_content = content.replace('@persepolisbin@', exec_path)
# write it to file
with open(dot_desktop_path, 'w') as file:
file.write(new_content)
# compatibility
persepolis_version = float(persepolis_setting.value('version/version', 2.5))
if persepolis_version < 2.6:
from persepolis.scripts.compatibility import compatibility
try:
compatibility()
except Exception as e:
# create an object for PersepolisDB
persepolis_db = PersepolisDB()
# create tables
persepolis_db.resetDataBase()
# close connections
persepolis_db.closeConnections()
# write error in log
logger.sendToLog(
"compatibility ERROR!", "ERROR")
logger.sendToLog(
str(e), "ERROR")
persepolis_version = 2.6
if persepolis_version < 3.1:
# create an object for PersepolisDB
persepolis_db = PersepolisDB()
# correct data base
persepolis_db.correctDataBase()
# close connections
persepolis_db.closeConnections()
persepolis_version = 3.1
if persepolis_version < 4.0:
persepolis_setting.beginGroup('settings')
for key in default_setting_dict.keys():
setting_value = default_setting_dict[key]
persepolis_setting.setValue(key, setting_value)
persepolis_setting.endGroup()
if persepolis_version < 4.1:
# create an object for PersepolisDB
persepolis_db = PersepolisDB()
# correct data base
persepolis_db.correctDataBaseForVersion410()
# close connections
persepolis_db.closeConnections()
persepolis_setting.setValue('version/version', 4.1)
if persepolis_version < 4.11:
# create an object for PersepolisDB
persepolis_db = PersepolisDB()
# correct data base
persepolis_db.correctDataBaseForVersion411()
# close connections
persepolis_db.closeConnections()
persepolis_setting.setValue('version/version', 4.11)
if persepolis_version < 5.2:
persepolis_setting.beginGroup('settings')
for key in default_setting_dict.keys():
setting_value = default_setting_dict[key]
persepolis_setting.setValue(key, setting_value)
persepolis_setting.endGroup()
persepolis_setting.setValue('version/version', 5.2)
persepolis_setting.sync()
================================================
FILE: persepolis/scripts/log_window.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from persepolis.scripts.useful_tools import determineConfigFolder
from persepolis.gui.log_window_ui import LogWindow_Ui
from persepolis.scripts import osCommands
import os
try:
from PySide6.QtCore import Qt, QPoint, QSize
from PySide6.QtGui import QIcon
from PySide6 import QtWidgets
except ImportError:
from PyQt5.QtCore import Qt, QPoint, QSize
from PyQt5.QtGui import QIcon
from PyQt5 import QtWidgets
# config_folder
config_folder = determineConfigFolder()
class LogWindow(LogWindow_Ui):
def __init__(self, persepolis_setting):
super().__init__(persepolis_setting)
self.persepolis_setting = persepolis_setting
self.copy_log_pushButton.setEnabled(False)
# log files address
self.initialization_log_file = os.path.join(str(config_folder), 'initialization_log_file.log')
self.downloads_log_file = os.path.join(str(config_folder), 'downloads_log_file.log')
self.errors_log_file = os.path.join(str(config_folder), 'errors_log_file.log')
# lists
self.log_files_list = [self.initialization_log_file, self.downloads_log_file, self.errors_log_file]
self.text_widgets_list = [self.initialization_text_edit, self.downloads_text_edit, self.errors_text_edit]
self.tabs_list = [self.initialization_tab, self.downloads_tab, self.errors_tab]
# Set downloads_tab for current tab
self.log_tabWidget.setCurrentWidget(self.downloads_tab)
# read logs
for index, file in enumerate(self.log_files_list):
text = ''
f = open(file, 'r')
f_lines = f.readlines()
f.close()
for line in f_lines:
text = text + str(line) + '\n'
self.text_widgets_list[index].insertPlainText(text)
self.text_widgets_list[index].copyAvailable.connect(
self.copyAvailableSignalHandler)
# signals and slots
self.copy_log_pushButton.clicked.connect(
self.copyPushButtonPressed)
self.report_pushButton.clicked.connect(
self.reportPushButtonPressed)
self.close_pushButton.clicked.connect(
self.closePushButtonPressed)
self.refresh_log_pushButton.clicked.connect(
self.refreshLogPushButtonPressed)
self.clear_log_pushButton.clicked.connect(
self.clearLogPushButtonPressed)
# setting window size and position
size = self.persepolis_setting.value(
'LogWindow/size', QSize(720, 300))
position = self.persepolis_setting.value(
'LogWindow/position', QPoint(300, 300))
self.resize(size)
self.move(position)
self.minimum_height = self.height()
def clearLogPushButtonPressed(self, button):
# Empty log files
for index, file in enumerate(self.log_files_list):
# erase files
f = open(file, 'w')
f.close()
# clear text editors
self.text_widgets_list[index].clear()
def reportPushButtonPressed(self, button):
osCommands.xdgOpen('https://github.com/persepolisdm/persepolis/issues')
def closePushButtonPressed(self, button):
self.close()
def copyAvailableSignalHandler(self, signal):
if signal:
self.copy_log_pushButton.setEnabled(True)
else:
self.copy_log_pushButton.setEnabled(False)
def copyPushButtonPressed(self, button):
# find current active tab
index = self.log_tabWidget.currentIndex()
for tab_index, tab in enumerate(self.tabs_list):
if self.log_tabWidget.indexOf(tab) == index:
# copy text
self.text_widgets_list[tab_index].copy()
# this method is refresh log messages in text_edit
def refreshLogPushButtonPressed(self, button):
# read logs
for index, file in enumerate(self.log_files_list):
f = open(file, 'r')
f_lines = f.readlines()
f.close()
text = 'Log file:\n'
for line in f_lines:
text = text + str(line) + '\n'
self.text_widgets_list[index].clear()
self.text_widgets_list[index].insertPlainText(text)
# close window with ESC key
def keyPressEvent(self, event):
if event.key() == Qt.Key_Escape:
self.close()
def closeEvent(self, event):
self.layout().setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint)
self.setMinimumSize(QSize(self.width(), self.minimum_height))
self.resize(QSize(self.width(), self.minimum_height))
self.persepolis_setting.setValue('LogWindow/size', self.size())
self.persepolis_setting.setValue('LogWindow/position', self.pos())
self.persepolis_setting.sync()
event.accept()
def changeIcon(self, icons):
icons = ':/' + str(icons) + '/'
self.close_pushButton.setIcon(QIcon(icons + 'remove'))
self.copy_log_pushButton.setIcon(QIcon(icons + 'clipboard'))
self.report_pushButton.setIcon(QIcon(icons + 'about'))
self.refresh_log_pushButton.setIcon(QIcon(icons + 'refresh'))
================================================
FILE: persepolis/scripts/logger.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
from persepolis.scripts.useful_tools import determineConfigFolder
from persepolis.scripts.osCommands import touch
import logging
import os
def setUpLogger(logger_name, log_file, level):
# define logging object
logObj = logging.getLogger(logger_name)
logObj.setLevel(level)
# don't show log in console
logObj.propagate = False
# create a file handler
handler = logging.FileHandler(log_file)
handler.setLevel(logging.INFO)
# create a logging format
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
# add the handlers to the logger
logObj.addHandler(handler)
return logObj
# config_folder
config_folder = determineConfigFolder()
# create a directory if it does not exist
if not os.path.exists(config_folder):
os.makedirs(config_folder)
# log files address
initialization_log_file = os.path.join(str(config_folder), 'initialization_log_file.log')
downloads_log_file = os.path.join(str(config_folder), 'downloads_log_file.log')
errors_log_file = os.path.join(str(config_folder), 'errors_log_file.log')
for file in [initialization_log_file, downloads_log_file, errors_log_file]:
if not os.path.isfile(file):
touch(file)
initialization_logger = setUpLogger('initialization', initialization_log_file, logging.INFO)
downloads_logger = setUpLogger('downloads', downloads_log_file, logging.INFO)
errors_logger = setUpLogger('errors', errors_log_file, logging.ERROR)
def sendToLog(text="", type="INFO"):
if type == "INITIALIZATION":
initialization_logger.info(text)
elif type == "ERROR":
errors_logger.error(text)
elif type == 'DOWNLOADS':
downloads_logger.info(text)
elif type == 'DOWNLOAD ERROR':
downloads_logger.error(text)
errors_logger.error(text)
else:
initialization_logger.info(text)
================================================
FILE: persepolis/scripts/mainwindow.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import os
import ast
import sys
import glob
import random
import requests
import tempfile
import subprocess
import urllib.parse
from time import sleep
from copy import deepcopy
from functools import partial
from persepolis.constants import OS
from persepolis.gui import resources
from persepolis.scripts import spider
from persepolis.scripts import logger
from persepolis.scripts import osCommands
from persepolis.scripts.about import AboutWindow
from persepolis.scripts.shutdown import shutDown
from persepolis.scripts.log_window import LogWindow
from persepolis.scripts.text_queue import TextQueue
from persepolis.scripts import persepolis_lib_prime
from persepolis.scripts.addlink import AddLinkWindow
from persepolis.scripts.progress import ProgressWindow
from persepolis.scripts.video_finder import VideoFinder
from persepolis.scripts.queue import Queue
from persepolis.scripts.setting import PreferencesWindow
from persepolis.scripts.download_link import DownloadLink
from persepolis.scripts.properties import PropertiesWindow
from persepolis.scripts.after_download import AfterDownloadWindow
from persepolis.scripts.browser_plugin_queue import BrowserPluginQueue
from persepolis.scripts.data_base import PluginsDB, PersepolisDB, TempDB
from persepolis.gui.mainwindow_ui import MainWindow_Ui, QTableWidgetItem
from persepolis.scripts.video_finder_progress import VideoFinderProgressWindow
from persepolis.scripts.bubble import notifySend, checkNotificationSounds, createNotificationSounds
from persepolis.scripts.useful_tools import nowDate, freeSpace, determineConfigFolder, osAndDesktopEnvironment, getExecPath, ffmpegVersion, findExternalAppPath
global pyside6_is_installed
try:
from PySide6.QtWidgets import QCheckBox, QLineEdit, QAbstractItemView, QFileDialog, QSystemTrayIcon, QMenu, QApplication, QInputDialog, QMessageBox
from PySide6.QtCore import QDir, QTime, QCoreApplication, QSize, QPoint, QThread, Signal, Qt, QTranslator, QLocale
from PySide6.QtGui import QFont, QIcon, QStandardItem, QCursor, QAction
from PySide6 import __version__ as PYQT_VERSION_STR
from PySide6.QtCore import __version__ as QT_VERSION_STR
pyside6_is_installed = True
except ImportError:
from PyQt5.QtWidgets import QCheckBox, QLineEdit, QAbstractItemView, QAction, QFileDialog, QSystemTrayIcon, QMenu, QApplication, QInputDialog, QMessageBox
from PyQt5.QtCore import QDir, QTime, QCoreApplication, QSize, QPoint, QThread, Qt, QTranslator, QLocale, QT_VERSION_STR
from PyQt5.QtGui import QFont, QIcon, QStandardItem, QCursor
from PyQt5.Qt import PYQT_VERSION_STR
from PyQt5.QtCore import pyqtSignal as Signal
pyside6_is_installed = False
global youtube_dl_is_installed
try:
from persepolis.scripts.video_finder_addlink import VideoFinderAddLink
from persepolis.scripts import ytdlp_downloader
youtube_dl_is_installed = True
except ModuleNotFoundError:
# if youtube_dl module is not installed:
logger.sendToLog(
"yt-dlp is not installed.", "ERROR")
youtube_dl_is_installed = False
# InitializationThread thread can change this variables.
global ffmpeg_is_installed
ffmpeg_is_installed = False
# check if notification sounds are available or not
global notification_sounds_are_available
notification_sounds_are_available = False
# shutdown_notification = 0 >> persepolis is running
# 1 >> persepolis is ready for closing(closeEvent is called)
# 2 >> OK, let's close application!
global shutdown_notification
shutdown_notification = 0
# checking_flag : 0 >> normal situation ;
# 1 >> remove button or delete button pressed or sorting form viewMenu or ... toggled by user ;
# 2 >> check_download_info function is stopping until remove operation done ;
# 3 >> deleteFileAction is done it's job and It is called removeButtonPressed.
global checking_flag
checking_flag = 0
global button_pressed_counter
button_pressed_counter = 0
global plugin_links_checked
plugin_links_checked = False
# find os platform
os_type, desktop_env = osAndDesktopEnvironment()
# config_folder
config_folder = determineConfigFolder()
download_info_folder = os.path.join(config_folder, "download_info")
# persepolis tmp folder path
persepolis_tmp = os.path.join(config_folder, 'persepolis_tmp')
# see persepolis.py file for show_window_file and plugin_ready
plugin_ready = os.path.join(persepolis_tmp, 'persepolis-plugin-ready')
show_window_file = os.path.join(persepolis_tmp, 'show-window')
# delete things that are no longer needed
class DeleteThingsThatAreNoLongerNeededThread(QThread):
NOTIFYSENDSIGNAL = Signal(list)
def __init__(self, gid, file_name, status, category, delete_download_file, main_window, video_finder_link):
QThread.__init__(self)
self.gid = gid
self.file_name = file_name
self.status = status
self.category = category
self.delete_download_file = delete_download_file
self.main_window = main_window
self.video_finder_link = video_finder_link
def run(self):
# find download_path
dictionary = self.main_window.persepolis_db.searchGidInAddLinkTable(self.gid)
if dictionary:
download_path = dictionary['download_path']
# remove file of download from download folder
if self.file_name != '***' and self.status != 'complete':
file_name_path = os.path.join(
download_path, str(self.file_name))
if self.video_finder_link:
# remove all yt-dlp file
yt_dlp_files_pattern = file_name_path + '*'
for file in glob.glob(yt_dlp_files_pattern):
osCommands.remove(file)
else:
osCommands.remove(file_name_path) # remove file
json_control_file = file_name_path + str('.persepolis')
osCommands.remove(json_control_file) # remove file.persepolis
# remove downloaded file, if download is completed
elif self.status == 'complete' and self.delete_download_file:
# download is complete. so download_path == file_name_path
remove_answer = osCommands.remove(download_path)
# if file not existed, notify user
if remove_answer == 'no':
self.NOTIFYSENDSIGNAL.emit([str(self.file_name), QCoreApplication.translate("mainwindow_src_ui_tr", 'Not Found'),
5000, 'fail'])
# remove download item from data base
self.main_window.persepolis_db.deleteItemInDownloadTable(self.gid, self.category)
# this thread checks:
# ffmpeg availability
# ffmpeg and python and pyqt and qt versions and write them in log file.
# writes os type and desktop env. in log file.
# checks notification sounds existance.
class InitializationThread(QThread):
def __init__(self, parent):
QThread.__init__(self)
self.parent = parent
def run(self):
global ffmpeg_is_installed
global notification_sounds_are_available
# check ffmpeg version
ffmpeg_is_installed, ffmpeg_output, ffmpeg_command_log_list = ffmpegVersion()
logger.sendToLog(ffmpeg_command_log_list[0], ffmpeg_command_log_list[1])
logger.sendToLog(ffmpeg_output, "INFO")
# write ffmpeg path to log
ffmpeg_command, log_list = findExternalAppPath('ffmpeg')
ffmpeg_command_is = 'ffmpeg command is: ' + str(ffmpeg_command)
logger.sendToLog(ffmpeg_command_is, "INFO")
# check notification sound files existance and create them if it's necessary.
notification_sounds_are_available = checkNotificationSounds()
if not (notification_sounds_are_available):
notification_sounds_are_available = createNotificationSounds(self.parent)
# log python version
logger.sendToLog('python version: ' + str(sys.version))
# log qt version
logger.sendToLog('QT version: ' + str(QT_VERSION_STR))
# log pyqt version
if pyside6_is_installed:
madule_str = 'PySide version: '
else:
madule_str = 'PyQt version: '
logger.sendToLog(madule_str + str(PYQT_VERSION_STR))
# log os and desktop env.
logger.sendToLog('Operating system: ' + os_type)
# windows and mac haven't desktop_env
if desktop_env:
logger.sendToLog('Desktop env.: ' + str(desktop_env))
# check clipboard
class CheckClipBoardThread(QThread):
CHECKCLIPBOARDSIGNAL = Signal()
def __init__(self, parent):
QThread.__init__(self)
def run(self):
# shutdown_notification = 0 >> persepolis is running
# 1 >> persepolis is ready for closing(closeEvent called)
# 2 >> OK, let's close application!
while shutdown_notification == 0:
sleep(1)
clipboard = QApplication.clipboard()
old_clipboard = ""
while shutdown_notification == 0:
sleep(0.5)
new_clipboard = clipboard.text()
if (new_clipboard != old_clipboard) and (new_clipboard != ""):
self.CHECKCLIPBOARDSIGNAL.emit()
old_clipboard = new_clipboard
# check if any thing in clipboard or not
class CheckClipboardStateThread(QThread):
WINDOWISACTIVESIGNAL = Signal()
def __init__(self):
QThread.__init__(self)
def run(self):
while (QApplication.clipboard().text() == ""):
sleep(0.1)
self.WINDOWISACTIVESIGNAL.emit()
# check for newer version of Persepolis
class CheckNewerVersionThread(QThread):
NEWVERSIONISAVAILABLESIGNAL = Signal(str)
def __init__(self, parent):
QThread.__init__(self)
# get current_version from QSettings
self.current_version = parent.persepolis_setting.value('version/version')
def run(self):
try:
# get information dictionary from github
updatesource = requests.get('https://persepolisdm.github.io/version', timeout=5)
updatesource_text = updatesource.text
updatesource_dict = ast.literal_eval(updatesource_text)
# get latest stable version
server_version = updatesource_dict['version']
# Comparison
if float(server_version) > float(self.current_version):
self.NEWVERSIONISAVAILABLESIGNAL.emit(str(server_version))
except Exception as e:
logger.sendToLog("An error occurred while checking for a new release:", 'ERROR')
logger.sendToLog("{}".format(str(e)), 'ERROR')
# This thread checking that which row in download_table highlighted by user
class CheckSelectedRowThread(QThread):
CHECKSELECTEDROWSIGNAL = Signal()
def __init__(self):
QThread.__init__(self)
def run(self):
while shutdown_notification == 0:
sleep(0.2)
self.CHECKSELECTEDROWSIGNAL.emit()
# This thread is getting download information and updating database
class CheckDownloadInfoThread(QThread):
DOWNLOAD_INFO_SIGNAL = Signal(list)
def __init__(self, parent):
QThread.__init__(self)
self.main_window = parent
def run(self):
global checking_flag
global shutdown_notification
while True:
# shutdown_notification = 0 >> persepolis is running
# 1 >> persepolis is ready for closing(closeEvent called)
# 2 >> OK, let's close application!
# checking_flag : 0 >> normal situation ;
# 1 >> remove button or delete button pressed or sorting form viewMenu selected by user ;
# 2 >> check_download_info function is stopping until remove operation done ;
# 3 >> deleteFileAction is done it's job and It is called removeButtonPressed.
# data base is updated one time in five times.
update_data_base_counter = 0
while shutdown_notification != 1:
sleep(0.2)
# if checking_flag is equal to 1, it means that user pressed
# remove or delete button . so checking download information
# must stop until removing is done! It avoids possibility of crashing!
if checking_flag == 1:
# Ok loop is stopped!
checking_flag = 2
# check that when job is done!
while checking_flag != 0:
sleep(0.2)
download_status_list = []
# get download information and append it to download_sessions_list
write_it = 0
for download_session_dict in self.main_window.download_sessions_list:
# get information
returned_dict = download_session_dict['download_session'].tellStatus()
if download_session_dict['download_session'].write_it_to_the_database is False:
download_session_dict['download_session'].write_it_to_the_database = True
write_it += 1
# add gid to download_session_dict
download_status_list.append(returned_dict)
# now we have a list that contains download information (download_status_list)
# lets update download table in main window and update data base!
# first emit a signal for updating MainWindow.
self.DOWNLOAD_INFO_SIGNAL.emit(download_status_list)
# data base is updated 1 time in 5 times.
if update_data_base_counter == 4 or write_it != 0:
self.main_window.persepolis_db.updateDownloadTable(download_status_list)
# data base is updated 1 time in 5 times.
update_data_base_counter = -1
else:
update_data_base_counter = update_data_base_counter + 1
# Ok exit loop! get ready for shutting down!
shutdown_notification = 2
break
# SpiderThread calls spider in spider.py .
# spider finds file size and file name of download file .
# spider works similar to spider in wget.
class SpiderThread(QThread):
SPIDERSIGNAL = Signal(dict)
def __init__(self, add_link_dictionary, parent):
QThread.__init__(self)
self.add_link_dictionary = add_link_dictionary
self.parent = parent
def run(self):
try:
# get file_name and file size with spider
file_name, size = spider.spider(self.add_link_dictionary)
# update data base
dictionary = {'file_name': file_name, 'size': size, 'gid': self.add_link_dictionary['gid']}
self.parent.persepolis_db.updateDownloadTable([dictionary])
# update table in MainWindow
self.SPIDERSIGNAL.emit(dictionary)
except Exception:
# write ERROR message
logger.sendToLog(
"Spider couldn't find download information", "ERROR")
# CheckingThread have 2 duty!
# 1-this class is checking that if user add a link with browsers plugin.
# 2-assume that user executed program before .
# if user is clicking on persepolis icon in menu this tread emits SHOWMAINWINDOWSIGNAL
class CheckingThread(QThread):
CHECKPLUGINDBSIGNAL = Signal()
SHOWMAINWINDOWSIGNAL = Signal()
def __init__(self):
QThread.__init__(self)
def run(self):
global plugin_links_checked
# shutdown_notification = 0 >> persepolis is running
# 1 >> persepolis is ready for closing(closeEvent called)
# 2 >> OK, let's close application!
while shutdown_notification == 0:
sleep(0.2)
# it means , user clicked on persepolis icon and persepolis is
# still running. see persepolis file for more details.
if os.path.isfile(show_window_file):
# OK! we catch notification! remove show_window_file now!
osCommands.remove(show_window_file)
# emit a signal to notify MainWindow for showing itself!
self.SHOWMAINWINDOWSIGNAL.emit()
# It means new browser plugin call is available!
if os.path.isfile(plugin_ready):
# OK! We received notification! remove plugin_ready file
osCommands.remove(plugin_ready)
# When checkPluginCall method considered request , then
# plugin_links_checked is changed to True
plugin_links_checked = False
self.CHECKPLUGINDBSIGNAL.emit() # notifying that we have browser_plugin request
while plugin_links_checked is not True: # wait for persepolis consideration!
sleep(0.5)
# if checking_flag is equal to 1, it means that user pressed remove or delete button or ... .
# so checking download information must be stopped until job is done!
# this thread checks checking_flag and when checking_flag changes to 2
# QTABLEREADY signal is emitted
class WaitThread(QThread):
QTABLEREADY = Signal()
def __init__(self):
QThread.__init__(self)
def run(self):
global checking_flag
checking_flag = 1
while checking_flag != 2:
sleep(0.05)
self.QTABLEREADY.emit()
# button_pressed_counter changed if user pressed move up and move down and ... actions
# this thread is changing checking_flag to zero if button_pressed_counter
# don't change for 2 seconds
class ButtonPressedThread(QThread):
def __init__(self):
QThread.__init__(self)
def run(self):
global checking_flag
current_button_pressed_value = deepcopy(button_pressed_counter) + 1
while current_button_pressed_value != button_pressed_counter:
current_button_pressed_value = deepcopy(button_pressed_counter)
sleep(2)
# job is done!
checking_flag = 0
class ShutDownThread(QThread):
def __init__(self, parent, category, password=None):
QThread.__init__(self)
self.category = category
self.password = password
self.parent = parent
self.crash = False
def run(self):
shutDown(self.parent, category=self.category, password=self.password)
# this thread is keeping system awake! because if system sleeps , then internet connection is disconnected!
# strategy is simple! a loop is checking mouse position every 20 seconds.
# if mouse position didn't change, cursor is moved by QCursor.setPos() (see keepAwake method) ! so this is keeping system awake!
#
class KeepAwakeThread(QThread):
KEEPSYSTEMAWAKESIGNAL = Signal(bool)
def __init__(self):
QThread.__init__(self)
def run(self):
while shutdown_notification == 0:
old_cursor_array = [0, 0]
add = True
while shutdown_notification == 0:
# sleep 20 if persepolis not exited.
for i in range(1, 20):
if shutdown_notification == 0:
sleep(1)
else:
break
# finding cursor position
cursor_position = QCursor.pos()
new_cursor_array = [int(cursor_position.x()), int(cursor_position.y())]
if new_cursor_array == old_cursor_array:
# So cursor position didn't change for 20 second.
if add: # Moving mouse position one time +10 pixel and one time -10 pixel!
self.KEEPSYSTEMAWAKESIGNAL.emit(add)
add = False
else:
self.KEEPSYSTEMAWAKESIGNAL.emit(add)
add = True
old_cursor_array = new_cursor_array
# This thread moves files to another destination.
# see moveSelectedDownloads method for more information.
class MoveThread(QThread):
NOTIFYSENDSIGNAL = Signal(list)
def __init__(self, parent, gid_list, new_folder_path):
QThread.__init__(self)
self.new_folder_path = new_folder_path
self.parent = parent
self.gid_list = gid_list
def run(self):
add_link_dict_list = []
# move selected downloads
# find row number for specific gid
for gid in self.gid_list:
# find download path
dictionary = self.parent.persepolis_db.searchGidInAddLinkTable(gid)
self.old_file_path = dictionary['download_path']
# find file_name
self.file_name = os.path.basename(self.old_file_path)
self.move = osCommands.moveFile(self.old_file_path, self.new_folder_path)
# if moving is not successful, notify user.
if not (self.move):
self.NOTIFYSENDSIGNAL.emit([str(self.file_name), QCoreApplication.translate("mainwindow_src_ui_tr", 'Operation was not successful!'),
5000, 'fail'])
else:
new_file_path = os.path.join(self.new_folder_path, self.file_name)
add_link_dict = {'gid': gid,
'download_path': new_file_path}
# add add_link_dict to add_link_dict_list
add_link_dict_list.append(add_link_dict)
# notify user that job is done!
self.NOTIFYSENDSIGNAL.emit([QCoreApplication.translate("mainwindow_src_ui_tr", "Moving is"),
QCoreApplication.translate("mainwindow_src_ui_tr", 'finished!'),
5000, 'warning'])
# update data base
self.parent.persepolis_db.updateAddLinkTable(add_link_dict_list)
class MainWindow(MainWindow_Ui):
def __init__(self, start_in_tray, persepolis_main, persepolis_setting):
super().__init__(persepolis_setting)
self.persepolis_setting = persepolis_setting
self.persepolis_main = persepolis_main
global icons
icons = ':/' + \
str(self.persepolis_setting.value('settings/icons')) + '/'
# add support for other languages
locale = str(self.persepolis_setting.value('settings/locale'))
QLocale.setDefault(QLocale(locale))
self.translator = QTranslator()
if self.translator.load(':/translations/locales/ui_' + locale, 'ts'):
QCoreApplication.installTranslator(self.translator)
# this variable is changed to True,
# if user highlights multiple items in download_table
self.multi_items_selected = False
# this variable is changed to False when
# user clicks on 'hide options' button in
# side panel.
# see showQueuePanelOptions method for more information.
self.show_queue_panel = True
# system_tray_icon
self.system_tray_icon = QSystemTrayIcon()
self.system_tray_icon.setIcon(
QIcon.fromTheme('persepolis-tray', QIcon(':/persepolis-tray.svg')))
# menu of system tray icon
system_tray_menu = QMenu()
system_tray_menu.addAction(self.addlinkAction)
system_tray_menu.addAction(self.videoFinderAddLinkAction)
system_tray_menu.addAction(self.stopAllAction)
system_tray_menu.addAction(self.addFromClipboardAction)
system_tray_menu.addAction(self.minimizeAction)
system_tray_menu.addAction(self.exitAction)
self.system_tray_icon.setContextMenu(system_tray_menu)
# if system tray icon pressed:
self.system_tray_icon.activated.connect(self.systemTrayPressed)
# show system_tray_icon
self.system_tray_icon.show()
# check trayAction
self.trayAction.setChecked(True)
# check user preference for showing or hiding system_tray_icon
if self.persepolis_setting.value('settings/tray-icon') != 'yes' and start_in_tray is False:
self.minimizeAction.setEnabled(False)
self.trayAction.setChecked(False)
self.system_tray_icon.hide()
# hide MainWindow if start_in_tray is equal to "yes"
if start_in_tray:
self.minimizeAction.setText(QCoreApplication.translate("mainwindow_src_ui_tr", 'Show main Window'))
self.minimizeAction.setIcon(QIcon(icons + 'window'))
# check user preference for showing or hiding menubar.
# (It's not for mac osx or DE that have global menu like kde plasma)
if self.persepolis_setting.value('settings/show-menubar') == 'yes':
self.menubar.show()
self.showMenuBarAction.setChecked(True)
self.toolBar2.hide()
else:
self.menubar.hide()
self.showMenuBarAction.setChecked(False)
self.toolBar2.show()
# In macosx hamburger menu shoud be hidden.
if os_type == OS.OSX:
self.showMenuBarAction.setEnabled(False)
self.toolBar2.hide()
# check user preferences for showing or hiding sidepanel.
if self.persepolis_setting.value('settings/show-sidepanel') == 'yes':
self.category_tree_qwidget.show()
self.showSidePanelAction.setChecked(True)
else:
self.category_tree_qwidget.hide()
self.showSidePanelAction.setChecked(False)
self.checkSelectedRow()
# list of threads
self.threadPool = []
# get execution path information
self.exec_dictionary = getExecPath()
self.exec_file_path = self.exec_dictionary['exec_file_path']
logger.sendToLog("Persepolis path is:\n\t" + self.exec_file_path, "INFO")
if self.exec_dictionary['bundle']:
# Persepolis is run as a bundle.
self.is_bundle = True
logger.sendToLog("Persepolis is run as a bundle.", "INFO")
else:
self.is_bundle = False
if self.exec_dictionary['test']:
# Persepolis is run from test directory.
self.is_test = True
logger.sendToLog("Persepolis is run from test directory.", "INFO")
else:
self.is_test = False
if not (self.is_bundle) and not (self.is_test):
self.is_test = False
logger.sendToLog("Persepolis is run as installed python madule.", "INFO")
# initializing
# create an object for PluginsDB
self.plugins_db = PluginsDB()
# create an object for PersepolisDB
self.persepolis_db = PersepolisDB()
# create an object fo TempDB
self.temp_db = TempDB()
# create tables
self.temp_db.createTables()
# check tables in data_base, and change required values to default value.
# see data_base.py for more information.
self.persepolis_db.setDBTablesToDefaultValue()
# get queues name from data base
queues_list = self.persepolis_db.categoriesList()
# add queues to category_tree(left side panel)
for category_name in queues_list:
new_queue_category = QStandardItem(category_name)
font = QFont()
font.setBold(True)
new_queue_category.setFont(font)
new_queue_category.setEditable(False)
self.category_tree_model.appendRow(new_queue_category)
# read from data base
# add download items to the download_table
# read download items from data base
download_table_dict = self.persepolis_db.returnItemsInDownloadTable()
# read gid_list from date base
category_dict = self.persepolis_db.searchCategoryInCategoryTable('All Downloads')
gid_list = category_dict['gid_list']
keys_list = ['file_name',
'status',
'size',
'downloaded_size',
'percent',
'connections',
'rate',
'estimate_time_left',
'gid',
'link',
'first_try_date',
'last_try_date',
'category'
]
# insert items in download_table
for gid in gid_list:
# create new row
self.download_table.insertRow(0)
dict = download_table_dict[gid]
i = 0
for key in keys_list:
item = QTableWidgetItem(str(dict[key]))
self.download_table.setItem(0, i, item)
i = i + 1
# get video_finder gids
self.all_video_finder_gid_list, self.all_video_finder_video_gid_list, self.all_video_finder_audio_gid_list = self.persepolis_db.returnVideoFinderGids()
# defining some lists and dictionaries for running addlinkwindows and
# propertieswindows and propertieswindows , ...
self.addlinkwindows_list = []
self.propertieswindows_list = []
self.progress_window_list = []
self.afterdownload_list = []
self.text_queue_window_list = []
self.about_window_list = []
self.plugin_queue_window_list = []
self.logwindow_list = []
self.progress_window_list_dict = {}
self.capturekeywindows_list = []
# download_sessions_list contains some dictionaries.
# every dictionary contains GID and session of that download process.
self.download_sessions_list = []
# queue_list_dict contains queue threads >> queue_list_dict[name of queue]
self.queue_list_dict = {}
# this dictionary contains VideoFinder threads
# key = video_gid and value = VideoFinder thread
self.video_finder_threads_dict = {}
# This list contains single video link gids
self.single_video_link_gid_list = []
# CheckDownloadInfoThread
check_download_info = CheckDownloadInfoThread(self)
self.threadPool.append(check_download_info)
self.threadPool[0].start()
self.threadPool[0].DOWNLOAD_INFO_SIGNAL.connect(self.checkDownloadInfo)
# CheckSelectedRowThread
check_selected_row = CheckSelectedRowThread()
self.threadPool.append(check_selected_row)
self.threadPool[1].start()
self.threadPool[1].CHECKSELECTEDROWSIGNAL.connect(
self.checkSelectedRow)
# CheckingThread
check_browser_plugin = CheckingThread()
self.threadPool.append(check_browser_plugin)
self.threadPool[2].start()
self.threadPool[2].CHECKPLUGINDBSIGNAL.connect(self.checkPluginCall)
self.threadPool[2].SHOWMAINWINDOWSIGNAL.connect(self.showMainWindow)
# Checking clipboard
if str(self.persepolis_setting.value('settings/check-clipboard')) == 'yes':
# QApplication.clipboard().dataChanged.connect(self.importLinksFromClipboard)
check_clipboard_thread = CheckClipBoardThread(self)
self.threadPool.append(check_clipboard_thread)
self.threadPool[-1].start()
self.threadPool[-1].CHECKCLIPBOARDSIGNAL.connect(
self.importLinksFromClipboard)
# keepawake
self.ongoing_downloads = 0
keep_awake = KeepAwakeThread()
self.threadPool.append(keep_awake)
self.threadPool[-1].start()
self.threadPool[-1].KEEPSYSTEMAWAKESIGNAL.connect(self.keepAwake)
# this thread checks ffmpeg availability.
# this thread checks ffmpeg and python and pyqt and qt versions and write them in log file.
# this thread writes os type and desktop env. in log file.
check_version_thread = InitializationThread(self)
self.threadPool.append(check_version_thread)
self.threadPool[-1].start()
# finding number or row that user selected!
self.download_table.itemSelectionChanged.connect(self.selectedRow)
# if user doubleclicks on an item in download_table , then openFile
# function executes
self.download_table.itemDoubleClicked.connect(self.openFile)
# connecting queue_panel_show_button to showQueuePanelOptions
self.queue_panel_show_button.clicked.connect(
self.showQueuePanelOptions)
# connecting start_checkBox to startFrame
self.start_checkBox.toggled.connect(self.startFrame)
self.start_checkBox.setChecked(False)
# connecting end_checkBox to endFrame
self.end_checkBox.toggled.connect(self.endFrame)
self.end_checkBox.setChecked(False)
# connecting after_checkBox to afterFrame
self.after_checkBox.toggled.connect(self.afterFrame)
self.after_checkBox.setChecked(False)
# speed limit
self.limit_dial.setValue(10)
self.limit_dial.sliderReleased.connect(self.limitDialIsReleased)
self.limit_dial.valueChanged.connect(self.limitDialIsChanged)
self.limit_label.setText('Speed : Maximum')
# connecting after_pushButton to afterPushButtonPressed
self.after_pushButton.clicked.connect(self.afterPushButtonPressed)
# setting index of all downloads for category_tree
global current_category_tree_index
current_category_tree_index = self.category_tree_model.index(0, 0)
self.category_tree.setCurrentIndex(current_category_tree_index)
# this line set toolBar And Context Menu Items
self.toolBarAndContextMenuItems('All Downloads')
self.category_tree_qwidget.setEnabled(True)
# keep_awake_checkBox
if str(self.persepolis_setting.value('settings/awake')) == 'yes':
self.keep_awake_checkBox.setChecked(True)
else:
self.keep_awake_checkBox.setChecked(False)
self.keep_awake_checkBox.toggled.connect(self.keepAwakeCheckBoxToggled)
self.muxing_pushButton.clicked.connect(self.muxingPushButtonPressed)
# finding windows_size
size = self.persepolis_setting.value(
'MainWindow/size', QSize(930, 554))
position = self.persepolis_setting.value(
'MainWindow/position', QPoint(300, 300))
# setting window size
self.resize(size)
self.move(position)
# download_table column size
# column 0
size = self.persepolis_setting.value(
'MainWindow/column0', '169')
self.download_table.setColumnWidth(0, int(size))
# column 1
size = self.persepolis_setting.value(
'MainWindow/column1', '100')
self.download_table.setColumnWidth(1, int(size))
# column 2
size = self.persepolis_setting.value(
'MainWindow/column2', '200')
self.download_table.setColumnWidth(2, int(size))
# column 3
size = self.persepolis_setting.value(
'MainWindow/column3', '200')
self.download_table.setColumnWidth(3, int(size))
# column 4
size = self.persepolis_setting.value(
'MainWindow/column4', '200')
self.download_table.setColumnWidth(4, int(size))
# column 5
size = self.persepolis_setting.value(
'MainWindow/column5', '100')
self.download_table.setColumnWidth(5, int(size))
# column 6
size = self.persepolis_setting.value(
'MainWindow/column6', '119')
self.download_table.setColumnWidth(6, int(size))
# column 7
size = self.persepolis_setting.value(
'MainWindow/column7', '109')
self.download_table.setColumnWidth(7, int(size))
# column 10
size = self.persepolis_setting.value(
'MainWindow/column10', '120')
self.download_table.setColumnWidth(10, int(size))
# column 11
size = self.persepolis_setting.value(
'MainWindow/column11', '134')
self.download_table.setColumnWidth(11, int(size))
# column 12
size = self.persepolis_setting.value(
'MainWindow/column11', '185')
self.download_table.setColumnWidth(12, int(size))
# check maximizing situation in persepolis_setting
if str(self.persepolis_setting.value('MainWindow/maximized')) == 'yes':
self.showMaximized()
# get columns visibility situation from persepolis_setting
if str(self.persepolis_setting.value('settings/column0')) == 'yes':
self.download_table.setColumnHidden(0, False)
else:
self.download_table.setColumnHidden(0, True)
if str(self.persepolis_setting.value('settings/column1')) == 'yes':
self.download_table.setColumnHidden(1, False)
else:
self.download_table.setColumnHidden(1, True)
if str(self.persepolis_setting.value('settings/column2')) == 'yes':
self.download_table.setColumnHidden(2, False)
else:
self.download_table.setColumnHidden(2, True)
if str(self.persepolis_setting.value('settings/column3')) == 'yes':
self.download_table.setColumnHidden(3, False)
else:
self.download_table.setColumnHidden(3, True)
if str(self.persepolis_setting.value('settings/column4')) == 'yes':
self.download_table.setColumnHidden(4, False)
else:
self.download_table.setColumnHidden(4, True)
if str(self.persepolis_setting.value('settings/column5')) == 'yes':
self.download_table.setColumnHidden(5, False)
else:
self.download_table.setColumnHidden(5, True)
if str(self.persepolis_setting.value('settings/column6')) == 'yes':
self.download_table.setColumnHidden(6, False)
else:
self.download_table.setColumnHidden(6, True)
if str(self.persepolis_setting.value('settings/column7')) == 'yes':
self.download_table.setColumnHidden(7, False)
else:
self.download_table.setColumnHidden(7, True)
if str(self.persepolis_setting.value('settings/column10')) == 'yes':
self.download_table.setColumnHidden(10, False)
else:
self.download_table.setColumnHidden(10, True)
if str(self.persepolis_setting.value('settings/column11')) == 'yes':
self.download_table.setColumnHidden(11, False)
else:
self.download_table.setColumnHidden(11, True)
if str(self.persepolis_setting.value('settings/column12')) == 'yes':
self.download_table.setColumnHidden(12, False)
else:
self.download_table.setColumnHidden(12, True)
icons_size = int(self.persepolis_setting.value('settings/toolbar_icon_size'))
self.toolBar.setIconSize(QSize(icons_size, icons_size))
self.toolBar2.setIconSize(QSize(icons_size, icons_size))
# check reverse_checkBox
self.reverse_checkBox.setChecked(False)
# this method used by shutdown script for changing shutdown_notification value
def changeShutdownValue(self, value):
global shutdown_notification
shutdown_notification = value
# this method used by shutdown script
def returnShutDownValue(self):
return shutdown_notification
# notifySend function uses QSoundEffect for playing notification sounds.
# QSoundEffect plays sound by executing a QThread.
# We can't run a QThread from another QThread in PyQt and PySide.
# So if a QThread want to send a notification,
# it will emit a signal to this method.
def notifySendFromThread(self, signal_list):
notifySend(signal_list[0], signal_list[1], signal_list[2], signal_list[3], parent=self)
# read KeepAwakeThread for more information
def keepAwake(self, add):
# finding cursor position
cursor_position = QCursor.pos()
cursor_array = [int(cursor_position.x()), int(cursor_position.y())]
# check user selected option.
# don't do anything if we haven't any active downloads
if self.persepolis_setting.value('settings/awake') == 'yes' and self.ongoing_downloads != 0:
if add is True and self.keep_awake_checkBox.isChecked() is True: # Moving mouse position one time +1 pixel and one time -1 pixel!
QCursor.setPos(cursor_array[0] + 1, cursor_array[1] + 1)
else:
QCursor.setPos(cursor_array[0] - 1, cursor_array[1] - 1)
# This method notifies user about newer version of Persepolis
def newVersionIsAvailable(self, message):
new_version = str(message)
new_version = new_version[0:-1] + '.' + new_version[-1]
# notify user about newer version
notifySend(QCoreApplication.translate("mainwindow_src_ui_tr", "Version {} is available!".format(new_version)),
QCoreApplication.translate("mainwindow_src_ui_tr", "Please update Persepolis."),
10000, '', parent=self)
# if keep_awake_checkBox toggled by user , this method is called.
def keepAwakeCheckBoxToggled(self, checkbox):
if self.keep_awake_checkBox.isChecked():
self.persepolis_setting.setValue('settings/awake', 'yes')
self.keep_awake_checkBox.setChecked(True)
else:
self.persepolis_setting.setValue('settings/awake', 'no')
self.keep_awake_checkBox.setChecked(False)
self.persepolis_setting.sync()
# this method updates download_table in MainWindow
#
# download_table_header = ['File Name', 'Status', 'Size', 'Downloaded', 'Percentage', 'Connections',
# 'Transfer rate', 'Estimated time left', 'Gid', 'Link', 'First try date', 'Last try date', 'Category']
def checkDownloadInfo(self, list):
# number of ongoing downloads.
# this variable helps keepAwake method.
self.ongoing_downloads = len(list)
systemtray_tooltip_text = 'Persepolis Download Manager'
for download_status_dict in list:
gid = download_status_dict['gid']
status = download_status_dict['status']
if status == 'complete' or status == 'error' or status == 'stopped':
# eliminate gid from active_downloads in data base
temp_dict = {'gid': gid,
'status': 'deactive'}
self.temp_db.updateSingleTable(temp_dict)
# add download percent to the tooltip text for persepolis system tray icon
try:
if status == 'downloading' and download_status_dict['percent'] != '0%':
system_tray_file_name = download_status_dict['file_name']
if len(system_tray_file_name) > 20:
system_tray_file_name = system_tray_file_name[0:19] + '...'
systemtray_tooltip_text = systemtray_tooltip_text + '\n'\
+ system_tray_file_name + ': '\
+ download_status_dict['percent']
except Exception:
pass
# Is the link related to VideoFinder?
video_finder_link = False
if gid in self.all_video_finder_gid_list:
video_finder_dictionary = self.persepolis_db.searchGidInVideoFinderTable(gid)
video_finder_link = True
if video_finder_dictionary['video_gid'] in self.video_finder_threads_dict.keys():
video_finder_thread = self.video_finder_threads_dict[video_finder_dictionary['video_gid']]
# is gid related to video? or audio
if gid == video_finder_dictionary['video_gid']:
video_finder_video_gid = True
else:
video_finder_video_gid = False
# if download is completed update video finder data base
if status == 'complete':
if video_finder_video_gid:
video_finder_dictionary['video_completed'] = 'yes'
video_finder_thread.video_completed = 'yes'
else:
video_finder_dictionary['audio_completed'] = 'yes'
video_finder_thread.audio_completed = 'yes'
# update data base
self.persepolis_db.updateVideoFinderTable([video_finder_dictionary])
# if download stopped, VideoFinder must be notified. so update data base.
if video_finder_dictionary['checking'] == 'yes' and (status == 'error' or status == 'stopped'):
video_finder_dictionary['checking'] = 'no'
video_finder_thread.checking = 'no'
# update data base
self.persepolis_db.updateVideoFinderTable([video_finder_dictionary])
else:
video_finder_link = False
# If link is single_video_link
if gid in self.single_video_link_gid_list:
single_video_link = True
else:
single_video_link = False
if status == 'error':
# check free space in download_folder
# perhaps insufficient space in hard disk caused this error!
# find free space in KiB
# find download path
dictionary = self.persepolis_db.searchGidInAddLinkTable(gid)
download_path = dictionary['download_path']
free_space = freeSpace(download_path)
# find file size
file_size = download_status_dict['size']
if file_size is not None:
if file_size[-2:] != ' B':
unit = file_size[-3:]
try:
if unit == 'TiB' or unit == 'GiB':
size_value = float(file_size[:-4])
else:
size_value = int(file_size[:-4])
except ValueError:
size_value = None
else:
unit = None
try:
size_value = int(file_size)
except ValueError:
size_value = None
if free_space is not None and size_value is not None:
if unit == 'TiB':
free_space = free_space / (1073741824 * 1024)
free_space = round(free_space, 2)
elif unit == 'GiB':
free_space = free_space / 1073741824
free_space = round(free_space, 2)
elif unit == 'MiB':
free_space = int(free_space / 1048576)
elif unit == 'KiB':
free_space = int(free_space / 1024)
else:
free_space = int(free_space)
if free_space < size_value:
error = 'Insufficient disk space!'
# write error_message in log file
error_message = 'Download failed - GID : '\
+ str(gid)\
+ '- Message : '\
+ error
logger.sendToLog(error_message, 'DOWNLOAD ERROR')
# show notification
notifySend(QCoreApplication.translate("mainwindow_src_ui_tr", "Error: ") + error,
QCoreApplication.translate("mainwindow_src_ui_tr",
'There is not enough disk space available at the download folder! Please choose another one or clear some space.'),
10000, 'fail', parent=self)
# find row of this gid in download_table!
row = None
for i in range(self.download_table.rowCount()):
row_gid = self.download_table.item(i, 8).text()
if gid == row_gid:
row = i
break
# update download_table items
if row is not None:
update_list = [download_status_dict['file_name'], download_status_dict['status'], download_status_dict['size'], download_status_dict['downloaded_size'], download_status_dict['percent'],
download_status_dict['connections'], download_status_dict['rate'], download_status_dict['estimate_time_left'], download_status_dict['gid'], None, None, None, None]
for i in range(12):
# update download_table cell if update_list item in not None
if update_list[i]:
text = update_list[i]
else:
text = self.download_table.item(row, i).text()
# create a QTableWidgetItem
item = QTableWidgetItem(text)
# set item
try:
self.download_table.setItem(row, i, item)
except Exception as problem:
logger.sendToLog(
"Error occurred while updating download table", "ERROR")
logger.sendToLog(problem, "ERROR")
# update download_table (refreshing!)
self.download_table.viewport().update()
# update progresswindow labels
# check that any progress_window is available for this gid or not!
if gid in self.progress_window_list_dict.keys():
# find progress_window for this gid
member_number = self.progress_window_list_dict[gid]
progress_window = self.progress_window_list[member_number]
# if link is related to video finder
if video_finder_link:
# download percent
value = download_status_dict['percent']
if not (value):
value = '0%'
if video_finder_dictionary['video_completed'] == 'yes':
video_status = 'Completed'
elif video_finder_video_gid:
video_status = value + ' downloaded'
else:
video_status = 'Not completed'
video_status = QCoreApplication.translate("video_finder_progress_ui_tr", "Video file status: ")\
+ video_status
progress_window.video_status_label.setText(video_status)
if video_finder_dictionary['audio_completed'] == 'yes':
audio_status = 'Completed'
elif not (video_finder_video_gid):
audio_status = value + ' downloaded'
else:
audio_status = 'Not completed'
audio_status = QCoreApplication.translate("video_finder_progress_ui_tr", "Audio file status: ")\
+ audio_status
progress_window.audio_status_label.setText(audio_status)
if video_finder_dictionary['video_completed'] == 'yes' and video_finder_dictionary['audio_completed'] == 'yes':
muxing_status = 'Started!'
else:
muxing_status = 'Not started!'
muxing_status = QCoreApplication.translate("video_finder_progress_ui_tr", "Muxing status: ")\
+ muxing_status
progress_window.muxing_status_label.setText(muxing_status)
# tell to progress_window what gid is in progress
progress_window.gid = gid
# link
link = QCoreApplication.translate("mainwindow_src_ui_tr", "Link: ") + str(download_status_dict['link'])
progress_window.link_label.setText(link)
progress_window.link_label.setToolTip(link)
# downloaded
downloaded_size = download_status_dict['downloaded_size']
if downloaded_size is None:
downloaded_size = 'None'
file_size = download_status_dict['size']
if file_size is None:
file_size = 'None'
if file_size != ' ':
if video_finder_link or single_video_link:
downloaded = QCoreApplication.translate("mainwindow_src_ui_tr", "Downloaded/Est. file size: ") \
+ str(downloaded_size) \
+ "/" \
+ str(file_size)
else:
downloaded = QCoreApplication.translate("mainwindow_src_ui_tr", "Downloaded: ") \
+ str(downloaded_size) \
+ "/" \
+ str(file_size)
else:
downloaded = QCoreApplication.translate("mainwindow_src_ui_tr", "Downloaded: ") \
+ str(downloaded_size) \
progress_window.downloaded_label.setText(downloaded)
# Transfer rate
rate = QCoreApplication.translate("mainwindow_src_ui_tr", "Transfer rate: ") \
+ str(download_status_dict['rate'])
progress_window.rate_label.setText(rate)
# Estimate time left
estimate_time_left = QCoreApplication.translate("mainwindow_src_ui_tr", "Estimated time left: ") \
+ str(download_status_dict['estimate_time_left'])
progress_window.time_label.setText(estimate_time_left)
# Connections
if video_finder_link or single_video_link:
connections = QCoreApplication.translate("mainwindow_src_ui_tr", "Fragments: ") \
+ str(download_status_dict['connections'])
else:
connections = QCoreApplication.translate("mainwindow_src_ui_tr", "Connections: ") \
+ str(download_status_dict['connections'])
progress_window.connections_label.setText(connections)
# progressbar
value = download_status_dict['percent']
file_name = str(download_status_dict['file_name'])
try:
value = int(value[:-1])
except ValueError:
value = 0
if value == 0 and downloaded_size != 0:
# show busy indicator
progress_window.download_progressBar.showBusyIndicator()
if file_name != "***":
windows_title = str(file_name)
progress_window.setWindowTitle(windows_title)
else:
progress_window.download_progressBar.setValueSmoothly(value)
if file_name != "***":
windows_title = '(' + str(value) + '%)' + str(file_name)
progress_window.setWindowTitle(windows_title)
# status
progress_window.status = str(download_status_dict['status'])
status = QCoreApplication.translate("mainwindow_src_ui_tr", "Status: ") + progress_window.status
progress_window.status_label.setText(status)
# activate/deactivate progress_window buttons according to status
if progress_window.status == "downloading":
progress_window.resume_pushButton.setEnabled(False)
progress_window.stop_pushButton.setEnabled(True)
progress_window.pause_pushButton.setEnabled(True)
elif progress_window.status == "paused":
progress_window.resume_pushButton.setEnabled(True)
progress_window.stop_pushButton.setEnabled(True)
progress_window.pause_pushButton.setEnabled(False)
elif progress_window.status == "waiting":
progress_window.resume_pushButton.setEnabled(False)
progress_window.stop_pushButton.setEnabled(True)
progress_window.pause_pushButton.setEnabled(False)
elif progress_window.status == "scheduled":
progress_window.resume_pushButton.setEnabled(False)
progress_window.stop_pushButton.setEnabled(True)
progress_window.pause_pushButton.setEnabled(False)
# if download stopped:
elif progress_window.status == "stopped":
# write message in log
stop_message = 'Download stopped - GID : '\
+ str(gid)
logger.sendToLog(stop_message, 'DOWNLOADS')
# show notification
notifySend(QCoreApplication.translate("mainwindow_src_ui_tr", "Download Stopped"),
str(download_status_dict['file_name']), 10000, 'no', parent=self)
# close progress_window
progress_window.close()
# eliminate window information from progress_window_list_dict
del self.progress_window_list_dict[gid]
# if download status is error!
elif progress_window.status == "error":
# get error message from dict
if 'error' in download_status_dict.keys():
error = download_status_dict['error']
else:
error = 'Error'
# write error_message in log file
error_message = 'Download failed - GID : '\
+ str(gid)\
+ '- Message : '\
+ error
logger.sendToLog(error_message, 'DOWNLOAD ERROR')
# show notification
notifySend(QCoreApplication.translate("mainwindow_src_ui_tr", "Error - ") + error, str(download_status_dict['file_name']),
10000, 'fail', parent=self)
# close progress_window
progress_window.close()
# eliminate window information from progress_window_list_dict
del self.progress_window_list_dict[gid]
elif progress_window.status == "complete":
# close progress_window if download status is stopped or
# completed or error
# if window is related to video finder and download is completed, don't close window
if (video_finder_link is True):
# disable stop and pause and push buttons
progress_window.resume_pushButton.setEnabled(False)
progress_window.stop_pushButton.setEnabled(False)
progress_window.pause_pushButton.setEnabled(False)
else:
progress_window.close()
# eliminate window information from progress_window_list_dict
del self.progress_window_list_dict[gid]
# write message in log file
complete_message = 'Download complete - GID : '\
+ str(gid)
logger.sendToLog(complete_message, 'DOWNLOADS')
# play notification
notifySend(QCoreApplication.translate("mainwindow_src_ui_tr", "Download Complete"),
download_status_dict['file_name'], 10000, 'ok', parent=self)
# sync persepolis_setting before checking!
self.persepolis_setting.sync()
# check user's Preferences
if self.persepolis_setting.value('settings/after-dialog') == 'yes':
# show download complete dialog
afterdownloadwindow = AfterDownloadWindow(
self, download_status_dict, self.persepolis_setting)
self.afterdownload_list.append(afterdownloadwindow)
self.afterdownload_list[-1].show()
# bringing AfterDownloadWindow on top
self.afterdownload_list[-1].raise_()
self.afterdownload_list[-1].activateWindow()
# it means download has finished!
# lets do finishing jobs!
if progress_window.status == "stopped" or progress_window.status == "error" or progress_window.status == "complete":
# set "None" for start_time and end_time and after_download value
# in data_base, because download has finished
self.persepolis_db.setDefaultGidInAddlinkTable(
gid=gid, start_time=True, end_time=True, after_download=True)
# THIS PART IS NOT RELATED TO VIDEO FINDER LINKS
# if user selects shutdown option for after download progress
# value of 'shutdown' in data base will changed to 'wait' for this category
# (see ShutDownThread and shutdown.py for more information)
# shutDown method will check that value in a loop.
# when "wait" changes to "shutdown" then shutdown.py script
# will shut down the system
shutdown_dict = self.temp_db.returnGid(gid)
# get shutdown value for this gid from data base
shutdown_status = shutdown_dict['shutdown']
# if status is complete or error, and user selected "shutdown after download" option:
if shutdown_status == 'wait':
# send notification
notifySend(QCoreApplication.translate("mainwindow_src_ui_tr", 'Persepolis is shutting down'),
QCoreApplication.translate("mainwindow_src_ui_tr", 'your system in a minute'),
15000, '', parent=self)
# write "shutdown" message in data base for this gid >> Shutdown system!
shutdown_dict = {'gid': gid, 'shutdown': 'shutdown'}
self.temp_db.updateSingleTable(shutdown_dict)
# set tooltip for system_tray_icon
self.system_tray_icon.setToolTip(systemtray_tooltip_text)
# drag and drop for links
def dragEnterEvent(self, droplink):
text = str(droplink.mimeData().text())
if ("tp:/" in text[2:6]) or ("tps:/" in text[2:7]):
droplink.accept()
else:
droplink.ignore()
def dropEvent(self, droplink):
link_clipboard = QApplication.clipboard()
link_clipboard.clear(mode=link_clipboard.Clipboard)
link_string = droplink.mimeData().text()
link_clipboard.setText(str(link_string), mode=link_clipboard.Clipboard)
self.addLinkButtonPressed(button=link_clipboard)
# persepolis identifies each download by the ID called GID.
# The GID must be hex string of 16 characters,
# thus [0-9a-zA-Z] are allowed and leading zeros must
# not be stripped. The GID all 0 is reserved and must
# not be used. The GID must be unique, otherwise error
# is reported and the download is not added.
# gidGenerator generates GID for downloads
def gidGenerator(self):
# this loop repeats until we have a unique GID
while True:
# generate a random hex value between 1152921504606846976 and 18446744073709551615
# for download GID
my_gid = hex(random.randint(1152921504606846976, 18446744073709551615))
my_gid = my_gid[2:18]
my_gid = str(my_gid)
# check my_gid used before or not!
category_dict = self.persepolis_db.searchCategoryInCategoryTable('All Downloads')
gid_list = category_dict['gid_list']
if not (my_gid in gid_list):
break
return my_gid
# this method returns index of all selected rows in list format
def userSelectedRows(self):
try:
# Find selected rows
rows_list = []
rows_index = self.download_table.selectionModel().selectedRows()
for index in rows_index:
rows_list.append(index.row())
# sort list by number
rows_list.sort()
except Exception:
rows_list = []
return rows_list
# this method returns number of selected row
# if user selected one row!
def selectedRow(self):
rows_list = self.userSelectedRows()
if len(rows_list) == 0:
return None
else:
return rows_list[0]
# this method activates/deactivates QActions according to selected row!
def checkSelectedRow(self):
rows_list = self.userSelectedRows()
# check if user selected multiple items
if len(rows_list) <= 1:
multi_items_selected = False
else:
multi_items_selected = True
# if any thing changed ...
if (multi_items_selected and not (self.multi_items_selected)) or (not (multi_items_selected) and self.multi_items_selected):
if multi_items_selected:
self.multi_items_selected = True
else:
self.multi_items_selected = False
self.selectDownloads()
if len(rows_list) != 0:
selected_row_return = rows_list[0]
status = self.download_table.item(selected_row_return, 1).text()
category = self.download_table.item(selected_row_return, 12).text()
link = self.download_table.item(selected_row_return, 9).text()
self.statusbar.showMessage(str(link))
self.removeSelectedAction.setEnabled(True)
self.deleteSelectedAction.setEnabled(True)
if category == 'Single Downloads':
if status == "scheduled":
self.resumeAction.setEnabled(False)
self.pauseAction.setEnabled(False)
self.stopAction.setEnabled(True)
self.propertiesAction.setEnabled(False)
self.progressAction.setEnabled(True)
self.openDownloadFolderAction.setEnabled(False)
self.openFileAction.setEnabled(False)
self.moveSelectedDownloadsAction.setEnabled(False)
elif status == "stopped" or status == "error":
self.stopAction.setEnabled(False)
self.pauseAction.setEnabled(False)
self.resumeAction.setEnabled(True)
self.propertiesAction.setEnabled(True)
self.progressAction.setEnabled(False)
self.openDownloadFolderAction.setEnabled(False)
self.openFileAction.setEnabled(False)
self.moveSelectedDownloadsAction.setEnabled(False)
elif status == "downloading":
self.resumeAction.setEnabled(False)
self.pauseAction.setEnabled(True)
self.stopAction.setEnabled(True)
self.propertiesAction.setEnabled(False)
self.progressAction.setEnabled(True)
self.openDownloadFolderAction.setEnabled(False)
self.openFileAction.setEnabled(False)
self.moveSelectedDownloadsAction.setEnabled(False)
elif status == "waiting":
self.stopAction.setEnabled(True)
self.resumeAction.setEnabled(False)
self.pauseAction.setEnabled(False)
self.propertiesAction.setEnabled(False)
self.progressAction.setEnabled(True)
self.openDownloadFolderAction.setEnabled(False)
self.openFileAction.setEnabled(False)
self.moveSelectedDownloadsAction.setEnabled(False)
elif status == "complete":
self.stopAction.setEnabled(False)
self.resumeAction.setEnabled(False)
self.pauseAction.setEnabled(False)
self.propertiesAction.setEnabled(True)
self.progressAction.setEnabled(False)
self.openDownloadFolderAction.setEnabled(True)
self.openFileAction.setEnabled(True)
self.moveSelectedDownloadsAction.setEnabled(True)
elif status == "paused":
self.stopAction.setEnabled(True)
self.resumeAction.setEnabled(True)
self.pauseAction.setEnabled(False)
self.propertiesAction.setEnabled(False)
self.progressAction.setEnabled(True)
self.openDownloadFolderAction.setEnabled(False)
self.openFileAction.setEnabled(False)
self.moveSelectedDownloadsAction.setEnabled(False)
else:
self.progressAction.setEnabled(False)
self.resumeAction.setEnabled(False)
self.stopAction.setEnabled(False)
self.pauseAction.setEnabled(False)
self.propertiesAction.setEnabled(False)
self.openDownloadFolderAction.setEnabled(False)
self.openFileAction.setEnabled(False)
self.moveSelectedDownloadsAction.setEnabled(False)
else:
self.resumeAction.setEnabled(True)
self.pauseAction.setEnabled(True)
self.stopAction.setEnabled(True)
if status == 'complete':
self.propertiesAction.setEnabled(True)
self.progressAction.setEnabled(False)
self.openDownloadFolderAction.setEnabled(True)
self.openFileAction.setEnabled(True)
self.moveSelectedDownloadsAction.setEnabled(True)
elif status == "stopped" or status == "error":
self.propertiesAction.setEnabled(True)
self.progressAction.setEnabled(False)
self.openDownloadFolderAction.setEnabled(False)
self.openFileAction.setEnabled(False)
self.moveSelectedDownloadsAction.setEnabled(False)
elif status == "scheduled" or status == "downloading" or status == "paused" or status == "waiting":
self.propertiesAction.setEnabled(False)
self.progressAction.setEnabled(False)
self.openDownloadFolderAction.setEnabled(False)
self.openFileAction.setEnabled(False)
self.moveSelectedDownloadsAction.setEnabled(False)
# video_finder_widget
# hide video_finder_widget if selected item is not related to video finder
# disable pauseAction for video finder links
if not (self.multi_items_selected):
gid = self.download_table.item(selected_row_return, 8).text()
if gid in self.all_video_finder_gid_list:
# show widget
self.video_finder_widget.show()
# disable pauseAction
self.pauseAction.setEnabled(False)
# gid is related to audio or video?!
if gid in self.all_video_finder_video_gid_list:
video_gid = gid
# set video_label
# get video download's percentage
self.video_label.setText(
QCoreApplication.translate("mainwindow_ui_tr", "Video file status: ")
+ self.download_table.item(selected_row_return, 4).text()
+ QCoreApplication.translate("mainwindow_ui_tr", " downloaded"))
# find audio information
# find row of audio_gid in download_table!
audio_gid = self.all_video_finder_audio_gid_list[self.all_video_finder_video_gid_list.index(
gid)]
row = None
for i in range(self.download_table.rowCount()):
row_gid = self.download_table.item(i, 8).text()
if audio_gid == row_gid:
row = i
break
# set audio_label
# get audio download's percentage
self.audio_label.setText(
QCoreApplication.translate("mainwindow_ui_tr", "Audio file status: ")
+ self.download_table.item(row, 4).text()
+ QCoreApplication.translate("mainwindow_ui_tr", " downloaded"))
else:
# set audio_label
# get audio download's percentage
self.audio_label.setText(
QCoreApplication.translate("mainwindow_ui_tr", "Audio file status: ")
+ self.download_table.item(selected_row_return, 4).text()
+ QCoreApplication.translate("mainwindow_ui_tr", " downloaded"))
# find video information
video_gid = self.all_video_finder_video_gid_list[self.all_video_finder_audio_gid_list.index(
gid)]
# find video row
row = None
for i in range(self.download_table.rowCount()):
row_gid = self.download_table.item(i, 8).text()
if video_gid == row_gid:
row = i
break
# set video_label
# get video download's percentage
self.video_label.setText(
QCoreApplication.translate("mainwindow_ui_tr", "Video file status: ")
+ self.download_table.item(row, 4).text()
+ QCoreApplication.translate("mainwindow_ui_tr", " downloaded"))
# set activity status and muxing status
# show/hide muxing_pushButton
if video_gid in self.video_finder_threads_dict.keys():
# find thread
video_finder_thread = self.video_finder_threads_dict[video_gid]
# check activity
if video_finder_thread.active == 'yes':
video_finder_status = QCoreApplication.translate('mainwindow_ui_tr', 'Active')
# hide muxing_pushButton
self.muxing_pushButton.hide()
else:
video_finder_status = QCoreApplication.translate('mainwindow_ui_tr', 'Not Active')
if video_finder_thread.video_completed == 'yes' and video_finder_thread.audio_completed == 'yes':
# show muxing_pushButton
self.muxing_pushButton.show()
# check muxing status
muxing = video_finder_thread.muxing
if muxing == 'no':
muxing_status = QCoreApplication.translate('mainwindow_ui_tr', 'Not Active')
elif muxing == 'started':
muxing_status = QCoreApplication.translate('mainwindow_ui_tr', 'Started')
elif muxing == 'error':
muxing_status = QCoreApplication.translate('mainwindow_ui_tr', 'Error')
elif muxing == 'complete':
muxing_status = QCoreApplication.translate('mainwindow_ui_tr', 'Complete')
else:
video_finder_status = QCoreApplication.translate('mainwindow_ui_tr', 'Not Active')
muxing_status = QCoreApplication.translate('mainwindow_ui_tr', 'Not Active')
if self.download_table.item(selected_row_return, 1).text() == 'complete' and self.download_table.item(row, 1).text() == 'complete':
# show muxing_pushButton
self.muxing_pushButton.show()
else:
# hide muxing_pushButton
self.muxing_pushButton.hide()
# set labels
self.video_finder_status_label.setText(
QCoreApplication.translate("mainwindow_ui_tr", "Status: ") + video_finder_status)
self.muxing_status_label.setText(
QCoreApplication.translate("mainwindow_ui_tr", "Muxing status: ") + muxing_status)
else:
# hide video_finder_widget
self.video_finder_widget.hide()
else:
# hide video_finder_widget
self.video_finder_widget.hide()
else:
self.progressAction.setEnabled(False)
self.resumeAction.setEnabled(False)
self.stopAction.setEnabled(False)
self.pauseAction.setEnabled(False)
self.propertiesAction.setEnabled(False)
self.openDownloadFolderAction.setEnabled(False)
self.openFileAction.setEnabled(False)
self.moveSelectedDownloadsAction.setEnabled(False)
# hide video_finder_widget
self.video_finder_widget.hide()
# Check if this link is related to video finder or not
def checkVideoFinderSupportedSites(self, link):
# add your favorite site in this list
# please don't add porn sites!
supported_sites_list = [
'youtube.com/watch',
'aparat.com/v/',
'vimeo.com/',
'dailymotion.com/video',
'https://soundcloud.com/'
]
video_finder_supported = False
for supported_site in supported_sites_list:
if supported_site in link:
video_finder_supported = True
break
return video_finder_supported
# when user requests calls persepolis with browser plugin,
# this method is called by CheckingThread.
def checkPluginCall(self):
global plugin_links_checked
# get new links from plugins_db
list_of_links = self.plugins_db.returnNewLinks()
# notify that job is done!and new links can be received form plugins_db
plugin_links_checked = True
not_video_finder_links = [] # Store non-video_finder links to process normally.
# get maximum of youtube,... link from persepolis_setting
max_links = int(self.persepolis_setting.value('settings/video_finder/max_links', 3))
for link in list_of_links:
video_finder_supported = self.checkVideoFinderSupportedSites(link['link'])
# if link is on of supported_sites_list member, the open video_finder_addlink_window
if max_links and video_finder_supported:
max_links = max_links - 1
self.showVideoFinderAddLinkWindow(input_dict=link)
else:
# if link is not on of supported_sites_list then add it to not_video_finder_links
not_video_finder_links.append(link)
# video_finder links also will stay here, those coming after specified max.
list_of_links = not_video_finder_links
# It means we have only one link in list_of_links
if len(list_of_links) == 1:
# this line calls pluginAddLink method and send a dictionary that contains
# link information
if str(self.persepolis_setting.value('settings/dont-show-addlinkwindow')) == 'yes':
# When a download request is sent from the browser extension,
# the download will start without showing the Add Link window.
# add default values to add_link_dictionary
for key in ['start_time', 'end_time', 'ip', 'port', 'proxy_user', 'proxy_passwd', 'proxy_type', 'download_user', 'download_passwd']:
list_of_links[0][key] = None
list_of_links[0]['connections'] = int(self.persepolis_setting.value('settings/connections'))
list_of_links[0]['limit_value'] = 0
list_of_links[0]['download_path'] = str(self.persepolis_setting.value('settings/download_path'))
# Call callBack methods instead of pluginAddLink method.
# In this case, the download will start without showing the add link window.
self.callBack(list_of_links[0], False, 'Single Downloads')
else:
self.pluginAddLink(list_of_links[0])
elif len(list_of_links): # we have queue request from browser plugin # Length non-zero
self.pluginQueue(list_of_links)
# this method creates an addlinkwindow when user calls Persepolis with
# browsers plugin (Single Download)
def pluginAddLink(self, add_link_dictionary):
# create an object for AddLinkWindow and add it to addlinkwindows_list.
addlinkwindow = AddLinkWindow(self, self.callBack, self.persepolis_setting, add_link_dictionary)
self.addlinkwindows_list.append(addlinkwindow)
self.addlinkwindows_list[-1].show()
# bring addlinkwindow on top
self.addlinkwindows_list[-1].raise_()
self.addlinkwindows_list[-1].activateWindow()
# This method creates addlinkwindow when user presses plus button in MainWindow
def addLinkButtonPressed(self, button=None):
addlinkwindow = AddLinkWindow(self, self.callBack, self.persepolis_setting, plugin_add_link_dictionary={})
self.addlinkwindows_list.append(addlinkwindow)
self.addlinkwindows_list[-1].show()
# callback of AddLinkWindow
def callBack(self, add_link_dictionary, download_later, category, single_video_link=False):
exists = self.persepolis_db.searchLinkInAddLinkTable(add_link_dictionary['link'])
if exists:
self.msgBox = QMessageBox()
self.msgBox.setText(QCoreApplication.translate("mainwindow_src_ui_tr", "
This link has been added before!\
Are you sure you want to add it again?
"))
self.msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
self.msgBox.setIcon(QMessageBox.Warning)
reply = self.msgBox.exec_()
# do nothing if user clicks NO
if reply != QMessageBox.Yes:
return
category = str(category)
# Persepolis identifies each download by the ID called GID. The GID must be
# hex string of 16 characters.
# if user presses ok button on add link window , a gid generates for download.
gid = self.gidGenerator()
# add gid to add_link_dictionary
add_link_dictionary['gid'] = gid
# download_info_file_list is a list that contains ['file_name' ,
# 'status' , 'size' , 'downloaded size' ,'download percentage' ,
# 'number of connections' ,'Transfer rate' , 'estimate_time_left' ,
# 'gid' , 'link' , 'first_try_date' , 'last_try_date', 'category']
# if user or browser_plugin defined filename then file_name is valid in
# add_link_dictionary['out']
if add_link_dictionary['out']:
file_name = add_link_dictionary['out']
# if file extension is m3u8 so it's single_video_link
file_name_split = file_name.split('.')
file_extension = file_name_split[-1]
# convert extension letters to lower case
# for example "JPG" will be converted in "jpg"
file_extension = file_extension.lower()
if file_extension == 'm3u8':
single_video_link = True
else:
file_name = '***'
# If user selected a queue in add_link window , then download must be
# added to queue and and download must be started with queue so >>
# download_later = True
if str(category) != 'Single Downloads':
download_later = True
if not (download_later):
status = 'waiting'
else:
status = 'stopped'
# get now time and date
date = nowDate()
download_table_dict = {'file_name': file_name,
'status': status,
'size': '***',
'downloaded_size': '***',
'percent': '***',
'connections': '***',
'rate': '***',
'estimate_time_left': '***',
'gid': gid,
'link': add_link_dictionary['link'],
'first_try_date': date,
'last_try_date': date,
'category': category}
# write information in data_base
self.persepolis_db.insertInDownloadTable([download_table_dict])
self.persepolis_db.insertInAddLinkTable([add_link_dictionary])
# find selected category in left side panel
for i in range(self.category_tree_model.rowCount()):
category_tree_item_text = str(
self.category_tree_model.index(i, 0).data())
if category_tree_item_text == category:
category_index = i
break
# highlight selected category in category_tree
category_tree_model_index = self.category_tree_model.index(
category_index, 0)
current_category_tree_text = current_category_tree_index.data()
self.category_tree.setCurrentIndex(category_tree_model_index)
if current_category_tree_text != category:
self.categoryTreeSelected(category_tree_model_index)
else:
# create a row in download_table for new download
download_table_list = [file_name, status, '***', '***', '***',
'***', '***', '***', gid, add_link_dictionary['link'], date, date, category]
self.download_table.insertRow(0)
j = 0
# add item in list to the row
for i in download_table_list:
item = QTableWidgetItem(i)
self.download_table.setItem(0, j, item)
j = j + 1
if single_video_link:
# create an item in data_base
# this item will updated by yt-dlp
# and contains download information.
video_Finder2_data_base = {'gid': gid,
'download_status': status,
'file_name': file_name,
'eta': '0',
'download_speed_str': '0',
'downloaded_size': 0,
'file_size': 0,
'download_percent': 0,
'fragments': '0/0',
'error_message': ''}
# write it in data_base
self.persepolis_db.insertInVideoFinderTable2([video_Finder2_data_base])
# if user didn't press download_later_pushButton in add_link window
# then create new qthread for new download!
if not (download_later):
if not (single_video_link):
# create download_session
download_session = persepolis_lib_prime.Download(add_link_dictionary, self, gid)
# add download_session and gid to download_session_dict
download_session_dict = {'gid': gid,
'download_session': download_session}
# append download_session_dict to download_sessions_list
self.download_sessions_list.append(download_session_dict)
# strat download in thread
new_download = DownloadLink(gid, download_session, self)
self.threadPool.append(new_download)
self.threadPool[-1].start()
else:
# start video downloading
# get add_link_dictionary for video
add_link_dictionary = self.persepolis_db.searchGidInAddLinkTable(gid)
# create download_session
video_download_session = ytdlp_downloader.Ytdp_Download(add_link_dictionary, self, gid, single_video_link=True)
# add gid to single_video_link_gid_list
self.single_video_link_gid_list.append(gid)
# add download_session and gid to download_session_dict
download_session_dict = {'gid': gid,
'download_session': video_download_session}
# append download_session_dict to download_sessions_list
self.download_sessions_list.append(download_session_dict)
# strat download in thread
new_download = DownloadLink(gid, video_download_session, self)
self.threadPool.append(new_download)
self.threadPool[-1].start()
# open progress window for download.
self.progressBarOpen(gid)
# notify user
# check that download scheduled or not
if not (add_link_dictionary['start_time']):
message = QCoreApplication.translate("mainwindow_src_ui_tr", "Download Starts")
else:
# get download information with spider.
new_spider = SpiderThread(add_link_dictionary, self)
self.threadPool.append(new_spider)
self.threadPool[-1].start()
self.threadPool[-1].SPIDERSIGNAL.connect(self.spiderUpdate)
message = QCoreApplication.translate("mainwindow_src_ui_tr", "Download Scheduled")
notifySend(message, '', 10000, 'no', parent=self)
else:
# get download information with spider.
new_spider = SpiderThread(add_link_dictionary, self)
self.threadPool.append(new_spider)
self.threadPool[-1].start()
self.threadPool[-1].SPIDERSIGNAL.connect(self.spiderUpdate)
# when user presses resume button this method is called
def resumeButtonPressed(self, button=None):
# disable the button
self.resumeAction.setEnabled(False)
# find user's selected row
selected_row_return = self.selectedRow()
if selected_row_return is not None:
# find download category
category = self.download_table.item(selected_row_return, 12).text()
# if category is not "single downloads" , then send notification for error
if category != "Single Downloads":
notifySend(QCoreApplication.translate("mainwindow_src_ui_tr", "Operation was not successful."),
QCoreApplication.translate("mainwindow_src_ui_tr",
"Please resume the following category: ") + category,
10000, 'fail', parent=self)
return
# find download gid
gid = self.download_table.item(selected_row_return, 8).text()
download_status = self.download_table.item(
selected_row_return, 1).text()
# this 'if' checks status of download before resuming! If download status
# is 'paused' then download must be resumed and if status isn't 'paused' new
# download thread must be created !
if download_status == "paused":
# search gid in download_sessions_list
for download_session_dict in self.download_sessions_list:
if download_session_dict['gid'] == gid:
# unpause download
download_session_dict['download_session'].downloadUnpause()
break
else:
# check if the gid is related to video finder
if gid in self.all_video_finder_gid_list:
result_dictionary = self.persepolis_db.searchGidInVideoFinderTable(gid)
if result_dictionary['checking'] == 'no':
# create new thread for this download
# see VideoFinder thread for more information
new_download = VideoFinder(result_dictionary, self)
self.threadPool.append(new_download)
self.threadPool[-1].start()
self.threadPool[-1].VIDEOFINDERCOMPLETED.connect(self.videoFinderCompleted)
# add thread to video_finder_threads_dict
self.video_finder_threads_dict[result_dictionary['video_gid']] = new_download
else:
# we already have an active tread for this download...
notifySend(QCoreApplication.translate("mainwindow_src_ui_tr", "Download is in progress by video finder!"),
QCoreApplication.translate("mainwindow_src_ui_tr", "be patient!"),
10000, 'warning', parent=self)
# show the download progress window
if gid in self.progress_window_list_dict.keys():
# find progress_window for this gid and show it to user
member_number = self.progress_window_list_dict[gid]
progress_window = self.progress_window_list[member_number]
progress_window.show()
progress_window.raise_()
progress_window.activateWindow()
else:
# check if last session of this gid is finished or not!
for download_session_dict in self.download_sessions_list:
if download_session_dict['gid'] == gid:
# we already have an active tread for this download...
notifySend(QCoreApplication.translate("mainwindow_src_ui_tr", "Please retry in a minute!"),
QCoreApplication.translate("mainwindow_src_ui_tr", "be patient!"),
10000, 'warning', parent=self)
return
# check if gid is related to single video download link or not.
result_dictionary = self.persepolis_db.searchGidInVideoFinderTable2(gid)
# get information from data_base
add_link_dictionary = self.persepolis_db.searchGidInAddLinkTable(gid)
# create download_session
if result_dictionary is None:
download_session = persepolis_lib_prime.Download(add_link_dictionary, self, gid)
else:
# single video link
download_session = ytdlp_downloader.Ytdp_Download(add_link_dictionary, self, gid, single_video_link=True)
# add gid to single_video_link_gid_list
self.single_video_link_gid_list.append(gid)
# add download_session and gid to download_session_dict
download_session_dict = {'gid': gid,
'download_session': download_session}
# append download_session_dict to download_sessions_list
self.download_sessions_list.append(download_session_dict)
# strat download in thread
new_download = DownloadLink(gid, download_session, self)
self.threadPool.append(new_download)
self.threadPool[-1].start()
# create new progress_window
self.progressBarOpen(gid)
# this method called if user presses stop button in MainWindow
def stopButtonPressed(self, button=None):
# disable stop button
self.stopAction.setEnabled(False)
# finding user's selected row
selected_row_return = self.selectedRow()
if selected_row_return is not None:
# find download category
category = self.download_table.item(selected_row_return, 12).text()
# if category is not "single downloads" , then send notification for error
if category != "Single Downloads":
notifySend(QCoreApplication.translate("mainwindow_src_ui_tr", "Operation was not successful."),
QCoreApplication.translate("mainwindow_src_ui_tr",
"Please stop the following category: ") + category,
10000, 'fail', parent=self)
return
gid = self.download_table.item(selected_row_return, 8).text()
# check if this gid is related to video finder
if gid in self.all_video_finder_gid_list:
result_dictionary = self.persepolis_db.searchGidInVideoFinderTable(gid)
video_finder_plus_gid = 'video_finder_' + str(result_dictionary['video_gid'])
# cancel shut down progress
dictionary = {'category': video_finder_plus_gid,
'shutdown': 'canceled'}
self.temp_db.updateQueueTable(dictionary)
else:
# change status of shutdown in temp_db
dictionary = {'gid': gid,
'shutdown': 'canceled'}
self.temp_db.updateSingleTable(dictionary)
# search gid in download_sessions_list
for download_session_dict in self.download_sessions_list:
if download_session_dict['gid'] == gid:
# stop download
download_session_dict['download_session'].downloadStop()
break
# this method called if user presses pause button in MainWindow
def pauseButtonPressed(self, button=None):
self.pauseAction.setEnabled(False)
# find selected row
selected_row_return = self.selectedRow()
if selected_row_return is not None:
# find download category
category = self.download_table.item(selected_row_return, 12).text()
# if category is not "single downloads" , then send notification for error
if category != "Single Downloads":
notifySend(QCoreApplication.translate("mainwindow_src_ui_tr", "Operation was not successful."),
QCoreApplication.translate("mainwindow_src_ui_tr",
"Please stop the following category: ") + category,
10000, 'fail', parent=self)
return
# find download gid
gid = self.download_table.item(selected_row_return, 8).text()
# search gid in download_sessions_list
for download_session_dict in self.download_sessions_list:
if download_session_dict['gid'] == gid:
# stop download
download_session_dict['download_session'].downloadUnpause()
break
# This method called if properties button pressed by user in MainWindow
def propertiesButtonPressed(self, button=None):
result_dictionary = None
self.propertiesAction.setEnabled(False)
selected_row_return = self.selectedRow() # finding user's selected row
if selected_row_return is not None:
# find gid of download
gid = self.download_table.item(selected_row_return, 8).text()
# check if the gid is related to video finder
if gid in self.all_video_finder_gid_list:
result_dictionary = self.persepolis_db.searchGidInVideoFinderTable(gid)
if result_dictionary['checking'] == 'yes':
# this link is in downloading queue by video finder
notifySend(QCoreApplication.translate("mainwindow_src_ui_tr", "Download is in progress by video finder!"),
QCoreApplication.translate("mainwindow_src_ui_tr", "be patient!"),
10000, 'warning', parent=self)
# show the download progress window
if gid in self.progress_window_list_dict.keys():
# find progress_window for this gid and show it to user
member_number = self.progress_window_list_dict[gid]
progress_window = self.progress_window_list[member_number]
progress_window.show()
progress_window.raise_()
progress_window.activateWindow()
return
# create propertieswindow
propertieswindow = PropertiesWindow(
self, self.propertiesCallback, gid, self.persepolis_setting, result_dictionary)
self.propertieswindows_list.append(propertieswindow)
self.propertieswindows_list[-1].show()
# callBack of PropertiesWindow
def propertiesCallback(self, add_link_dictionary, gid, category,
video_finder_dictionary=None):
# if checking_flag is equal to 1, it means that user pressed remove or
# delete button or ... . so checking download information must be
# stopped until job is done!
if checking_flag != 2:
wait_check = WaitThread()
self.threadPool.append(wait_check)
self.threadPool[-1].start()
self.threadPool[-1].QTABLEREADY.connect(
partial(self.propertiesCallback2, add_link_dictionary, gid,
category, video_finder_dictionary))
else:
self.propertiesCallback2(add_link_dictionary, gid, category,
video_finder_dictionary)
def propertiesCallback2(self, add_link_dictionary, gid, category,
video_finder_dictionary=None):
# highlight category of this download item
# find selected category in left side panel
for i in range(self.category_tree_model.rowCount()):
category_tree_item_text = str(
self.category_tree_model.index(i, 0).data())
if category_tree_item_text == category:
category_index = i
break
# highlight selected category in category_tree
category_tree_model_index = self.category_tree_model.index(
category_index, 0)
current_category_tree_text = current_category_tree_index.data()
self.category_tree.setCurrentIndex(category_tree_model_index)
if current_category_tree_text != category:
self.categoryTreeSelected(category_tree_model_index)
# tell the CheckDownloadInfoThread that job is done!
global checking_flag
checking_flag = 0
# This method is called if user presses "show/hide progress window" button in
# MainWindow
def progressButtonPressed(self, button=None):
# find user's selected row
selected_row_return = self.selectedRow()
if selected_row_return is not None:
gid = self.download_table.item(selected_row_return, 8).text()
# if gid is in self.progress_window_list_dict , it means that progress
# window for this gid (for this download) is created before and it's
# available! See progressBarOpen method for more information.
if gid in self.progress_window_list_dict:
# find member_number of window in progress_window_list_dict
member_number = self.progress_window_list_dict[gid]
# if window is visible >> hide it ,
# and if window is hide >> make it visible!
if self.progress_window_list[member_number].isVisible():
self.progress_window_list[member_number].hide()
else:
self.progress_window_list[member_number].show()
else:
# if window is not availabile in progress_window_list_dict
# so let's create it!
self.progressBarOpen(gid)
# This method creates new ProgressWindow
def progressBarOpen(self, gid):
dictionary = None
# check if it's related to video finder or not
if gid in self.all_video_finder_gid_list:
dictionary = self.persepolis_db.searchGidInVideoFinderTable(gid)
# it's related to video finder. so make a gid list for video and audio link!
gid_list = [dictionary['video_gid'], dictionary['audio_gid']]
# create a video finder progress window.
progress_window = VideoFinderProgressWindow(self, gid_list, self.persepolis_setting)
else:
# create an ordinary progress_window
# check if it's single_video_link or not
answer_dictionary = self.persepolis_db.searchGidInVideoFinderTable2(gid)
if answer_dictionary is not None:
single_video_link = True
else:
single_video_link = False
progress_window = ProgressWindow(
parent=self, gid=gid, persepolis_setting=self.persepolis_setting, single_video_link=single_video_link)
# add progress window to progress_window_list
self.progress_window_list.append(progress_window)
member_number = len(self.progress_window_list) - 1
# in progress_window_list_dict , key is gid and value is member's
# rank(number) in progress_window_list
if dictionary:
self.progress_window_list_dict[dictionary['video_gid']] = member_number
self.progress_window_list_dict[dictionary['audio_gid']] = member_number
else:
self.progress_window_list_dict[gid] = member_number
# check user preferences
# user can hide progress window in settings window.
if str(self.persepolis_setting.value('settings/show-progress')) == 'yes':
# show progress window
self.progress_window_list[member_number].show()
else:
# hide progress window
self.progress_window_list[member_number].hide()
# close window with ESC key
def keyPressEvent(self, event):
if event.key() == Qt.Key_Escape:
self.close()
def cleanTempFolder(self):
temp_files_pattern = os.path.join(persepolis_tmp, '.*')
# delete all unwanted files
for filename in glob.glob(temp_files_pattern):
osCommands.remove(filename)
# close event
# when user closes application then this method is called
def closeEvent(self, event=None):
if str(self.persepolis_setting.value('settings/hide-window')) == 'yes':
# set close event just for minimizing to tray
self.minimizeAction.setText(QCoreApplication.translate("mainwindow_src_ui_tr", 'Show main Window'))
self.minimizeAction.setIcon(QIcon(icons + 'window'))
else:
# close window and exit application
self.closeAction(event)
# close application actions is in this method (to close program completely this method must call)
def closeAction(self, event=None):
# make sure we have no active downloads
# get active download list from data base
active_gid_list = self.persepolis_db.findActiveDownloads()
if active_gid_list:
# notify user
self.msgBox = QMessageBox()
self.msgBox.setText(QCoreApplication.translate("mainwindow_src_ui_tr", "
A download or downloads are in progress.\
Stop them before closing the app!
"))
self.msgBox.setIcon(QMessageBox.Warning)
self.msgBox.exec_()
return
print('Persepolis will be closing soon. Please wait a moment.')
# save window size and position
self.persepolis_setting.setValue('MainWindow/size', self.size())
self.persepolis_setting.setValue('MainWindow/position', self.pos())
# save columns size
self.persepolis_setting.setValue('MainWindow/column0', self.download_table.columnWidth(0))
self.persepolis_setting.setValue('MainWindow/column1', self.download_table.columnWidth(1))
self.persepolis_setting.setValue('MainWindow/column2', self.download_table.columnWidth(2))
self.persepolis_setting.setValue('MainWindow/column3', self.download_table.columnWidth(3))
self.persepolis_setting.setValue('MainWindow/column4', self.download_table.columnWidth(4))
self.persepolis_setting.setValue('MainWindow/column5', self.download_table.columnWidth(5))
self.persepolis_setting.setValue('MainWindow/column6', self.download_table.columnWidth(6))
self.persepolis_setting.setValue('MainWindow/column7', self.download_table.columnWidth(7))
self.persepolis_setting.setValue('MainWindow/column10', self.download_table.columnWidth(10))
self.persepolis_setting.setValue('MainWindow/column11', self.download_table.columnWidth(11))
self.persepolis_setting.setValue('MainWindow/column12', self.download_table.columnWidth(12))
# save maximizing situation
if self.isMaximized():
self.persepolis_setting.setValue('MainWindow/maximized', 'yes')
else:
self.persepolis_setting.setValue('MainWindow/maximized', 'no')
# sync persepolis_setting
# make sure all settings is saved.
self.persepolis_setting.sync()
# hide MainWindow
self.hide()
# hide system_tray_icon
self.system_tray_icon.hide()
# Make sure all sessions have ended.
while self.download_sessions_list:
sleep(0.5)
global shutdown_notification # see start of this script and see inherited QThreads
# shutdown_notification = 0 >> persepolis running , 1 >> persepolis is
# ready for close(closeEvent called) , 2 >> OK, let's close application!
shutdown_notification = 1
while shutdown_notification != 2:
sleep(0.1)
# close data bases connections
for db in self.persepolis_db, self.plugins_db, self.temp_db:
db.closeConnections()
for i in self.threadPool:
i.quit()
i.wait()
self.cleanTempFolder()
QCoreApplication.instance().quit
logger.sendToLog("Persepolis closed!", "INFO")
sys.exit(0)
# showTray method shows/hides persepolis's icon in system tray icon
def showTray(self, menu=None):
# check if user checked trayAction in menu or not
if self.trayAction.isChecked():
# show system_tray_icon
self.system_tray_icon.show()
# enable minimizeAction in menu
self.minimizeAction.setEnabled(True)
tray_icon = 'yes'
else:
# hide system_tray_icon
self.system_tray_icon.hide()
# disabaling minimizeAction in menu
self.minimizeAction.setEnabled(False)
tray_icon = 'no'
# write changes in persepolis_setting
self.persepolis_setting.setValue('settings/tray-icon', tray_icon)
self.persepolis_setting.sync()
# this method shows/hides menubar and
# it's called when user toggles showMenuBarAction in view menu
def showMenuBar(self, menu=None):
# persepolis has 2 menu bar
# 1. menubar in main window
# 2. qmenu(see mainwindow_ui.py file for more information)
# qmenu is in toolBar2
# user can toggle between viewing menu1 or menu2 with showMenuBarAction
# check if showMenuBarAction is checked or unchecked
if self.showMenuBarAction.isChecked():
# show menubar and hide toolBar2
self.menubar.show()
self.toolBar2.hide()
show_menubar = 'yes'
else:
# hide menubar and show toolBar2
self.menubar.hide()
self.toolBar2.show()
show_menubar = 'no'
# writing changes to persepolis_setting
self.persepolis_setting.setValue('settings/show-menubar', show_menubar)
self.persepolis_setting.sync()
# this method shows/hides left side panel
# this method is called if user toggles showSidePanelAction in view menu
def showSidePanel(self, menu=None):
if self.showSidePanelAction.isChecked():
self.category_tree_qwidget.show()
show_sidepanel = 'yes'
else:
self.category_tree_qwidget.hide()
show_sidepanel = 'no'
# write changes to persepolis_setting
self.persepolis_setting.setValue(
'settings/show-sidepanel', show_sidepanel)
self.persepolis_setting.sync()
# when user left clicks on persepolis's system tray icon,then
# this method is called
def systemTrayPressed(self, click):
if click == QSystemTrayIcon.Trigger:
self.minMaxTray(click)
# when minMaxTray method called ,this method shows/hides main window
def minMaxTray(self, menu=None):
# hide MainWindow if it's visible
# Show MainWindow if it's hided
if self.isVisible():
self.minimizeAction.setText(QCoreApplication.translate("mainwindow_src_ui_tr", 'Show main Window'))
self.minimizeAction.setIcon(QIcon(icons + 'window'))
self.hide()
else:
self.show()
self.minimizeAction.setText(QCoreApplication.translate("mainwindow_src_ui_tr", 'Minimize to system tray'))
self.minimizeAction.setIcon(QIcon(icons + 'minimize'))
# showMainWindow shows main window in normal mode , see CheckingThread
def showMainWindow(self):
self.showNormal()
self.minimizeAction.setText(QCoreApplication.translate("mainwindow_src_ui_tr", 'Minimize to system tray'))
self.minimizeAction.setIcon(QIcon(icons + 'minimize'))
# stopAllDownloads stops all downloads
def stopAllDownloads(self, menu=None):
# stop all queues
for queue in self.queue_list_dict.values():
queue.stop = True
queue.start = False
# stop single downloads
# get active download list from data base
active_gid_list = self.persepolis_db.findActiveDownloads('Single Downloads')
for gid in active_gid_list:
# search gid in download_sessions_list
for download_session_dict in self.download_sessions_list:
if download_session_dict['gid'] == gid:
# stop download
download_session_dict['download_session'].downloadStop()
break
# this method creates Preferences window
def openPreferences(self, menu=None):
self.preferenceswindow = PreferencesWindow(
self, self.persepolis_setting)
# show Preferences Window
self.preferenceswindow.show()
# this method is creating AboutWindow
def openAbout(self, menu=None):
about_window = AboutWindow(self.persepolis_setting)
self.about_window_list.append(about_window)
self.about_window_list[-1].show()
# This method opens user's default download folder
def openDefaultDownloadFolder(self, menu=None):
# find user's default download folder from persepolis_setting
self.persepolis_setting.sync()
download_path = self.persepolis_setting.value('settings/download_path')
# check that if download folder is availabile or not
if os.path.isdir(download_path):
# open folder
osCommands.xdgOpen(download_path, 'folder', 'folder')
else:
# show error message if folder didn't existed
notifySend(str(download_path), QCoreApplication.translate("mainwindow_src_ui_tr", 'Not Found'), 5000,
'warning', parent=self)
# this method opens download folder , if download was finished
def openDownloadFolder(self, menu=None):
# find user's selected row
selected_row_return = self.selectedRow()
if selected_row_return is not None:
# find gid
gid = self.download_table.item(
selected_row_return, 8).text()
# find status
download_status = self.download_table.item(
selected_row_return, 1).text()
if download_status == 'complete':
# check if this link is related to video finder
# don't open download folder, if download progress for video and audio aren't completed yet.
video_finder_dictionary = self.persepolis_db.searchGidInVideoFinderTable(gid)
if video_finder_dictionary:
notifySend(QCoreApplication.translate("mainwindow_src_ui_tr", "Download is in progress by video finder!"),
QCoreApplication.translate("mainwindow_src_ui_tr", "be patient!"),
10000, 'warning', parent=self)
return
# find download path
dictionary = self.persepolis_db.searchGidInAddLinkTable(gid)
download_path = dictionary['download_path']
# check that if download_path existed
if os.path.isfile(download_path):
# open file
osCommands.xdgOpen(download_path, 'folder', 'file')
else:
# showing error message , if folder didn't existed
notifySend(str(download_path), QCoreApplication.translate("mainwindow_src_ui_tr", 'Not Found'), 5000,
'warning', parent=self)
# this method executes(opens) download file if download's progress was finished
def openFile(self, menu=None):
# find user's selected row
selected_row_return = self.selectedRow()
if selected_row_return is not None:
# find gid
gid = self.download_table.item(
selected_row_return, 8).text()
# find status
download_status = self.download_table.item(
selected_row_return, 1).text()
if download_status == 'complete':
# check if this link is related to video finder
# don't open download folder, if download progress for video and audio aren't completed yet.
video_finder_dictionary = self.persepolis_db.searchGidInVideoFinderTable(gid)
if video_finder_dictionary:
notifySend(QCoreApplication.translate("mainwindow_src_ui_tr", "Download is in progress by video finder!"),
QCoreApplication.translate("mainwindow_src_ui_tr", "be patient!"),
10000, 'warning', parent=self)
return
# find download path
dictionary = self.persepolis_db.searchGidInAddLinkTable(gid)
file_path = dictionary['download_path']
if os.path.isfile(file_path):
# open file
osCommands.xdgOpen(file_path)
else:
# show error message , if file was deleted or moved
notifySend(str(file_path), QCoreApplication.translate("mainwindow_src_ui_tr", 'Not Found'), 5000,
'warning', parent=self)
# this method is called when multiple items is selected by user!
def selectDownloads(self):
# find highlighted item in category_tree
current_category_tree_text = str(current_category_tree_index.data())
self.toolBarAndContextMenuItems(current_category_tree_text)
# change actions icon
if self.multi_items_selected:
self.removeSelectedAction.setIcon(QIcon(icons + 'multi_remove'))
self.deleteSelectedAction.setIcon(QIcon(icons + 'multi_trash'))
self.moveUpSelectedAction.setIcon(QIcon(icons + 'multi_up'))
self.moveDownSelectedAction.setIcon(QIcon(icons + 'multi_down'))
self.propertiesAction.setVisible(False)
else:
self.removeSelectedAction.setIcon(QIcon(icons + 'remove'))
self.deleteSelectedAction.setIcon(QIcon(icons + 'trash'))
self.moveUpSelectedAction.setIcon(QIcon(icons + 'up'))
self.moveDownSelectedAction.setIcon(QIcon(icons + 'down'))
self.propertiesAction.setVisible(True)
# this method is called when user presses 'remove selected items' button
def removeSelected(self, menu=None):
# if checking_flag is equal to 1, it means that user pressed remove or
# delete button or ... . so checking download information must be
# stopped until job is done!
if checking_flag != 2:
wait_check = WaitThread()
self.threadPool.append(wait_check)
self.threadPool[-1].start()
self.threadPool[-1].QTABLEREADY.connect(self.removeSelected2)
else:
self.removeSelected2()
def removeSelected2(self):
# find selected rows!
gid_list = []
for row in self.userSelectedRows():
# get download status
status = self.download_table.item(row, 1).text()
# find category
category = self.download_table.item(row, 12).text()
if category != "Single Downloads":
# check queue condition!
# queue must be stopped first
if str(category) in self.queue_list_dict.keys():
queue_status = self.queue_list_dict[str(category)].start
else:
queue_status = False
if queue_status: # if queue was started
# show error message
notifySend(QCoreApplication.translate("mainwindow_src_ui_tr", "Operation was not successful!"),
QCoreApplication.translate(
"mainwindow_src_ui_tr", "Operation was not successful! Please stop the following category first: ") + category,
5000, 'fail', parent=self)
continue
# find gid
gid = self.download_table.item(row, 8).text()
# check if this link is related to video finder
video_finder_link = False
if gid in self.all_video_finder_gid_list:
video_finder_dictionary = self.persepolis_db.searchGidInVideoFinderTable(gid)
video_finder_link = True
if gid in self.video_finder_threads_dict.keys():
# check the Video Finder tread status
video_finder_thread = self.video_finder_threads_dict[video_finder_dictionary['video_gid']]
if video_finder_thread.active == 'yes':
notifySend(QCoreApplication.translate("mainwindow_src_ui_tr", "Download is in progress by video finder!"),
QCoreApplication.translate("mainwindow_src_ui_tr", "be patient!"),
10000, 'warning', parent=self)
continue
# if Video Finder thread is not active so remove both of video and audio link.
else:
gid_list.append(video_finder_dictionary['video_gid'])
gid_list.append(video_finder_dictionary['audio_gid'])
continue
# if Video Finder thread is not active so remove both of video and audio link.
else:
gid_list.append(video_finder_dictionary['video_gid'])
gid_list.append(video_finder_dictionary['audio_gid'])
continue
# only download items with "complete", "error" and "stopped" can be removed
if (status == 'complete' or status == 'error' or status == 'stopped'):
if not (video_finder_link):
# check if gid is related to a single_video_link or not
answer_dictionary = self.persepolis_db.searchGidInVideoFinderTable2(gid)
if answer_dictionary:
video_finder_link = True
# add gid to gid_list
gid_list.append(gid)
else:
# find filename
file_name = self.download_table.item(row, 0).text()
# show error message
notifySend(QCoreApplication.translate("mainwindow_src_ui_tr", "Operation was not successful!"),
QCoreApplication.translate("mainwindow_src_ui_tr",
"Please stop the following download first: ") + file_name,
5000, 'fail', parent=self)
# remove duplicate items
gid_list = set(gid_list)
for gid in gid_list:
# find row number for specific gid
for i in range(self.download_table.rowCount()):
row_gid = self.download_table.item(i, 8).text()
if gid == row_gid:
row = i
break
# find status
status = self.download_table.item(row, 1).text()
# find filename
file_name = self.download_table.item(row, 0).text()
# find category
category = self.download_table.item(row, 12).text()
# remove row from download_table
self.download_table.removeRow(row)
# remove download files, remove from data_base and remove from download_sessions_list
delete_download_file = False
delete_things_that_are_no_longer_needed_thread = DeleteThingsThatAreNoLongerNeededThread(gid, file_name, status, category,
delete_download_file, self, video_finder_link)
self.threadPool.append(delete_things_that_are_no_longer_needed_thread)
self.threadPool[-1].start()
self.threadPool[-1].NOTIFYSENDSIGNAL.connect(self.notifySendFromThread)
# tell the CheckDownloadInfoThread that job is done!
global checking_flag
checking_flag = 0
# this method is called when user presses 'delete selected items'
def deleteSelected(self, menu=None):
# showing Warning message to the user.
# checking persepolis_setting first!
# perhaps user was checking "do not show this message again"
delete_warning_message = self.persepolis_setting.value(
'MainWindow/delete-warning', 'yes')
if delete_warning_message == 'yes':
self.msgBox = QMessageBox()
self.msgBox.setText(QCoreApplication.translate("mainwindow_src_ui_tr", "
This operation will delete \
downloaded files from your hard disk PERMANENTLY!
"))
self.msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
self.msgBox.setIcon(QMessageBox.Warning)
dont_show_checkBox = QCheckBox("don't show this message again")
self.msgBox.setCheckBox(dont_show_checkBox)
reply = self.msgBox.exec_()
# if user checks "do not show this message again!", change persepolis_setting!
if self.msgBox.checkBox().isChecked():
self.persepolis_setting.setValue(
'MainWindow/delete-warning', 'no')
# do nothing if user clicks NO
if reply != QMessageBox.Yes:
return
# if checking_flag is equal to 1, it means that user pressed remove or
# delete button or ... . so checking download information must be
# stopped until job is done!
if checking_flag != 2:
wait_check = WaitThread()
self.threadPool.append(wait_check)
self.threadPool[-1].start()
self.threadPool[-1].QTABLEREADY.connect(self.deleteSelected2)
else:
self.deleteSelected2()
def deleteSelected2(self):
gid_list = []
# find selected rows!
for row in self.userSelectedRows():
# get download status
status = self.download_table.item(row, 1).text()
# find category
category = self.download_table.item(row, 12).text()
if category != "Single Downloads":
# check queue condition!
# queue must be stopped first
if str(category) in self.queue_list_dict.keys():
queue_status = self.queue_list_dict[str(category)].start
else:
queue_status = False
if queue_status: # if queue was started
# show error message
notifySend(QCoreApplication.translate("mainwindow_src_ui_tr", "Operation was not successful!"),
QCoreApplication.translate("mainwindow_src_ui_tr",
"Please stop the following category first: ") + category,
5000, 'fail', parent=self)
continue
# find gid
gid = self.download_table.item(row, 8).text()
# check if this link is related to video finder
video_finder_link = False
if gid in self.all_video_finder_gid_list:
video_finder_dictionary = self.persepolis_db.searchGidInVideoFinderTable(gid)
video_finder_link = True
if gid in self.video_finder_threads_dict.keys():
# check the Video Finder tread status
video_finder_thread = self.video_finder_threads_dict[video_finder_dictionary['video_gid']]
if video_finder_thread.active == 'yes':
notifySend(QCoreApplication.translate("mainwindow_src_ui_tr", "Download is in progress by video finder!"),
QCoreApplication.translate("mainwindow_src_ui_tr", "be patient!"),
10000, 'warning', parent=self)
continue
# if Video Finder thread is not active so remove both of video and audio link.
else:
gid_list.append(video_finder_dictionary['video_gid'])
gid_list.append(video_finder_dictionary['audio_gid'])
continue
else:
gid_list.append(video_finder_dictionary['video_gid'])
gid_list.append(video_finder_dictionary['audio_gid'])
continue
# only download items with "complete", "error" and "stopped" can be removed
if (status == 'complete' or status == 'error' or status == 'stopped'):
if not (video_finder_link):
# check if gid is related to a single_video_link or not
answer_dictionary = self.persepolis_db.searchGidInVideoFinderTable2(gid)
if answer_dictionary:
video_finder_link = True
# add gid to gid_list
gid_list.append(gid)
else:
# find filename
file_name = self.download_table.item(row, 0).text()
# show error message
notifySend(QCoreApplication.translate("mainwindow_src_ui_tr", "Operation was not successful!"),
QCoreApplication.translate("mainwindow_src_ui_tr",
"Stop the following download first: ") + file_name,
5000, 'fail', parent=self)
# remove selected rows
# remove duplicate items
gid_list = set(gid_list)
for gid in gid_list:
# find row number for specific gid
for i in range(self.download_table.rowCount()):
row_gid = self.download_table.item(i, 8).text()
if gid == row_gid:
row = i
break
# find file_name
file_name = self.download_table.item(row, 0).text()
# find category
category = self.download_table.item(row, 12).text()
# find status
status = self.download_table.item(row, 1).text()
# remove row from download_table
self.download_table.removeRow(row)
# remove download files, remove from data_base and remove from download_sessions_list
delete_download_file = True
delete_things_that_are_no_longer_needed_thread = DeleteThingsThatAreNoLongerNeededThread(gid, file_name, status, category,
delete_download_file, self, video_finder_link)
self.threadPool.append(delete_things_that_are_no_longer_needed_thread)
self.threadPool[-1].start()
self.threadPool[-1].NOTIFYSENDSIGNAL.connect(self.notifySendFromThread)
# telling the CheckDownloadInfoThread that job is done!
global checking_flag
checking_flag = 0
# this method sorts download table by name
def sortByName(self, menu=None):
# if checking_flag is equal to 1, it means that user pressed remove or
# delete button or ... . so checking download information must be
# stopped until job is done!
if checking_flag != 2:
wait_check = WaitThread()
self.threadPool.append(wait_check)
self.threadPool[-1].start()
self.threadPool[-1].QTABLEREADY.connect(self.sortByName2)
else:
self.sortByName2()
def sortByName2(self):
# find names and gid of downloads and save them in name_gid_dict
# gid is key and name is value.
gid_name_dict = {}
for row in range(self.download_table.rowCount()):
name = self.download_table.item(row, 0).text()
gid = self.download_table.item(row, 8).text()
gid_name_dict[gid] = name
# sort names
gid_sorted_list = sorted(gid_name_dict, key=gid_name_dict.get)
# clear download_table and add sorted items
self.download_table.clearContents()
# find name of selected category
current_category_tree_text = str(current_category_tree_index.data())
# get download information from data base
if current_category_tree_text == 'All Downloads':
downloads_dict = self.persepolis_db.returnItemsInDownloadTable()
else:
downloads_dict = self.persepolis_db.returnItemsInDownloadTable(current_category_tree_text)
j = 0
for gid in gid_sorted_list:
# enter download rows according to gid_sorted_list
download_info = downloads_dict[gid]
keys_list = ['file_name',
'status',
'size',
'downloaded_size',
'percent',
'connections',
'rate',
'estimate_time_left',
'gid',
'link',
'first_try_date',
'last_try_date',
'category'
]
i = 0
for key in keys_list:
item = QTableWidgetItem(download_info[key])
# insert item in download_table
self.download_table.setItem(j, i, item)
i = i + 1
j = j + 1
# save sorted list (gid_sorted_list) in data base
category_dict = {'category': current_category_tree_text}
# update gid_list
gid_sorted_list.reverse()
category_dict['gid_list'] = gid_sorted_list
# update category_db_table
self.persepolis_db.updateCategoryTable([category_dict])
# tell the CheckDownloadInfoThread that job is done!
global checking_flag
checking_flag = 0
# this method sorts items in download_table by size
def sortBySize(self, menu=None):
# if checking_flag is equal to 1, it means that user pressed remove or
# delete button or ... . so checking download information must be
# stopped until job is done!
if checking_flag != 2:
wait_check = WaitThread()
self.threadPool.append(wait_check)
self.threadPool[-1].start()
self.threadPool[-1].QTABLEREADY.connect(self.sortBySize2)
else:
self.sortBySize2()
def sortBySize2(self):
# find name of selected category
current_category_tree_text = str(current_category_tree_index.data())
# find gid and size of downloads
gid_size_dict = {}
for row in range(self.download_table.rowCount()):
size_str = self.download_table.item(row, 2).text()
gid = self.download_table.item(row, 8).text()
# convert file size to the Byte
try:
size_int = float(size_str[:-3])
size_symbol = str(size_str[-2])
if size_symbol == 'G':
size = size_int * 1073741824
elif size_symbol == 'M':
size = size_int * 1048576
elif size_symbol == 'K':
size = size_int * 1024
else: # Byte
size = size_int
except ValueError:
size = 0
# create a dictionary from gid and size of files in Bytes
# gid as key and size as value
gid_size_dict[gid] = size
# sort gid_size_dict
gid_sorted_list = sorted(
gid_size_dict, key=gid_size_dict.get, reverse=True)
# clear download_table by size
self.download_table.clearContents()
# get download information from data base
if current_category_tree_text == 'All Downloads':
downloads_dict = self.persepolis_db.returnItemsInDownloadTable()
else:
downloads_dict = self.persepolis_db.returnItemsInDownloadTable(current_category_tree_text)
j = 0
for gid in gid_sorted_list:
# enter download rows according to gid_sorted_list
download_info = downloads_dict[gid]
keys_list = ['file_name',
'status',
'size',
'downloaded_size',
'percent',
'connections',
'rate',
'estimate_time_left',
'gid',
'link',
'first_try_date',
'last_try_date',
'category'
]
i = 0
for key in keys_list:
item = QTableWidgetItem(download_info[key])
# insert item in download_table
self.download_table.setItem(j, i, item)
i = i + 1
j = j + 1
# save sorted list (gid_sorted_list) in data base
category_dict = {'category': current_category_tree_text}
# update gid_list
gid_sorted_list.reverse()
category_dict['gid_list'] = gid_sorted_list
# update category_db_table
self.persepolis_db.updateCategoryTable([category_dict])
# tell the CheckDownloadInfoThread that job is done!
global checking_flag
checking_flag = 0
# this method sorts download_table items with status
def sortByStatus(self, menu=None):
# if checking_flag is equal to 1, it means that user pressed remove or
# delete button or ... . so checking download information must be
# stopped until job is done!
if checking_flag != 2:
wait_check = WaitThread()
self.threadPool.append(wait_check)
self.threadPool[-1].start()
self.threadPool[-1].QTABLEREADY.connect(self.sortByStatus2)
else:
self.sortByStatus2()
def sortByStatus2(self):
# find name of selected category
current_category_tree_text = str(current_category_tree_index.data())
# find gid and status of downloads
gid_status_dict = {}
for row in range(self.download_table.rowCount()):
status = self.download_table.item(row, 1).text()
gid = self.download_table.item(row, 8).text()
# assign a number to every status
if status == 'complete':
status_int = 1
elif status == 'stopped':
status_int = 2
elif status == 'error':
status_int = 3
elif status == 'downloading':
status_int = 4
elif status == 'waiting':
status_int = 5
else:
status_int = 6
# create a dictionary from gid and size_int of files in Bytes
gid_status_dict[gid] = status_int
# sort gid_status_dict
gid_sorted_list = sorted(gid_status_dict, key=gid_status_dict.get)
# get download information from data base
if current_category_tree_text == 'All Downloads':
downloads_dict = self.persepolis_db.returnItemsInDownloadTable()
else:
downloads_dict = self.persepolis_db.returnItemsInDownloadTable(current_category_tree_text)
# clear download_table
self.download_table.clearContents()
j = 0
for gid in gid_sorted_list:
# enter download rows according to gid_sorted_list
download_info = downloads_dict[gid]
keys_list = ['file_name',
'status',
'size',
'downloaded_size',
'percent',
'connections',
'rate',
'estimate_time_left',
'gid',
'link',
'first_try_date',
'last_try_date',
'category'
]
i = 0
for key in keys_list:
item = QTableWidgetItem(download_info[key])
# insert item in download_table
self.download_table.setItem(j, i, item)
i = i + 1
j = j + 1
# save sorted list (gid_sorted_list) in data base
category_dict = {'category': current_category_tree_text}
# update gid_list
gid_sorted_list.reverse()
category_dict['gid_list'] = gid_sorted_list
# update category_db_table
self.persepolis_db.updateCategoryTable([category_dict])
# tell the CheckDownloadInfoThread that job is done!
global checking_flag
checking_flag = 0
# this method sorts download table with date added information
def sortByFirstTry(self, menu=None):
# if checking_flag is equal to 1, it means that user pressed remove or
# delete button or ... . so checking download information must be
# stopped until job is done!
if checking_flag != 2:
wait_check = WaitThread()
self.threadPool.append(wait_check)
self.threadPool[-1].start()
self.threadPool[-1].QTABLEREADY.connect(self.sortByFirstTry2)
else:
self.sortByFirstTry2()
def sortByFirstTry2(self):
# find gid and first try date
gid_try_dict = {}
for row in range(self.download_table.rowCount()):
first_try_date = self.download_table.item(row, 10).text()
gid = self.download_table.item(row, 8).text()
# convert date and hour in first_try_date to a number
# for example , first_try_date = '2016/11/05 , 07:45:38'
# must be converted to 20161105074538
first_try_date_splited = first_try_date.split(' , ')
date_list = first_try_date_splited[0].split('/')
hour_list = first_try_date_splited[1].split(':')
date_joind = "".join(date_list)
hour_joind = "".join(hour_list)
date_hour_str = date_joind + hour_joind
date_hour = int(date_hour_str)
# create a dictionary
# gid as key and date_hour as value
gid_try_dict[gid] = date_hour
# sort
gid_sorted_list = sorted(
gid_try_dict, key=gid_try_dict.get, reverse=True)
# clear download_table
self.download_table.clearContents()
# find name of selected category
current_category_tree_text = str(current_category_tree_index.data())
# get download information from data base
if current_category_tree_text == 'All Downloads':
downloads_dict = self.persepolis_db.returnItemsInDownloadTable()
else:
downloads_dict = self.persepolis_db.returnItemsInDownloadTable(current_category_tree_text)
j = 0
for gid in gid_sorted_list:
# enter download rows according to gid_sorted_list
download_info = downloads_dict[gid]
keys_list = ['file_name',
'status',
'size',
'downloaded_size',
'percent',
'connections',
'rate',
'estimate_time_left',
'gid',
'link',
'first_try_date',
'last_try_date',
'category'
]
i = 0
for key in keys_list:
item = QTableWidgetItem(download_info[key])
# insert item in download_table
self.download_table.setItem(j, i, item)
i = i + 1
j = j + 1
# save sorted list (gid_list) in data base
category_dict = {'category': current_category_tree_text}
# update gid_sorted_list
gid_sorted_list.reverse()
category_dict['gid_list'] = gid_sorted_list
# update category_db_table
self.persepolis_db.updateCategoryTable([category_dict])
# tell the CheckDownloadInfoThread that job is done!
global checking_flag
checking_flag = 0
# this method sorts download_table with order of last modify date
def sortByLastTry(self, menu=None):
# if checking_flag is equal to 1, it means that user pressed remove or
# delete button or ... . so checking download information must be
# stopped until job is done!
if checking_flag != 2:
wait_check = WaitThread()
self.threadPool.append(wait_check)
self.threadPool[-1].start()
self.threadPool[-1].QTABLEREADY.connect(self.sortByLastTry2)
else:
self.sortByLastTry2()
def sortByLastTry2(self):
# create a dictionary
# gid as key and date_hour as value
gid_try_dict = {}
# find gid and last try date for download items in download_table
for row in range(self.download_table.rowCount()):
last_try_date = self.download_table.item(row, 11).text()
gid = self.download_table.item(row, 8).text()
# convert date and hour in last_try_date to a number
# for example , last_try_date = '2016/11/05 , 07:45:38'
# must be converted to 20161105074538
last_try_date_splited = last_try_date.split(' , ')
date_list = last_try_date_splited[0].split('/')
hour_list = last_try_date_splited[1].split(':')
date_joind = "".join(date_list)
hour_joind = "".join(hour_list)
date_hour_str = date_joind + hour_joind
date_hour = int(date_hour_str)
# add gid and date_hour to gid_try_dict
gid_try_dict[gid] = date_hour
# sort
gid_sorted_list = sorted(
gid_try_dict, key=gid_try_dict.get, reverse=True)
# clear download_table
self.download_table.clearContents()
# find name of selected category
current_category_tree_text = str(current_category_tree_index.data())
# get download information from data base
if current_category_tree_text == 'All Downloads':
downloads_dict = self.persepolis_db.returnItemsInDownloadTable()
else:
downloads_dict = self.persepolis_db.returnItemsInDownloadTable(current_category_tree_text)
j = 0
for gid in gid_sorted_list:
# enter download rows according to gid_sorted_list
download_info = downloads_dict[gid]
keys_list = ['file_name',
'status',
'size',
'downloaded_size',
'percent',
'connections',
'rate',
'estimate_time_left',
'gid',
'link',
'first_try_date',
'last_try_date',
'category'
]
i = 0
for key in keys_list:
item = QTableWidgetItem(download_info[key])
# insert item in download_table
self.download_table.setItem(j, i, item)
i = i + 1
j = j + 1
# save sorted list (gid_list) in data base
category_dict = {'category': current_category_tree_text}
# update gid_sorted_list
gid_sorted_list.reverse()
category_dict['gid_list'] = gid_sorted_list
# update category_db_table
self.persepolis_db.updateCategoryTable([category_dict])
# tell the CheckDownloadInfoThread that job is done!
global checking_flag
checking_flag = 0
# this method called , when user clicks on 'create new queue' button in
# main window.
def createQueue(self, menu=None):
text, ok = QInputDialog.getText(
self, 'Queue', 'Enter queue name:', text='queue')
if not (ok):
return None
queue_name = str(text)
if ok and queue_name != '' and queue_name != 'Single Downloads':
# check queue_name if exists!
answer = self.persepolis_db.searchCategoryInCategoryTable(queue_name)
# show Error window if queue before
if answer:
error_messageBox = QMessageBox()
error_messageBox.setText(
'"' + queue_name + QCoreApplication.translate("mainwindow_src_ui_tr", '" already exists!'))
error_messageBox.setWindowTitle('Error!')
error_messageBox.exec_()
return None
# insert new item in category_tree
new_queue_category = QStandardItem(queue_name)
font = QFont()
font.setBold(True)
new_queue_category.setFont(font)
new_queue_category.setEditable(False)
self.category_tree_model.appendRow(new_queue_category)
dict = {'category': queue_name,
'start_time_enable': 'no',
'start_time': '0:0',
'end_time_enable': 'no',
'end_time': '0:0',
'reverse': 'no',
'limit_enable': 'no',
'limit_value': '0K',
'after_download': 'no',
'gid_list': '[]'
}
# insert new category in data base
self.persepolis_db.insertInCategoryTable(dict)
# highlight new category in category_tree
# find item
for i in range(self.category_tree_model.rowCount()):
category_tree_item_text = str(
self.category_tree_model.index(i, 0).data())
if category_tree_item_text == queue_name:
category_index = i
break
# highlighting
category_tree_model_index = self.category_tree_model.index(
category_index, 0)
self.category_tree.setCurrentIndex(category_tree_model_index)
self.categoryTreeSelected(category_tree_model_index)
# return queue_name
return queue_name
# this method creates a BrowserPluginQueue window for list of links.
def pluginQueue(self, list_of_links):
# create window
plugin_queue_window = BrowserPluginQueue(
self, list_of_links, self.queueCallback, self.persepolis_setting)
self.plugin_queue_window_list.append(plugin_queue_window)
self.plugin_queue_window_list[-1].show()
# bring plugin_queue_window on top
self.plugin_queue_window_list[-1].raise_()
self.plugin_queue_window_list[-1].activateWindow()
# this method is importing a text file for creating queue .
# text file must contain links . 1 link per line!
def importText(self, menu=None):
# get file path
f_path, filters = QFileDialog.getOpenFileName(
self, 'Select the text file that contains links')
# if path is correct:
if os.path.isfile(str(f_path)):
# create a text_queue_window for getting information.
text_queue_window = TextQueue(
self, f_path, self.queueCallback, self.persepolis_setting)
self.text_queue_window_list.append(text_queue_window)
self.text_queue_window_list[-1].show()
# this method is importing download links from clipboard.
# clipboard must contain links.
def importLinksFromClipboard(self, menu=None):
# show main window
self.showMainWindow()
check_main_window_state_thread = CheckClipboardStateThread()
self.threadPool.append(check_main_window_state_thread)
self.threadPool[-1].start()
self.threadPool[-1].WINDOWISACTIVESIGNAL.connect(
self.importLinksFromClipboard2)
def importLinksFromClipboard2(self):
# get links from clipboard
clipboard = QApplication.clipboard().text()
# create a list from links
links_list = clipboard.splitlines()
for item in links_list:
if (("tp:/" in item[2:6]) or ("tps:/" in item[2:7])):
continue
else:
links_list.remove(item)
# create temp file to save links
if len(links_list) == 1:
video_finder_supported = self.checkVideoFinderSupportedSites(links_list[0])
if video_finder_supported is True:
self.showVideoFinderAddLinkWindow()
else:
self.addLinkButtonPressed(button=None)
elif len(links_list) > 1:
temp = tempfile.NamedTemporaryFile(mode="w+", prefix="persepolis")
temp.write(clipboard)
temp.flush()
temp_file_path = temp.name
# create a text_queue_window for getting information.
text_queue_window = TextQueue(
self, temp_file_path, self.queueCallback, self.persepolis_setting)
self.text_queue_window_list.append(text_queue_window)
self.text_queue_window_list[-1].show()
# close temp file (delete file)
temp.close()
# callback of text_queue_window and plugin_queue_window.AboutWindow
# See importText and pluginQueue method for more information.
def queueCallback(self, add_link_dictionary_list, category):
download_table_dict_list = []
video_finder_2_dict_list = []
# defining path of category_file
selected_category = str(category)
# highlight selected category in category_tree
# first of all find category_index of item!
for i in range(self.category_tree_model.rowCount()):
category_tree_item_text = str(
self.category_tree_model.index(i, 0).data())
if category_tree_item_text == selected_category:
category_index = i
break
# second: find category_tree_model_index
category_tree_model_index = self.category_tree_model.index(
category_index, 0)
# third: highlight item
self.category_tree.setCurrentIndex(category_tree_model_index)
self.categoryTreeSelected(category_tree_model_index)
download_table_list = []
# get now time and date
date = nowDate()
# add dictionary of downloads to data base
for add_link_dictionary in add_link_dictionary_list:
single_video_link = False
# persepolis identifies each download by the ID called GID. The GID must
# be hex string of 16 characters.
gid = self.gidGenerator()
add_link_dictionary['gid'] = gid
# download_info_file_list is a list that contains ['file_name' ,
# 'status' , 'size' , 'downloaded size' ,'download percentage' ,
# 'number of connections' ,'Transfer rate' , 'estimate_time_left' ,
# 'gid' , 'link' , 'first_try_date' , 'last_try_date', 'category']
# if user or browser_plugin defined filename then file_name is valid in
# add_link_dictionary['out']
if add_link_dictionary['out']:
file_name = add_link_dictionary['out']
# if file extension is m3u8 so it's single_video_link
file_name_split = file_name.split('.')
file_extension = file_name_split[-1]
# convert extension letters to lower case
# for example "JPG" will be converted in "jpg"
file_extension = file_extension.lower()
if file_extension == 'm3u8':
single_video_link = True
else:
file_name = '***'
download_table_list = [file_name, 'stopped', '***', '***', '***',
'***', '***', '***', gid, add_link_dictionary['link'],
date, date, category]
dictionary = {'file_name': file_name,
'status': 'stopped',
'size': '***',
'downloaded_size': '***',
'percent': '***',
'connections': '***',
'rate': '***',
'estimate_time_left': '***',
'gid': gid,
'link': add_link_dictionary['link'],
'first_try_date': date,
'last_try_date': date,
'category': category}
download_table_dict_list.append(dictionary)
# create a row in download_table
self.download_table.insertRow(0)
j = 0
for i in download_table_list:
item = QTableWidgetItem(i)
self.download_table.setItem(0, j, item)
j = j + 1
if single_video_link:
# create an item in data_base
# this item will updated by yt-dlp
# and contains download information.
video_Finder2_data_base = {'gid': gid,
'download_status': 'stopped',
'file_name': file_name,
'eta': '0',
'download_speed_str': '0',
'downloaded_size': 0,
'file_size': 0,
'download_percent': 0,
'fragments': '0/0',
'error_message': ''}
video_finder_2_dict_list.append(video_Finder2_data_base)
# spider is finding file size and file name
new_spider = SpiderThread(add_link_dictionary, self)
self.threadPool.append(new_spider)
self.threadPool[-1].start()
self.threadPool[-1].SPIDERSIGNAL.connect(self.spiderUpdate)
# write information in data_base
self.persepolis_db.insertInDownloadTable(download_table_dict_list)
self.persepolis_db.insertInAddLinkTable(add_link_dictionary_list)
self.persepolis_db.insertInVideoFinderTable2(video_finder_2_dict_list)
# this method is called , when user clicks on an item in
# category_tree (left side panel)
def categoryTreeSelected(self, item):
new_selection = item
if current_category_tree_index != new_selection:
# if checking_flag is equal to 1, it means that user pressed remove
# or delete button or ... . so checking download information must
# be stopped until job is done!
if checking_flag != 2:
wait_check = WaitThread()
self.threadPool.append(wait_check)
self.threadPool[-1].start()
self.threadPool[-1].QTABLEREADY.connect(
partial(self.categoryTreeSelected2, new_selection))
else:
self.categoryTreeSelected2(new_selection)
def categoryTreeSelected2(self, new_selection):
global current_category_tree_index
# clear download_table
self.download_table.setRowCount(0)
# old_selection_index
old_selection_index = current_category_tree_index
# finding name of old_selection_index
old_category_tree_item_text = str(old_selection_index.data())
queue_dict = {'category': old_category_tree_item_text}
# start_checkBox
if self.start_checkBox.isChecked():
queue_dict['start_time_enable'] = 'yes'
else:
queue_dict['start_time_enable'] = 'no'
# end_checkBox
if self.end_checkBox.isChecked():
queue_dict['end_time_enable'] = 'yes'
else:
queue_dict['end_time_enable'] = 'no'
# start_time_qDataTimeEdit
start_time = self.start_time_qDataTimeEdit.text()
queue_dict['start_time'] = str(start_time)
# end_time_qDateTimeEdit
end_time = self.end_time_qDateTimeEdit.text()
queue_dict['end_time'] = str(end_time)
# reverse_checkBox
if self.reverse_checkBox.isChecked():
queue_dict['reverse'] = 'yes'
else:
queue_dict['reverse'] = 'no'
# after_checkBox
if self.after_checkBox.isChecked():
queue_dict['after_download'] = 'yes'
else:
queue_dict['after_download'] = 'no'
# if old_selection_index.data() is equal to None >> It means queue is
# deleted! and no text (data) available for it
if old_selection_index.data():
# update data base
self.persepolis_db.updateCategoryTable([queue_dict])
# update download_table
current_category_tree_index = new_selection
# find category
current_category_tree_text = str(
self.category_tree.currentIndex().data())
# read download items from data base
if current_category_tree_text == 'All Downloads':
download_table_dict = self.persepolis_db.returnItemsInDownloadTable()
else:
download_table_dict = self.persepolis_db.returnItemsInDownloadTable(current_category_tree_text)
# get gid_list
category_dict = self.persepolis_db.searchCategoryInCategoryTable(current_category_tree_text)
gid_list = category_dict['gid_list']
keys_list = ['file_name',
'status',
'size',
'downloaded_size',
'percent',
'connections',
'rate',
'estimate_time_left',
'gid',
'link',
'first_try_date',
'last_try_date',
'category'
]
# insert items in download_table
for gid in gid_list:
# create new row
self.download_table.insertRow(0)
dictionary = download_table_dict[gid]
i = 0
for key in keys_list:
item = QTableWidgetItem(str(dictionary[key]))
self.download_table.setItem(0, i, item)
i = i + 1
# tell the CheckDownloadInfoThread that job is done!
global checking_flag
checking_flag = 0
# update toolBar and tablewidget_menu items
self.toolBarAndContextMenuItems(str(current_category_tree_text))
# this method changes toolabr and context menu items when new item
# highlighted by user in category_tree
def toolBarAndContextMenuItems(self, category):
# clear toolBar and context menus.
# it makes them ready for adding
# new items that suitable with new selected category.
# clear toolBar
self.toolBar.clear()
# clear context menu of download_table
self.download_table.tablewidget_menu.clear()
# clear context menu of category_tree
self.category_tree.category_tree_menu.clear()
queueAction = QAction(QIcon(icons + 'add'), 'Single Downloads', self,
statusTip="Add to Single Downloads", triggered=partial(self.addToQueue, 'Single Downloads'))
# check if user checked selection mode
if self.multi_items_selected:
self.download_table.sendMenu = self.download_table.tablewidget_menu.addMenu(
QCoreApplication.translate("mainwindow_src_ui_tr", 'Send selected downloads to'))
else:
self.download_table.sendMenu = self.download_table.tablewidget_menu.addMenu(
QCoreApplication.translate("mainwindow_src_ui_tr", 'Send to'))
# get categories list from data base
categories_list = self.persepolis_db.categoriesList()
# add categories name to sendMenu
for category_name in categories_list:
if category_name != category and category_name != 'All Downloads':
queueAction = QAction(QIcon(icons + 'add_queue'), category_name, self, statusTip="Add to" + category_name,
triggered=partial(self.addToQueue, category_name))
self.download_table.sendMenu.addAction(queueAction)
if category == 'All Downloads':
# hide queue_panel_widget(lef side down panel)
self.queue_panel_widget.hide()
# update toolBar
list = [self.addlinkAction, self.videoFinderAddLinkAction, self.resumeAction, self.pauseAction,
self.stopAction, self.removeSelectedAction, self.deleteSelectedAction,
self.propertiesAction, self.progressAction, self.minimizeAction, self.exitAction]
for i in list:
self.toolBar.addAction(i)
self.toolBar.insertSeparator(self.resumeAction)
self.toolBar.insertSeparator(self.removeSelectedAction)
self.toolBar.insertSeparator(self.propertiesAction)
self.toolBar.insertSeparator(self.minimizeAction)
self.toolBar.addSeparator()
# add actions to download_table's context menu
list = [self.openFileAction, self.openDownloadFolderAction, self.resumeAction,
self.pauseAction, self.stopAction, self.removeSelectedAction,
self.deleteSelectedAction, self.propertiesAction, self.progressAction, self.moveSelectedDownloadsAction]
for action in list:
self.download_table.tablewidget_menu.addAction(action)
elif category == 'Single Downloads':
# hide queue_panel_widget
self.queue_panel_widget.hide()
self.queuePanelWidget(category)
# update toolBar
list = [self.addlinkAction, self.videoFinderAddLinkAction, self.resumeAction, self.pauseAction,
self.stopAction, self.removeSelectedAction, self.deleteSelectedAction,
self.propertiesAction, self.progressAction, self.minimizeAction, self.exitAction]
for i in list:
self.toolBar.addAction(i)
self.toolBar.insertSeparator(self.resumeAction)
self.toolBar.insertSeparator(self.removeSelectedAction)
self.toolBar.insertSeparator(self.propertiesAction)
self.toolBar.insertSeparator(self.minimizeAction)
self.toolBar.addSeparator()
# add actions to download_table's context menu
list = [self.openFileAction, self.openDownloadFolderAction, self.resumeAction,
self.pauseAction, self.stopAction, self.removeSelectedAction,
self.deleteSelectedAction, self.propertiesAction, self.progressAction, self.moveSelectedDownloadsAction]
for action in list:
self.download_table.tablewidget_menu.addAction(action)
elif (category != 'All Downloads' and category != 'Single Downloads'):
# show queue_panel_widget
self.queue_panel_widget.show()
self.queuePanelWidget(category)
# update toolBar
list = [self.addlinkAction, self.videoFinderAddLinkAction, self.removeSelectedAction, self.deleteSelectedAction,
self.propertiesAction, self.startQueueAction, self.stopQueueAction,
self.removeQueueAction, self.moveUpSelectedAction, self.moveDownSelectedAction,
self.minimizeAction, self.exitAction]
for i in list:
self.toolBar.addAction(i)
self.toolBar.insertSeparator(self.removeSelectedAction)
self.toolBar.insertSeparator(self.propertiesAction)
self.toolBar.insertSeparator(self.startQueueAction)
self.toolBar.insertSeparator(self.minimizeAction)
self.toolBar.addSeparator()
# add actions to download_table's context menu
for action in [self.openFileAction, self.openDownloadFolderAction, self.removeSelectedAction, self.deleteSelectedAction, self.propertiesAction, self.moveSelectedDownloadsAction]:
self.download_table.tablewidget_menu.addAction(action)
# update category_tree_menu(right click menu for category_tree items)
for i in self.startQueueAction, self.stopQueueAction, self.removeQueueAction:
self.category_tree.category_tree_menu.addAction(i)
# check queue condition
if category != 'All Downloads' and category != 'Single Downloads':
if str(category) in self.queue_list_dict.keys():
queue_status = self.queue_list_dict[str(category)].start
else:
queue_status = False
if queue_status:
# if queue started before
self.stopQueueAction.setEnabled(True)
self.startQueueAction.setEnabled(False)
self.removeQueueAction.setEnabled(False)
self.moveUpSelectedAction.setEnabled(False)
self.moveDownSelectedAction.setEnabled(False)
else:
# if queue didn't start
self.stopQueueAction.setEnabled(False)
self.startQueueAction.setEnabled(True)
self.removeQueueAction.setEnabled(True)
self.moveUpSelectedAction.setEnabled(True)
self.moveDownSelectedAction.setEnabled(True)
else:
# if category is All Downloads or Single Downloads
self.stopQueueAction.setEnabled(False)
self.startQueueAction.setEnabled(False)
self.removeQueueAction.setEnabled(False)
self.moveUpSelectedAction.setEnabled(False)
self.moveDownSelectedAction.setEnabled(False)
# add sortMenu to download_table context menu
sortMenu = self.download_table.tablewidget_menu.addMenu(
QCoreApplication.translate("mainwindow_src_ui_tr", 'Sort by'))
sortMenu.addAction(self.sort_file_name_Action)
sortMenu.addAction(self.sort_file_size_Action)
sortMenu.addAction(self.sort_first_try_date_Action)
sortMenu.addAction(self.sort_last_try_date_Action)
sortMenu.addAction(self.sort_download_status_Action)
# this method removes the queue that is selected in category_tree
def removeQueue(self, menu=None):
# show Warning message to user.
# checks persepolis_setting first!
# perhaps user was checking "do not show this message again"
remove_warning_message = self.persepolis_setting.value(
'MainWindow/remove-queue-warning', 'yes')
if remove_warning_message == 'yes':
self.remove_queue_msgBox = QMessageBox()
self.remove_queue_msgBox.setText(QCoreApplication.translate("mainwindow_src_ui_tr", '
This operation will remove \
all download items in this queue from "All Downloads" list!
"))
self.remove_queue_msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
self.remove_queue_msgBox.setIcon(QMessageBox.Warning)
dont_show_checkBox = QCheckBox("don't show this message again")
self.remove_queue_msgBox.setCheckBox(dont_show_checkBox)
reply = self.remove_queue_msgBox.exec_()
# if user checks "do not show this message again!", change persepolis_setting!
if self.remove_queue_msgBox.checkBox().isChecked():
self.persepolis_setting.setValue(
'MainWindow/remove-queue-warning', 'no')
# do nothing if user clicks NO
if reply != QMessageBox.Yes:
return
# find name of queue
current_category_tree_text = str(current_category_tree_index.data())
if current_category_tree_text == 'Scheduled Downloads':
error_messageBox = QMessageBox()
error_messageBox.setText(
QCoreApplication.translate("mainwindow_src_ui_tr", "Sorry! You can't remove default queue!"))
error_messageBox.setWindowTitle('Error!')
error_messageBox.exec_()
return
if current_category_tree_text != 'All Downloads' and current_category_tree_text != 'Single Downloads':
# remove queue from category_tree
row_number = current_category_tree_index.row()
self.category_tree_model.removeRow(row_number)
# delete category from data base
self.persepolis_db.deleteCategory(current_category_tree_text)
# highlight "All Downloads" in category_tree
all_download_index = self.category_tree_model.index(0, 0)
self.category_tree.setCurrentIndex(all_download_index)
self.categoryTreeSelected(all_download_index)
# this method starts the queue that is selected in category_tree
def startQueue(self, menu=None):
self.startQueueAction.setEnabled(False)
# current_category_tree_text is the name of queue that is selected by user
current_category_tree_text = str(current_category_tree_index.data())
# create an item for this category in temp_db if not exists!
try:
self.temp_db.insertInQueueTable(current_category_tree_text)
except Exception:
# release lock
self.temp_db.lock = False
queue_info_dict = {'category': current_category_tree_text}
# check that if user checks start_checkBox or not.
if self.start_checkBox.isChecked():
queue_info_dict['start_time_enable'] = 'yes'
# read start_time value
start_time = self.start_time_qDataTimeEdit.text()
else:
queue_info_dict['start_time_enable'] = 'no'
start_time = None
# check that if user checked end_checkBox or not.
if self.end_checkBox.isChecked():
queue_info_dict['end_time_enable'] = 'yes'
# read end_time value
end_time = self.end_time_qDateTimeEdit.text()
else:
queue_info_dict['end_time_enable'] = 'no'
end_time = None
# reverse_checkBox
if self.reverse_checkBox.isChecked():
queue_info_dict['reverse'] = 'yes'
else:
queue_info_dict['reverse'] = 'no'
# update data base
self.persepolis_db.updateCategoryTable([queue_info_dict])
# create new Queue thread
new_queue = Queue(current_category_tree_text, start_time,
end_time, self)
self.queue_list_dict[current_category_tree_text] = new_queue
self.queue_list_dict[current_category_tree_text].start()
self.queue_list_dict[current_category_tree_text].REFRESHTOOLBARSIGNAL.connect(
self.toolBarAndContextMenuItems)
self.queue_list_dict[current_category_tree_text].NOTIFYSENDSIGNAL.connect(self.notifySendFromThread)
self.toolBarAndContextMenuItems(current_category_tree_text)
# this method stops the queue that is selected
# by user in the left side panel
def stopQueue(self, menu=None):
self.stopQueueAction.setEnabled(False)
# current_category_tree_text is the name of queue that is selected by user
current_category_tree_text = str(current_category_tree_index.data())
queue = self.queue_list_dict[current_category_tree_text]
queue.start = False
queue.stop = True
self.startQueueAction.setEnabled(True)
# this method is called , when user want to add a download to a queue with
# context menu. see also toolBarAndContextMenuItems() method
def addToQueue(self, data, menu=None):
# if checking_flag is equal to 1, it means that user pressed remove or
# delete button or ... . so checking download information must be
# stopped until job is done!
if checking_flag != 2:
wait_check = WaitThread()
self.threadPool.append(wait_check)
self.threadPool[-1].start()
self.threadPool[-1].QTABLEREADY.connect(partial(self.addToQueue2, data))
else:
self.addToQueue2(data)
def addToQueue2(self, data):
send_message = False
# new selected category
new_category = str(data)
gid_list = []
# find selected rows!
for row in self.userSelectedRows():
status = self.download_table.item(row, 1).text()
category = self.download_table.item(row, 12).text()
# check status of old category
if category in self.queue_list_dict.keys():
if self.queue_list_dict[category].start:
# It means queue is in download progress
status = 'downloading'
# download must be in stopped situation.
if (status == 'error' or status == 'stopped' or status == 'complete'):
# find gid
gid = self.download_table.item(row, 8).text()
# check if this gid is related to video finder
if gid in self.all_video_finder_gid_list:
video_finder_dictionary = self.persepolis_db.searchGidInVideoFinderTable(gid)
# check the Video Finder tread status
if video_finder_dictionary['video_gid'] in self.video_finder_threads_dict:
video_finder_thread = self.video_finder_threads_dict[video_finder_dictionary['video_gid']]
if video_finder_thread.active == 'no':
# add both of video and audio links
gid_list.append(video_finder_dictionary['video_gid'])
gid_list.append(video_finder_dictionary['audio_gid'])
continue
else:
send_message = True
continue
else:
# add both of video and audio links
gid_list.append(video_finder_dictionary['video_gid'])
gid_list.append(video_finder_dictionary['audio_gid'])
continue
# append gid to gid_list
gid_list.append(gid)
else:
send_message = True
# remove duplicate items
gid_list = set(gid_list)
# find row number for specific gid
for gid in gid_list:
for i in range(self.download_table.rowCount()):
row_gid = self.download_table.item(i, 8).text()
if gid == row_gid:
row = i
break
# current_category = former selected category
current_category = self.download_table.item(row, 12).text()
if current_category != new_category:
# write changes in data base
dict = {'gid': gid, 'category': new_category}
self.persepolis_db.updateDownloadTable([dict])
self.persepolis_db.setDefaultGidInAddlinkTable(gid, start_time=True, end_time=True, after_download=True)
# delete item from gid_list in current_category
current_category_dict = self.persepolis_db.searchCategoryInCategoryTable(current_category)
# get gid_list
current_category_gid_list = current_category_dict['gid_list']
# delete item
current_category_gid_list = current_category_gid_list.remove(gid)
# update category_db_table
self.persepolis_db.updateCategoryTable([current_category_dict])
# add item to gid_list of new_category
# get category_dict from data base
new_category_dict = self.persepolis_db.searchCategoryInCategoryTable(new_category)
# get gid_list
new_category_gid_list = new_category_dict['gid_list']
# add gid of item to gid_list
new_category_gid_list = new_category_gid_list.append(gid)
# update category_db_table
self.persepolis_db.updateCategoryTable([new_category_dict])
# update category in download_table
current_category_tree_text = str(current_category_tree_index.data())
if current_category_tree_text == 'All Downloads':
item = QTableWidgetItem(new_category)
self.download_table.setItem(row, 12, item)
else:
self.download_table.removeRow(row)
if send_message:
# notify user that transfer was unsuccessful
notifySend(QCoreApplication.translate("mainwindow_src_ui_tr", "Some items didn't transferred successfully!"),
QCoreApplication.translate("mainwindow_src_ui_tr", "Please stop download progress first."),
5000, 'no', parent=self)
global checking_flag
checking_flag = 0
# this method activates or deactivates start_frame according to situation
def startFrame(self, checkBox):
if self.start_checkBox.isChecked():
self.start_frame.setEnabled(True)
else:
self.start_frame.setEnabled(False)
# this method activates or deactivates end_frame according to situation
def endFrame(self, checkBox):
if self.end_checkBox.isChecked():
self.end_frame.setEnabled(True)
else:
self.end_frame.setEnabled(False)
# this method showing/hiding queue_panel_widget according to
# queue_panel_show_button text
def showQueuePanelOptions(self, button):
if not (self.show_queue_panel):
self.show_queue_panel = True
self.queue_panel_widget_frame.show()
self.queue_panel_show_button.setText(QCoreApplication.translate("mainwindow_src_ui_tr", 'Hide options'))
else:
self.show_queue_panel = False
self.queue_panel_widget_frame.hide()
self.queue_panel_show_button.setText(QCoreApplication.translate("mainwindow_src_ui_tr", 'Show options'))
def limitDialIsReleased(self):
# current_category_tree_text is the name of queue that selected by user
current_category_tree_text = str(current_category_tree_index.data())
# informing queue about changes
self.queue_list_dict[current_category_tree_text].limit_changed = True
def limitDialIsChanged(self, button):
if self.limit_dial.value() == 10:
self.limit_label.setText('Speed : Maximum')
elif self.limit_dial.value() == 0:
self.limit_label.setText('Speed : Minimum')
else:
self.limit_label.setText('Speed')
# this method handles user's shutdown request
def afterPushButtonPressed(self, button):
# current_category_tree_text is the name of queue that selected by user
current_category_tree_text = str(current_category_tree_index.data())
self.after_pushButton.setEnabled(False)
if os_type != OS.WINDOWS: # For Linux and Mac OSX
# get root password from user
passwd, ok = QInputDialog.getText(
self, 'PassWord', 'Please enter root password:', QLineEdit.Password)
if ok:
pipe = subprocess.Popen(['sudo', '-S', 'echo', 'hello'],
stdout=subprocess.DEVNULL,
stdin=subprocess.PIPE,
stderr=subprocess.DEVNULL,
shell=False)
pipe.communicate(passwd.encode())
answer = pipe.wait()
while answer != 0:
# ask password again!
passwd, ok = QInputDialog.getText(
self, 'PassWord', 'Wrong Password!\nPlease try again.', QLineEdit.Password)
if ok:
# checking password
pipe = subprocess.Popen(['sudo', '-S', 'echo', 'hello'],
stdout=subprocess.DEVNULL,
stdin=subprocess.PIPE,
stderr=subprocess.DEVNULL,
shell=False)
pipe.communicate(passwd.encode())
answer = pipe.wait()
else:
ok = False
self.after_pushButton.setEnabled(True)
break
if ok:
self.queue_list_dict[current_category_tree_text].after = True
# send password and queue name to ShutDownThread
shutdown_enable = ShutDownThread(
self, current_category_tree_text, passwd)
self.threadPool.append(shutdown_enable)
self.threadPool[-1].start()
else:
self.after_checkBox.setChecked(False)
self.queue_list_dict[current_category_tree_text].after = False
else:
self.after_checkBox.setChecked(False)
self.queue_list_dict[current_category_tree_text].after = False
else: # for windows
shutdown_enable = ShutDownThread(self, current_category_tree_text)
self.threadPool.append(shutdown_enable)
self.threadPool[-1].start()
# this method activates or deactivates after_frame according to
# after_checkBox situation
def afterFrame(self, checkBox):
# current_category_tree_text is the name of queue that selected by user
current_category_tree_text = str(current_category_tree_index.data())
if self.after_checkBox.isChecked(): # enable after_frame
self.after_frame.setEnabled(True)
self.after_pushButton.setEnabled(True)
else:
self.after_frame.setEnabled(False) # disable after_frame
# write 'canceled' for this category in temp_db .
# see shutdown.py for more information
if current_category_tree_text in self.queue_list_dict.keys():
if self.queue_list_dict[current_category_tree_text].after:
shutdown_dict = {'category': current_category_tree_text,
'shutdown': 'canceled'}
self.temp_db.updateQueueTable(shutdown_dict)
self.queue_list_dict[current_category_tree_text].after = False
# this method checks that queue started or not,
# and it shows or hides widgets in queue_panel_widget
# according to situation and set widgets in panel.
def queuePanelWidget(self, category):
# update queue panel widget items
# read queue_info_dict from data base
queue_info_dict = self.persepolis_db.searchCategoryInCategoryTable(category)
# check queue condition
if str(category) in self.queue_list_dict.keys():
queue_status = self.queue_list_dict[str(category)].start
else:
queue_status = False
if queue_status: # queue started
self.start_end_frame.hide()
self.limit_after_frame.show()
# check that if user selected 'shutdown after download'
after_status = self.queue_list_dict[str(category)].after
# if after_status is True,
# it means that user was selected
# shutdown option, after queue completed.
if after_status:
self.after_checkBox.setChecked(True)
else:
self.after_checkBox.setChecked(False)
else:
# so queue is stopped
self.start_end_frame.show()
self.limit_after_frame.hide()
# start time
# start_checkBox
if queue_info_dict['start_time_enable'] == 'yes':
self.start_checkBox.setChecked(True)
else:
self.start_checkBox.setChecked(False)
hour, minute = queue_info_dict['start_time'].split(':')
q_time = QTime(int(hour), int(minute))
self.start_time_qDataTimeEdit.setTime(q_time)
# end time
# end_checkBox
if queue_info_dict['end_time_enable'] == 'yes':
self.end_checkBox.setChecked(True)
else:
self.end_checkBox.setChecked(False)
hour, minute = queue_info_dict['end_time'].split(':')
# set time
q_time = QTime(int(hour), int(minute))
self.end_time_qDateTimeEdit.setTime(q_time)
# reverse_checkBox
if queue_info_dict['reverse'] == 'yes':
self.reverse_checkBox.setChecked(True)
else:
self.reverse_checkBox.setChecked(False)
self.afterFrame(category)
self.startFrame(category)
self.endFrame(category)
# this method opens issues page in github
def reportIssue(self, menu=None):
osCommands.xdgOpen('https://github.com/persepolisdm/persepolis/issues')
# this method opens persepolis wiki page in github
def persepolisHelp(self, menu=None):
osCommands.xdgOpen('https://github.com/persepolisdm/persepolis/wiki')
# this method opens LogWindow
def showLog(self, menu=None):
logwindow = LogWindow(
self.persepolis_setting)
self.logwindow_list.append(logwindow)
self.logwindow_list[-1].show()
# this method is called when user pressed moveUpSelectedAction
# this method subtituts selected items with upper one
def moveUpSelected(self, menu=None):
global button_pressed_counter
button_pressed_counter = button_pressed_counter + 1
# if checking_flag is equal to 1, it means that user pressed remove or
# delete button or ... . so checking download information must be stopped
# until job is done!
if checking_flag != 2:
button_pressed_thread = ButtonPressedThread()
self.threadPool.append(button_pressed_thread)
self.threadPool[-1].start()
wait_check = WaitThread()
self.threadPool.append(wait_check)
self.threadPool[-1].start()
self.threadPool[-1].QTABLEREADY.connect(self.moveUpSelected2)
else:
self.moveUpSelected2()
def moveUpSelected2(self):
# current_category_tree_text is the name of queue that selected by user
current_category_tree_text = str(current_category_tree_index.data())
# get gid_list from data base
category_dict = self.persepolis_db.searchCategoryInCategoryTable(current_category_tree_text)
gid_list = category_dict['gid_list']
# find selected rows
rows_list = self.userSelectedRows()
new_rows_list = []
# move up selected rows
for old_row in rows_list:
new_row = int(old_row) - 1
old_row_items_list = []
new_row_items_list = []
if new_row >= 0:
new_rows_list.append(new_row)
# old index and new index of item in gid_list
old_index = len(gid_list) - old_row - 1
new_index = old_index + 1
# subtitute items in gid_list
gid_list[old_index], gid_list[new_index] = gid_list[new_index], gid_list[old_index]
# subtitute items in download_table
# read current items in download_table
for i in range(13):
old_row_items_list.append(
self.download_table.item(old_row, i).text())
new_row_items_list.append(
self.download_table.item(new_row, i).text())
# substituting
for i in range(13):
# old row
item = QTableWidgetItem(new_row_items_list[i])
self.download_table.setItem(old_row, i, item)
# new row
item = QTableWidgetItem(old_row_items_list[i])
self.download_table.setItem(new_row, i, item)
# remove highlight from old rows
self.download_table.clearSelection()
# Visit this link for more information
# doc.qt.io/qt-5/qabstractitemview.html
self.download_table.setSelectionMode(QAbstractItemView.MultiSelection)
# Highlight newer rows
for row in new_rows_list:
self.download_table.selectRow(row)
# change selection mode to the normal situation
self.download_table.setSelectionMode(QAbstractItemView.ExtendedSelection)
# update data base
self.persepolis_db.updateCategoryTable([category_dict])
# this method is called if user pressed moveDownSelected action
# this method is substituting selected download item with lower download item
def moveDownSelected(self, menu=None):
global button_pressed_counter
button_pressed_counter = button_pressed_counter + 1
# if checking_flag is equal to 1, it means that user pressed remove or
# delete button or ... . so checking download information must be stopped
# until job is done!
if checking_flag != 2:
button_pressed_thread = ButtonPressedThread()
self.threadPool.append(button_pressed_thread)
self.threadPool[-1].start()
wait_check = WaitThread()
self.threadPool.append(wait_check)
self.threadPool[-1].start()
self.threadPool[-1].QTABLEREADY.connect(self.moveDownSelected2)
else:
self.moveDownSelected2()
def moveDownSelected2(self):
# an old row and new row must be substituted by each other
# find selected rows
rows_list = self.userSelectedRows()
# current_category_tree_text is the name of queue that selected by user
current_category_tree_text = str(current_category_tree_index.data())
# get gid_list from data base
category_dict = self.persepolis_db.searchCategoryInCategoryTable(
current_category_tree_text)
gid_list = category_dict['gid_list']
rows_list.reverse()
new_rows_list = []
# move up selected rows
for old_row in rows_list:
new_row = int(old_row) + 1
if new_row < self.download_table.rowCount():
new_rows_list.append(new_row)
# old index and new index in gid_list
old_index = len(gid_list) - old_row - 1
new_index = old_index - 1
# subtitute gids in gid_list
gid_list[old_index], gid_list[new_index] = gid_list[new_index], gid_list[old_index]
# subtitute items in download_table
old_row_items_list = []
new_row_items_list = []
# read current items in download_table
for i in range(13):
old_row_items_list.append(
self.download_table.item(old_row, i).text())
new_row_items_list.append(
self.download_table.item(new_row, i).text())
# substituting
for i in range(13):
# old row
item = QTableWidgetItem(new_row_items_list[i])
self.download_table.setItem(old_row, i, item)
# new_row
item = QTableWidgetItem(old_row_items_list[i])
self.download_table.setItem(new_row, i, item)
# remove highlight from old rows
self.download_table.clearSelection()
# Visit this link for more information
# doc.qt.io/qt-5/qabstractitemview.html
self.download_table.setSelectionMode(QAbstractItemView.MultiSelection)
# Highlight newer rows
for row in new_rows_list:
self.download_table.selectRow(row)
# change selection mode to the normal situation
self.download_table.setSelectionMode(QAbstractItemView.ExtendedSelection)
# update data base
self.persepolis_db.updateCategoryTable([category_dict])
# this method is called if user pressed moveSelectedDownloads action
# this method moves download files to another destination.
def moveSelectedDownloads(self, menu=None):
# initialize the path.
initializing_path = self.persepolis_setting.value(
'MainWindow/moving_path', None)
# if initializing_path is not available, so use default download_path.
if not (initializing_path):
initializing_path = str(
self.persepolis_setting.value('settings/download_path'))
# open file manager and get new download path
fname = QFileDialog.getExistingDirectory(
self, 'Select a directory', initializing_path)
if fname:
# Returns pathName with the '/' separators converted to separators that are appropriate for the underlying operating system.
# On Windows, toNativeSeparators("c:/winnt/system32") returns
# "c:\winnt\system32".
new_folder_path = QDir.toNativeSeparators(fname)
# save new_folder_path as initializing_path
self.persepolis_setting.setValue(
'MainWindow/moving_path', new_folder_path)
else:
return
gid_list = []
# find selected rows!
for row in self.userSelectedRows():
# get download status
status = self.download_table.item(row, 1).text()
# only download items with "complete" can be moved.
if (status == 'complete'):
# find gid
gid = self.download_table.item(row, 8).text()
# add gid to gid_list
gid_list.append(gid)
else:
# find filename
file_name = self.download_table.item(row, 0).text()
# show error message
# TODO: no value for message2
notifySend(QCoreApplication.translate("mainwindow_src_ui_tr",
message1='Operation was not successful! Following download must be completed first: '),
message2=file_name, time=5000, sound='fail', parent=self)
# move files with MoveThread
# MoveThread is created to pervent UI freezing.
move_thread = MoveThread(self, gid_list, new_folder_path)
self.threadPool.append(move_thread)
self.threadPool[-1].start()
self.threadPool[-1].NOTIFYSENDSIGNAL.connect(self.notifySendFromThread)
# see browser_plugin_queue.py file
def queueSpiderCallBack(self, filename, child, row_number):
item = QTableWidgetItem(str(filename))
# add checkbox to the item
item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
if child.links_table.item(int(row_number), 0).checkState() == Qt.Checked:
item.setCheckState(Qt.Checked)
else:
item.setCheckState(Qt.Unchecked)
child.links_table.setItem(int(row_number), 0, item)
# see addlink.py file
def addLinkSpiderCallBack(self, spider_dict, child):
# get file_name and file_size
file_name = spider_dict['file_name']
file_size = spider_dict['file_size']
if file_size:
file_size = 'Size: ' + str(file_size)
if child.size_label.text() == 'None' or child.size_label.text() == '':
child.size_label.setText(file_size)
else:
# It's updated before! dont change it.
return
if file_name and not (child.change_name_checkBox.isChecked()):
child.change_name_lineEdit.setText(file_name)
child.change_name_checkBox.setChecked(True)
def spiderUpdate(self, dict):
gid = dict['gid']
row = None
for i in range(self.download_table.rowCount()):
row_gid = self.download_table.item(i, 8).text()
if gid == row_gid:
row = i
break
# update download_table items
if row is not None:
update_list = [dict['file_name'], dict['status'], dict['size'], dict['downloaded_size'], dict['percent'],
dict['connections'], dict['rate'], dict['estimate_time_left'], dict['gid'], None, None, None, None]
for i in range(12):
# update download_table cell if update_list item in not None
if update_list[i]:
text = update_list[i]
else:
text = self.download_table.item(row, i).text()
# create a QTableWidgetItem
item = QTableWidgetItem(text)
# set item
try:
self.download_table.setItem(row, i, item)
except Exception as problem:
logger.sendToLog(
"Error occurred while updating download table", "ERROR")
logger.sendToLog(problem, "ERROR")
# this method deletes all items in data base
def clearDownloadList(self, item):
# if checking_flag is equal to 1, it means that user pressed remove or
# delete button or ... . so checking download information must be
# stopped until job is done!
if checking_flag != 2:
wait_check = WaitThread()
self.threadPool.append(wait_check)
self.threadPool[-1].start()
self.threadPool[-1].QTABLEREADY.connect(self.clearDownloadList2)
else:
self.clearDownloadList2()
def clearDownloadList2(self):
# all Downloads must be stopped by user
gid_list = self.persepolis_db.findActiveDownloads()
if len(gid_list) != 0:
error_messageBox = QMessageBox()
error_messageBox.setText(
QCoreApplication.translate("mainwindow_src_ui_tr", 'Stop all downloads first!'))
error_messageBox.setWindowTitle('Error!')
error_messageBox.exec_()
return
# reset data base
self.persepolis_db.resetDataBase()
self.temp_db.resetDataBase()
# highlight "All Downloads" in category_tree
all_download_index = self.category_tree_model.index(0, 0)
self.category_tree.setCurrentIndex(all_download_index)
self.categoryTreeSelected(all_download_index)
# clear download_table
self.download_table.setRowCount(0)
# tell the CheckDownloadInfoThread that job is done!
global checking_flag
checking_flag = 0
def showVideoFinderAddLinkWindow(self, input_dict=None, menu=None):
# first check youtube_dl_is_installed and ffmpeg_is_installed value!
# if youtube_dl or ffmpeg is not installed show an error message.
if youtube_dl_is_installed and ffmpeg_is_installed:
if not (input_dict):
input_dict = {}
video_finder_addlink_window = VideoFinderAddLink(
parent=self, receiver_slot=self.videoFinderCallBack, settings=self.persepolis_setting, video_dict=input_dict)
self.addlinkwindows_list.append(video_finder_addlink_window)
video_finder_addlink_window.show()
video_finder_addlink_window.raise_()
video_finder_addlink_window.activateWindow()
else:
error_message = ''
if not (youtube_dl_is_installed):
error_message = QCoreApplication.translate("mainwindow_src_ui_tr", 'yt-dlp is not installed!')
error_message = error_message + '\n'
if not (ffmpeg_is_installed):
error_message = error_message + \
QCoreApplication.translate("mainwindow_src_ui_tr", 'ffmpeg is not installed!')
error_messageBox = QMessageBox()
error_messageBox.setText(error_message)
error_messageBox.setWindowTitle('Error!')
error_messageBox.exec_()
return
# call back of VideoFinderAddLink window.
def videoFinderCallBack(self, add_link_dictionary_list, download_later, category):
# if we have only one link so we can download it as a single_video_link.
# but if we have seperated video and audio, then we must use VideoFinder thread and ...
if len(add_link_dictionary_list) == 1:
self.callBack(add_link_dictionary=add_link_dictionary_list[0], download_later=download_later, category=category, single_video_link=True)
return
category = str(category)
for add_link_dictionary in add_link_dictionary_list:
# persepolis identifies each download by the ID called GID. The GID must be
# hex string of 16 characters.
# if user presses ok button on add link window , a gid generates for download.
gid = self.gidGenerator()
# add gid to add_link_dictionary
add_link_dictionary['gid'] = gid
# download_info_file_list is a list that contains ['file_name' ,
# 'status' , 'size' , 'downloaded size' ,'download percentage' ,
# 'number of connections' ,'Transfer rate' , 'estimate_time_left' ,
# 'gid' , 'link' , 'first_try_date' , 'last_try_date', 'category']
# if user or browser_plugin defined filename then file_name is valid in
# add_link_dictionary['out']
if add_link_dictionary['out']:
file_name = add_link_dictionary['out']
else:
file_name = '***'
# If user selected a queue in add_link window , then download must be
# added to queue and and download must be started with queue so >>
# download_later = True
if str(category) != 'Single Downloads':
download_later = True
# change video status to waiting
if not (download_later) and gid == add_link_dictionary_list[0]['gid']:
status = 'waiting'
else:
status = 'stopped'
# get now time and date
date = nowDate()
dictionary = {'file_name': file_name,
'status': status,
'size': '***',
'downloaded_size': '***',
'percent': '***',
'connections': '***',
'rate': '***',
'estimate_time_left': '***',
'gid': gid,
'link': add_link_dictionary['link'],
'first_try_date': date,
'last_try_date': date,
'category': category}
# write information in data_base
self.persepolis_db.insertInDownloadTable([dictionary])
self.persepolis_db.insertInAddLinkTable([add_link_dictionary])
# find selected category in left side panel
for i in range(self.category_tree_model.rowCount()):
category_tree_item_text = str(
self.category_tree_model.index(i, 0).data())
if category_tree_item_text == category:
category_index = i
break
# highlight selected category in category_tree
category_tree_model_index = self.category_tree_model.index(
category_index, 0)
current_category_tree_text = current_category_tree_index.data()
self.category_tree.setCurrentIndex(category_tree_model_index)
if current_category_tree_text != category:
self.categoryTreeSelected(category_tree_model_index)
else:
# create a row in download_table for new download
list = [file_name, status, '***', '***', '***',
'***', '***', '***', gid, add_link_dictionary['link'], date, date, category]
self.download_table.insertRow(0)
j = 0
# add item in list to the row
for i in list:
item = QTableWidgetItem(i)
self.download_table.setItem(0, j, item)
j = j + 1
# create an item in data_base
# this item will updated by yt-dlp
# and contains download information.
video_Finder2_data_base = {'gid': gid,
'download_status': status,
'file_name': file_name,
'eta': '0',
'download_speed_str': '0',
'downloaded_size': 0,
'file_size': 0,
'download_percent': 0,
'fragments': '0/0',
'error_message': ''}
# write it in data_base
self.persepolis_db.insertInVideoFinderTable2([video_Finder2_data_base])
# add video_gid and audio_gid to data base
dictionary = {'video_gid': add_link_dictionary_list[0]['gid'],
'audio_gid': add_link_dictionary_list[1]['gid'],
'video_completed': 'no',
'audio_completed': 'no',
'muxing_status': 'no',
'checking': 'no',
'download_path': add_link_dictionary_list[0]['download_path']}
self.persepolis_db.insertInVideoFinderTable([dictionary])
# add video_gid and audio_gid to all_video_finder_gid_list
self.all_video_finder_gid_list.append(dictionary['video_gid'])
self.all_video_finder_video_gid_list.append(dictionary['video_gid'])
self.all_video_finder_gid_list.append(dictionary['audio_gid'])
self.all_video_finder_audio_gid_list.append(dictionary['audio_gid'])
# if user didn't press download_later_pushButton in add_link window
# then create new qthread for new download!
if not (download_later):
new_download = VideoFinder(dictionary, self)
self.threadPool.append(new_download)
self.threadPool[-1].start()
self.threadPool[-1].VIDEOFINDERCOMPLETED.connect(self.videoFinderCompleted)
# add thread to video_finder_threads_dict
self.video_finder_threads_dict[dictionary['video_gid']] = new_download
# open progress window for download.
self.progressBarOpen(dictionary['video_gid'])
# notify user
if not (add_link_dictionary_list[0]['start_time']):
message = QCoreApplication.translate("mainwindow_src_ui_tr", "Download Starts")
notifySend(message, '', 10000, 'no', parent=self)
else:
# write name and size of download files in download's table
for add_link_dictionary in add_link_dictionary_list:
new_spider = SpiderThread(add_link_dictionary, self)
self.threadPool.append(new_spider)
self.threadPool[-1].start()
self.threadPool[-1].SPIDERSIGNAL.connect(self.spiderUpdate)
else:
# write name and size of download files in download's table
for add_link_dictionary in add_link_dictionary_list:
new_spider = SpiderThread(add_link_dictionary, self)
self.threadPool.append(new_spider)
self.threadPool[-1].start()
self.threadPool[-1].SPIDERSIGNAL.connect(self.spiderUpdate)
# this method is called by VideoFinder thread
# this method handles error_message
# if video finder done it's job successfully,
# then this method shows AfterDownloadWindow
def videoFinderCompleted(self, complete_dictionary):
# if checking_flag is equal to 1, it means that user pressed remove or
# delete button or ... . so checking download information must be
# stopped until job is done!
if checking_flag != 2:
wait_check = WaitThread()
self.threadPool.append(wait_check)
self.threadPool[-1].start()
self.threadPool[-1].QTABLEREADY.connect(
partial(self.videoFinderCompleted2, complete_dictionary))
else:
self.videoFinderCompleted2(complete_dictionary)
def videoFinderCompleted2(self, complete_dictionary):
# remove item from video_finder_threads_dict
del self.video_finder_threads_dict[complete_dictionary['video_gid']]
error_message = complete_dictionary['error']
# close progress window
if complete_dictionary['video_gid'] in self.progress_window_list_dict.keys():
# find progress_window for this gid
member_number = self.progress_window_list_dict[complete_dictionary['video_gid']]
progress_window = self.progress_window_list[member_number]
# close progress window
progress_window.close()
# download was successful
if error_message == 'no error':
# delete gids from all_video_finder_gid_list
self.all_video_finder_gid_list.remove(complete_dictionary['video_gid'])
self.all_video_finder_video_gid_list.remove(complete_dictionary['video_gid'])
self.all_video_finder_gid_list.remove(complete_dictionary['audio_gid'])
self.all_video_finder_audio_gid_list.remove(complete_dictionary['audio_gid'])
# delete audio file
# find row
row = None
for i in range(self.download_table.rowCount()):
row_gid = self.download_table.item(i, 8).text()
if complete_dictionary['audio_gid'] == row_gid:
row = i
break
# muxing is complete
# so remove unused files
# find download path
audio_add_link_dictionary = self.persepolis_db.searchGidInAddLinkTable(complete_dictionary['audio_gid'])
video_add_link_dictionary = self.persepolis_db.searchGidInAddLinkTable(complete_dictionary['video_gid'])
audio_file_path = audio_add_link_dictionary['download_path']
video_file_path = video_add_link_dictionary['download_path']
osCommands.remove(audio_file_path)
osCommands.remove(video_file_path)
# remove audio row from download_table
if row is not None:
self.download_table.removeRow(row)
# remove download item from data base
self.persepolis_db.deleteItemInDownloadTable(
complete_dictionary['audio_gid'], complete_dictionary['category'])
# file name and file size and downloaded size and download path must be changed for video
video_add_link_dictionary['download_path'] = complete_dictionary['final_path']
# update data base
self.persepolis_db.updateAddLinkTable([video_add_link_dictionary])
# get download_table_dict for video_gid
video_download_table_dict = self.persepolis_db.searchGidInDownloadTable(complete_dictionary['video_gid'])
video_download_table_dict['size'] = complete_dictionary['final_size']
video_download_table_dict['status'] = 'complete'
video_download_table_dict['percent'] = '100%'
video_download_table_dict['downloaded_size'] = complete_dictionary['final_size']
video_download_table_dict['file_name'] = urllib.parse.unquote(
os.path.basename(complete_dictionary['final_path']))
# update data base
self.persepolis_db.updateDownloadTable([video_download_table_dict])
# update download_table
# find row
row = None
for i in range(self.download_table.rowCount()):
row_gid = self.download_table.item(i, 8).text()
if complete_dictionary['video_gid'] == row_gid:
row = i
break
if row is not None:
# create a QTableWidgetItem
item = QTableWidgetItem(str(video_download_table_dict['file_name']))
# set item
self.download_table.setItem(row, 0, item)
# create a QTableWidgetItem
item = QTableWidgetItem(str(video_download_table_dict['size']))
# set item
self.download_table.setItem(row, 2, item)
# create a QTableWidgetItem
item = QTableWidgetItem(str(video_download_table_dict['downloaded_size']))
# set item
self.download_table.setItem(row, 3, item)
# update download_table (refreshing!)
self.download_table.viewport().update()
if complete_dictionary['category'] == 'Single Downloads':
# show download complete dialog
afterdownloadwindow = AfterDownloadWindow(
self, video_download_table_dict, self.persepolis_setting)
self.afterdownload_list.append(afterdownloadwindow)
self.afterdownload_list[-1].show()
# bringing AfterDownloadWindow on top
self.afterdownload_list[-1].raise_()
self.afterdownload_list[-1].activateWindow()
elif error_message == 'not enough free space':
# show error message
notifySend(QCoreApplication.translate("mainwindow_src_ui_tr", "Not enough free space in:"),
complete_dictionary['download_path'],
10000, 'fail', parent=self)
elif error_message == 'ffmpeg error':
# show error message
notifySend(QCoreApplication.translate("mainwindow_src_ui_tr", "an error occurred"),
QCoreApplication.translate("mainwindow_src_ui_tr", "muxing error"),
10000, 'fail', parent=self)
# telling the CheckDownloadInfoThread that job is done!
global checking_flag
checking_flag = 0
# this method is called, if user clicks on muxing_pushButton
def muxingPushButtonPressed(self, button):
# find user's selected row
selected_row_return = self.selectedRow()
if selected_row_return is not None:
# find download category
category = self.download_table.item(selected_row_return, 12).text()
# if category is not "single downloads" , then send notification for error
if category != "Single Downloads":
notifySend(QCoreApplication.translate("mainwindow_src_ui_tr", "Operation was not successful."),
QCoreApplication.translate("mainwindow_src_ui_tr",
"Please resume the following category: ") + category,
10000, 'fail', parent=self)
return
# find download gid
gid = self.download_table.item(selected_row_return, 8).text()
# read data from data base
result_dictionary = self.persepolis_db.searchGidInVideoFinderTable(gid)
# create new thread for this download
# see VideoFinder thread for more information
new_download = VideoFinder(result_dictionary, self)
self.threadPool.append(new_download)
self.threadPool[-1].start()
self.threadPool[-1].VIDEOFINDERCOMPLETED.connect(self.videoFinderCompleted)
# add thread to video_finder_threads_dict
self.video_finder_threads_dict[result_dictionary['video_gid']] = new_download
# create new progress_window
self.progressBarOpen(gid)
def changeIcon(self, new_icons):
global icons
icons = ':/' + str(new_icons) + '/'
action_icon_dict = {self.stopAllAction: 'stop_all', self.minimizeAction: 'minimize', self.addlinkAction: 'add', self.addtextfileAction: 'file', self.addFromClipboardAction: 'clipboard', self.resumeAction: 'play', self.pauseAction: 'pause', self.stopAction: 'stop', self.propertiesAction: 'setting', self.progressAction: 'window', self.openFileAction: 'file', self.openDownloadFolderAction: 'folder', self.openDefaultDownloadFolderAction: 'folder', self.exitAction: 'exit',
self.createQueueAction: 'add_queue', self.removeQueueAction: 'remove_queue', self.startQueueAction: 'start_queue', self.stopQueueAction: 'stop_queue', self.preferencesAction: 'preferences', self.aboutAction: 'about', self.issueAction: 'about', self.videoFinderAddLinkAction: 'video_finder', self.qmenu: 'menu'}
for key in action_icon_dict.keys():
key.setIcon(QIcon(icons + str(action_icon_dict[key])))
self.selectDownloads()
================================================
FILE: persepolis/scripts/newopen.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import ast
# This function is writting a list in file_path in dictionary format
def writeList(file_path, list):
dictionary = {'list': list}
f = open(file_path, 'w')
f.writelines(str(dictionary))
f.close()
# This function is reading file_path and return content of file in list format
def readList(file_path, mode='dictionary'):
f = open(file_path, 'r')
f_string = f.readline()
f.close()
dictionary = ast.literal_eval(f_string.strip())
list = dictionary['list']
if mode == 'string':
list[9] = str(list[9])
return list
# this function is reading a file that contains dictionary , and extracts
# dictionary from it.
def readDict(file_path):
f = open(file_path)
f_lines = f.readlines()
f.close()
dict_str = str(f_lines[0].strip())
return_dict = ast.literal_eval(dict_str)
return return_dict
================================================
FILE: persepolis/scripts/osCommands.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
from persepolis.constants import OS
import subprocess
import platform
import shutil
import os
os_type = platform.system()
home_address = os.path.expanduser("~")
# this method finds file manager in linux
def findFileManager():
pipe = subprocess.check_output(['xdg-mime',
'query',
'default',
'inode/directory'], shell=False)
file_manager = pipe.decode('utf-8').strip().lower()
return file_manager
def touch(file_path):
if not (os.path.isfile(file_path)):
f = open(file_path, 'w')
f.close()
# xdgOpen opens files or folders
def xdgOpen(file_path, f_type='file', path='file'):
# we have a file path and we want to open it's directory.
# highlit(select) file in file manager after opening.
# it's help to find file easier :)
if f_type == 'folder' and path == 'file':
highlight = True
else:
highlight = False
# for linux and bsd
if os_type in OS.UNIX_LIKE:
try:
file_manager = findFileManager()
file_manager_found = True
except Exception as e:
file_manager_found = False
from persepolis.scripts import logger
logger.sendToLog(str(e), "ERROR")
# check default file manager.
# some file managers wouldn't support highlighting.
if highlight and file_manager_found:
# dolphin is kde plasma file manager
if 'dolphin' in file_manager:
subprocess.Popen(['dolphin',
'--select', file_path],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
shell=False)
# dde-file-manager is deepin file manager
elif 'dde-file-manager' in file_manager:
subprocess.Popen(['dde-file-manager',
'--show-item', file_path],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
shell=False)
# if file manager is nautilus or nemo or pantheon-file-manager
elif file_manager in ['org.gnome.nautilus.desktop', 'nemo.desktop', 'io.elementary.files.desktop', 'thunar.desktop']:
# nautilus is gnome file manager.
if 'nautilus' in file_manager:
file_manager = 'nautilus'
# pantheon-files is pantheon file manager(elementary OS).
elif 'elementary' in file_manager:
file_manager = 'io.elementary.files'
# nemo is cinnamon's file manager.
elif 'nemo' in file_manager:
file_manager = 'nemo'
# thunar is xfce file manager
elif 'thunar' in file_manager:
file_manager = 'thunar'
subprocess.Popen([file_manager,
file_path],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
shell=False)
# caja is mate file manager
elif 'caja' in file_manager:
subprocess.Popen(['caja',
'--select', file_path],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
shell=False)
else:
# find folder path
file_name = os.path.basename(str(file_path))
file_path_split = file_path.split(file_name)
del file_path_split[-1]
folder_path = file_name.join(file_path_split)
subprocess.Popen(['xdg-open', folder_path],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
shell=False)
else:
if highlight:
# find folder path
file_name = os.path.basename(str(file_path))
file_path_split = file_path.split(file_name)
del file_path_split[-1]
folder_path = file_name.join(file_path_split)
subprocess.Popen(['xdg-open', folder_path],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
shell=False)
else:
subprocess.Popen(['xdg-open', file_path],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
shell=False)
# for Mac OS X
elif os_type == OS.OSX:
if highlight:
subprocess.Popen(['open', '-R', file_path],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
shell=False)
else:
subprocess.Popen(['open', file_path],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
shell=False)
# for MS Windows
elif os_type == OS.WINDOWS:
CREATE_NO_WINDOW = 0x08000000
if highlight:
subprocess.Popen(['explorer.exe', '/select,', file_path],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
shell=False,
creationflags=CREATE_NO_WINDOW)
else:
subprocess.Popen(['cmd', '/C', 'start', file_path, file_path],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
shell=False,
creationflags=CREATE_NO_WINDOW)
# remove file with path of file_path
def remove(file_path):
if os.path.isfile(file_path):
try:
# function returns ok, if operation was successful
os.remove(file_path)
return 'ok'
except FileNotFoundError:
# function returns this, if operation was not successful
return 'cant'
else:
# function returns this , if file is not existed
return 'no'
# removeDir removes folder : folder_path
def removeDir(folder_path):
# check folder_path existence
if os.path.isdir(folder_path):
try:
# remove folder
shutil.rmtree(folder_path)
return 'ok'
except FileNotFoundError:
# return 'cant' if removing was not successful
return 'cant'
else:
# return 'no' if file didn't existed
return 'no'
# make directory
def makeDirs(folder_path, hidden=False):
if hidden:
# create hidden attribute directory.
if os_type == OS.WINDOWS:
os.makedirs(folder_path, exist_ok=True)
# in MS Windows "attrib +h" command hidden directory.
CREATE_NO_WINDOW = 0x08000000
subprocess.Popen(['attrib', '+h', folder_path],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
shell=False,
creationflags=CREATE_NO_WINDOW)
else:
# In linux and bsd a dot character must be added in the start of the directory's name
dir_name = os.path.basename(folder_path)
dir_name = '.' + dir_name
folder_path = os.path.join(os.path.dirname(folder_path), dir_name)
os.makedirs(folder_path, exist_ok=True)
else:
os.makedirs(folder_path, exist_ok=True)
return folder_path
# this function returns mount point
def findMountPoint(path):
while not os.path.ismount(path):
path = os.path.dirname(path)
return path
# move downloaded file to another destination.
def moveFile(old_file_path, new_path, new_path_type='folder'):
# new_path_type can be file or folder
# if it's folder so we have folder path
# else we have new file path that includes file name
if os.path.isfile(old_file_path):
if new_path_type == 'folder':
# check availability of directory
check_path = os.path.isdir(new_path)
else:
check_path = True
if check_path:
try:
# move file to new_path
shutil.move(old_file_path, new_path)
return True
except FileNotFoundError:
return False
else:
return False
else:
return False
# copy a file to another destination
# Warining! new_path must be a complete file path not only a directory
def copyFile(old_path, new_path):
# make new directory if it's necessary
dir_path = os.path.dirname(new_path)
makeDirs(dir_path)
try:
shutil.copy(old_path, new_path)
return new_path
except FileNotFoundError:
return False
================================================
FILE: persepolis/scripts/persepolis.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
try:
from PySide6 import QtWidgets, QtCore
from PySide6.QtCore import __version__ as QT_VERSION_STR
from PySide6.QtGui import QFont
from PySide6.QtCore import QFile, QTextStream, QSettings, Qt
except ImportError:
from PyQt5 import QtWidgets, QtCore
from PyQt5.QtGui import QFont
from PyQt5.QtCore import QFile, QTextStream, QSettings, Qt, QT_VERSION_STR
from persepolis.gui import resources
import traceback
from persepolis.scripts.error_window import ErrorWindow
import json
import struct
import argparse
from persepolis.scripts import osCommands
from persepolis.scripts.useful_tools import osAndDesktopEnvironment, determineConfigFolder
from persepolis.constants import OS
from persepolis.constants import VERSION
from copy import deepcopy
import sys
import os
# finding os platform
os_type, desktop_env = osAndDesktopEnvironment()
# Don't run persepolis as root!
if os_type in (OS.UNIX_LIKE + [OS.OSX]):
uid = os.getuid()
if uid == 0:
print('Do not run persepolis as root.')
sys.exit(1)
# initialization
# find home address
home_address = os.path.expanduser("~")
# persepolis config_folder
config_folder = determineConfigFolder()
# persepolis tmp folder path
persepolis_tmp = os.path.join(config_folder, 'persepolis_tmp')
# if lock_file_validation == True >> not another instance running,
# else >> another instance of persepolis is running now.
global lock_file_validation
if os_type != OS.WINDOWS:
import fcntl
user_name_split = home_address.split('/')
user_name = user_name_split[2]
# persepolis lock file
lock_file = '/tmp/persepolis_exec_' + user_name + '.lock'
# create lock file
fp = open(lock_file, 'w')
try:
fcntl.lockf(fp, fcntl.LOCK_EX | fcntl.LOCK_NB)
lock_file_validation = True # Lock file created successfully!
except IOError:
lock_file_validation = False # creating lock_file was unsuccessful! So persepolis is still running
else: # for windows
# pypiwin32 must be installed by pip
from win32event import CreateMutex
from win32api import GetLastError
from winerror import ERROR_ALREADY_EXISTS
handle = CreateMutex(None, 1, 'persepolis_download_manager')
if GetLastError() == ERROR_ALREADY_EXISTS:
lock_file_validation = False
else:
lock_file_validation = True
# run persepolis mainwindow
if lock_file_validation:
# execute initialization script
from persepolis.scripts import initialization
from persepolis.scripts.mainwindow import MainWindow
# set "persepolis" name for this process in linux and bsd
if os_type in OS.UNIX_LIKE:
try:
from setproctitle import setproctitle
setproctitle("persepolisdm")
except ImportError:
from persepolis.scripts import logger
logger.sendToLog('setproctitle is not installed!', "ERROR")
# load persepolis_settings
persepolis_setting = QSettings('persepolis_download_manager', 'persepolis')
class PersepolisApplication(QtWidgets.QApplication):
def __init__(self, argv):
super().__init__(argv)
# set style
def setPersepolisStyle(self, style):
self.persepolis_style = style
self.setStyle(style)
# font and font_size
def setPersepolisFont(self, font, font_size, custom_font):
self.persepolis_font = font
self.persepolis_font_size = font_size
if custom_font == 'yes':
self.setFont(QFont(font, font_size))
# color_scheme
def setPersepolisColorScheme(self, color_scheme):
self.persepolis_color_scheme = color_scheme
if color_scheme == 'Dark Fusion':
file = QFile(":/dark_style.qss")
file.open(QFile.ReadOnly | QFile.Text)
stream = QTextStream(file)
self.setStyleSheet(stream.readAll())
elif color_scheme == 'Light Fusion':
file = QFile(":/light_style.qss")
file.open(QFile.ReadOnly | QFile.Text)
stream = QTextStream(file)
self.setStyleSheet(stream.readAll())
# create terminal arguments
parser = argparse.ArgumentParser(description='Persepolis Download Manager')
# parser.add_argument('chromium', nargs = '?', default = 'no', help='this switch is used for chrome native messaging in Linux and Mac')
parser.add_argument('--link', action='store', nargs=1, help='Download link.(Use "" for links)')
parser.add_argument('--referer', action='store', nargs=1,
help='Set an http referrer (Referer). This affects all http/https downloads. If * is given, the download URI is also used as the referrer.')
parser.add_argument('--load-cookies', action='store', nargs=1, help='Set cookies file path.')
parser.add_argument('--agent', action='store', nargs=1,
help='Set user agent for HTTP(S) downloads. Default: aria2/$VERSION, $VERSION is replaced by package version.')
parser.add_argument('--headers', action='store', nargs=1, help='Append HEADER to HTTP request header. ')
parser.add_argument('--name', action='store', nargs=1, help='The file name of the downloaded file. ')
parser.add_argument('--default', action='store_true', help='restore default setting')
parser.add_argument('--clear', action='store_true', help='Clear download list and user setting!')
parser.add_argument('--tray', action='store_true',
help="Persepolis is starting in tray icon. It's useful when you want to put persepolis in system's startup.")
parser.add_argument('--parent-window', action='store', nargs=1,
help='this switch is used for chrome native messaging in Windows')
parser.add_argument('--version', action='version', version='Persepolis Download Manager ' + VERSION.version_str)
# Clears unwanted args ( like args from Browers via NHM )
# unknown arguments (may sent by browser) will save in unknownargs.
args, unknownargs = parser.parse_known_args()
# if --execute >> yes >>> persepolis main window will start.
# if --execute >> no >>> persepolis started before!
browser_url = True
add_link_dictionary = {}
plugin_list = []
browser_plugin_dict = {'link': None,
'referer': None,
'load_cookies': None,
'user_agent': None,
'header': None,
'out': None
}
# This dirty trick will show Persepolis version when there are unknown args
# Unknown args are sent by Browsers for NHM
if args.parent_window or unknownargs:
# Platform specific configuration
if os_type == OS.WINDOWS:
# Set the default I/O mode to O_BINARY in windows
import msvcrt
msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
# Send message to browsers plugin
message = '{"enable": true, "version": "1.85"}'.encode('utf-8')
sys.stdout.buffer.write((struct.pack('i', len(message))))
sys.stdout.buffer.write(message)
sys.stdout.flush()
text_length_bytes = sys.stdin.buffer.read(4)
# Unpack message length as 4 byte integer.
text_length = struct.unpack('@I', text_length_bytes)[0]
# Read the text (JSON object) of the message.
text = sys.stdin.buffer.read(text_length).decode("utf-8")
if text:
new_dict = json.loads(text)
if 'url_links' in new_dict:
# new_dict is sended by persepolis browser add-on.
# new_dict['url_links'] contains some lists.
# every list contains link information.
for item in new_dict['url_links']:
copy_dict = deepcopy(browser_plugin_dict)
if 'url' in item.keys():
copy_dict['link'] = str(item['url'])
if 'header' in item.keys() and item['header'] != '':
copy_dict['header'] = item['header']
if 'referrer' in item.keys() and item['referrer'] != '':
copy_dict['referer'] = item['referrer']
if 'filename' in item.keys() and item['filename'] != '':
copy_dict['out'] = os.path.basename(str(item['filename']))
if 'useragent' in item.keys() and item['useragent'] != '':
copy_dict['user_agent'] = item['useragent']
if 'load_cookies' in item.keys() and item['load_cookies'] != '':
copy_dict['load_cookies'] = item['load_cookies']
plugin_list.append(copy_dict)
else:
browser_url = False
# persepolis --clear >> remove config_folder
if args.clear:
# get config_folder address
config_folder = determineConfigFolder()
# remove config_folder
osCommands.removeDir(config_folder)
# exit
sys.exit(0)
# persepolis --default >> remove persepolis setting.
if args.default:
persepolis_setting.clear()
persepolis_setting.sync()
print('Persepolis restored default')
sys.exit(0)
if args.link:
add_link_dictionary['link'] = "".join(args.link)
# if plugins call persepolis, then just start persepolis in system tray
args.tray = True
if args.referer:
add_link_dictionary['referer'] = "".join(args.referer)
else:
add_link_dictionary['referer'] = None
if args.load_cookies:
add_link_dictionary['load_cookies'] = "".join(args.load_cookies)
else:
add_link_dictionary['load_cookies'] = None
if args.agent:
add_link_dictionary['user_agent'] = "".join(args.agent)
else:
add_link_dictionary['user_agent'] = None
if args.headers:
add_link_dictionary['header'] = "".join(args.headers)
else:
add_link_dictionary['header'] = None
if args.name:
add_link_dictionary['out'] = "".join(args.name)
else:
add_link_dictionary['out'] = None
if args.tray:
start_in_tray = True
else:
start_in_tray = False
# when browsers plugin calls persepolis or user runs persepolis by terminal arguments,
# then persepolis creates a request file in persepolis_tmp folder and link information added to
# plugins_db.db file(see data_base.py for more information).
# persepolis mainwindow checks persepolis_tmp for plugins request file every 2 seconds (see CheckingThread class in mainwindow.py)
# when request received in CheckingThread, a popup window (AddLinkWindow) comes up and window gets additional download information
# from user (port , proxy , ...) and download starts and request file deleted
if ('link' in add_link_dictionary.keys()):
plugin_dict = {'link': add_link_dictionary['link'],
'referer': add_link_dictionary['referer'],
'load_cookies': add_link_dictionary['load_cookies'],
'user_agent': add_link_dictionary['user_agent'],
'header': add_link_dictionary['header'],
'out': add_link_dictionary['out']
}
plugin_list.append(plugin_dict)
if len(plugin_list) != 0:
# import PluginsDB
from persepolis.scripts.data_base import PluginsDB
# create an object for PluginsDB
plugins_db = PluginsDB()
# add plugin_list to plugins_table in plugins.db file.
plugins_db.insertInPluginsTable(plugin_list)
# Job is done! close connections.
plugins_db.closeConnections()
# notify that a link is added!
plugin_ready = os.path.join(persepolis_tmp, 'persepolis-plugin-ready')
osCommands.touch(plugin_ready)
# start persepolis in system tray
start_in_tray = True
# start persepolis in system tray if browser executed
# and if user select this option in preferences window.
if str(persepolis_setting.value('settings/browser-persepolis')) == 'yes' and (args.parent_window or unknownargs):
start_persepolis_if_browser_executed = True
start_in_tray = True
else:
start_persepolis_if_browser_executed = False
def main():
# if lock_file is existed , it means persepolis is still running!
if lock_file_validation and (not ((args.parent_window or unknownargs) and browser_url is False) or ((args.parent_window or unknownargs) and start_persepolis_if_browser_executed)):
QAPP = QtWidgets.QApplication.instance()
if QAPP is None:
# We do not have an already instantiated QApplication
# let's add some sane defaults
# hidpi handling
qtVersionCompare = tuple(map(int, QT_VERSION_STR.split(".")))
if qtVersionCompare > (6, 0):
# Qt6 seems to support hidpi without needing to do anything so continue
pass
elif qtVersionCompare > (5, 14):
try:
os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1"
QtWidgets.QApplication.setHighDpiScaleFactorRoundingPolicy(
QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
)
except Exception as error_message:
from persepolis.scripts import logger
logger.sendToLog(str(error_message), "ERROR")
else: # qt 5.12 and 5.13
try:
QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps)
except Exception as error_message:
from persepolis.scripts import logger
logger.sendToLog(str(error_message), "ERROR")
# set QT_AUTO_SCREEN_SCALE_FACTOR to 1 for "high DPI displays"
# os.environ['QT_AUTO_SCREEN_SCALE_FACTOR'] = '1'
# run mainwindow
# set color_scheme and style
# see palettes.py and setting.py
# create QApplication
persepolis_download_manager = PersepolisApplication(sys.argv)
# setQuitOnLastWindowClosed(False) is needed to prevent persepolis exiting,
# when it's minimized in system tray.
persepolis_download_manager.setQuitOnLastWindowClosed(False)
# # Enable High DPI display
# try:
# if hasattr(QStyleFactory, 'AA_UseHighDpiPixmaps'):
# persepolis_download_manager.setAttribute(Qt.AA_UseHighDpiPixmaps)
# except:
# from persepolis.scripts import logger
# # write error_message in log file.
# logger.sendToLog('Qt.AA_UseHighDpiPixmaps is not available!', "ERROR")
# this line is added fot fixing persepolis view in HighDpi displays
# more information at: https://doc.qt.io/qt-5/highdpi.html
# try:
# persepolis_download_manager.setAttribute(Qt.AA_EnableHighDpiScaling)
# except:
# from persepolis.scripts import logger
# # write error_message in log file.
# logger.sendToLog('Qt.AA_EnableHighDpiScaling is not available!', "ERROR")
# set organization name and domain and application name
persepolis_download_manager.setOrganizationName('com.github.persepolisdm.persepolis')
persepolis_download_manager.setApplicationName('PersepolisDM')
persepolis_download_manager.setDesktopFileName('com.github.persepolisdm.persepolis')
persepolis_download_manager.setApplicationVersion(VERSION.version_str)
# Persepolis setting
persepolis_download_manager.setting = QSettings('persepolis_download_manager', 'persepolis')
# get user's desired font and style , ... from setting
custom_font = persepolis_download_manager.setting.value('settings/custom-font')
font = persepolis_download_manager.setting.value('settings/font')
font_size = int(persepolis_download_manager.setting.value('settings/font-size'))
style = persepolis_download_manager.setting.value('settings/style')
color_scheme = persepolis_download_manager.setting.value('settings/color-scheme')
ui_direction = persepolis_download_manager.setting.value('ui_direction')
# set style
# check available style first
available_styles = QtWidgets.QStyleFactory.keys()
# add system style
available_styles.append('System')
# if user selected style not available, set 'Fusion' for style
if style not in available_styles:
style = 'Fusion'
color_scheme = 'Dark Fusion'
persepolis_download_manager.setPersepolisStyle(style)
# set font
persepolis_download_manager.setPersepolisFont(font, font_size, custom_font)
# set color_scheme
persepolis_download_manager.setPersepolisColorScheme(color_scheme)
# set ui direction
if ui_direction == 'rtl':
persepolis_download_manager.setLayoutDirection(Qt.RightToLeft)
elif ui_direction in 'ltr':
persepolis_download_manager.setLayoutDirection(Qt.LeftToRight)
# run mainwindow
try:
mainwindow = MainWindow(start_in_tray, persepolis_download_manager, persepolis_download_manager.setting)
if start_in_tray:
mainwindow.hide()
else:
mainwindow.show()
except Exception:
from persepolis.scripts import logger
error_message = str(traceback.format_exc())
# write error_message in log file.
logger.sendToLog(error_message, "ERROR")
# Reset persepolis
error_window = ErrorWindow(error_message)
error_window.show()
sys.exit(persepolis_download_manager.exec_())
elif not ((args.parent_window or unknownargs)):
# this section warns user that program is still running and no need to run it again
# and creating a file to notify mainwindow for showing itself!
# (see CheckingThread in mainwindow.py for more information)
if len(plugin_list) == 0:
show_window_file = os.path.join(persepolis_tmp, 'show-window')
f = open(show_window_file, 'w')
f.close()
sys.exit(0)
================================================
FILE: persepolis/scripts/persepolis_lib_prime.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import requests
import time
import random
import threading
import os
import errno
from persepolis.scripts.useful_tools import convertTime, humanReadableSize, freeSpace, headerToDict, readCookieJar, getFileNameFromLink, returnNewFileName
from persepolis.scripts.osCommands import makeDirs, moveFile
from persepolis.scripts import logger
from persepolis.constants import VERSION
import json
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
class Download():
def __init__(self, add_link_dictionary, main_window, gid):
self.downloaded_size = 0
self.finished_threads = 0
self.eta = "0"
self.resume = False
self.main_window = main_window
self.download_speed_str = "0"
self.gid = gid
# download_status can be in waiting, downloading, stop, error, paused, creating download file
self.download_status = 'waiting'
self.link = add_link_dictionary['link']
self.name = add_link_dictionary['out']
self.download_path = add_link_dictionary['download_path']
self.ip = add_link_dictionary['ip']
self.port = add_link_dictionary['port']
self.proxy_user = add_link_dictionary['proxy_user']
self.proxy_passwd = add_link_dictionary['proxy_passwd']
self.proxy_type = add_link_dictionary['proxy_type']
self.download_user = add_link_dictionary['download_user']
self.download_passwd = add_link_dictionary['download_passwd']
self.header = add_link_dictionary['header']
self.user_agent = add_link_dictionary['user_agent']
self.load_cookies = add_link_dictionary['load_cookies']
self.referer = add_link_dictionary['referer']
self.start_time = add_link_dictionary['start_time']
self.end_time = add_link_dictionary['end_time']
self.number_of_parts = 0
self.file_name = None
self.file_size = None
self.timeout = int(main_window.persepolis_setting.value('settings/timeout'))
self.retry = int(main_window.persepolis_setting.value('settings/max-tries'))
self.retry_wait = int(main_window.persepolis_setting.value('settings/retry-wait'))
self.python_request_chunk_size = int(main_window.persepolis_setting.value('settings/chunk-size'))
self.lock = False
self.sleep_for_speed_limiting = 0
self.not_converted_download_speed = 0
self.download_percent = 0
self.error_message = ''
# this flag notify that download finished(stopped, complete or error)
# in this situation download status must be written to the database
# None means, Download not finished yet.
# False meanse, Download has been finished, but download status must be written to the database
# True meanse, Download status has been written to the database
self.write_it_to_the_database = None
# check certificate
if str(main_window.persepolis_setting.value('settings/dont-check-certificate')) == 'yes':
self.check_certificate = False
else:
self.check_certificate = True
# number_of_threads can't be more that 64
self.number_of_threads = int(add_link_dictionary['connections'])
self.number_of_active_connections = self.number_of_threads
self.thread_list = []
# this dictionary contains information about each part is downloaded by which thread.
self.part_thread_dict = {}
# create requests session
def createSession(self):
# define a requests session
self.requests_session = requests.Session()
# check if user set proxy
if self.ip:
ip_port = '://' + str(self.ip) + ":" + str(self.port)
if self.proxy_user:
ip_port = ('://' + self.proxy_user + ':' + self.proxy_passwd + '@' + ip_port)
if self.proxy_type == 'socks5':
ip_port = 'socks5' + ip_port
else:
ip_port = 'http' + ip_port
proxies = {'http': ip_port,
'https': ip_port}
# set proxy to the session
self.requests_session.proxies.update(proxies)
# check if download session needs authenthication
if self.download_user:
# set download user pass to the session
self.requests_session.auth = (self.download_user,
self.download_passwd)
# set cookies
if self.load_cookies:
jar = readCookieJar(self.load_cookies)
if jar:
self.requests_session.cookies = jar
# set referer
if self.referer:
# setting referer to the session
self.requests_session.headers.update({'referer': self.referer})
# set user_agent
if self.user_agent:
# setting user_agent to the session
self.requests_session.headers.update(
{'user-agent': self.user_agent})
else:
self.user_agent = 'PersepolisDM/' + str(VERSION.version_str)
# setting user_agent to the session
self.requests_session.headers.update(
{'user-agent': self.user_agent})
if self.header is not None:
# convert header to dictionary
dict_ = headerToDict(self.header)
# update headers
self.requests_session.headers.update(dict_)
def setRetry(self):
# set retry numbers.
# backoff_factor will help to apply delays between attempts to avoid failing again
retry_strategy = Retry(
total=self.retry,
backoff_factor=self.retry_wait,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["HEAD", "GET", "OPTIONS"]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
self.requests_session.mount('http://', adapter)
self.requests_session.mount('https://', adapter)
# get file size
# if file size is not available, then download link is invalid
def getFileSize(self):
error_message = None
error_message2 = None
# find file size
try:
self.file_header = {}
response = self.requests_session.head(self.link, allow_redirects=True, timeout=self.timeout, verify=self.check_certificate)
# response.raise_for_status()
self.file_header = response.headers
self.file_size = int(self.file_header['content-length'])
except requests.exceptions.HTTPError as error:
error_message = 'HTTP error'
error_message2 = str(error)
except requests.exceptions.ConnectionError as error:
error_message = 'Connection error'
error_message2 = str(error)
except requests.exceptions.Timeout as error:
error_message = 'Timeout error'
error_message2 = str(error)
except requests.exceptions.RequestException as error:
error_message = 'Request error'
error_message2 = str(error)
except Exception as error:
error_message = 'Error'
error_message2 = str(error)
if error_message:
logger.sendToLog(error_message + ' - ' + error_message2 + ' - GID: ' + self.gid, 'DOWNLOAD ERROR')
self.error_message = error_message
self.file_size = None
return self.file_header
# get file name if available
# if file name is not available, then set a file name
def getFileName(self):
# set default file name
# get file_name from link
self.file_name = getFileNameFromLink(self.link)
# check if user set file name or not
if self.name:
self.file_name = self.name
# check if filename is available in header
elif 'Content-Disposition' in self.file_header.keys():
content_disposition = self.file_header['Content-Disposition']
if content_disposition.find('filename') != -1:
# so file name is available in header
filename_splited = content_disposition.split('filename=')
filename_splited = filename_splited[-1]
# getting file name in desired format
self.file_name = filename_splited.strip()
# this method gives etag from header
# ETag is an HTTP response header field that helps with caching behavior by making
# it easy to check whether a resource has changed, without having to re-download it.
def getFileTag(self):
if 'ETag' in self.file_header.keys():
self.etag = self.file_header['ETag']
else:
self.etag = None
# Check if server supports multi threading and resuming or not
def resumingSupport(self):
self.resuming_suppurt = False
if 'Accept-Ranges' in self.file_header.keys():
if self.file_header['Accept-Ranges'] == 'bytes':
logger.sendToLog('Server supports multi thread downloading and resuming download!' + ' - GID: ' + self.gid, 'DOWNLOADS')
self.resuming_suppurt = True
else:
logger.sendToLog('Server dosn\'t support multi thread downloading and resuming download!' + ' - GID: ' + self.gid, 'DOWNLOAD ERROR')
else:
logger.sendToLog('Server dosn\'t support multi thread downloading and resuming download!' + ' - GID: ' + self.gid, 'DOWNLOAD ERROR')
def createControlFile(self):
# find file_path and control_json_file_path
# If the file is partially downloaded, the download information is available in the control file.
# The format of this file is Jason. the control file extension is .persepolis.
# the control file name is same as download file name.
# control file path is same as download file path.
# If the file_path is set to default path, the the control file path will be created in "Download" directory.
control_json_file = self.file_name + '.persepolis'
# Create download_path if not existed
makeDirs(self.download_path)
# if user set download path
self.file_path = os.path.join(self.download_path, self.file_name)
self.control_json_file_path = os.path.join(
self.download_path, control_json_file)
# If we have diffrent files but with same name, So
# this fle must be renamed.
rename = False
# create json control file if not created before
try:
with open(self.control_json_file_path, 'x') as f:
f.write("")
except OSError as e:
# it means control_json_file_path characters is more than 256 byte
if e.errno == errno.ENAMETOOLONG:
# reduce file_name lenght
reduce_bytes = len(self.control_json_file_path.encode('utf-8')) - 255
# seperate extension from file_name
split_file_name = self.file_name.split('.')
# check we have extension or not
extension = ""
if len(split_file_name) > 1:
# remove extension
extension = split_file_name.pop(-1)
# join file_name without extension
file_name_without_extension = ''.join(split_file_name)
if len(file_name_without_extension.encode('utf-8')) > reduce_bytes:
# Calculate how many characters must be removed
for i in range(len(file_name_without_extension)):
string_ = file_name_without_extension[(-1 * i):]
string_size = len(string_.encode('utf-8'))
if string_size >= reduce_bytes:
# reduce characters
file_name_without_extension = file_name_without_extension[:(-1 * i)]
break
# create new file_name and file_path and control_json_file
self.file_name = file_name_without_extension + extension
self.file_path = os.path.join(self.download_path, self.file_name)
control_json_file = self.file_name + '.persepolis'
self.control_json_file_path = os.path.join(
self.download_path, control_json_file)
# try again
with open(self.control_json_file_path, 'x') as f:
f.write("")
else:
# so the control file is already exists
# read control file
with open(self.control_json_file_path, "r") as f:
try:
# save json file information in dictionary format
data_dict = json.load(f)
# check if the download is duplicated
# If download item is duplicated, so resume download
# check ETag. Else rename file
if 'ETag' in data_dict:
if data_dict['ETag'] == self.etag:
self.resume = True
else:
self.resume = False
rename = True
# if ETag is not available, then check file size
elif 'file_size' in data_dict:
if data_dict['file_size'] == self.file_size:
self.resume = True
else:
self.resume = False
rename = True
else:
self.resume = False
rename = True
# control file is corrupted.
except Exception as e:
logger.sendToLog(str(e) + ' - GID: ' + self.gid, 'DOWNLOAD ERROR')
self.resume = False
if self.resuming_suppurt is False:
self.resume = False
# check if uncomplete download file exists
if os.path.isfile(self.file_path):
download_file_existance = True
else:
download_file_existance = False
# If download file not exists so we can rewrite .persepolis file
# Else download file name and json file name must be renamed.
if rename and download_file_existance:
download_file_existance = False
# create new file name
self.file_name = returnNewFileName(self.download_path, self.file_name)
# create new name for json file
control_json_file = self.file_name + '.persepolis'
# create new file path
self.file_path = os.path.join(self.download_path, self.file_name)
# create new control file path
self.control_json_file_path = os.path.join(
self.download_path, control_json_file)
# create json control file
try:
with open(self.control_json_file_path, 'x') as f:
f.write("")
except Exception as e:
logger.sendToLog(str(e) + ' - GID: ' + self.gid, 'DOWNLOAD ERROR')
self.resume = False
if self.resume and not (download_file_existance):
self.resume = False
create_download_file = True
elif self.resume and download_file_existance:
create_download_file = False
else:
create_download_file = True
# create an empty file
if create_download_file:
fp = open(self.file_path, "wb")
# if file_size is specified, create an empty file with file_size
if self.file_size:
# check for free space
free_space = freeSpace(self.download_path)
if free_space is not None:
# compare free disk space and file_size
if free_space >= self.file_size:
# sets the size of each chunk to 1MiB
CHUNK_SIZE = (1024 ** 2)
# creates a byte string of zeroes with a size of 1MiB.
# These bytes will be used for writing to the file later.
zero_chunk = b'\0' * CHUNK_SIZE
# This variable indicates how many bytes still need to be written.
remaining = self.file_size
# continue the loop until writing ends.
while remaining > 0 and self.download_status != 'stopped':
# determines how much data should be written in this iteration.
# This value can be equal to CHUNK_SIZE,
# but if remaining is less than CHUNK_SIZE, only the remaining amount will be written.
to_write = min(CHUNK_SIZE, remaining)
# writes the zero data up to the calculated amount (to_write) to the file.
fp.write(zero_chunk[:to_write])
# updates the remaining amount to indicate how much of the file still needs to be written.
remaining -= to_write
if self.download_status != 'stopped':
logger.sendToLog('Empty file has been created!' + ' - GID: ' + self.gid, 'DOWNLOADS')
fp.close()
return True
else:
# Download canceled by user! Delete unfinished empty file.
fp.close()
os.remove(self.file_path)
return False
else:
fp.close()
return False
else:
fp.close()
return False
else:
fp.close()
return True
else:
return True
def definePartSizes(self):
# download_infromation_list contains 64 lists.
# Every list contains:
# [start byte number for this part, downloaded size, download status for this part, number of retryingfor this part]
# Status can be stopped, pending, downloading, error, complete
# All part statuses start with a lowercase letter.
# Retry number is -1, because askForNewPart method add 1 to it in the first call.
if self.resume:
# read control file
with open(self.control_json_file_path, "r") as f:
data_dict = json.load(f)
# read number of threads
self.download_infromation_list = data_dict['download_infromation_list']
# number_of_parts
self.number_of_parts = data_dict['number_of_parts']
# set pending status for uncomplete parts
for i in range(0, self.number_of_parts):
if self.download_infromation_list[i][2] != 'complete':
self.download_infromation_list[i] = [self.download_infromation_list[i][0], self.download_infromation_list[i][1], 'pending', -1]
# Calculate downloded size
self.downloaded_size = self.downloaded_size + self.download_infromation_list[i][1]
else:
if self.file_size:
part_size = int(self.file_size // 64)
# create new list.
self.download_infromation_list = [[]] * 64
# if part_size greater than 1 MiB
if part_size >= 1024**2:
self.number_of_parts = 64
for i in range(0, 64):
self.download_infromation_list[i] = [i * part_size, 0, 'pending', -1]
else:
# Calculate how many parts of one MiB we need.
self.number_of_parts = int((self.file_size - 1) // (1024**2)) + 1
self.number_of_threads = self.number_of_parts
for i in range(0, self.number_of_parts):
self.download_infromation_list[i] = [i * 1024 * 1024, 0, 'pending', -1]
# Set the starting byte number of the remaining parts equal to the size of the file.
# The size of the file is equal to the last byte of the file.
# The status of these parts is complete. Because we have nothing to download.
# The starting bit number of the file is 0 and the ending bit number of the file
# is equal to the file size minus one.
for i in range(self.number_of_parts, 64):
self.download_infromation_list[i] = [self.file_size - 1, 0, 'complete', -1]
else:
# create new list.
self.download_infromation_list = [[]] * 64
self.number_of_parts = 1
self.number_of_threads = 1
self.download_infromation_list[0] = [0, 0, 'pending', -1]
# Set the starting byte number of the remaining parts equal to the size of the file.
# The size of the file is equal to the last byte of the file.
# The status of these parts is complete. Because we have nothing to download.
for i in range(self.number_of_parts, 64):
self.download_infromation_list[i] = [0, 0, 'complete', -1]
for i in range(0, self.number_of_parts):
# self.part_thread_dict[part_number] = thread_number
self.part_thread_dict[i] = None
# this method calculates download rate and ETA every second
def downloadSpeed(self):
# Calculate the difference between downloaded volume and elapsed time
# and divide them to get the download speed.
last_download_value = self.downloaded_size
end_time = time.perf_counter()
# this loop repeated every 0.5 second.
while self.download_status == 'downloading' or self.download_status == 'paused':
diffrence_time = time.perf_counter() - end_time
diffrence_size = self.downloaded_size - last_download_value
diffrence_size_converted, speed_unit = humanReadableSize(diffrence_size, 'speed')
download_speed = round(float(diffrence_size_converted) / diffrence_time,
2)
self.download_speed_str = (str(download_speed) + " " + speed_unit + "/s")
not_converted_download_speed = diffrence_size / diffrence_time
try:
# estimated time the download will be completed.
eta_second = (self.file_size - self.downloaded_size) / not_converted_download_speed
except Exception:
eta_second = 0
self.eta = convertTime(eta_second)
end_time = time.perf_counter()
last_download_value = self.downloaded_size
time.sleep(2)
# this method runs progress bar and speed calculator
def runProgressBar(self):
# run a thread for calculating download speed.
calculate_speed_thread = threading.Thread(
target=self.downloadSpeed)
calculate_speed_thread.setDaemon(True)
calculate_speed_thread.start()
self.thread_list.append(calculate_speed_thread)
# threadHandler asks new part for download from this method.
def askForNewPart(self):
self.lock = True
for i in range(0, self.number_of_parts):
# Check that this part is not being downloaded or it is not complete.
# Check that the number of retries of this part has not reached the set limit.
if (self.download_infromation_list[i][2] not in ['complete', 'downloading']) and (self.download_infromation_list[i][3] != self.retry):
# set 'downloding' status for this part
self.download_infromation_list[i][2] = 'downloading'
# add 1 to retry number for this part
self.download_infromation_list[i][3] += 1
# number of retries can't be less than 0
if self.download_infromation_list[i][3] < 0:
self.download_infromation_list[i][3] = 0
self.lock = False
return i
self.lock = False
return None
# The below code is used for each chunk of file handled
# by each thread for downloading the content from specified
# location to storage
def threadHandler(self, thread_number):
while self.download_status in ['downloading', 'paused']:
is_break = False
# Wait for the lock to be released.
while self.lock is True:
# Random sleep prevents two threads from downloading the same part at the same time.
# sleep random time
time.sleep(random.uniform(1, 3))
part_number = self.askForNewPart()
# If part_number is None, no part is available for download. So exit the loop.
if part_number is None:
is_break = True
break
error_message = None
error_message2 = None
self.part_thread_dict[part_number] = thread_number
try:
if self.file_size:
# Calculate part size
# If it's not the last part:
if part_number != (self.number_of_parts - 1):
part_size = self.download_infromation_list[part_number + 1][0] - self.download_infromation_list[part_number][0]
else:
# for last part
part_size = self.file_size - 1 - self.download_infromation_list[part_number][0]
# get start byte number of this part and add it to downloaded size. download resume from this byte number
downloaded_part = self.download_infromation_list[part_number][1]
start = self.download_infromation_list[part_number][0] + downloaded_part
# end of part is equal to start of the next part
# The starting bit number of the file is 0 and the ending bit number of the file
# is equal to the file size minus one.
if part_number != (self.number_of_parts - 1):
end = self.download_infromation_list[part_number + 1][0] - 1
else:
end = self.file_size - 1
# download from begining!
if self.resuming_suppurt is False:
start = 0
# specify the start and end of the part for request header.
chunk_headers = {'Range': 'bytes=%d-%d' % (start, end)}
# request the specified part and get into variable
# When stream=True is set on the request, this avoids
# reading the content at once into memory for large responses
self.requests_session.headers.update(chunk_headers)
response = self.requests_session.get(
self.link, allow_redirects=True, stream=True,
timeout=self.timeout, verify=self.check_certificate)
# open the file and write the content of the html page
# into file.
# r+b mode is open the binary file in read or write mode.
with open(self.file_path, "r+b") as fp:
# The seek() method sets the current file position in a file stream.
fp.seek(start)
# The Python File tell() method is used to find the current position of
# the file cursor (or pointer) within the file.
fp.tell()
# why we use iter_content
# Iterates over the response data. When stream=True is set on
# the request, this avoids reading the content at once into
# memory for large responses. The chunk size is the number
# of bytes it should read into memory. This is not necessarily
# the length of each item returned as decoding can take place.
# so we divide our chunk to smaller chunks. default is 100 Kib
python_request_chunk_size = (1024 * self.python_request_chunk_size)
for data in response.iter_content(
chunk_size=python_request_chunk_size):
if self.download_status in ['downloading', 'paused']:
fp.write(data)
# if this part is downloaded by another thread then exit thread
if self.part_thread_dict[part_number] != thread_number:
# This loop does not end due to an error in the request.
# Therefore, no number should be added to the number of retries.
self.download_infromation_list[part_number][3] -= 1
is_break = True
break
# maybe the last chunk is less than default chunk size
if (part_size - downloaded_part) >= python_request_chunk_size:
update_size = python_request_chunk_size
# if update_size is not equal with actual data length,
# then redownload this chunk.
# exit this "for loop" for redownloading this chunk.
if update_size != len(data):
# This loop does not end due to an error in the request.
# Therefore, no number should be added to the number of retries.
self.download_infromation_list[part_number][3] -= 1
break
else:
# so the last small chunk is equal to :
update_size = (part_size - downloaded_part)
# some times last chunks are smaller
if len(data) < update_size:
update_size = len(data)
# update downloaded_part
downloaded_part = (downloaded_part + update_size)
# save value to downloaded_size_list
self.download_infromation_list[part_number][1] = downloaded_part
# this variable saves amount of total downloaded size
# update downloaded_size
self.downloaded_size = (self.downloaded_size + update_size)
# perhaps user set limitation for download rate.
# downloadrate limitation
# "Speed limit" is whole number. The more it is, the more sleep time is given to the data
# receiving loop, which reduces the download speed.
time.sleep(self.sleep_for_speed_limiting)
if self.download_status == 'paused':
# wait for unpausing
while self.download_status == 'paused':
time.sleep(0.2)
else:
self.download_infromation_list[part_number][2] = 'stopped'
is_break = True
break
# If file_size is unspecified.
else:
download_finished_successfully = False
# get start byte number of this part and add it to downloaded size. download resume from this byte number
start = self.download_infromation_list[part_number][1]
downloaded_part = start
# download from begining!
if self.resuming_suppurt is False:
start = 0
# specify the start and end of the part for request header.
chunk_headers = {'Range': 'bytes=%d-' % (start)}
# request the specified part and get into variable
# When stream=True is set on the request, this avoids
# reading the content at once into memory for large responses
self.requests_session.headers.update(chunk_headers)
response = self.requests_session.get(
self.link, allow_redirects=True, stream=True,
timeout=self.timeout, verify=self.check_certificate)
# open the file and write the content of the html page
# into file.
# r+b mode is open the binary file in read or write mode.
with open(self.file_path, "r+b") as fp:
# The seek() method sets the current file position in a file stream.
fp.seek(start)
# The Python File tell() method is used to find the current position of
# the file cursor (or pointer) within the file.
fp.tell()
# why we use iter_content
# Iterates over the response data. When stream=True is set on
# the request, this avoids reading the content at once into
# memory for large responses. The chunk size is the number
# of bytes it should read into memory. This is not necessarily
# the length of each item returned as decoding can take place.
# so we divide our chunk to smaller chunks. default is 100 Kib
python_request_chunk_size = (1024 * self.python_request_chunk_size)
for data in response.iter_content(
chunk_size=python_request_chunk_size):
if self.download_status in ['downloading', 'paused']:
fp.write(data)
update_size = len(data)
# update downloaded_part
downloaded_part = (downloaded_part + update_size)
# save value to downloaded_size_list
self.download_infromation_list[part_number][1] = downloaded_part
# this variable saves amount of total downloaded size
# update downloaded_size
self.downloaded_size = (self.downloaded_size + update_size)
# perhaps user set limitation for download rate.
# downloadrate limitation
# "Speed limit" is whole number. The more it is, the more sleep time is given to the data
# receiving loop, which reduces the download speed.
time.sleep(self.sleep_for_speed_limiting)
if self.download_status == 'paused':
# wait for unpausing
while self.download_status == 'paused':
time.sleep(0.2)
else:
self.download_infromation_list[part_number][2] = 'stopped'
is_break = True
break
download_finished_successfully = True
except requests.exceptions.HTTPError as error:
error_message = 'HTTP error'
error_message2 = str(error)
self.download_infromation_list[part_number][2] = 'error'
except requests.exceptions.ConnectionError as error:
error_message = 'Connection error'
error_message2 = str(error)
self.download_infromation_list[part_number][2] = 'error'
except requests.exceptions.Timeout as error:
error_message = 'Timeout error'
error_message2 = str(error)
self.download_infromation_list[part_number][2] = 'error'
except requests.exceptions.RequestException as error:
error_message = 'Request error'
error_message2 = str(error)
self.download_infromation_list[part_number][2] = 'error'
except Exception as error:
error_message = 'Error'
error_message2 = str(error)
self.download_infromation_list[part_number][2] = 'error'
if error_message:
self.error_message = error_message
logger.sendToLog(error_message + ' - ' + error_message2 + ' - GID: ' + self.gid, 'ERROR')
# so it's complete successfully.
if self.file_size:
if (downloaded_part == part_size):
self.download_infromation_list[part_number][2] = 'complete'
self.part_thread_dict[part_number] = None
elif not (is_break):
self.download_infromation_list[part_number][2] = 'error'
self.part_thread_dict[part_number] = None
else:
if download_finished_successfully:
self.file_size = self.downloaded_size
self.download_infromation_list[part_number][2] = 'complete'
self.part_thread_dict[part_number] = None
elif not (is_break):
self.download_infromation_list[part_number][2] = 'error'
self.part_thread_dict[part_number] = None
# This thread is finished.
self.finished_threads = self.finished_threads + 1
# this method save download information in json format every 1 second
def saveInfo(self):
while self.download_status == 'downloading' or self.download_status == 'paused':
control_dict = {
'ETag': self.etag,
'file_name': self.file_name,
'file_size': self.file_size,
'number_of_parts': self.number_of_parts,
'download_infromation_list': self.download_infromation_list}
# write control_dict in json file
with open(self.control_json_file_path, "w") as outfile:
json.dump(control_dict, outfile, indent=2)
time.sleep(1)
# this method runs download threads
def runDownloadThreads(self):
# check if server supports multithread downloading or not!
if self.resuming_suppurt is False:
self.thread_number = 1
for i in range(0, self.number_of_threads):
# sleep between starting new thread.
# it solves "Connection refused" error.
time.sleep(0.1)
# create threads
t = threading.Thread(
target=self.threadHandler,
kwargs={'thread_number': i})
t.setDaemon(True)
t.start()
self.thread_list.append(t)
# run saveInfo thread for updating control file
save_control_thread = threading.Thread(
target=self.saveInfo)
save_control_thread.setDaemon(True)
save_control_thread.start()
self.thread_list.append(save_control_thread)
# this method checks and manages download progress.
def checkDownloadProgress(self):
logger.sendToLog("Download starts! - GID:" + self.gid, "DOWNLOADS")
# Run this loop until the download is finished.
while (self.download_status == 'downloading' or self.download_status == 'paused') and \
(self.finished_threads != self.number_of_threads):
# Calculate download percent
if self.file_size:
self.download_percent = int((self.downloaded_size / self.file_size) * 100)
else:
self.download_percent = 0
# Calculate number of active threads
self.number_of_active_connections = self.number_of_threads - self.finished_threads
time.sleep(1)
# Check if all parts downloaded completely.
number_of_completed_parts = 0
for i in range(0, self.number_of_parts):
if self.download_infromation_list[i][2] == 'complete':
number_of_completed_parts += 1
if number_of_completed_parts == self.number_of_parts:
# Download complete!
self.download_status = 'complete'
# Calculate download percent
if self.file_size:
self.download_percent = int((self.downloaded_size / self.file_size) * 100)
else:
self.download_percent = 0
self.number_of_active_connections = 0
# If the downloaded size is the same as the file size, then the download has been completed successfully.
if self.download_status == 'complete':
logger.sendToLog('Download complete. - GID: ' + self.gid, 'DOWNLOADS')
self.download_percent = 100
self.eta = '0s'
# If the download is not complete and the user has not stopped the download, then the download has encountered an error.
elif self.download_status != 'stopped':
self.download_status = 'error'
logger.sendToLog('Download Error - GID: ' + self.gid, 'DOWNLOADS')
elif self.download_status == 'stopped':
logger.sendToLog('Download stopped. - GID: ' + self.gid, 'DOWNLOADS')
# This method returns data and time in string format
# for example >> 2017/09/09 , 13:12:26
def nowDate(self):
date = time.strftime("%Y/%m/%d , %H:%M:%S")
return date
def sigmaTime(self, time):
hour, minute = time.split(":")
return (int(hour) * 60 + int(minute))
# nowTime returns now time in HH:MM format!
def nowTime(self):
now_time = time.strftime("%H:%M")
return self.sigmaTime(now_time)
# this method creates sleep time,if user sets "start time" for download.
def startTime(self):
# write some messages
logger.sendToLog("Download starts at " + self.start_time + ' - GID: ' + self.gid, "DOWNLOADS")
# start_time that specified by user
sigma_start = self.sigmaTime(self.start_time)
# get current time
sigma_now = self.nowTime()
# this loop is continuing until download time arrival!
while sigma_start != sigma_now and self.download_status == 'scheduled':
time.sleep(2.1)
sigma_now = self.nowTime()
# This method will stop the download when the end_time is reached.
def endTime(self):
logger.sendToLog("End time is activated: " + self.end_time + ' - GID: ' + self.gid, "DOWNLOADS")
sigma_end = self.sigmaTime(self.end_time)
# get current time
sigma_now = self.nowTime()
# while current time is not equal to end_time, continue the loop
while sigma_end != sigma_now and (self.download_status not in ['stopped', 'error']):
# get current time
sigma_now = self.nowTime()
time.sleep(2.1)
# Time is up!
if self.download_status not in ['stopped', 'error']:
logger.sendToLog("Time is up! - GID:" + self.gid, "DOWNLOADS")
# stop download
self.downloadStop()
# job is done so change end_time value to None in data_base
self.main_window.persepolis_db.setDefaultGidInAddlinkTable(self.gid, end_time=True)
# this method runs endTime in a thread.
def runEndTimeThread(self):
end_time_thread = threading.Thread(
target=self.endTime)
end_time_thread.setDaemon(True)
end_time_thread.start()
self.thread_list.append(end_time_thread)
# this method starts download
def start(self):
# create new download session.
self.createSession()
# update status and last_try_date in data_base
if self.start_time:
self.download_status = "scheduled"
else:
self.download_status = "waiting"
# get last_try_date
now_date = self.nowDate()
# update data_base
dict_ = {'gid': self.gid, 'status': self.download_status, 'last_try_date': now_date}
self.main_window.persepolis_db.updateDownloadTable([dict_])
# call startTime if start_time is available
# startTime creates sleep loop if user set start_time
# see startTime method for more information.
if self.start_time:
self.startTime()
# now startTime work is done! update data_base
# if download stopped by user don't update data_base
if self.download_status == "scheduled":
# set start_time value to None in data_base!
self.main_window.persepolis_db.setDefaultGidInAddlinkTable(self.gid, start_time=True)
if self.download_status != 'stopped':
self.download_status = 'creating download file'
# start download
headers = self.getFileSize()
if headers != {}:
self.setRetry()
# if user set end_time
if self.end_time:
self.runEndTimeThread()
self.resumingSupport()
self.getFileName()
self.getFileTag()
enough_free_space = self.createControlFile()
if self.download_status != 'stopped':
self.download_status = 'downloading'
if enough_free_space:
self.definePartSizes()
self.runProgressBar()
self.runDownloadThreads()
self.checkDownloadProgress()
else:
self.download_status = 'error'
self.close()
else:
# headers is missing
self.download_status = 'error'
logger.sendToLog('Download Error - GID: ' + self.gid, 'DOWNLOADS')
self.close()
else:
# if start_time_status is "stopped" it means download Canceled by user
logger.sendToLog("Download Canceled", "DOWNLOADS")
def downloadPause(self):
self.download_status = 'paused'
def downloadUnpause(self):
self.download_status = 'downloading'
def downloadStop(self):
self.download_status = 'stopped'
# self.exit_event.set()
def close(self):
# if download complete, so delete control file
if self.download_status == 'complete':
os.remove(self.control_json_file_path)
# move file to download folder
self.main_window.persepolis_setting.sync()
# if user specified download_path is equal to persepolis_setting download_path,
# then subfolder must added to download path.
if self.main_window.persepolis_setting.value('settings/download_path') == self.download_path:
# return new download_path according to file extension.
new_download_path = self.findDownloadPath(
self.file_name, self.download_path, self.main_window.persepolis_setting.value('settings/subfolder'))
file_path = self.downloadCompleteAction(new_download_path)
else:
# keep user specified download_path
file_path = self.file_path
# update download_path in addlink_db_table
# find user preferred download_path from addlink_db_table in data_base
add_link_dictionary = self.main_window.persepolis_db.searchGidInAddLinkTable(self.gid)
add_link_dictionary['download_path'] = file_path
self.main_window.persepolis_db.updateAddLinkTable([add_link_dictionary])
# close requests session
self.requests_session.close()
# ask threads for exiting.
for thread in self.thread_list:
thread.join()
self.write_it_to_the_database = False
logger.sendToLog("persepolis_lib is closed!", 'DOWNLOADS')
# remove it from download_sessions_list when download status has been written to the database.
for download_session_dict in self.main_window.download_sessions_list:
if download_session_dict['gid'] == self.gid:
# Wait until the information is written to the database.
while self.write_it_to_the_database is False:
time.sleep(0.1)
# remove item
self.main_window.download_sessions_list.remove(download_session_dict)
# This method returns download status
def tellStatus(self):
downloaded_size, downloaded_size_unit = humanReadableSize(self.downloaded_size)
if self.file_size:
file_size, file_size_unit = humanReadableSize(self.file_size)
else:
file_size = ''
file_size_unit = ''
if self.eta == '0s':
self.eta = ''
# return information in dictionary format
download_info = {
'gid': self.gid,
'file_name': self.file_name,
'status': self.download_status,
'size': str(file_size) + ' ' + file_size_unit,
'downloaded_size': str(downloaded_size) + ' ' + downloaded_size_unit,
'percent': str(self.download_percent) + '%',
'connections': str(self.number_of_active_connections),
'rate': self.download_speed_str,
'estimate_time_left': self.eta,
'link': self.link,
'error': self.error_message
}
return download_info
# This method limits download speed
def limitSpeed(self, limit_value):
# Calculate sleep time between data receiving. It's reduce download speed.
self.sleep_for_speed_limiting = (10 - limit_value) * 0.005 * (self.number_of_active_connections)
# download complete actions!
# this method is returning file_path of file in the user's download folder
# and move downloaded file after download completion.
def downloadCompleteAction(self, new_download_path):
# rename file if file already existed
self.file_name = returnNewFileName(new_download_path, self.file_name)
new_file_path = os.path.join(new_download_path, self.file_name)
# move the file to the download folder
move_answer = moveFile(str(self.file_path), str(new_file_path), 'file')
if not (move_answer):
# write error message in log
logger.sendToLog('Persepolis can not move file' + ' - GID: ' + self.gid, "ERROR")
new_file_path = self.file_path
return str(new_file_path)
# this function returns folder of download according to file extension
def findDownloadPath(self, file_name, download_path, subfolder):
file_name_split = file_name.split('.')
file_extension = file_name_split[-1]
# convert extension letters to lower case
# for example "JPG" will be converted in "jpg"
file_extension = file_extension.lower()
# remove query from file_extension if existed
# if '?' in file_extension, then file_name contains query components.
if '?' in file_extension:
file_extension = file_extension.split('?')[0]
# audio formats
audio = ['act', 'aiff', 'aac', 'amr', 'ape', 'au', 'awb', 'dct', 'dss', 'dvf', 'flac', 'gsm', 'iklax', 'ivs', 'm4a',
'm4p', 'mmf', 'mp3', 'mpc', 'msv', 'ogg', 'oga', 'opus', 'ra', 'raw', 'sln', 'tta', 'vox', 'wav', 'wma', 'wv']
# video formats
video = ['3g2', '3gp', 'asf', 'avi', 'drc', 'flv', 'm4v', 'mkv', 'mng', 'mov', 'qt', 'mp4', 'm4p', 'mpg', 'mp2',
'mpeg', 'mpe', 'mpv', 'm2v', 'mxf', 'nsv', 'ogv', 'rmvb', 'roq', 'svi', 'vob', 'webm', 'wmv', 'yuv', 'rm']
# document formats
document = ['doc', 'docx', 'html', 'htm', 'fb2', 'odt', 'sxw', 'pdf', 'ps', 'rtf', 'tex', 'txt', 'epub', 'pub'
'mobi', 'azw', 'azw3', 'azw4', 'kf8', 'chm', 'cbt', 'cbr', 'cbz', 'cb7', 'cba', 'ibooks', 'djvu', 'md']
# compressed formats
compressed = ['a', 'ar', 'cpio', 'shar', 'LBR', 'iso', 'lbr', 'mar', 'tar', 'bz2', 'F', 'gz', 'lz', 'lzma', 'lzo',
'rz', 'sfark', 'sz', 'xz', 'Z', 'z', 'infl', '7z', 's7z', 'ace', 'afa', 'alz', 'apk', 'arc', 'arj', 'b1',
'ba', 'bh', 'cab', 'cfs', 'cpt', 'dar', 'dd', 'dgc', 'dmg', 'ear', 'gca', 'ha', 'hki', 'ice', 'jar', 'kgb',
'lzh', 'lha', 'lzx', 'pac', 'partimg', 'paq6', 'paq7', 'paq8', 'pea', 'pim', 'pit', 'qda', 'rar', 'rk', 'sda',
'sea', 'sen', 'sfx', 'sit', 'sitx', 'sqx', 'tar.gz', 'tgz', 'tar.Z', 'tar.bz2', 'tbz2', 'tar.lzma', 'tlz', 'uc',
'uc0', 'uc2', 'ucn', 'ur2', 'ue2', 'uca', 'uha', 'war', 'wim', 'xar', 'xp3', 'yz1', 'zip', 'zipx', 'zoo', 'zpaq',
'zz', 'ecc', 'par', 'par2']
# return download_path
if str(subfolder) == 'yes':
if file_extension in audio:
return os.path.join(download_path, 'Audios')
# aria2c downloads youtube links file_name with 'videoplayback' name?!
elif (file_extension in video) or (file_name == 'videoplayback'):
return os.path.join(download_path, 'Videos')
elif file_extension in document:
return os.path.join(download_path, 'Documents')
elif file_extension in compressed:
return os.path.join(download_path, 'Compressed')
else:
return os.path.join(download_path, 'Others')
else:
return download_path
================================================
FILE: persepolis/scripts/play.py
================================================
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
try:
from PySide6.QtCore import QUrl, QSettings
from PySide6.QtMultimedia import QSoundEffect
except ImportError:
from PyQt5.QtCore import QUrl, QSettings
from PyQt5.QtMultimedia import QSoundEffect
from persepolis.scripts import logger
from persepolis.constants import OS
import platform
os_type = platform.system()
# It's weird! but effect must be global variable
# See this issue:
# https://stackoverflow.com/questions/64794912/
global effect
def playNotification(file):
# getting user setting from persepolis_setting
persepolis_setting = QSettings('persepolis_download_manager', 'persepolis')
# enabling or disabling notification sound in persepolis_setting
enable_notification = str(persepolis_setting.value('settings/sound'))
# volume of notification in persepolis_setting(an integer between 0 to 100)
volume_percent = int(persepolis_setting.value('settings/sound-volume'))
global effect
if enable_notification == 'yes':
if os_type in OS.UNIX_LIKE:
try:
effect = QSoundEffect()
effect.setSource(QUrl.fromLocalFile(file))
effect.setLoopCount(1)
effect.setVolume(volume_percent / 100)
effect.play()
except Exception as error:
logger.sendToLog(
str(error), "ERROR")
elif os_type == OS.OSX:
try:
effect = QSoundEffect()
effect.setSource(QUrl.fromLocalFile(file))
effect.setLoopCount(1)
effect.setVolume(volume_percent / 100)
effect.play()
except Exception as error:
logger.sendToLog(
str(error), "ERROR")
================================================
FILE: persepolis/scripts/progress.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
try:
from PySide6.QtCore import Qt, QSize, QPoint, QThread, QTranslator, QCoreApplication, QLocale
from PySide6.QtWidgets import QLineEdit, QInputDialog
from PySide6.QtGui import QIcon
except ImportError:
from PyQt5.QtCore import Qt, QSize, QPoint, QThread, QTranslator, QCoreApplication, QLocale
from PyQt5.QtWidgets import QLineEdit, QInputDialog
from PyQt5.QtGui import QIcon
from persepolis.gui.progress_ui import ProgressWindow_Ui
from persepolis.scripts.shutdown import shutDown
from persepolis.constants import OS
import subprocess
import platform
os_type = platform.system()
class ShutDownThread(QThread):
def __init__(self, parent, gid, password=None):
QThread.__init__(self)
self.gid = gid
self.password = password
self.main_window = parent
def run(self):
shutDown(self.main_window, gid=self.gid, password=self.password)
class ProgressWindow(ProgressWindow_Ui):
def __init__(self, parent, gid, persepolis_setting, single_video_link=False):
super().__init__(persepolis_setting, parent)
self.persepolis_setting = persepolis_setting
self.main_window = parent
self.gid = gid
self.status = None
self.single_video_link = single_video_link
self.resume_pushButton.clicked.connect(self.resumePushButtonPressed)
self.stop_pushButton.clicked.connect(self.stopPushButtonPressed)
self.pause_pushButton.clicked.connect(self.pausePushButtonPressed)
self.download_progressBar.setValue(0)
self.after_frame.setEnabled(False)
self.after_checkBox.toggled.connect(self.afterCheckBoxToggled)
self.after_pushButton.clicked.connect(self.afterPushButtonPressed)
# add support for other languages
locale = str(self.persepolis_setting.value('settings/locale'))
QLocale.setDefault(QLocale(locale))
self.translator = QTranslator()
if self.translator.load(':/translations/locales/ui_' + locale, 'ts'):
QCoreApplication.installTranslator(self.translator)
# speed limit
self.limit_dial.setValue(10)
self.limit_dial.sliderReleased.connect(self.limitDialIsReleased)
self.limit_dial.valueChanged.connect(self.limitDialIsChanged)
self.limit_label.setText('Speed : Maximum')
self.after_comboBox.currentIndexChanged.connect(self.afterComboBoxChanged)
if self.single_video_link:
# hide pause and resume button
# hide limit speed frame
self.pause_pushButton.hide()
self.resume_pushButton.hide()
self.limit_frame.hide()
# set window size and position
size = self.persepolis_setting.value(
'ProgressWindow/size', QSize(617, 304))
position = self.persepolis_setting.value(
'ProgressWindow/position', QPoint(300, 300))
self.resize(size)
self.move(position)
# close window with ESC key
def keyPressEvent(self, event):
if event.key() == Qt.Key_Escape:
self.close()
def closeEvent(self, event):
# save window size and position
self.persepolis_setting.setValue('ProgressWindow/size', self.size())
self.persepolis_setting.setValue('ProgressWindow/position', self.pos())
self.persepolis_setting.sync()
self.hide()
def resumePushButtonPressed(self, button):
if self.status == "paused":
# search gid in download_sessions_list
for download_session_dict in self.main_window.download_sessions_list:
if download_session_dict['gid'] == self.gid:
# unpause download
download_session_dict['download_session'].downloadUnpause()
break
def pausePushButtonPressed(self, button):
if self.status == "downloading":
# search gid in download_sessions_list
for download_session_dict in self.main_window.download_sessions_list:
if download_session_dict['gid'] == self.gid:
# unpause download
download_session_dict['download_session'].downloadPause()
break
def stopPushButtonPressed(self, button):
dict = {'gid': self.gid,
'shutdown': 'canceled'}
self.main_window.temp_db.updateSingleTable(dict)
if self.status != "stopped":
# search gid in download_sessions_list
for download_session_dict in self.main_window.download_sessions_list:
if download_session_dict['gid'] == self.gid:
# stop download
download_session_dict['download_session'].downloadStop()
break
def afterComboBoxChanged(self, connect):
self.after_pushButton.setEnabled(True)
def afterCheckBoxToggled(self, checkBoxes):
if self.after_checkBox.isChecked():
self.after_frame.setEnabled(True)
self.after_pushButton.setEnabled(True)
else:
# so user canceled shutdown after download
# write cancel value in data_base for this gid
self.after_frame.setEnabled(False)
dict = {'gid': self.gid,
'shutdown': 'canceled'}
self.main_window.temp_db.updateSingleTable(dict)
def afterPushButtonPressed(self, button):
self.after_pushButton.setEnabled(False)
if os_type != OS.WINDOWS: # For Linux and Mac OSX and BSD
# get root password
passwd, ok = QInputDialog.getText(
self, 'PassWord', 'Please enter root password:', QLineEdit.Password)
if ok:
# check password is true or not!
pipe = subprocess.Popen(['sudo', '-S', 'echo', 'hello'],
stdout=subprocess.DEVNULL,
stdin=subprocess.PIPE,
stderr=subprocess.DEVNULL,
shell=False)
pipe.communicate(passwd.encode())
answer = pipe.wait()
# Wrong password
while answer != 0:
passwd, ok = QInputDialog.getText(
self, 'PassWord', 'Wrong Password!\nPlease try again.', QLineEdit.Password)
if ok:
# checking password
pipe = subprocess.Popen(['sudo', '-S', 'echo', 'hello'],
stdout=subprocess.DEVNULL,
stdin=subprocess.PIPE,
stderr=subprocess.DEVNULL,
shell=False)
pipe.communicate(passwd.encode())
answer = pipe.wait()
else:
ok = False
self.after_pushButton.setEnabled(True)
break
if ok is not False:
# if user selects shutdown option after download progress,
# value of 'shutdown' will changed in temp_db for this gid
# and "wait" word will be written for this value.
# (see ShutDownThread and shutdown.py for more information)
# shutDown method will check that value in a loop .
# when "wait" changes to "shutdown" then shutdown.py script
# will shut down the system.
shutdown_enable = ShutDownThread(self.main_window, self.gid, passwd)
self.main_window.threadPool.append(shutdown_enable)
self.main_window.threadPool[-1].start()
else:
self.after_checkBox.setChecked(False)
else:
self.after_checkBox.setChecked(False)
else: # for Windows
shutdown_enable = ShutDownThread(self.main_window, self.gid)
self.main_window.threadPool.append(shutdown_enable)
self.main_window.threadPool[-1].start()
def limitDialIsReleased(self):
limit_value = self.limit_dial.value()
# set speed limit value
for download_session_dict in self.main_window.download_sessions_list:
if download_session_dict['gid'] == self.gid:
# limit download speed
download_session_dict['download_session'].limitSpeed(limit_value)
break
def limitDialIsChanged(self, button):
if self.limit_dial.value() == 10:
self.limit_label.setText('Speed : Maximum')
elif self.limit_dial.value() == 0:
self.limit_label.setText('Speed : Minimum')
else:
self.limit_label.setText('Speed')
def changeIcon(self, icons):
icons = ':/' + str(icons) + '/'
self.resume_pushButton.setIcon(QIcon(icons + 'play'))
self.pause_pushButton.setIcon(QIcon(icons + 'pause'))
self.stop_pushButton.setIcon(QIcon(icons + 'stop'))
================================================
FILE: persepolis/scripts/properties.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
try:
from PySide6.QtCore import Qt, QSize, QPoint, QDir, QTime, QCoreApplication
from PySide6.QtWidgets import QLabel, QLineEdit, QFileDialog
from PySide6.QtGui import QIcon
except ImportError:
from PyQt5.QtCore import Qt, QSize, QPoint, QDir, QTime, QCoreApplication
from PyQt5.QtWidgets import QLabel, QLineEdit, QFileDialog
from PyQt5.QtGui import QIcon
from persepolis.gui.addlink_ui import AddLinkWindow_Ui
from persepolis.scripts.check_proxy import getProxy
import os
class PropertiesWindow(AddLinkWindow_Ui):
def __init__(self, parent, callback, gid, persepolis_setting, video_finder_dictionary=None):
super().__init__(persepolis_setting)
self.parent = parent
self.persepolis_setting = persepolis_setting
self.video_finder_dictionary = video_finder_dictionary
self.download_later_pushButton.hide() # hide download_later_pushButton
self.change_name_checkBox.hide() # hide change_name_checkBox
self.change_name_lineEdit.hide() # hide change_name_lineEdit
# add new QLineEdit and QLineEdit for audio link if we have video finder links
if self.video_finder_dictionary:
self.link_label_2 = QLabel(self.link_frame)
self.link_horizontalLayout.addWidget(self.link_label_2)
self.link_lineEdit_2 = QLineEdit(self.link_frame)
self.link_horizontalLayout.addWidget(self.link_lineEdit_2)
self.link_lineEdit_2.textChanged.connect(self.linkLineChanged)
self.link_label.setText(QCoreApplication.translate("addlink_ui_tr", "Video Link: "))
self.link_label_2.setText(QCoreApplication.translate("addlink_ui_tr", "Audio Link: "))
# gid_1 >> video_gid , gid_2 >> audio_gid
self.gid_1 = self.video_finder_dictionary['video_gid']
self.gid_2 = self.video_finder_dictionary['audio_gid']
else:
self.gid_1 = gid
self.callback = callback
# detect_proxy_pushButton
self.detect_proxy_pushButton.clicked.connect(
self.detectProxy)
# connect folder_pushButton
self.folder_pushButton.clicked.connect(self.changeFolder)
self.download_folder_lineEdit.setEnabled(False)
self.ok_pushButton.setEnabled(False)
self.link_lineEdit.textChanged.connect(self.linkLineChanged)
# connect OK and cancel button
self.cancel_pushButton.clicked.connect(self.close)
self.ok_pushButton.clicked.connect(self.okButtonPressed)
# frames and checkBoxes
self.proxy_frame.setEnabled(False)
self.proxy_checkBox.toggled.connect(self.proxyFrame)
self.download_frame.setEnabled(False)
self.download_checkBox.toggled.connect(self.downloadFrame)
self.start_frame.setEnabled(False)
self.start_checkBox.toggled.connect(self.startFrame)
self.end_frame.setEnabled(False)
self.end_checkBox.toggled.connect(self.endFrame)
# get information from data base
self.add_link_dictionary_1 = self.parent.persepolis_db.searchGidInAddLinkTable(self.gid_1)
self.download_table_dict_1 = self.parent.persepolis_db.searchGidInDownloadTable(self.gid_1)
if video_finder_dictionary:
self.add_link_dictionary_2 = self.parent.persepolis_db.searchGidInAddLinkTable(self.gid_2)
self.download_table_dict_2 = self.parent.persepolis_db.searchGidInDownloadTable(self.gid_2)
# create a copy from add_link_dictionary for checking changes finally!
self.add_link_dictionary_1_backup = {}
for key in self.add_link_dictionary_1.keys():
self.add_link_dictionary_1_backup[key] = self.add_link_dictionary_1[key]
if video_finder_dictionary:
self.add_link_dictionary_2_backup = {}
for key in self.add_link_dictionary_2.keys():
self.add_link_dictionary_2_backup[key] = self.add_link_dictionary_2[key]
# initialization
# disable folder_frame when download is complete
if self.video_finder_dictionary:
if self.video_finder_dictionary['video_completed'] == 'yes' or self.video_finder_dictionary['audio_completed'] == 'yes':
self.folder_frame.setEnabled(False)
else:
if self.download_table_dict_1['status'] == 'complete':
self.folder_frame.setEnabled(False)
# link
self.link_lineEdit.setText(self.add_link_dictionary_1['link'])
if self.video_finder_dictionary:
self.link_lineEdit_2.setText(self.add_link_dictionary_2['link'])
# ip_lineEdit initialization
if self.add_link_dictionary_1['ip']:
self.proxy_checkBox.setChecked(True)
self.ip_lineEdit.setText(self.add_link_dictionary_1['ip'])
# port_spinBox initialization
try:
self.port_spinBox.setValue(
int(self.add_link_dictionary_1['port']))
except Exception:
pass
# proxy user lineEdit initialization
try:
self.proxy_user_lineEdit.setText(
self.add_link_dictionary_1['proxy_user'])
except Exception:
pass
# proxy pass lineEdit initialization
try:
self.proxy_pass_lineEdit.setText(
self.add_link_dictionary_1['proxy_passwd'])
except Exception:
pass
# proxy type
proxy_type = self.add_link_dictionary_1['proxy_type']
# default is http
if proxy_type == 'socks5':
self.socks5_radioButton.setChecked(True)
elif proxy_type == 'https':
self.https_radioButton.setChecked(True)
else:
self.http_radioButton.setChecked(True)
# download UserName initialization
if self.add_link_dictionary_1['download_user']:
self.download_checkBox.setChecked(True)
self.download_user_lineEdit.setText(
self.add_link_dictionary_1['download_user'])
# download PassWord initialization
try:
self.download_pass_lineEdit.setText(
self.add_link_dictionary_1['download_passwd'])
except Exception:
pass
# folder_path
try:
self.download_folder_lineEdit.setText(
self.add_link_dictionary_1['download_path'])
except Exception:
pass
# connections
try:
self.connections_spinBox.setValue(
int(self.add_link_dictionary_1['connections']))
except Exception:
pass
# get categories name and add them to add_queue_comboBox
categories_list = self.parent.persepolis_db.categoriesList()
for queue in categories_list:
if queue != 'All Downloads':
self.add_queue_comboBox.addItem(queue)
# finding current queue and setting it!
self.current_category = self.download_table_dict_1['category']
current_category_index = self.add_queue_comboBox.findText(
self.current_category)
self.add_queue_comboBox.setCurrentIndex(current_category_index)
# add_queue_comboBox event
self.add_queue_comboBox.currentIndexChanged.connect(self.queueChanged)
# start_time
if self.add_link_dictionary_1['start_time']:
# get hour and minute
hour, minute = self.add_link_dictionary_1['start_time'].split(':')
# set time
q_time = QTime(int(hour), int(minute))
self.start_time_qDataTimeEdit.setTime(q_time)
self.start_checkBox.setChecked(True)
# end_time
if self.add_link_dictionary_1['end_time']:
# get hour and minute
hour, minute = self.add_link_dictionary_1['end_time'].split(':')
# set time
q_time = QTime(int(hour), int(minute))
self.end_time_qDateTimeEdit.setTime(q_time)
self.end_checkBox.setChecked(True)
# referer
if self.add_link_dictionary_1['referer']:
self.referer_lineEdit.setText(str(self.add_link_dictionary_1['referer']))
if self.add_link_dictionary_1['header']:
self.header_lineEdit.setText(str(self.add_link_dictionary_1['header']))
if self.add_link_dictionary_1['user_agent']:
self.user_agent_lineEdit.setText(str(self.add_link_dictionary_1['user_agent']))
if self.add_link_dictionary_1['load_cookies']:
self.load_cookies_lineEdit.setText((self.add_link_dictionary_1['load_cookies']))
# set window size and position
size = self.persepolis_setting.value(
'PropertiesWindow/size', QSize(520, 425))
position = self.persepolis_setting.value(
'PropertiesWindow/position', QPoint(300, 300))
self.resize(size)
self.move(position)
# detect system proxy setting, and set ip_lineEdit and port_spinBox
def detectProxy(self, button):
# get system proxy information
system_proxy_dict = getProxy()
enable_proxy_frame = False
# ip
if 'http_proxy_ip' in system_proxy_dict.keys():
self.ip_lineEdit.setText(str(system_proxy_dict['http_proxy_ip']))
enable_proxy_frame = True
# port
if 'http_proxy_port' in system_proxy_dict.keys():
self.port_spinBox.setValue(int(system_proxy_dict['http_proxy_port']))
enable_proxy_frame = True
# enable proxy frame if http_proxy_ip or http_proxy_port is valid.
if enable_proxy_frame:
self.proxy_checkBox.setChecked(True)
self.detect_proxy_label.setText('')
else:
self.proxy_checkBox.setChecked(False)
self.detect_proxy_label.setText('No proxy detected!')
# activate frames if checkBoxes checked
def proxyFrame(self, checkBox):
if self.proxy_checkBox.isChecked():
self.proxy_frame.setEnabled(True)
else:
self.proxy_frame.setEnabled(False)
def downloadFrame(self, checkBox):
if self.download_checkBox.isChecked():
self.download_frame.setEnabled(True)
else:
self.download_frame.setEnabled(False)
def startFrame(self, checkBox):
if self.start_checkBox.isChecked():
self.start_frame.setEnabled(True)
else:
self.start_frame.setEnabled(False)
def endFrame(self, checkBox):
if self.end_checkBox.isChecked():
self.end_frame.setEnabled(True)
else:
self.end_frame.setEnabled(False)
def changeFolder(self, button):
fname = QFileDialog.getExistingDirectory(self, 'Open f', '/home')
if fname:
# Returns pathName with the '/' separators converted to separators that are appropriate for the underlying operating system.
# On Windows, toNativeSeparators("c:/winnt/system32") returns
# "c:\winnt\system32".
fname = QDir.toNativeSeparators(fname)
if os.path.isdir(fname):
self.download_folder_lineEdit.setText(fname)
def linkLineChanged(self, lineEdit):
if str(self.link_lineEdit.text()) == '':
self.ok_pushButton.setEnabled(False)
else:
self.ok_pushButton.setEnabled(True)
def queueChanged(self, combo):
# if one of the queues selected by user , start time and end time must
# be deactivated
if self.add_queue_comboBox.currentIndex() != 0:
self.start_checkBox.setCheckState(Qt.Unchecked)
self.start_checkBox.setEnabled(False)
self.end_checkBox.setCheckState(Qt.Unchecked)
self.end_checkBox.setEnabled(False)
else:
self.start_checkBox.setEnabled(True)
self.end_checkBox.setEnabled(True)
# this method returns proxy information.
def getProxyInformation(self):
# http, https or socks5 proxy
if self.http_radioButton.isChecked() is True:
proxy_type = 'http'
elif self.https_radioButton.isChecked() is True:
proxy_type = 'https'
else:
proxy_type = 'socks5'
# get proxy information
if not (self.proxy_checkBox.isChecked()):
ip = None
port = None
proxy_user = None
proxy_passwd = None
proxy_type = None
else:
ip = self.ip_lineEdit.text()
if not (ip):
ip = None
port = self.port_spinBox.value()
if not (port):
port = None
proxy_user = self.proxy_user_lineEdit.text()
if not (proxy_user):
proxy_user = None
proxy_passwd = self.proxy_pass_lineEdit.text()
if not (proxy_passwd):
proxy_passwd = None
return ip, port, proxy_user, proxy_passwd, proxy_type
def getUserPass(self):
# get download username and password information
if not (self.download_checkBox.isChecked()):
download_user = None
download_passwd = None
else:
download_user = self.download_user_lineEdit.text()
if not (download_user):
download_user = None
download_passwd = self.download_pass_lineEdit.text()
if not (download_passwd):
download_passwd = None
return download_user, download_passwd
def getAdditionalInformation(self):
# referer
if self.referer_lineEdit.text() != '':
referer = self.referer_lineEdit.text()
else:
referer = None
# header
if self.header_lineEdit.text() != '':
header = self.header_lineEdit.text()
else:
header = None
# user_agent
if self.user_agent_lineEdit.text() != '':
user_agent = self.user_agent_lineEdit.text()
else:
user_agent = None
# load_cookies
if self.load_cookies_lineEdit.text() != '':
load_cookies = self.load_cookies_lineEdit.text()
else:
load_cookies = None
return referer, header, user_agent, load_cookies
def okButtonPressed(self, button):
# write user's new inputs in persepolis_setting for next time if needed
if self.folder_checkBox.isChecked() is True:
self.persepolis_setting.setValue(
'settings/download_path', self.download_folder_lineEdit.text())
# get proxy information
ip, port, proxy_user, proxy_passwd, proxy_type = self.getProxyInformation()
if proxy_type is not None:
self.persepolis_setting.setValue('add_link_initialization/proxy_type', proxy_type)
# get download username and password information
download_user, download_passwd = self.getUserPass()
if not (self.start_checkBox.isChecked()):
start_time = None
else:
start_time = self.start_time_qDataTimeEdit.text()
if not (self.end_checkBox.isChecked()):
end_time = None
else:
end_time = self.end_time_qDateTimeEdit.text()
connections = self.connections_spinBox.value()
download_path = self.download_folder_lineEdit.text()
# get additinal information
referer, header, user_agent, load_cookies = self.getAdditionalInformation()
self.add_link_dictionary_1['start_time'] = start_time
self.add_link_dictionary_1['end_time'] = end_time
self.add_link_dictionary_1['link'] = self.link_lineEdit.text()
self.add_link_dictionary_1['ip'] = ip
self.add_link_dictionary_1['port'] = port
self.add_link_dictionary_1['proxy_user'] = proxy_user
self.add_link_dictionary_1['proxy_passwd'] = proxy_passwd
self.add_link_dictionary_1['proxy_type'] = proxy_type
self.add_link_dictionary_1['download_user'] = download_user
self.add_link_dictionary_1['download_passwd'] = download_passwd
self.add_link_dictionary_1['download_path'] = download_path
self.add_link_dictionary_1['connections'] = connections
self.add_link_dictionary_1['referer'] = referer
self.add_link_dictionary_1['header'] = header
self.add_link_dictionary_1['user_agent'] = user_agent
self.add_link_dictionary_1['load_cookies'] = load_cookies
if self.video_finder_dictionary:
self.add_link_dictionary_2['start_time'] = start_time
self.add_link_dictionary_2['end_time'] = end_time
self.add_link_dictionary_2['link'] = self.link_lineEdit_2.text()
self.add_link_dictionary_2['ip'] = ip
self.add_link_dictionary_2['port'] = port
self.add_link_dictionary_2['proxy_user'] = proxy_user
self.add_link_dictionary_2['proxy_passwd'] = proxy_passwd
self.add_link_dictionary_2['proxy_type'] = proxy_type
self.add_link_dictionary_2['download_user'] = download_user
self.add_link_dictionary_2['download_passwd'] = download_passwd
self.add_link_dictionary_2['download_path'] = download_path
self.add_link_dictionary_2['connections'] = connections
self.add_link_dictionary_2['referer'] = referer
self.add_link_dictionary_2['header'] = header
self.add_link_dictionary_2['user_agent'] = user_agent
self.add_link_dictionary_2['load_cookies'] = load_cookies
new_category = str(self.add_queue_comboBox.currentText())
# it means category changed and data base must be updated.
if new_category != self.current_category:
self.download_table_dict_1['category'] = new_category
# update data base
self.parent.persepolis_db.updateDownloadTable([self.download_table_dict_1])
# update category_db_table
# remove download item from old category
old_category_dict = self.parent.persepolis_db.searchCategoryInCategoryTable(self.current_category)
old_category_gid_list = old_category_dict['gid_list']
old_category_gid_list.remove(self.gid_1)
self.parent.persepolis_db.updateCategoryTable([old_category_dict])
# add download item to new category
new_category_dict = self.parent.persepolis_db.searchCategoryInCategoryTable(new_category)
new_category_gid_list = new_category_dict['gid_list']
new_category_gid_list.append(self.gid_1)
self.parent.persepolis_db.updateCategoryTable([new_category_dict])
if self.video_finder_dictionary:
# category for audio and video must be same as each other
self.download_table_dict_2['category'] = new_category
self.parent.persepolis_db.updateDownloadTable([self.download_table_dict_2])
# update category_db_table
# remove download item from old category
old_category_dict = self.parent.persepolis_db.searchCategoryInCategoryTable(self.current_category)
old_category_gid_list = old_category_dict['gid_list']
old_category_gid_list.remove(self.gid_2)
self.parent.persepolis_db.updateCategoryTable([old_category_dict])
# add download item to new category
new_category_dict = self.parent.persepolis_db.searchCategoryInCategoryTable(new_category)
new_category_gid_list = new_category_dict['gid_list']
new_category_gid_list.append(self.gid_2)
self.parent.persepolis_db.updateCategoryTable([new_category_dict])
# if any thing in add_link_dictionary_1 is changed,then update data base!
for key in self.add_link_dictionary_1.keys():
if self.add_link_dictionary_1[key] != self.add_link_dictionary_1_backup[key]:
# update data base
self.parent.persepolis_db.updateAddLinkTable([self.add_link_dictionary_1])
# break the loop
break
# if link changed, then update download_db_table in data base
if self.add_link_dictionary_1['link'] != self.add_link_dictionary_1_backup['link']:
dictionary = {'gid': self.gid_1, 'link': self.add_link_dictionary_1['link']}
self.parent.persepolis_db.updateDownloadTable([dictionary])
# if any thing in add_link_dictionary_2 is changed,then update data base!
if self.video_finder_dictionary:
for key in self.add_link_dictionary_2.keys():
if self.add_link_dictionary_2[key] != self.add_link_dictionary_2_backup[key]:
# update data base
self.parent.persepolis_db.updateAddLinkTable([self.add_link_dictionary_2])
# break the loop
break
# if link changed, then update download_db_table in data base
if self.add_link_dictionary_2['link'] != self.add_link_dictionary_2_backup['link']:
dictionary = {'gid': self.gid_2, 'link': self.add_link_dictionary_2['link']}
self.parent.persepolis_db.updateDownloadTable([dictionary])
# if download_path was changed, then update video_finder_db_table in data base
if self.add_link_dictionary_1['download_path'] != self.add_link_dictionary_1_backup['download_path']:
dictionary = {'video_gid': self.gid_1,
'download_path': download_path}
self.parent.persepolis_db.updateVideoFinderTable[dictionary]
# callback to mainwindow
self.callback(self.add_link_dictionary_1, self.gid_1, new_category, self.video_finder_dictionary)
# close window
self.close()
# close window with ESC key
def keyPressEvent(self, event):
if event.key() == Qt.Key_Escape:
self.close()
def closeEvent(self, event):
# save window size and position
self.persepolis_setting.setValue('PropertiesWindow/size', self.size())
self.persepolis_setting.setValue(
'PropertiesWindow/position', self.pos())
self.persepolis_setting.sync()
event.accept()
def changeIcon(self, icons):
icons = ':/' + str(icons) + '/'
self.folder_pushButton.setIcon(QIcon(icons + 'folder'))
self.download_later_pushButton.setIcon(QIcon(icons + 'stop'))
self.cancel_pushButton.setIcon(QIcon(icons + 'remove'))
self.ok_pushButton.setIcon(QIcon(icons + 'ok'))
================================================
FILE: persepolis/scripts/queue.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import time
from time import sleep
from persepolis.scripts import logger
from persepolis.scripts import persepolis_lib_prime
from persepolis.scripts.video_finder import VideoFinder
from persepolis.scripts.download_link import DownloadLink
try:
from PySide6.QtCore import QThread, Signal, QCoreApplication
except ImportError:
from PyQt5.QtCore import QThread, QCoreApplication
from PyQt5.QtCore import pyqtSignal as Signal
try:
from persepolis.scripts import ytdlp_downloader
except ModuleNotFoundError:
# if youtube_dl module is not installed:
logger.sendToLog(
"yt-dlp is not installed.", "ERROR")
# this thread is managing queue
class Queue(QThread):
# this signal emitted when download status of queue changes to stop
REFRESHTOOLBARSIGNAL = Signal(str)
NOTIFYSENDSIGNAL = Signal(list)
def __init__(self, category, start_time, end_time, parent):
QThread.__init__(self)
self.category = str(category)
self.main_window = parent
self.start_time = start_time
self.end_time = end_time
def run(self):
self.start = True
self.stop = False
self.limit_changed = False
self.after = False
self.break_for_loop = False
queue_counter = 0
# this list contains gid_list of all active video finder in queue.
video_finder_list = []
# queue repeats 5 times!
# and every time loads queue list again!
# It is helps for checking new downloads in queue
# and retrying for failed downloads.
for counter in range(5):
# read downloads information from data base
download_table_dict = self.main_window.persepolis_db.returnItemsInDownloadTable(self.category)
category_table_dict = self.main_window.persepolis_db.searchCategoryInCategoryTable(self.category)
gid_list = category_table_dict['gid_list']
# sort downloads top to the bottom of the list OR bottom to the top
if not (self.main_window.reverse_checkBox.isChecked()):
gid_list.reverse()
# check that if user set start time
if self.start_time and counter == 0:
# find first download
# set start time for first download in queue
# status of first download must not be complete
for gid in gid_list:
# get download information dictionary
dictionary = download_table_dict[gid]
# find status of download
status = dictionary['status']
if status != 'complete':
# We find first item! GREAT!
add_link_dict = {'gid': gid}
# set start_time for this download
add_link_dict['start_time'] = self.start_time
# write changes in data base
self.main_window.persepolis_db.updateAddLinkTable([add_link_dict])
# delete add_link_dict
del add_link_dict
# job is done! break the loop
break
for gid in gid_list:
# if gid is related to video finder, so start Video Finder thread for checking status
# check video_finder_threads_dict, perhaps a thread started before for this gid
if (gid in self.main_window.all_video_finder_gid_list):
video_finder_link = True
video_finder_dictionary = self.main_window.persepolis_db.searchGidInVideoFinderTable(gid)
if video_finder_dictionary['video_gid'] not in self.main_window.video_finder_threads_dict.keys():
# start new video finder thread
video_finder_gid_list = [video_finder_dictionary['video_gid'],
video_finder_dictionary['audio_gid']]
new_video_finder = VideoFinder(video_finder_dictionary, self.main_window)
self.main_window.threadPool.append(new_video_finder)
self.main_window.threadPool[-1].start()
self.main_window.threadPool[-1].VIDEOFINDERCOMPLETED.connect(self.main_window.videoFinderCompleted)
# add thread to video_finder_threads_dict
self.main_window.video_finder_threads_dict[video_finder_dictionary['video_gid']] = new_video_finder
video_finder_list.append(video_finder_gid_list)
else:
video_finder_link = False
add_link_dict = {'gid': gid}
# find download information
dictionary = download_table_dict[gid]
# if download was completed, continue the loop
# with the next iteration of the loop!
# We don't want to download it two times :)
if dictionary['status'] == 'complete':
continue
queue_counter = queue_counter + 1
# change status of download to waiting
status = 'waiting'
dictionary['status'] = status
if self.end_time:
# it means user was set end time for download
# set end_hour and end_minute
add_link_dict['end_time'] = self.end_time
# user can set sleep time between download items in queue.
# see preferences window!
# find wait_queue value
wait_queue_list = self.main_window.persepolis_setting.value('settings/wait-queue')
wait_queue_hour = int(wait_queue_list[0])
wait_queue_minute = int(wait_queue_list[1])
# check if user set sleep time between downloads in queue in setting window.
# if queue_counter is 1 , it means we are in the first download item in queue.
# and no need to wait for first item.
if (wait_queue_hour != 0 or wait_queue_minute != 0) and queue_counter != 1:
now_time_hour = int(time.strftime("%H"))
now_time_minute = int(time.strftime("%M"))
now_time_second = int(time.strftime("%S"))
# add extra minute if we are in second half of minute
if now_time_second > 30:
now_time_minute = now_time_minute + 1
# hour value can not be more than 23 and minute value can not be more than 59.
sigma_minute = wait_queue_minute + now_time_minute
sigma_hour = wait_queue_hour + now_time_hour
if sigma_minute > 59:
sigma_minute = sigma_minute - 60
sigma_hour = sigma_hour + 1
if sigma_hour > 23:
sigma_hour = sigma_hour - 24
# setting sigma_hour and sigma_minute for download's start time!
add_link_dict['start_time'] = str(sigma_hour) + ':' + str(sigma_minute)
# write changes in data base
self.main_window.persepolis_db.updateAddLinkTable([add_link_dict])
add_link_dict = self.main_window.persepolis_db.searchGidInAddLinkTable(gid)
if video_finder_link:
# create video download_session
download_session = ytdlp_downloader.Ytdp_Download(add_link_dict, self.main_window, gid)
else:
# create download_session
# check if it's single_video_link or not
answer_dictionary = self.main_window.persepolis_db.searchGidInVideoFinderTable2(gid)
if answer_dictionary is not None:
single_video_link = True
else:
single_video_link = False
if not (single_video_link):
download_session = persepolis_lib_prime.Download(add_link_dict, self.main_window, gid)
# check limit speed value
download_session.limitSpeed(self.main_window.limit_dial.value())
else:
download_session = ytdlp_downloader.Ytdp_Download(add_link_dict, self.main_window, gid, single_video_link=True)
# add gid to single_video_link_gid_list
self.main_window.single_video_link_gid_list.append(gid)
# add download_session and gid to download_session_dict
download_session_dict = {'gid': gid,
'download_session': download_session}
# append download_session_dict to download_sessions_list
self.main_window.download_sessions_list.append(download_session_dict)
# strat download in thread
new_download = DownloadLink(gid, download_session, self.main_window)
self.main_window.threadPool.append(new_download)
self.main_window.threadPool[-1].start()
# delete add_link_dict
del add_link_dict
sleep(3)
# continue loop until download has finished
while status == 'downloading' or status == 'waiting' or status == 'paused' or status == 'scheduled':
sleep(1)
dictionary = self.main_window.persepolis_db.searchGidInDownloadTable(gid)
status = dictionary['status']
if status == 'error':
error = 'error'
# write error_message in log file
error_message = 'Download failed - GID : '\
+ str(gid)\
+ '- Message : '\
+ error
logger.sendToLog(error_message, 'DOWNLOAD ERROR')
elif status == 'complete':
complete_message = 'Download complete - GID : '\
+ str(gid)
# write in log the complete_message
logger.sendToLog(complete_message, 'DOWNLOADS')
# check that is this related to video finder thread or not.
if gid in self.main_window.all_video_finder_gid_list:
# find related thread
for list in video_finder_list:
if gid in list:
video_gid = list[0]
if video_gid in self.main_window.video_finder_threads_dict:
video_finder_thread = self.main_window.video_finder_threads_dict[video_gid]
# check the video and audio and muxing_status
if video_finder_thread.video_completed == 'yes' and video_finder_thread.audio_completed == 'yes':
# wait until end of muxing
while video_finder_thread.active == 'yes':
sleep(0.5)
break
if self.stop:
# it means user stopped queue
# search gid in download_sessions_list
for download_session_dict in self.main_window.download_sessions_list:
if download_session_dict['gid'] == gid:
# stop download
download_session_dict['download_session'].downloadStop()
break
if status == 'downloading' and self.limit_changed:
# It means user want to limit download speed
# get limitation value
limit_value = self.main_window.limit_dial.value()
# apply limitation
for download_session_dict in self.main_window.download_sessions_list:
if download_session_dict['gid'] == gid:
download_session_dict['download_session'].limitSpeed(limit_value)
break
# done!
self.limit_changed = False
# it means queue stopped at end time or user stopped queue
if status == 'stopped':
for video_finder_gid_list in video_finder_list:
video_gid = video_finder_gid_list[0]
video_finder_dictionary = self.main_window.persepolis_db.searchGidInVideoFinderTable(video_gid)
if video_finder_dictionary:
# tell video finder thread to stop checking
if video_finder_dictionary['video_completed'] == 'no' or video_finder_dictionary['audio_completed'] == 'no':
video_finder_dictionary['checking'] = 'no'
self.main_window.persepolis_db.updateVideoFinderTable([video_finder_dictionary])
video_finder_thread = self.main_window.video_finder_threads_dict[video_gid]
video_finder_thread.checking = 'no'
elif not (self.stop) and self.after and video_finder_dictionary['muxing_status'] == 'started':
# downloads were completed and video finder started Muxing
# wait until the end of muxing
# don't turn of the computer.
# video finder will be deleted from data base when muxing ended.
# so check data base every second
video_finder_thread = self.main_window.video_finder_threads_dict[video_finder_dictionary['video_gid']]
while video_finder_thread.active == 'yes':
sleep(1)
if self.stop and self.after:
# It means user activated shutdown before and now user
# stopped queue . so after download must be canceled
self.main_window.after_checkBox.setChecked(False)
self.stop = True
self.limit_changed = False
# it means that break outer "for" loop
self.break_for_loop = True
if str(self.main_window.category_tree.currentIndex().data()) == str(self.category):
self.REFRESHTOOLBARSIGNAL.emit(self.category)
# show notification
self.NOTIFYSENDSIGNAL.emit([QCoreApplication.translate("mainwindow_src_ui_tr", "Persepolis"),
QCoreApplication.translate("mainwindow_src_ui_tr", "Queue Stopped!"),
10000, 'no'])
# write message in log
logger.sendToLog('Queue stopped', 'DOWNLOADS')
break
if self.break_for_loop:
break
if self.start:
# if queue finished :
self.start = False
# this section is sending shutdown signal to the shutdown script(if user
# select shutdown for after download)
if self.after:
# write 'shutdown' value for this category in temp_db
shutdown_dict = {'category': self.category, 'shutdown': 'shutdown'}
self.main_window.temp_db.updateQueueTable(shutdown_dict)
# show a notification about system is shutting down now!
self.NOTIFYSENDSIGNAL.emit([QCoreApplication.translate("mainwindow_src_ui_tr", 'Persepolis is shutting down'),
QCoreApplication.translate("mainwindow_src_ui_tr", 'your system in 20 seconds'),
15000, 'warning'])
# show notification for queue completion
self.NOTIFYSENDSIGNAL.emit([QCoreApplication.translate("mainwindow_src_ui_tr", "Persepolis"),
QCoreApplication.translate("mainwindow_src_ui_tr", 'Queue completed!'),
10000, 'queue'])
# write a message in log
logger.sendToLog('Queue completed', 'DOWNLOADS')
self.stop = True
self.limit_changed = False
self.after = False
if str(self.main_window.category_tree.currentIndex().data()) == str(self.category):
self.REFRESHTOOLBARSIGNAL.emit(self.category)
================================================
FILE: persepolis/scripts/queue_prime.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
from time import sleep
from persepolis.scripts import logger
from persepolis.scripts.bubble import notifySend
from persepolis.scripts import persepolis_lib_prime
from persepolis.scripts.video_finder import VideoFinder
from persepolis.scripts.download_link import DownloadLink
try:
from PySide6.QtCore import QThread, Signal, QCoreApplication
except ImportError:
from PyQt5.QtCore import QThread, QCoreApplication
from PyQt5.QtCore import pyqtSignal as Signal
try:
from persepolis.scripts import ytdlp_downloader
except ModuleNotFoundError:
# if youtube_dl module is not installed:
logger.sendToLog(
"yt-dlp is not installed.", "ERROR")
class Queue():
def __init__(self, queue_dict, main_window):
self.queue_name = queue_dict['category']
self.start_time_enable = queue_dict['start_time_enable']
self.start_time = queue_dict['start_time']
self.end_time_enable = queue_dict['end_time_enable']
self.end_time = queue_dict['end_time']
self.reverse = queue_dict['reverse']
self.limit_enable = queue_dict['limit_enable']
self.limit_value = queue_dict['limit_value']
self.after_download = queue_dict['after_download']
self.gid_list = queue_dict['gid_list']
self.main_window = main_window
def start(self):...
================================================
FILE: persepolis/scripts/setting.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
try:
from PySide6.QtCore import Qt, QEvent, QTime, QSize, QPoint, QDir, QTranslator, QCoreApplication, QLocale
from PySide6.QtWidgets import QFileDialog, QStyleFactory, QMessageBox, QTableWidgetItem
from PySide6.QtGui import QFont, QKeySequence
except ImportError:
from PyQt5.QtCore import Qt, QEvent, QTime, QSize, QPoint, QDir, QTranslator, QCoreApplication, QLocale
from PyQt5.QtWidgets import QFileDialog, QStyleFactory, QMessageBox, QTableWidgetItem
from PyQt5.QtGui import QFont, QKeySequence
from persepolis.constants import OS
from persepolis.gui.setting_ui import Setting_Ui, KeyCapturingWindow_Ui
from persepolis.scripts.useful_tools import returnDefaultSettings, getExecPath
from persepolis.scripts.browser_integration import browserIntegration, browserIsolation
from persepolis.scripts import osCommands
from persepolis.scripts import startup
from persepolis.scripts import logger
import platform
import os
home_address = os.path.expanduser("~")
os_type = platform.system()
class KeyCapturingWindow(KeyCapturingWindow_Ui):
def __init__(self, callback, persepolis_setting):
super().__init__(persepolis_setting)
self.persepolis_setting = persepolis_setting
self.callback = callback
self.ok_pushButton.clicked.connect(self.okPushButtonPressed)
self.cancel_pushButton.clicked.connect(self.close)
self.installEventFilter(self)
def eventFilter(self, source, event):
if event.type() == QEvent.KeyPress:
if event.key():
# show new keys in window
self.capturedKeyLabel.setText(str(QKeySequence(event.modifiers() | event.key()).toString()))
return super(KeyCapturingWindow, self).eventFilter(source, event)
def okPushButtonPressed(self, button):
# return new keys
self.callback(self.capturedKeyLabel.text())
self.close()
def closeEvent(self, event):
event.accept()
class PreferencesWindow(Setting_Ui):
def __init__(self, parent, persepolis_setting):
super().__init__(persepolis_setting)
self.persepolis_setting = persepolis_setting
self.parent = parent
self.grandparent = parent.persepolis_main
self.persepolis_setting.beginGroup('settings')
# initialization
self.tries_spinBox.setValue(
int(self.persepolis_setting.value('max-tries')))
self.chunk_size_spinBox.setValue(
int(self.persepolis_setting.value('chunk-size')))
self.wait_spinBox.setValue(
int(self.persepolis_setting.value('retry-wait')))
self.time_out_spinBox.setValue(
int(self.persepolis_setting.value('timeout')))
self.connections_spinBox.setValue(
int(self.persepolis_setting.value('connections')))
# check certificate
if str(self.persepolis_setting.value('dont-check-certificate')) == 'yes':
self.dont_check_certificate_checkBox.setChecked(True)
else:
self.dont_check_certificate_checkBox.setChecked(False)
# add support for other languages
locale = str(self.persepolis_setting.value('settings/locale'))
QLocale.setDefault(QLocale(locale))
self.translator = QTranslator()
if self.translator.load(':/translations/locales/ui_' + locale, 'ts'):
QCoreApplication.installTranslator(self.translator)
# wait_queue
wait_queue_list = self.persepolis_setting.value('wait-queue')
try:
q_time = QTime(int(wait_queue_list[0]), int(wait_queue_list[1]))
except Exception:
q_time = QTime(0, 0)
self.wait_queue_time.setTime(q_time)
# save_as_tab
self.download_folder_lineEdit.setText(
str(self.persepolis_setting.value('download_path')))
# subfolder
if str(self.persepolis_setting.value('subfolder')) == 'yes':
self.subfolder_checkBox.setChecked(True)
else:
self.subfolder_checkBox.setChecked(False)
# notifications_tab
self.volume_label.setText(
'Volume : ' + str(self.persepolis_setting.value('sound-volume')))
self.volume_dial.setValue(
int(self.persepolis_setting.value('sound-volume')))
# set style
# find available styles(It's depends on operating system and desktop environments).
available_styles = QStyleFactory.keys()
for style in available_styles:
# 'bb10dark', 'GTK', 'gtk' styles may cause to crashing! Eliminate them!
style_black_list = ['bb10dark', 'bb10bright', 'GTK', 'gtk', 'gtk2']
if style not in style_black_list:
self.style_comboBox.addItem(style)
# System >> for system default style
# when user select System for style section, the default system style is using.
self.style_comboBox.addItem('System')
current_style_index = self.style_comboBox.findText(
str(self.persepolis_setting.value('style')))
# If style not found, set style to Fusion
if current_style_index == -1:
current_style_index = self.style_comboBox.findText('Fusion')
self.style_comboBox.setCurrentIndex(current_style_index)
# available language
available_language = ['en_US', 'fa_IR', 'ar', 'es_ES', 'fr_FR', 'ko', 'pl_PL',
'pt', 'ru', 'tr', 'zh_CN', 'de', 'hu', 'nl_NL', 'pt_BR',
'sv', 'tr_TR', 'zh_TW']
for lang in available_language:
self.lang_comboBox.addItem(str(QLocale(lang).nativeLanguageName()), lang)
current_locale = self.lang_comboBox.findData(
str(self.persepolis_setting.value('locale')))
self.lang_comboBox.setCurrentIndex(current_locale)
self.current_icon = self.persepolis_setting.value('icons')
# icon size
size = ['128', '64', '48', '32', '24', '16']
self.icons_size_comboBox.addItems(size)
current_icons_size_index = self.icons_size_comboBox.findText(
str(self.persepolis_setting.value('toolbar_icon_size')))
self.icons_size_comboBox.setCurrentIndex(current_icons_size_index)
# call setDarkLightIcon if index is changed
self.icons_size_comboBox.currentIndexChanged.connect(self.setDarkLightIcon)
# set notification
notifications = ['Native notification', 'QT notification']
self.notification_comboBox.addItems(notifications)
exec_dict = getExecPath()
is_bundle = exec_dict['bundle']
# If Persepolis does not run as a bundle on Linux(Only Linux), it can use the native notification system.
if (os_type not in OS.UNIX_LIKE) or (os_type in OS.UNIX_LIKE and is_bundle):
self.notification_comboBox.hide()
self.notification_label.hide()
current_notification_index = self.notification_comboBox.findText('QT notification')
else:
current_notification_index = self.notification_comboBox.findText(
str(self.persepolis_setting.value('notification')))
self.notification_comboBox.setCurrentIndex(current_notification_index)
# set font
font_setting = QFont()
font_setting.setFamily(str(self.persepolis_setting.value('font')))
self.fontComboBox.setCurrentFont(font_setting)
self.font_size_spinBox.setValue(
int(self.persepolis_setting.value('font-size')))
# sound frame
self.sound_frame.setEnabled(False)
self.enable_notifications_checkBox.toggled.connect(self.soundFrame)
if str(self.persepolis_setting.value('sound')) == 'yes':
self.enable_notifications_checkBox.setChecked(True)
else:
self.enable_notifications_checkBox.setChecked(False)
# connect folder buttons
self.download_folder_lineEdit.setEnabled(False)
self.download_folder_pushButton.clicked.connect(
self.downloadFolderPushButtonClicked)
# dial
self.volume_dial.setNotchesVisible(True)
self.volume_dial.valueChanged.connect(self.dialChanged)
# start_persepolis_if_browser_executed_checkBox
if str(self.persepolis_setting.value('browser-persepolis')) == 'yes':
self.start_persepolis_if_browser_executed_checkBox.setChecked(True)
else:
self.start_persepolis_if_browser_executed_checkBox.setChecked(False)
# hide window
if str(self.persepolis_setting.value('hide-window')) == 'yes':
self.hide_window_checkBox.setChecked(True)
else:
self.hide_window_checkBox.setChecked(False)
# tray icon
if str(self.persepolis_setting.value('tray-icon')) == 'yes':
self.enable_system_tray_checkBox.setChecked(True)
else:
self.enable_notifications_checkBox.setChecked(False)
# show_menubar
if str(self.persepolis_setting.value('show-menubar')) == 'yes':
self.show_menubar_checkbox.setChecked(True)
else:
self.show_menubar_checkbox.setChecked(False)
if platform.system() == 'Darwin':
self.show_menubar_checkbox.setChecked(True)
self.show_menubar_checkbox.hide()
# show_sidepanel
if str(self.persepolis_setting.value('show-sidepanel')) == 'yes':
self.show_sidepanel_checkbox.setChecked(True)
else:
self.show_sidepanel_checkbox.setChecked(False)
# show ProgressWindow
if str(self.persepolis_setting.value('show-progress')) == 'yes':
self.show_progress_window_checkbox.setChecked(True)
else:
self.show_progress_window_checkbox.setChecked(False)
# after download dialog
if str(self.persepolis_setting.value('after-dialog')) == 'yes':
self.after_download_checkBox.setChecked(True)
else:
self.after_download_checkBox.setChecked(False)
# run persepolis at startup checkBox
if str(self.persepolis_setting.value('startup')) == 'yes':
self.startup_checkbox.setChecked(True)
else:
self.startup_checkbox.setChecked(False)
# font_checkBox
if str(self.persepolis_setting.value('custom-font')) == 'yes':
self.font_checkBox.setChecked(True)
else:
self.font_checkBox.setChecked(False)
self.fontCheckBoxState(self.font_checkBox)
# keep_awake_checkBox
if str(self.persepolis_setting.value('awake')) == 'yes':
self.keep_awake_checkBox.setChecked(True)
else:
self.keep_awake_checkBox.setChecked(False)
# check_clipboard_checkBox
if str(self.persepolis_setting.value('check-clipboard')) == 'yes':
self.check_clipboard_checkBox.setChecked(True)
else:
self.check_clipboard_checkBox.setChecked(False)
# When a download request is sent from the browser extension,
# the download will start without showing the Add Link window.
if str(self.persepolis_setting.value('dont-show-addlinkwindow')) == 'yes':
self.dont_show_add_link_window_checkBox.setChecked(True)
else:
self.dont_show_add_link_window_checkBox.setChecked(False)
# columns_tab
if str(self.persepolis_setting.value('column0')) == 'yes':
self.column0_checkBox.setChecked(True)
else:
self.column0_checkBox.setChecked(False)
if str(self.persepolis_setting.value('column1')) == 'yes':
self.column1_checkBox.setChecked(True)
else:
self.column1_checkBox.setChecked(False)
if str(self.persepolis_setting.value('column2')) == 'yes':
self.column2_checkBox.setChecked(True)
else:
self.column2_checkBox.setChecked(False)
if str(self.persepolis_setting.value('column3')) == 'yes':
self.column3_checkBox.setChecked(True)
else:
self.column3_checkBox.setChecked(False)
if str(self.persepolis_setting.value('column4')) == 'yes':
self.column4_checkBox.setChecked(True)
else:
self.column4_checkBox.setChecked(False)
if str(self.persepolis_setting.value('column5')) == 'yes':
self.column5_checkBox.setChecked(True)
else:
self.column5_checkBox.setChecked(False)
if str(self.persepolis_setting.value('column6')) == 'yes':
self.column6_checkBox.setChecked(True)
else:
self.column6_checkBox.setChecked(False)
if str(self.persepolis_setting.value('column7')) == 'yes':
self.column7_checkBox.setChecked(True)
else:
self.column7_checkBox.setChecked(False)
if str(self.persepolis_setting.value('column10')) == 'yes':
self.column10_checkBox.setChecked(True)
else:
self.column10_checkBox.setChecked(False)
if str(self.persepolis_setting.value('column11')) == 'yes':
self.column11_checkBox.setChecked(True)
else:
self.column11_checkBox.setChecked(False)
if str(self.persepolis_setting.value('column12')) == 'yes':
self.column12_checkBox.setChecked(True)
else:
self.column12_checkBox.setChecked(False)
# video_finder
try: # Integer casting may raise exception.
self.max_links_spinBox.setValue(int(persepolis_setting.value('video_finder/max_links', 3)))
except ValueError:
pass
# shortcuts
self.qshortcuts_list = [self.parent.exitAction_shortcut,
self.parent.minimizeAction_shortcut,
self.parent.removeSelectedAction_shortcut,
self.parent.deleteSelectedAction_shortcut,
self.parent.moveUpSelectedAction_shortcut,
self.parent.moveDownSelectedAction_shortcut,
self.parent.addlinkAction_shortcut,
self.parent.videoFinderAddLinkAction_shortcut,
self.parent.addtextfileAction_shortcut]
self.shortcuts_list = [self.parent.exitAction_shortcut.key().toString(),
self.parent.minimizeAction_shortcut.key().toString(),
self.parent.removeSelectedAction_shortcut.key().toString(),
self.parent.deleteSelectedAction_shortcut.key().toString(),
self.parent.moveUpSelectedAction_shortcut.key().toString(),
self.parent.moveDownSelectedAction_shortcut.key().toString(),
self.parent.addlinkAction_shortcut.key().toString(),
self.parent.videoFinderAddLinkAction_shortcut.key().toString(),
self.parent.addtextfileAction_shortcut.key().toString()]
# add shortcuts to the shortcut_table
j = 0
for shortcut in self.shortcuts_list:
item = QTableWidgetItem(shortcut)
# align center
item.setTextAlignment(0x0004 | 0x0080)
# insert item in shortcut_table
self.shortcut_table.setItem(j, 1, item)
j = j + 1
# If user doubleclicks on a row, then run showCaptureKeyboardWindow method
self.shortcut_table.itemDoubleClicked.connect(self.showCaptureKeyboardWindow)
# ok cancel default button
self.cancel_pushButton.clicked.connect(self.close)
self.defaults_pushButton.clicked.connect(
self.defaultsPushButtonPressed)
self.ok_pushButton.clicked.connect(self.okPushButtonPressed)
# font_checkBox connect
self.font_checkBox.stateChanged.connect(self.fontCheckBoxState)
# saving initial value of self.persepolis_setting in self.first_key_value_dict
# at the end! in the okPushButtonPressed method, first_key_value_dict will compared with second_key_value_dict.
# if any thing changed , then a message box notify user about "some changes take effect after restarting persepolis".
self.first_key_value_dict = {}
for member in self.persepolis_setting.allKeys():
self.first_key_value_dict[member] = str(self.persepolis_setting.value(member))
# if style_comboBox is changed, self.styleComboBoxChanged is called.
self.style_comboBox.currentIndexChanged.connect(self.styleComboBoxChanged)
self.styleComboBoxChanged()
self.color_comboBox.currentIndexChanged.connect(self.setDarkLightIcon)
self.persepolis_setting.endGroup()
# browser_integration_tab
# Load checkbox states from settings
self.persepolis_setting.beginGroup('settings/native_messaging')
for browser, checkbox in self.browser_checkboxes.items():
value = self.persepolis_setting.value(browser)
if value == 'true':
checkbox.setChecked(True)
else:
checkbox.setChecked(False)
self.persepolis_setting.endGroup()
# setting window size and position
size = self.persepolis_setting.value(
'PreferencesWindow/size', QSize(578, 597))
position = self.persepolis_setting.value(
'PreferencesWindow/position', QPoint(300, 300))
self.resize(size)
self.move(position)
# run this method if user doubleclicks on an item in shortcut_table
def showCaptureKeyboardWindow(self):
# show KeyCapturingWindow
keyboard_capture_window = KeyCapturingWindow(self.callBack, self.persepolis_setting)
self.parent.capturekeywindows_list.append(keyboard_capture_window)
self.parent.capturekeywindows_list[-1].show()
def callBack(self, keys):
# do nothing if keys is empty
if not (keys):
return
# check that if shortcut used before.
if keys in self.shortcuts_list:
self.msgBox = QMessageBox()
self.msgBox.setText(QCoreApplication.translate("setting_src_ui_tr", "
This shortcut has been used before!\
Use another one!
"))
self.msgBox.setIcon(QMessageBox.Warning)
# set new shortcut
else:
selected_row = self.shortcut_table.selectionModel().selectedRows()[0].row()
item = QTableWidgetItem(keys)
# align center
item.setTextAlignment(0x0004 | 0x0080)
# insert item in shortcut_table
self.shortcut_table.setItem(selected_row, 1, item)
# set keys in shortcuts_list
self.shortcuts_list[selected_row] = keys
# active color_comboBox only when user is select "Fusion" style.
def styleComboBoxChanged(self, index=None):
# clear color_comboBox
self.color_comboBox.clear()
# get current style
selected_style = self.style_comboBox.currentText()
if selected_style != 'Fusion':
# color_comboBox item
color_scheme = ['System']
# add item
self.color_comboBox.addItems(color_scheme)
# set 'System' for color_scheme
current_color_index = self.color_comboBox.findText('System')
self.color_comboBox.setCurrentIndex(current_color_index)
# disable color_comboBox
self.color_comboBox.setEnabled(False)
else:
# enable color_comboBox
self.color_comboBox.setEnabled(True)
# color_comboBox items
color_scheme = ['Dark Fusion', 'Light Fusion']
# add items
self.color_comboBox.addItems(color_scheme)
current_color_index = self.color_comboBox.findText(
str(self.persepolis_setting.value('color-scheme')))
# it means user's preferred color_scheme is not valid in color_comboBox.
if current_color_index == -1:
current_color_index = 0
self.color_comboBox.setCurrentIndex(current_color_index)
self.setDarkLightIcon()
# this method sets dark icons for dark color schemes
# and light icons for light color schemes.
def setDarkLightIcon(self, index=None):
dark_theme = None
# find selected style
selected_style = self.style_comboBox.currentText()
# clear icon_comboBox
self.icon_comboBox.clear()
# Papirus icons can be used with small sizes(smaller than 48)
# get user's selected icons size
selected_size = int(self.icons_size_comboBox.currentText())
if selected_style == 'Fusion':
if self.color_comboBox.currentText() == 'Dark Fusion':
dark_theme = True
else:
dark_theme = False
elif selected_style == 'Adwaita-Dark':
dark_theme = True
elif selected_style == 'Adwaita':
dark_theme = False
if dark_theme is True:
self.icon_comboBox.clear()
if selected_size < 48:
icons = ['Breeze-Dark', 'Papirus-Dark', 'Papirus']
else:
icons = ['Breeze-Dark']
self.icon_comboBox.addItems(icons)
# current_icons_index is -1, if findText couldn't find icon index.
current_icons_index = self.icon_comboBox.findText(
str(self.persepolis_setting.value('icons', self.current_icon)))
if current_icons_index == -1:
current_icons_index = 0
self.icon_comboBox.setCurrentIndex(current_icons_index)
elif dark_theme is False:
if selected_size < 48:
icons = ['Breeze', 'Papirus', 'Papirus-Light']
else:
icons = ['Breeze', 'Papirus']
self.icon_comboBox.addItems(icons)
# current_icons_index is -1, if findText couldn't find icon index.
current_icons_index = self.icon_comboBox.findText(
str(self.persepolis_setting.value('icons', self.current_icon)))
if current_icons_index == -1:
current_icons_index = 0
self.icon_comboBox.setCurrentIndex(current_icons_index)
else:
if selected_size < 48:
icons = ['Breeze', 'Breeze-Dark', 'Papirus',
'Papirus-Dark', 'Papirus-Light']
else:
icons = ['Breeze', 'Breeze-Dark', 'Papirus']
self.icon_comboBox.addItems(icons)
# current_icons_index is -1, if findText couldn't find icon index.
current_icons_index = self.icon_comboBox.findText(
str(self.persepolis_setting.value('icons', self.current_icon)))
if current_icons_index == -1:
current_icons_index = 0
self.icon_comboBox.setCurrentIndex(current_icons_index)
def fontCheckBoxState(self, checkBox):
# deactivate fontComboBox and font_size_spinBox if font_checkBox not checked!
if self.font_checkBox.isChecked():
self.fontComboBox.setEnabled(True)
self.font_size_spinBox.setEnabled(True)
else:
self.fontComboBox.setEnabled(False)
self.font_size_spinBox.setEnabled(False)
# close window with ESC key
def keyPressEvent(self, event):
if event.key() == Qt.Key_Escape:
self.close()
def closeEvent(self, event):
# saving window size and position
self.persepolis_setting.setValue('PreferencesWindow/size', self.size())
self.persepolis_setting.setValue(
'PreferencesWindow/position', self.pos())
self.persepolis_setting.sync()
event.accept()
if self.parent.isVisible() is False:
self.parent.minMaxTray(event)
def browserIntegrationSettings(self):
# native messaging
# The group is already 'settings' from the caller (okPushButtonPressed),
# so we just need to enter the 'native_messaging' subgroup.
# Using an absolute path here would be wrong and cause nesting issues.
self.persepolis_setting.beginGroup("native_messaging")
previous_enabled = []
for key in self.persepolis_setting.childKeys():
if self.persepolis_setting.value(key) == 'true':
previous_enabled.append(key)
enabled_browsers = [b for b, cb in self.browser_checkboxes.items() if cb.isChecked()]
# Save checkbox states
for browser, cb in self.browser_checkboxes.items():
self.persepolis_setting.setValue(browser, 'true' if cb.isChecked() else 'false')
self.persepolis_setting.endGroup()
# Remove unselected manifests
removed_browsers = set(previous_enabled) - set(enabled_browsers)
for browser in removed_browsers:
logg_message, logg_message2 = browserIsolation(browser)
logger.sendToLog(logg_message)
logger.sendToLog(logg_message2)
# Install newly enabled manifests
for browser in enabled_browsers:
json_done, intermediary_done, logg_message2 = browserIntegration(browser)
logg_message = browser
if json_done is True:
logg_message = logg_message + ': ' + 'Json file is created successfully.\n'
else:
logg_message = logg_message + ': ' + 'Json ERROR!\n'
if intermediary_done is True:
logg_message = logg_message + 'persepolis intermediary file is created successfully.\n'
elif intermediary_done is False:
logg_message = logg_message + ': ' + 'persepolis executer file ERROR!\n'
logger.sendToLog(logg_message)
logger.sendToLog(logg_message2)
def soundFrame(self, checkBox):
if self.enable_notifications_checkBox.isChecked():
self.sound_frame.setEnabled(True)
else:
self.sound_frame.setEnabled(False)
def downloadFolderPushButtonClicked(self, button):
download_path = str(
self.persepolis_setting.value('settings/download_path'))
fname = QFileDialog.getExistingDirectory(
self, 'Select a directory', download_path)
if fname:
# Returns pathName with the '/' separators converted to separators that are appropriate for the underlying operating system.
# On Windows, toNativeSeparators("c:/winnt/system32") returns
# "c:\winnt\system32".
fname = QDir.toNativeSeparators(fname)
self.download_folder_lineEdit.setText(fname)
self.persepolis_setting.setValue(
'settings/download_path', str(fname))
def dialChanged(self, dial):
self.volume_label.setText('Volume : ' + str(self.volume_dial.value()))
def defaultsPushButtonPressed(self, button):
self.persepolis_setting.beginGroup('settings')
self.setting_dict = returnDefaultSettings()
self.tries_spinBox.setValue(int(self.setting_dict['max-tries']))
self.chunk_size_spinBox.setValue(int(self.setting_dict['chunk-size']))
self.wait_spinBox.setValue(int(self.setting_dict['retry-wait']))
self.time_out_spinBox.setValue(int(self.setting_dict['timeout']))
self.connections_spinBox.setValue(
int(self.setting_dict['connections']))
# wait-queue
wait_queue_list = self.setting_dict['wait-queue']
q_time = QTime(wait_queue_list[0], wait_queue_list[1])
self.wait_queue_time.setTime(q_time)
# dont_check_certificate_checkBox
self.dont_check_certificate_checkBox.setChecked(False)
# save_as_tab
self.download_folder_lineEdit.setText(
str(self.setting_dict['download_path']))
self.subfolder_checkBox.setChecked(True)
# notifications_tab
self.volume_label.setText(
'Volume : ' + str(self.setting_dict['sound-volume']))
self.volume_dial.setValue(int(self.setting_dict['sound-volume']))
# set style
current_style_index = self.style_comboBox.findText(
str(self.setting_dict['style']))
self.style_comboBox.setCurrentIndex(current_style_index)
# set language
current_locale = self.lang_comboBox.findData(
str(self.setting_dict['locale']))
self.lang_comboBox.setCurrentIndex(current_locale)
# set color_scheme
current_color_index = self.color_comboBox.findText(
str(self.setting_dict['color-scheme']))
self.color_comboBox.setCurrentIndex(current_color_index)
# set icons
current_icons_index = self.icon_comboBox.findText(
str(self.setting_dict['icons']))
self.icon_comboBox.setCurrentIndex(current_icons_index)
# set icons size
current_icons_size_index = self.icons_size_comboBox.findText(
str(self.setting_dict['toolbar_icon_size']))
self.icons_size_comboBox.setCurrentIndex(current_icons_size_index)
# set notification
current_notification_index = self.notification_comboBox.findText(
str(self.setting_dict['notification']))
self.notification_comboBox.setCurrentIndex(current_notification_index)
# set font
self.font_checkBox.setChecked(False)
font_setting = QFont()
font_setting.setFamily(str(self.setting_dict['font']))
self.fontComboBox.setCurrentFont(font_setting)
self.font_size_spinBox.setValue(int(self.setting_dict['font-size']))
# sound frame
self.enable_notifications_checkBox.setChecked(True)
# start_persepolis_if_browser_executed_checkBox
self.start_persepolis_if_browser_executed_checkBox.setChecked(True)
# hide window
self.hide_window_checkBox.setChecked(True)
# tray icon
self.enable_system_tray_checkBox.setChecked(True)
# after_download_checkBox
self.after_download_checkBox.setChecked(True)
# hide menubar for linux
if platform.system == 'Darwin':
self.show_menubar_checkbox.setChecked(True)
else:
self.show_menubar_checkbox.setChecked(False)
# show side panel
self.show_sidepanel_checkbox.setChecked(True)
# show progress window
self.show_progress_window_checkbox.setChecked(True)
# run persepolis at startup checkBox
self.startup_checkbox.setChecked(False)
# keep_awake_checkBox
self.keep_awake_checkBox.setChecked(False)
# check clipboard
self.check_clipboard_checkBox.setChecked(False)
# don't show addlinkwindows
self.dont_show_add_link_window_checkBox.setChecked(False)
# columns_tab
self.column0_checkBox.setChecked(True)
self.column1_checkBox.setChecked(True)
self.column2_checkBox.setChecked(True)
self.column3_checkBox.setChecked(True)
self.column4_checkBox.setChecked(True)
self.column5_checkBox.setChecked(True)
self.column6_checkBox.setChecked(True)
self.column7_checkBox.setChecked(True)
self.column10_checkBox.setChecked(True)
self.column11_checkBox.setChecked(True)
self.column12_checkBox.setChecked(True)
# video finder
self.max_links_spinBox.setValue(3)
# shortcuts
self.shortcuts_list = [self.setting_dict['shortcuts/quit_shortcut'],
self.setting_dict['shortcuts/hide_window_shortcut'],
self.setting_dict['shortcuts/remove_shortcut'],
self.setting_dict['shortcuts/delete_shortcut'],
self.setting_dict['shortcuts/move_up_selection_shortcut'],
self.setting_dict['shortcuts/move_down_selection_shortcut'],
self.setting_dict['shortcuts/add_new_download_shortcut'],
self.setting_dict['shortcuts/video_finder_shortcut'],
self.setting_dict['shortcuts/import_text_shortcut']]
# add shortcuts to the shortcut_table
j = 0
for shortcut in self.shortcuts_list:
item = QTableWidgetItem(shortcut)
# align center
item.setTextAlignment(0x0004 | 0x0080)
# insert item in shortcut_table
self.shortcut_table.setItem(j, 1, item)
j = j + 1
self.persepolis_setting.endGroup()
def okPushButtonPressed(self, button):
self.persepolis_setting.beginGroup('settings')
# Save browser integration settings
self.browserIntegrationSettings()
self.persepolis_setting.setValue(
'max-tries', self.tries_spinBox.value())
self.persepolis_setting.setValue(
'chunk-size', self.chunk_size_spinBox.value())
self.persepolis_setting.setValue(
'retry-wait', self.wait_spinBox.value())
self.persepolis_setting.setValue(
'timeout', self.time_out_spinBox.value())
self.persepolis_setting.setValue(
'connections', self.connections_spinBox.value())
self.persepolis_setting.setValue(
'download_path', self.download_folder_lineEdit.text())
self.persepolis_setting.setValue(
'sound-volume', self.volume_dial.value())
self.persepolis_setting.setValue(
'notification', self.notification_comboBox.currentText())
self.persepolis_setting.setValue(
'wait-queue', self.wait_queue_time.text().split(':'))
# don't check certificate
if self.dont_check_certificate_checkBox.isChecked():
self.persepolis_setting.setValue('dont-check-certificate', 'yes')
else:
self.persepolis_setting.setValue('dont-check-certificate', 'no')
# don't show addlinkwindows
if self.dont_show_add_link_window_checkBox.isChecked():
self.persepolis_setting.setValue('dont-show-addlinkwindow', 'yes')
else:
self.persepolis_setting.setValue('dont-show-addlinkwindow', 'no')
# changing icons
icons = self.icon_comboBox.currentText()
self.persepolis_setting.setValue('icons', icons)
if icons != self.current_icon: # it means icons changed
for windows_list in [self.parent.logwindow_list, self.parent.about_window_list,
self.parent.addlinkwindows_list, self.parent.propertieswindows_list,
self.parent.afterdownload_list, self.parent.text_queue_window_list,
self.parent.progress_window_list, self.parent.plugin_queue_window_list]:
for window in windows_list:
window.changeIcon(icons)
self.parent.changeIcon(icons)
# icons size
icons_size = self.icons_size_comboBox.currentText()
self.persepolis_setting.setValue('toolbar_icon_size', icons_size)
icons_size = int(icons_size)
self.parent.toolBar.setIconSize(QSize(icons_size, icons_size))
self.parent.toolBar2.setIconSize(QSize(icons_size, icons_size))
# style
style = str(self.style_comboBox.currentText())
self.persepolis_setting.setValue('style', style)
# language
locale = str(self.lang_comboBox.itemData(self.lang_comboBox.currentIndex()))
self.persepolis_setting.setValue('locale', locale)
# color_scheme
color_scheme = self.color_comboBox.currentText()
self.persepolis_setting.setValue('color-scheme', color_scheme)
# font and font size
current_font = self.fontComboBox.currentFont()
current_font = current_font.key()
current_font = current_font.split(',')
font = str(current_font[0])
self.persepolis_setting.setValue('font', font)
font_size = self.font_size_spinBox.value()
self.persepolis_setting.setValue('font-size', font_size)
if self.font_checkBox.isChecked():
custom_font = 'yes'
else:
custom_font = 'no'
self.persepolis_setting.setValue('custom-font', custom_font)
# if user select qt notification >> enable_system_tray icon
if self.persepolis_setting.value('notification') == 'QT notification':
self.enable_system_tray_checkBox.setChecked(True)
# start_persepolis_if_browser_executed_checkBox
if self.start_persepolis_if_browser_executed_checkBox.isChecked():
self.persepolis_setting.setValue('browser-persepolis', 'yes')
else:
self.persepolis_setting.setValue('browser-persepolis', 'no')
# hide_window_checkBox
if self.hide_window_checkBox.isChecked():
self.persepolis_setting.setValue('hide-window', 'yes')
else:
self.persepolis_setting.setValue('hide-window', 'no')
# enable_system_tray_checkBox
if self.enable_system_tray_checkBox.isChecked():
self.persepolis_setting.setValue('tray-icon', 'yes')
self.parent.system_tray_icon.show()
self.parent.minimizeAction.setEnabled(True)
self.parent.trayAction.setChecked(True)
else:
self.persepolis_setting.setValue('tray-icon', 'no')
self.parent.system_tray_icon.hide()
self.parent.minimizeAction.setEnabled(False)
self.parent.trayAction.setChecked(False)
# after_download_checkBox
if self.after_download_checkBox.isChecked():
self.persepolis_setting.setValue('after-dialog', 'yes')
else:
self.persepolis_setting.setValue('after-dialog', 'no')
# show_menubar_checkbox
if self.show_menubar_checkbox.isChecked():
self.persepolis_setting.setValue('show-menubar', 'yes')
self.parent.menubar.show()
self.parent.toolBar2.hide()
self.parent.showMenuBarAction.setChecked(True)
else:
self.persepolis_setting.setValue('show-menubar', 'no')
self.parent.menubar.hide()
self.parent.toolBar2.show()
self.parent.showMenuBarAction.setChecked(False)
# show_sidepanel_checkbox
if self.show_sidepanel_checkbox.isChecked():
self.persepolis_setting.setValue('show-sidepanel', 'yes')
self.parent.category_tree_qwidget.show()
else:
self.persepolis_setting.setValue('show-sidepanel', 'no')
self.parent.category_tree_qwidget.hide()
# show_progress_window_checkbox
if self.show_progress_window_checkbox.isChecked():
self.persepolis_setting.setValue('show-progress', 'yes')
else:
self.persepolis_setting.setValue('show-progress', 'no')
if self.startup_checkbox.isChecked():
self.persepolis_setting.setValue('startup', 'yes')
if not (startup.checkStartUp()): # checking existence of Persepolis in system's startup
startup.addStartUp(self.parent) # adding Persepolis to system's startup
else:
self.persepolis_setting.setValue('startup', 'no')
if startup.checkStartUp(): # checking existence of Persepolis in system's startup
startup.removeStartUp() # removing Persepolis from system's startup
# keep_awake_checkBox
if self.keep_awake_checkBox.isChecked():
self.persepolis_setting.setValue('awake', 'yes')
self.parent.keep_awake_checkBox.setChecked(True)
else:
self.persepolis_setting.setValue('awake', 'no')
self.parent.keep_awake_checkBox.setChecked(False)
# check_clipboard_checkBox
if self.check_clipboard_checkBox.isChecked():
self.persepolis_setting.setValue('check-clipboard', 'yes')
else:
self.persepolis_setting.setValue('check-clipboard', 'no')
# this section creates download folder and
# download sub folders if they did not existed.
download_path = self.persepolis_setting.value('download_path')
folder_list = [download_path]
if self.subfolder_checkBox.isChecked():
self.persepolis_setting.setValue('subfolder', 'yes')
for folder in ['Audios', 'Videos', 'Others', 'Documents', 'Compressed']:
folder_list.append(os.path.join(download_path, folder))
else:
self.persepolis_setting.setValue('subfolder', 'no')
for folder in folder_list:
osCommands.makeDirs(folder)
if self.enable_notifications_checkBox.isChecked():
self.persepolis_setting.setValue('sound', 'yes')
else:
self.persepolis_setting.setValue('sound', 'no')
# columns_tab
if self.column0_checkBox.isChecked():
self.persepolis_setting.setValue('column0', 'yes')
self.parent.download_table.setColumnHidden(0, False)
if self.parent.download_table.isColumnHidden(0):
self.parent.download_table.setColumnWidth(0, 100)
else:
self.persepolis_setting.setValue('column0', 'no')
self.parent.download_table.setColumnHidden(0, True)
if self.column1_checkBox.isChecked():
self.persepolis_setting.setValue('column1', 'yes')
self.parent.download_table.setColumnHidden(1, False)
if self.parent.download_table.isColumnHidden(1):
self.parent.download_table.setColumnWidth(1, 100)
else:
self.persepolis_setting.setValue('column1', 'no')
self.parent.download_table.setColumnHidden(1, True)
if self.column2_checkBox.isChecked():
self.persepolis_setting.setValue('column2', 'yes')
self.parent.download_table.setColumnHidden(2, False)
if self.parent.download_table.isColumnHidden(2):
self.parent.download_table.setColumnWidth(2, 100)
else:
self.persepolis_setting.setValue('column2', 'no')
self.parent.download_table.setColumnHidden(2, True)
if self.column3_checkBox.isChecked():
self.persepolis_setting.setValue('column3', 'yes')
self.parent.download_table.setColumnHidden(3, False)
if self.parent.download_table.isColumnHidden(3):
self.parent.download_table.setColumnWidth(3, 100)
else:
self.persepolis_setting.setValue('column3', 'no')
self.parent.download_table.setColumnHidden(3, True)
if self.column4_checkBox.isChecked():
self.persepolis_setting.setValue('column4', 'yes')
self.parent.download_table.setColumnHidden(4, False)
if self.parent.download_table.isColumnHidden(4):
self.parent.download_table.setColumnWidth(4, 100)
else:
self.persepolis_setting.setValue('column4', 'no')
self.parent.download_table.setColumnHidden(4, True)
if self.column5_checkBox.isChecked():
self.persepolis_setting.setValue('column5', 'yes')
self.parent.download_table.setColumnHidden(5, False)
if self.parent.download_table.isColumnHidden(5):
self.parent.download_table.setColumnWidth(5, 100)
else:
self.persepolis_setting.setValue('column5', 'no')
self.parent.download_table.setColumnHidden(5, True)
if self.column6_checkBox.isChecked():
self.persepolis_setting.setValue('column6', 'yes')
self.parent.download_table.setColumnHidden(6, False)
if self.parent.download_table.isColumnHidden(6):
self.parent.download_table.setColumnWidth(6, 100)
else:
self.persepolis_setting.setValue('column6', 'no')
self.parent.download_table.setColumnHidden(6, True)
if self.column7_checkBox.isChecked():
self.persepolis_setting.setValue('column7', 'yes')
self.parent.download_table.setColumnHidden(7, False)
if self.parent.download_table.isColumnHidden(7):
self.parent.download_table.setColumnWidth(7, 100)
else:
self.persepolis_setting.setValue('column7', 'no')
self.parent.download_table.setColumnHidden(7, True)
if self.column10_checkBox.isChecked():
self.persepolis_setting.setValue('column10', 'yes')
self.parent.download_table.setColumnHidden(10, False)
if self.parent.download_table.isColumnHidden(10):
self.parent.download_table.setColumnWidth(10, 100)
else:
self.persepolis_setting.setValue('column10', 'no')
self.parent.download_table.setColumnHidden(10, True)
if self.column11_checkBox.isChecked():
self.persepolis_setting.setValue('column11', 'yes')
self.parent.download_table.setColumnHidden(11, False)
if self.parent.download_table.isColumnHidden(11):
self.parent.download_table.setColumnWidth(11, 100)
else:
self.persepolis_setting.setValue('column11', 'no')
self.parent.download_table.setColumnHidden(11, True)
if self.column12_checkBox.isChecked():
self.persepolis_setting.setValue('column12', 'yes')
self.parent.download_table.setColumnHidden(12, False)
if self.parent.download_table.isColumnHidden(12):
self.parent.download_table.setColumnWidth(12, 100)
else:
self.persepolis_setting.setValue('column12', 'no')
self.parent.download_table.setColumnHidden(12, True)
# shortcuts
# set new shortcuts
i = 0
for qshortcut in self.qshortcuts_list:
# set keys for QShortcut
qshortcut.setKey(self.shortcuts_list[i])
i = i + 1
# video_finder
self.persepolis_setting.setValue('video_finder/max_links', self.max_links_spinBox.value())
# saving value of persepolis_setting in second_key_value_dict.
self.second_key_value_dict = {}
for member in self.persepolis_setting.allKeys():
self.second_key_value_dict[member] = str(self.persepolis_setting.value(member))
# comparing first_key_value_dict with second_key_value_dict
show_message_box = False
for key in self.first_key_value_dict.keys():
if self.first_key_value_dict[key] != self.second_key_value_dict[key]:
if key in ['locale', 'download_path',
'custom-font', 'max-tries', 'chunk-size',
'retry-wait', 'timeout', 'connections',
'style', 'font', 'font-size', 'color-scheme',
'check-clipboard']:
show_message_box = True
# if any thing changed that needs restarting, then notify user about "Some changes take effect after restarting persepolis"
if show_message_box:
restart_messageBox = QMessageBox()
restart_messageBox.setText(QCoreApplication.translate(
"setting_src_ui_tr", '
Restart Persepolis Please!
Some changes take effect after restarting Persepolis
'))
restart_messageBox.setWindowTitle(QCoreApplication.translate("setting_src_ui_tr", 'Restart Persepolis!'))
restart_messageBox.exec_()
# applying changes
self.persepolis_setting.endGroup()
self.persepolis_setting.sync()
self.close()
================================================
FILE: persepolis/scripts/shutdown.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
from persepolis.scripts import logger
from persepolis.constants import OS
from time import sleep
import subprocess
import platform
os_type = platform.system()
def shutDown(parent, gid=None, category=None, password=None):
# for queue >> gid = None
# for single downloads >> category = None
# change value of shutdown in data base
if category is not None:
dict = {'category': category,
'shutdown': 'wait'}
# update data base
parent.temp_db.updateQueueTable(dict)
else:
# so we have single download
dict = {'gid': gid,
'shutdown': 'wait'}
# update data base
parent.temp_db.updateSingleTable(dict)
shutdown_status = "wait"
while shutdown_status == "wait":
sleep(5)
# get shutdown status from data_base
if category is not None:
dict = parent.temp_db.returnCategory(category)
else:
dict = parent.temp_db.returnGid(gid)
shutdown_status = dict['shutdown']
if shutdown_status == "shutdown":
logger.sendToLog("Shutting down in a minute", "INFO")
# Make sure all download progresses are stopped.
parent.stopAllDownloads()
sleep(20)
# Make sure all sessions have ended.
while parent.download_sessions_list:
sleep(0.5)
# shutdown_notification = 0 >> persepolis running , 1 >> persepolis is
# ready for close(closeEvent called) , 2 >> OK, let's close application!
parent.changeShutdownValue(1)
while parent.returnShutDownValue() != 2:
sleep(0.1)
# close data bases connections
for db in parent.persepolis_db, parent.plugins_db, parent.temp_db:
db.closeConnections()
for i in parent.threadPool:
i.quit()
i.wait()
parent.cleanTempFolder()
if os_type == OS.LINUX:
pipe = subprocess.Popen(['sudo', '-S', 'poweroff'],
stdout=subprocess.DEVNULL,
stdin=subprocess.PIPE,
stderr=subprocess.DEVNULL,
shell=False)
pipe.communicate(password.encode())
elif os_type == OS.DARWIN:
pipe = subprocess.Popen(['sudo', '-S', 'shutdown', '-h', 'now'],
stdout=subprocess.DEVNULL,
stdin=subprocess.PIPE,
stderr=subprocess.DEVNULL,
shell=False)
pipe.communicate(password.encode())
elif os_type == OS.WINDOWS:
CREATE_NO_WINDOW = 0x08000000
subprocess.Popen(['shutdown', '-S'],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
shell=False,
creationflags=CREATE_NO_WINDOW)
elif os_type in OS.BSD_FAMILY:
pipe = subprocess.Popen(['sudo', '-S', 'shutdown', '-p', 'now'],
stdout=subprocess.DEVNULL,
stdin=subprocess.PIPE,
stderr=subprocess.DEVNULL,
shell=False)
pipe.communicate(password.encode())
================================================
FILE: persepolis/scripts/spider.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
from persepolis.scripts.useful_tools import humanReadableSize, headerToDict, readCookieJar, getFileNameFromLink
from persepolis.constants import VERSION
import requests
try:
from PySide6.QtCore import QSettings
except ImportError:
from PyQt5.QtCore import QSettings
# for more information about "requests" library , please see
# http://docs.python-requests.org/en/master/
# load persepolis_settings
persepolis_setting = QSettings('persepolis_download_manager', 'persepolis')
# check certificate
if str(persepolis_setting.value('settings/dont-check-certificate')) == 'yes':
check_certificate = False
else:
check_certificate = True
timeout = int(persepolis_setting.value('settings/timeout'))
# spider function finds name of file and file size from header
def spider(add_link_dictionary):
# get user's download request from add_link_dictionary
link = add_link_dictionary['link']
ip = add_link_dictionary['ip']
port = add_link_dictionary['port']
proxy_user = add_link_dictionary['proxy_user']
proxy_passwd = add_link_dictionary['proxy_passwd']
proxy_type = add_link_dictionary['proxy_type']
download_user = add_link_dictionary['download_user']
download_passwd = add_link_dictionary['download_passwd']
header = add_link_dictionary['header']
out = add_link_dictionary['out']
user_agent = add_link_dictionary['user_agent']
load_cookies = add_link_dictionary['load_cookies']
referer = add_link_dictionary['referer']
# define a requests session
requests_session = requests.Session()
# check if user set proxy
if ip:
ip_port = '://' + str(ip) + ":" + str(port)
if proxy_user:
ip_port = ('://' + proxy_user + ':' + proxy_passwd + '@' + ip_port)
if proxy_type == 'socks5':
ip_port = 'socks5' + ip_port
else:
ip_port = 'http' + ip_port
proxies = {'http': ip_port,
'https': ip_port}
# set proxy to the session
requests_session.proxies.update(proxies)
if download_user:
# set download user pass to the session
requests_session.auth = (download_user, download_passwd)
# set cookies
if load_cookies:
jar = readCookieJar(load_cookies)
if jar:
requests_session.cookies = jar
if header is not None:
# convert header to dictionary
dict_ = headerToDict(header)
# update headers
requests_session.headers.update(dict_)
# set referer
if referer:
requests_session.headers.update({'referer': referer}) # setting referer to the session
# set user_agent
if user_agent:
requests_session.headers.update({'user-agent': user_agent}) # setting user_agent to the session
else:
user_agent = 'PersepolisDM/' + str(VERSION.version_str)
# setting user_agent to the session
requests_session.headers.update(
{'user-agent': user_agent})
# find headers
try:
response = requests_session.head(link, allow_redirects=True, timeout=timeout, verify=check_certificate)
header = response.headers
except Exception:
header = {}
filename = None
file_size = None
# check if filename is available in header
if 'Content-Disposition' in header.keys():
content_disposition = header['Content-Disposition']
if content_disposition.find('filename') != -1:
# so file name is available in header
filename_splited = content_disposition.split('filename=')
filename_splited = filename_splited[-1]
# getting file name in desired format
# remove "" from file namme(some times it's happend for github files).
filename = filename_splited.strip('"')
# remove any leading and trailing whitespace.
filename = filename.strip()
if not (filename):
filename = getFileNameFromLink(link)
# if user set file name before in add_link_dictionary['out'],
# then set "out" for filename
if out:
filename = out
# check if file_size is available
if 'Content-Length' in header.keys():
try:
file_size = int(header['Content-Length'])
# converting file_size to KiB or MiB or GiB
file_size, unit = humanReadableSize(file_size)
file_size_with_unit = str(file_size) + ' ' + unit
except Exception:
file_size_with_unit = 'None'
else:
file_size_with_unit = 'None'
requests_session.close()
# return results
return filename, file_size_with_unit
# this function finds and returns file name for links.
def queueSpider(add_link_dictionary):
filename = addLinkSpider(add_link_dictionary)[0]
return filename
def addLinkSpider(add_link_dictionary):
# get user's download information from add_link_dictionary
for i in ['link', 'ip', 'port', 'proxy_user', 'proxy_passwd', 'download_user', 'download_passwd',
'header', 'out', 'user_agent', 'proxy_type', 'load_cookies', 'referer']:
if not (i in add_link_dictionary):
add_link_dictionary[i] = None
return spider(add_link_dictionary)
================================================
FILE: persepolis/scripts/startup.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import platform
import sys
import os
from persepolis.constants import OS
home_address = os.path.expanduser("~")
# finding os_type
os_type = platform.system()
if os_type == OS.WINDOWS:
import winreg
# check startup
def checkStartUp():
# check if it is linux
if os_type in OS.UNIX_LIKE:
# check if the startup exists
if os.path.exists(home_address + "/.config/autostart/persepolis.desktop"):
return True
else:
return False
# check if it is mac
elif os_type == OS.OSX:
# OS X
if os.path.exists(home_address + "/Library/LaunchAgents/com.persepolisdm.plist"):
return True
else:
return False
# check if it is Windows
elif os_type == OS.WINDOWS:
# try to open startup key and check persepolis value
try:
aKey = winreg.OpenKey(
winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Run", 0, winreg.KEY_ALL_ACCESS)
startupvalue = winreg.QueryValueEx(aKey, 'persepolis')
startup = True
except WindowsError:
startup = False
# Close the connection
winreg.CloseKey(aKey)
# if the startup enabled or disabled
if startup:
return True
if not startup:
return False
# add startup file
def addStartUp(parent):
# check if it is linux
if os_type in OS.UNIX_LIKE:
entry = '''
[Desktop Entry]
Name=Persepolis Download Manager
Name[fa]=پرسپولیس
Comment=Download Manager
GenericName=Download Manager
GenericName[fa]=نرم افزار مدیریت بارگیری
Keywords=Internet;WWW;Web;
Terminal=false
Type=Application
Categories=Qt;Network;
StartupNotify=true
Exec={} --tray
Icon=com.github.persepolisdm.persepolis
StartupWMClass=persepolis-download-Manager
'''.format(parent.exec_dictionary['modified_exec_file_path'])
# check if the autostart directory exists & create entry
if not os.path.exists(home_address + "/.config/autostart"):
os.makedirs(home_address + "/.config/autostart", 0o755)
startupfile = open(
home_address + "/.config/autostart/persepolis.desktop", 'w+')
startupfile.write(entry)
os.chmod(home_address
+ "/.config/autostart/persepolis.desktop", 0o644)
# check if it is mac
elif os_type == OS.OSX:
# OS X
cwd = sys.argv[0]
cwd = os.path.dirname(cwd)
entry = '''
Labelcom.persepolisdm.persepolisProgram{}ProgramArguments--trayRunAtLoad\n
'''.format(parent.exec_dictionary['exec_file_path'])
startupfile = open(
home_address + '/Library/LaunchAgents/com.persepolisdm.plist', 'w+')
startupfile.write(entry)
os.system('launchctl load ' + home_address
+ "/Library/LaunchAgents/com.persepolisdm.plist")
# check if it is Windows
elif os_type == OS.WINDOWS:
# Connect to the startup path in Registry
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER,
"Software\\Microsoft\\Windows\\CurrentVersion\\Run", 0, winreg.KEY_ALL_ACCESS)
# find current persepolis exe path
persepolisexetray = '"{}" --tray'.format(parent.exec_dictionary['exec_file_path'])
# add persepolis to startup
winreg.SetValueEx(key, 'persepolis', 0,
winreg.REG_SZ, persepolisexetray)
# Close connection
winreg.CloseKey(key)
# remove startup file
def removeStartUp():
# check if it is linux
if os_type in OS.UNIX_LIKE:
# remove it
os.remove(home_address + "/.config/autostart/persepolis.desktop")
# check if it is mac OS
elif os_type == OS.OSX:
# OS X
if checkStartUp():
os.system('launchctl unload ' + home_address
+ "/Library/LaunchAgents/com.persepolisdm.plist")
os.remove(home_address
+ "/Library/LaunchAgents/com.persepolisdm.plist")
# check if it is Windows
elif os_type == OS.WINDOWS:
if checkStartUp():
# Connect to the startup path in Registry
key = winreg.OpenKey(
winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Run", 0, winreg.KEY_ALL_ACCESS)
# remove persepolis from startup
winreg.DeleteValue(key, 'persepolis')
# Close connection
winreg.CloseKey(key)
================================================
FILE: persepolis/scripts/text_queue.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
try:
from PySide6.QtCore import Qt, QDir, QPoint, QSize, QThread, Signal
from PySide6.QtWidgets import QTableWidgetItem, QFileDialog
from PySide6.QtGui import QIcon
except ImportError:
from PyQt5.QtCore import Qt, QDir, QPoint, QSize, QThread
from PyQt5.QtWidgets import QTableWidgetItem, QFileDialog
from PyQt5.QtCore import pyqtSignal as Signal
from PyQt5.QtGui import QIcon
from persepolis.gui.text_queue_ui import TextQueue_Ui
from persepolis.scripts import logger
from persepolis.scripts import spider
from functools import partial
import os
# This thread finds filename
class QueueSpiderThread(QThread):
QUEUESPIDERRETURNEDFILENAME = Signal(str)
def __init__(self, dict_):
QThread.__init__(self)
self.dict_ = dict_
def run(self):
try:
filename = spider.queueSpider(self.dict_)
if filename:
self.QUEUESPIDERRETURNEDFILENAME.emit(filename)
else:
logger.logObj.error(
"Spider couldn't find download information", exc_info=True)
except Exception as e:
# write error in log
logger.logObj.error(
"Spider couldn't find download information", exc_info=True)
logger.logObj.error(
str(e), exc_info=True)
class TextQueue(TextQueue_Ui):
def __init__(self, parent, file_path, callback, persepolis_setting):
super().__init__(persepolis_setting)
self.persepolis_setting = persepolis_setting
self.callback = callback
self.file_path = file_path
self.parent = parent
global icons
icons = ':/' + \
str(self.persepolis_setting.value('settings/icons')) + '/'
# read text file lines and put links in list format.
f = open(self.file_path)
f_links_list = f.readlines()
f.close()
f_links_list.reverse()
# check links! links must be started with http or https or ftp
self.link_list = []
for link in f_links_list:
text = link.strip()
if ("tp:/" in text[2:6]) or ("tps:/" in text[2:7]):
self.link_list.append(text)
k = 1
for link in self.link_list:
self.links_table.insertRow(0)
# file_name
file_name = '***'
dict_ = {'link': link}
# spider finds file name
new_spider = QueueSpiderThread(dict_)
self.parent.threadPool.append(new_spider)
self.parent.threadPool[-1].start()
self.parent.threadPool[-1].QUEUESPIDERRETURNEDFILENAME.connect(
partial(self.parent.queueSpiderCallBack, child=self,
row_number=len(self.link_list) - k))
k = k + 1
item = QTableWidgetItem(file_name)
# add checkbox to the item
item.setFlags(Qt.ItemIsUserCheckable
| Qt.ItemIsEnabled)
item.setCheckState(Qt.Checked)
# insert file_name
self.links_table.setItem(0, 0, item)
# insert link
item = QTableWidgetItem(str(link))
self.links_table.setItem(0, 1, item)
# get categories name and add them to add_queue_comboBox
categories_list = self.parent.persepolis_db.categoriesList()
for queue in categories_list:
if queue != 'All Downloads':
self.add_queue_comboBox.addItem(queue)
self.add_queue_comboBox.addItem(
QIcon(icons + 'add_queue'), 'Create new queue')
# entry initialization
# get values from persepolis_setting
global connections
connections = int(
self.persepolis_setting.value('settings/connections'))
global download_path
download_path = str(
self.persepolis_setting.value('settings/download_path'))
self.connections_spinBox.setValue(connections)
self.download_folder_lineEdit.setText(download_path)
self.download_folder_lineEdit.setEnabled(False)
# ip_lineEdit initialization
settings_ip = self.persepolis_setting.value(
'add_link_initialization/ip', None)
if settings_ip:
self.ip_lineEdit.setText(str(settings_ip))
# proxy user lineEdit initialization
settings_proxy_user = self.persepolis_setting.value(
'add_link_initialization/proxy_user', None)
if settings_proxy_user:
self.proxy_user_lineEdit.setText(str(settings_proxy_user))
# port_spinBox initialization
settings_port = self.persepolis_setting.value(
'add_link_initialization/port', 0)
self.port_spinBox.setValue(int(int(settings_port)))
# http or socks5 initialization
settings_proxy_type = self.persepolis_setting.value(
'add_link_initialization/proxy_type', None)
# default is http
if settings_proxy_type == 'socks5':
self.socks5_radioButton.setChecked(True)
elif settings_proxy_type == 'https':
self.https_radioButton.setChecked(True)
else:
self.http_radioButton.setChecked(True)
# download UserName initialization
settings_download_user = self.persepolis_setting.value(
'add_link_initialization/download_user', None)
if settings_download_user:
self.download_user_lineEdit.setText(str(settings_download_user))
# connect folder_pushButton
self.folder_pushButton.clicked.connect(self.changeFolder)
# connect OK and cancel button
self.cancel_pushButton.clicked.connect(self.close)
self.ok_pushButton.clicked.connect(self.okButtonPressed)
# connect select_all_pushButton deselect_all_pushButton
self.select_all_pushButton.clicked.connect(self.selectAll)
self.deselect_all_pushButton.clicked.connect(self.deselectAll)
# frames and checkBoxes
self.proxy_frame.setEnabled(False)
self.proxy_checkBox.toggled.connect(self.proxyFrame)
self.download_frame.setEnabled(False)
self.download_checkBox.toggled.connect(self.downloadFrame)
self.queue_tabWidget.currentChanged.connect(self.currentTabChanged)
# set focus to ok button
self.ok_pushButton.setFocus()
# add_queue_comboBox event
self.add_queue_comboBox.currentIndexChanged.connect(self.queueChanged)
# setting window size and position
size = self.persepolis_setting.value('TextQueue/size', QSize(700, 500))
position = self.persepolis_setting.value(
'TextQueue/position', QPoint(300, 300))
self.resize(size)
self.move(position)
# if user clicked on link_tab so send spider again
# perhaps proxy or user password , ... set!
def currentTabChanged(self, index):
if index == 0:
# get proxy information
ip, port, proxy_user, proxy_passwd, proxy_type = self.getProxyInformation()
# get download username and password information
download_user, download_passwd = self.getUserPass()
dict_ = {'link': None,
'ip': ip,
'port': port,
'proxy_user': proxy_user,
'proxy_passwd': proxy_passwd,
'proxy_type': proxy_type,
'download_user': download_user,
'download_passwd': download_passwd,
'referer': None,
'header': None,
'user_agent': None,
'load_cookies': None}
k = 1
for link in self.link_list:
dict_['link'] = link
# spider finds file name
new_spider = QueueSpiderThread(dict_)
self.parent.threadPool.append(new_spider)
self.parent.threadPool[-1].start()
self.parent.threadPool[-1].QUEUESPIDERRETURNEDFILENAME.connect(
partial(self.parent.queueSpiderCallBack, child=self,
row_number=len(self.link_list) - k))
k = k + 1
# this method checks all check boxes
def selectAll(self, button):
for i in range(self.links_table.rowCount()):
item = self.links_table.item(i, 0)
item.setCheckState(Qt.Checked)
# this method deselect all check boxes
def deselectAll(self, button):
for i in range(self.links_table.rowCount()):
item = self.links_table.item(i, 0)
item.setCheckState(Qt.Unchecked)
# this method is called, when user changes add_queue_comboBox
def queueChanged(self, combo):
if str(self.add_queue_comboBox.currentText()) == 'Create new queue':
# if user want to create new queue, then callback
# createQueue method from mainwindow(parent)
new_queue = self.parent.createQueue(combo)
if new_queue:
# clear comboBox
self.add_queue_comboBox.clear()
# load queue list again!
queues_list = self.parent.persepolis_db.categoriesList()
for queue in queues_list:
if queue != 'All Downloads':
self.add_queue_comboBox.addItem(queue)
self.add_queue_comboBox.addItem(
QIcon(icons + 'add_queue'), 'Create new queue')
# finding index of new_queue and setting comboBox for it
index = self.add_queue_comboBox.findText(str(new_queue))
self.add_queue_comboBox.setCurrentIndex(index)
else:
self.add_queue_comboBox.setCurrentIndex(0)
# activate frames if checkBoxes checked
def proxyFrame(self, checkBox):
if self.proxy_checkBox.isChecked():
self.proxy_frame.setEnabled(True)
else:
self.proxy_frame.setEnabled(False)
def downloadFrame(self, checkBox):
if self.download_checkBox.isChecked():
self.download_frame.setEnabled(True)
else:
self.download_frame.setEnabled(False)
def changeFolder(self, button):
fname = QFileDialog.getExistingDirectory(
self, 'Select a directory', download_path)
if fname:
# Returns pathName with the '/' separators converted to
# separators that are appropriate for the underlying
# operating system.
# On Windows, toNativeSeparators("c:/winnt/system32") returns
# "c:\winnt\system32".
fname = QDir.toNativeSeparators(fname)
if os.path.isdir(fname):
self.download_folder_lineEdit.setText(fname)
# this method returns proxy information.
def getProxyInformation(self):
# http, https or socks5 proxy
if self.http_radioButton.isChecked() is True:
proxy_type = 'http'
elif self.https_radioButton.isChecked() is True:
proxy_type = 'https'
else:
proxy_type = 'socks5'
# get proxy information
if not (self.proxy_checkBox.isChecked()):
ip = None
port = None
proxy_user = None
proxy_passwd = None
proxy_type = None
else:
ip = self.ip_lineEdit.text()
if not (ip):
ip = None
port = self.port_spinBox.value()
if not (port):
port = None
proxy_user = self.proxy_user_lineEdit.text()
if not (proxy_user):
proxy_user = None
proxy_passwd = self.proxy_pass_lineEdit.text()
if not (proxy_passwd):
proxy_passwd = None
return ip, port, proxy_user, proxy_passwd, proxy_type
def getUserPass(self):
# get download username and password information
if not (self.download_checkBox.isChecked()):
download_user = None
download_passwd = None
else:
download_user = self.download_user_lineEdit.text()
if not (download_user):
download_user = None
download_passwd = self.download_pass_lineEdit.text()
if not (download_passwd):
download_passwd = None
return download_user, download_passwd
def okButtonPressed(self, button):
# write user's input data to init file
self.persepolis_setting.setValue(
'add_link_initialization/ip', self.ip_lineEdit.text())
self.persepolis_setting.setValue(
'add_link_initialization/port', self.port_spinBox.value())
self.persepolis_setting.setValue(
'add_link_initialization/proxy_user',
self.proxy_user_lineEdit.text())
self.persepolis_setting.setValue(
'add_link_initialization/download_user',
self.download_user_lineEdit.text())
# get proxy information
ip, port, proxy_user, proxy_passwd, proxy_type = self.getProxyInformation()
if proxy_type is not None:
self.persepolis_setting.setValue('add_link_initialization/proxy_type', proxy_type)
# get download username and password information
download_user, download_passwd = self.getUserPass()
category = str(self.add_queue_comboBox.currentText())
connections = self.connections_spinBox.value()
download_path = self.download_folder_lineEdit.text()
dict_ = {'out': None,
'start_time': None,
'end_time': None,
'link': None,
'ip': ip,
'port': port,
'proxy_user': proxy_user,
'proxy_passwd': proxy_passwd,
'download_user': download_user,
'download_passwd': download_passwd,
'proxy_type': proxy_type,
'connections': connections,
'limit_value': 10,
'download_path': download_path,
'referer': None,
'load_cookies': None,
'user_agent': None,
'header': None,
'after_download': None
}
# find checked links in links_table
self.add_link_dictionary_list = []
i = 0
for row in range(self.links_table.rowCount()):
item = self.links_table.item(row, 0)
# if item is checked
if (item.checkState() == Qt.Checked):
# Create a copy from dict_ and add it
# to add_link_dictionary_list
self.add_link_dictionary_list.append(
dict_.copy())
# get link and add it to dict_
link = self.links_table.item(row, 1).text()
self.add_link_dictionary_list[i]['link'] = str(link)
# add file name to the dict_
self.add_link_dictionary_list[i]['out'] = self.links_table.item(
row, 0).text()
i = i + 1
# reverse list
self.add_link_dictionary_list.reverse()
# Create callback for mainwindow
self.callback(self.add_link_dictionary_list, category)
# close window
self.close()
# close window with ESC key
def keyPressEvent(self, event):
if event.key() == Qt.Key_Escape:
self.close()
def closeEvent(self, event):
self.persepolis_setting.setValue('TextQueue/size', self.size())
self.persepolis_setting.setValue('TextQueue/position', self.pos())
self.persepolis_setting.sync()
event.accept()
def changeIcon(self, icons):
icons = ':/' + str(icons) + '/'
self.folder_pushButton.setIcon(QIcon(icons + 'folder'))
self.ok_pushButton.setIcon(QIcon(icons + 'ok'))
self.cancel_pushButton.setIcon(QIcon(icons + 'remove'))
================================================
FILE: persepolis/scripts/useful_tools.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
from persepolis.constants.Os import OS
from pathlib import Path
import urllib.parse
import subprocess
import requests
import platform
import textwrap
import time
import sys
import os
try:
from PySide6.QtCore import QThread, Signal, QProcess
from PySide6.QtWidgets import QStyleFactory
except ImportError:
from PyQt5.QtWidgets import QStyleFactory
from PyQt5.QtCore import QThread, QProcess
from PyQt5.QtCore import pyqtSignal as Signal
try:
from persepolis.scripts import logger
logger_availability = True
except ImportError:
logger_availability = False
# find operating system
# os_type >> Linux or Darwin(Mac osx) or Windows(Microsoft Windows) or
# FreeBSD or OpenBSD
os_type = platform.system()
# user home address
home_address = os.path.expanduser("~")
# runApplication in a thread.
class RunApplicationThread(QThread):
RUNAPPCALLBACKSIGNAL = Signal(list)
def __init__(self, command_argument, call_back=False):
QThread.__init__(self)
self.command_argument = command_argument
self.call_back = call_back
def run(self):
pipe = runApplication(self.command_argument)
if self.call_back:
self.RUNAPPCALLBACKSIGNAL.emit([pipe])
# determine the config folder path based on the operating system
def determineConfigFolder():
if os_type in OS.UNIX_LIKE:
config_folder = os.path.join(
home_address, ".config/persepolis_download_manager")
elif os_type == OS.OSX:
config_folder = os.path.join(
home_address, "Library/Application Support/persepolis_download_manager")
elif os_type == OS.WINDOWS:
config_folder = os.path.join(
home_address, 'AppData', 'Local', 'persepolis_download_manager')
return config_folder
# this function returns operating system and desktop environment(for linux and bsd).
def osAndDesktopEnvironment():
desktop_env = None
if os_type in OS.UNIX_LIKE:
# find desktop environment('KDE', 'GNOME', ...)
xdg_desktop = os.environ.get("XDG_CURRENT_DESKTOP")
if xdg_desktop is not None:
desktop_env = xdg_desktop.lower()
else:
desktop_env = "unknown"
if logger_availability:
logger.sendToLog(
"XDG_CURRENT_DESKTOP environment variable not found.", "WARNING"
)
return os_type, desktop_env
# this function converts file_size to KiB or MiB or GiB
def humanReadableSize(size, input_type='file_size'):
labels = ['KiB', 'MiB', 'GiB', 'TiB']
i = -1
if size < 1024:
return str(size), 'B'
while size >= 1024:
i += 1
size = size / 1024
if i > 1:
return round(size, 2), labels[i]
elif i == 1 and input_type == 'speed':
return round(size, 1), labels[i]
else:
return round(size, None), labels[i]
# this function converts second to hour and minute
def convertTime(time):
minutes = int(time // 60)
if minutes == 0:
return str(int(time)) + 's'
elif minutes < 60:
return str(minutes) + 'm'
else:
hours = minutes // 60
minutes = minutes - (hours * 60)
return str(hours) + 'h ' + str(minutes) + 'm'
# this function converts human readable size to byte
def convertToByte(file_size):
# if unit is not in Byte
if file_size[-2:] != ' B':
unit = file_size[-3:]
# persepolis uses float type for GiB and TiB
if unit == 'GiB' or unit == 'TiB':
size_value = float(file_size[:-4])
else:
size_value = int(float(file_size[:-4]))
else:
unit = None
size_value = int(float(file_size[:-3]))
# covert them in byte
if not (unit):
in_byte_value = size_value
elif unit == 'KiB':
in_byte_value = size_value * 1024
elif unit == 'MiB':
in_byte_value = size_value * 1024 * 1024
elif unit == 'GiB':
in_byte_value = size_value * 1024 * 1024 * 1024
elif unit == 'TiB':
in_byte_value = size_value * 1024 * 1024 * 1024 * 1024
return int(in_byte_value)
# this function checks free space in hard disk.
def freeSpace(dir):
try:
import psutil
except ImportError:
if logger_availability:
logger.sendToLog("psutil in not installed!", "ERROR")
return None
try:
dir_space = psutil.disk_usage(dir)
free_space = dir_space.free
return int(free_space)
except Exception as e:
# log in to the log file
if logger_availability:
logger.sendToLog("persepolis couldn't find free space value:\n" + str(e), "ERROR")
return None
def returnDefaultSettings():
os_type, desktop_env = osAndDesktopEnvironment()
# user download folder path
download_path = os.path.join(home_address, 'Downloads', 'Persepolis')
# set dark fusion for default style settings.
style = 'Fusion'
color_scheme = 'Dark Fusion'
icons = 'Papirus'
style = 'Fusion'
# find available styles(It's depends on operating system and desktop environments).
available_styles = QStyleFactory.keys()
if os_type in OS.UNIX_LIKE:
if desktop_env in ['kde', 'lxqt', 'paperde', 'plainde', 'thedesk', 'lumina']:
style = 'System'
color_scheme = 'System'
else:
if 'Breeze' in available_styles:
style = 'Breeze'
color_scheme = 'System'
elif 'Adwaita' in available_styles:
style = 'Adwaita'
color_scheme = 'System'
else:
style = 'Fusion'
color_scheme = 'Dark Fusion'
elif os_type == OS.OSX:
if 'macOS' in available_styles:
style = 'macOS'
color_scheme = 'System'
elif os_type == OS.WINDOWS:
if 'windows11' in available_styles:
style = 'windows11'
color_scheme = 'System'
# keyboard shortcuts
delete_shortcut = "Ctrl+D"
remove_shortcut = "Ctrl+R"
add_new_download_shortcut = "Ctrl+N"
import_text_shortcut = "Ctrl+O"
video_finder_shortcut = "Ctrl+V"
quit_shortcut = "Ctrl+Q"
hide_window_shortcut = "Ctrl+W"
move_up_selection_shortcut = "Ctrl+Up"
move_down_selection_shortcut = "Ctrl+Down"
# Persepolis default setting
default_setting_dict = {'locale': 'en_US', 'toolbar_icon_size': 32, 'wait-queue': [0, 0], 'awake': 'no', 'custom-font': 'no', 'column0': 'yes',
'column1': 'yes', 'column2': 'yes', 'column3': 'yes', 'column4': 'yes', 'column5': 'yes', 'column6': 'yes', 'column7': 'yes',
'column10': 'yes', 'column11': 'yes', 'column12': 'yes', 'subfolder': 'yes', 'startup': 'no', 'show-progress': 'yes',
'show-menubar': 'no', 'show-sidepanel': 'yes', 'notification': 'QT notification', 'after-dialog': 'yes',
'tray-icon': 'yes', 'browser-persepolis': 'yes', 'hide-window': 'yes', 'max-tries': 5, 'retry-wait': 5, 'timeout': 10,
'connections': 64, 'download_path': download_path, 'sound': 'yes', 'sound-volume': 100, 'chunk-size': 100,
'style': style, 'color-scheme': color_scheme, 'icons': icons, 'font': 'Ubuntu', 'font-size': 9,
'video_finder/max_links': '3', 'shortcuts/delete_shortcut': delete_shortcut, 'shortcuts/remove_shortcut': remove_shortcut,
'shortcuts/add_new_download_shortcut': add_new_download_shortcut, 'shortcuts/import_text_shortcut': import_text_shortcut,
'shortcuts/video_finder_shortcut': video_finder_shortcut, 'shortcuts/quit_shortcut': quit_shortcut,
'shortcuts/hide_window_shortcut': hide_window_shortcut, 'shortcuts/move_up_selection_shortcut': move_up_selection_shortcut,
'shortcuts/move_down_selection_shortcut': move_down_selection_shortcut, 'dont-check-certificate': 'no',
'native_messaging/chrome': 'true', 'native_messaging/chromium': 'true', 'native_messaging/firefox': 'true',
'native_messaging/brave': 'false', 'native_messaging/librewolf': 'false', 'native_messaging/opera': 'false',
'native_messaging/vivaldi': 'false'}
return default_setting_dict
# mix video and audio that downloads by video finder
def muxer(parent, video_finder_dictionary):
result_dictionary = {'error': 'no_error',
'ffmpeg_error_message': None,
'final_path': None,
'final_size': None}
# find file path
video_file_dictionary = parent.persepolis_db.searchGidInAddLinkTable(video_finder_dictionary['video_gid'])
audio_file_dictionary = parent.persepolis_db.searchGidInAddLinkTable(video_finder_dictionary['audio_gid'])
# find inputs and output file path for ffmpeg
video_file_path = video_file_dictionary['download_path']
audio_file_path = audio_file_dictionary['download_path']
final_path = video_finder_dictionary['download_path']
# calculate final file size
video_file_size = parent.persepolis_db.searchGidInDownloadTable(video_finder_dictionary['video_gid'])['size']
audio_file_size = parent.persepolis_db.searchGidInDownloadTable(video_finder_dictionary['audio_gid'])['size']
# convert size to byte
video_file_size = convertToByte(video_file_size)
audio_file_size = convertToByte(audio_file_size)
final_file_size = video_file_size + audio_file_size
# check free space
free_space = freeSpace(final_path)
if free_space:
if final_file_size > free_space:
result_dictionary['error'] = 'not enough free space'
else:
# find final file's name
final_file_name = urllib.parse.unquote(os.path.basename(video_file_path))
# if video's extension is 'mp4' then the final output file's extension is 'mp4'
# if video's extension is 'webm' then the final output file's extension is 'mkv'
file_name_split = final_file_name.split('.')
video_extension = file_name_split[-1]
if video_extension == 'webm':
extension_length = len(file_name_split[-1]) + 1
final_file_name = final_file_name[0:-extension_length] + '.mkv'
if parent.persepolis_setting.value('settings/download_path') == final_path:
if parent.persepolis_setting.value('settings/subfolder') == 'yes':
final_path = os.path.join(final_path, 'Videos')
# rename file if file already existed
i = 1
final_path_plus_name = os.path.join(final_path, final_file_name)
while os.path.isfile(final_path_plus_name):
extension_length = len(file_name_split[-1]) + 1
new_name = final_file_name[0:-extension_length] + \
'_' + str(i) + final_file_name[-extension_length:]
final_path_plus_name = os.path.join(final_path, new_name)
i = i + 1
# start muxing
# find ffmpeg path
ffmpeg_command, log_list = findExternalAppPath('ffmpeg')
# run ffmpeg
command_argument = ['ffmpeg', '-i', video_file_path,
'-i', audio_file_path,
'-c', 'copy',
'-shortest',
'-map', '0:v:0',
'-map', '1:a:0',
'-loglevel', 'error',
'-strict', '-2',
final_path_plus_name]
pipe = runApplication(command_argument)
if pipe.wait() == 0:
# muxing was finished successfully.
result_dictionary['error'] = 'no error'
result_dictionary['final_path'] = final_path_plus_name
file_size, file_size_unit = humanReadableSize(final_file_size)
result_dictionary['final_size'] = str(file_size) + ' ' + str(file_size_unit)
else:
result_dictionary['error'] = 'ffmpeg error'
out, ffmpeg_error_message = pipe.communicate()
result_dictionary['ffmpeg_error_message'] = ffmpeg_error_message.decode('utf-8', 'ignore')
return result_dictionary
# return version of ffmpeg
def ffmpegVersion():
# find ffmpeg path
ffmpeg_command, log_list = findExternalAppPath('ffmpeg')
# Try to test ffmpeg
command_argument = [ffmpeg_command, '-version']
try:
pipe = runApplication(command_argument)
if pipe.wait() == 0:
ffmpeg_is_installed = True
ffmpeg_output, error = pipe.communicate()
ffmpeg_output = ffmpeg_output.decode('utf-8')
else:
ffmpeg_is_installed = False
ffmpeg_output = 'ffmpeg is not installed'
except Exception:
ffmpeg_is_installed = False
ffmpeg_output = 'ffmpeg is not installed'
# wrap ffmpeg_output with width=70
wrapper = textwrap.TextWrapper()
ffmpeg_output = wrapper.fill(ffmpeg_output)
ffmpeg_output = '\n**********\n'\
+ str(ffmpeg_output)\
+ '\n**********\n'
return ffmpeg_is_installed, ffmpeg_output, log_list
# run apllication with qprocess
def qRunApplication(command: str, command_argument: list, parent=None):
process = QProcess(parent=parent)
process.start(command, command_argument)
return process
# run an application
def runApplication(command_argument):
if os_type == OS.WINDOWS:
# NO_WINDOW option avoids opening additional CMD in MS Windows.
NO_WINDOW = 0x08000000
pipe = subprocess.Popen(command_argument,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
stdin=subprocess.PIPE,
shell=False,
creationflags=NO_WINDOW)
else:
pipe = subprocess.Popen(
command_argument,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=False)
return pipe
# find exeternal application execution path
def findExternalAppPath(app_name):
# get Persepolis type information first.
persepolis_path_infromation = getExecPath()
is_bundle = persepolis_path_infromation['bundle']
is_test = persepolis_path_infromation['test']
if os_type == OS.WINDOWS:
app_name = app_name + '.exe'
# If Persepolis run as a bundle.
if is_bundle:
# alongside of the bundle path
cwd = sys.argv[0]
current_directory = os.path.dirname(cwd)
app_alongside = os.path.join(current_directory, app_name)
# inside of the bundle path.
if os_type in OS.UNIX_LIKE:
# we use nuikita for creating bundle
bundle_path = os.path.dirname(sys.executable)
app_inside = os.path.join(bundle_path, app_name)
else:
# we use pyinstaller for creating bundle
base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
app_inside = os.path.join(base_path, app_name)
if os_type in OS.UNIX_LIKE:
# Check outside of the bundle first.
if os.path.exists(app_alongside):
app_command = app_alongside
log_list = ["{}'s file is detected alongside of bundle.".format(app_name), "INFO"]
# Check inside of the bundle.
elif os.path.exists(app_inside):
# get executable pathAdd commentMore actions
app_command = app_inside
log_list = ["{}'s file is detected inside of bundle.".format(app_name), "INFO"]
else:
# use app that installed on user's system
app_command = app_name
log_list = ["Persepolis will use {} that installed on user's system.".format(app_name), "INFO"]
else:
# for Mac OSX and MicroSoft Windows
app_command = app_alongside
log_list = ["{}'s file is detected alongside of bundle.".format(app_name), "INFO"]
# I Persepolis run from test directory.
if is_test:
# Check inside of test directory.
cwd = sys.argv[0]
current_directory = os.path.dirname(cwd)
app_alongside = os.path.join(current_directory, app_name)
if os.path.exists(app_alongside):
app_command = app_alongside
log_list = ["{}'s file is detected inside of test directory.".format(app_name), "INFO"]
else:
# use app that installed on user's system
app_command = app_name
log_list = ["Persepolis will use {} that installed on user's system.".format(app_name), "INFO"]
if not (is_bundle) and not (is_test):
app_command = app_name
log_list = ["Persepolis will use {} that installed on user's system.".format(app_name), "INFO"]
return app_command, log_list
# This function returns persepolis's execution path.
def getExecPath():
exec_dictionary = {'bundle': None,
'test': False,
'exec_file_path': None,
'modified_exec_file_path': None}
# check if persepolis is run as a bundle.
# On Windows and Mac we use pyinstaller to build the bundle,
# and on Linux and BSD we use nuitka.
# the output of this code for pyinstaller bundle is True
# getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS')
# But this code doesn't work for nuitka!
# So we have to use a workaround to identify nuitka bundle.
# We check the inside of the bundle and look for the
# com.github.persepolisdm.persepolis.svg file(icon).
# file that we placed. If it was, then the bundle file
# generated with nuitka is running.
if os_type in OS.UNIX_LIKE:
bundle_path = os.path.dirname(sys.executable)
icon_file_path = os.path.join(bundle_path, 'com.github.persepolisdm.persepolis.svg')
# check availability of com.github.persepolisdm.persepolis.svg
if os.path.exists(icon_file_path):
exec_dictionary['bundle'] = True
# for nuitka
# get executable path
bundle_path = os.path.abspath(sys.argv[0])
exec_file_path = bundle_path
# for windows and osx(pyinstaller bundle)
else:
# check pyinstaller bundle
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
exec_dictionary['bundle'] = True
# get executable path
bundle_path = os.path.dirname(sys.executable)
# get bundle name
bundle_name = os.path.basename(sys.executable)
exec_file_path = os.path.join(bundle_path, bundle_name)
if exec_dictionary['bundle'] is not True:
# persepolis is run from python script
exec_dictionary['bundle'] = False
# get execution path
script_path = os.path.dirname(os.path.abspath(sys.modules['__main__'].__file__))
script_name = os.path.basename(sys.argv[0])
if script_name == 'test.py':
# persepolis is run from test directory
exec_dictionary['test'] = True
exec_file_path = os.path.join(script_path, script_name)
# replace space with \+space for UNIX_LIKE and OSX
if os_type in OS.UNIX_LIKE or os_type == OS.OSX:
modified_exec_file_path = exec_file_path.replace(" ", r"\ ")
elif os_type == OS.WINDOWS:
modified_exec_file_path = exec_file_path.replace('\\', r'\\')
# write it in dictionary
exec_dictionary['exec_file_path'] = exec_file_path
exec_dictionary['modified_exec_file_path'] = modified_exec_file_path
# return ressults
return exec_dictionary
# This method returns data and time in string format
# for example >> 2017/09/09 , 13:12:26
def nowDate():
date = time.strftime("%Y/%m/%d , %H:%M:%S")
return date
def fold(header):
line = "%s: %s" % (header[0], header[1])
if len(line) < 998:
return line
# fold
else:
lines = [line]
while len(lines[-1]) > 998:
split_this = lines[-1]
# find last space in longest chunk admissible
split_here = split_this[:998].rfind(" ")
del lines[-1]
lines = lines + [split_this[:split_here],
split_this[split_here:]] # this may still be too long
# hence the while on lines[-1]
return "\n".join(lines)
def dictToHeader(data):
return "\n".join((fold(header) for header in data.items()))
# this method get http header as string and convert it to dictionary
def headerToDict(headers):
dic = {}
for line in headers.split("\n"):
if line.startswith(("GET", "POST")):
continue
point_index = line.find(":")
dic[line[:point_index].strip()] = line[point_index + 1:].strip()
return dic
def readCookieJar(load_cookies):
jar = None
if os.path.isfile(load_cookies):
# Open cookie file
cookies_txt = open(load_cookies, 'r')
# Initialize RequestsCookieJar
jar = requests.cookies.RequestsCookieJar()
for line in cookies_txt.readlines():
words = line.split()
# Filter out lines that don't contain cookies
if (len(words) == 7) and (words[0] != "#"):
# Split cookies into the appropriate parameters
jar.set(words[5], words[6], domain=words[0], path=words[2])
return jar
# get file name from link string
def getFileNameFromLink(link):
link = requests.utils.unquote(link)
parsed_linkd = urllib.parse.urlparse(link)
file_name = Path(parsed_linkd.path).name
return file_name
# Return a new name for the file, if a file with the current name exists.
def returnNewFileName(folder_path, file_name):
i = 1
file_path = os.path.join(folder_path, file_name)
# If file is already exists.
while os.path.isfile(file_path):
# split file name to file_name + extension
file_name_split = list(os.path.splitext(file_name))
# add _i to the end of file name
if file_name_split[0][-2] == '_':
try:
# check the last character of file_name.
# If it's integer, add 1 to it.
j = file_name_split[0][-1]
j = int(j)
j += 1
file_name = file_name_split[0][:-1] + str(j) + file_name_split[-1]
except Exception:
file_name = file_name_split[0] + '_' + str(i) + file_name_split[-1]
i += 1
else:
file_name = file_name_split[0] + '_' + str(i) + file_name_split[-1]
i += 1
# create new file_path
file_path = os.path.join(folder_path, file_name)
return file_name
================================================
FILE: persepolis/scripts/video_finder.py
================================================
# -*- coding: utf-8 -*-
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
import os
from time import sleep
from persepolis.scripts import logger
from persepolis.scripts.download_link import DownloadLink
from persepolis.scripts.useful_tools import muxer
try:
from PySide6.QtCore import QThread, Signal
except ImportError:
from PyQt5.QtCore import QThread
from PyQt5.QtCore import pyqtSignal as Signal
try:
from persepolis.scripts import ytdlp_downloader
except ModuleNotFoundError:
# if youtube_dl module is not installed:
logger.sendToLog(
"yt-dlp is not installed.", "ERROR")
# Persepolis download audio and video separately and the muxing them :)
# VideoFinder do this job for Persepolis.
# see data_base.py for understanding the code
# we have video_finder_db_table in data base. it's contains some items that helps
# VideoFinder for managing the situation.
# video_gid >> GID of video link
# audio_gid >> GID of audio link
# video_completed >> Is video downloaded completely?
# audio_completed >> Is audio downloaded completely?
# checking >> VideoFinder must checking or not!
class VideoFinder(QThread):
VIDEOFINDERCOMPLETED = Signal(dict)
def __init__(self, video_finder_dictionary, main_window):
QThread.__init__(self)
self.main_window = main_window
self.video_finder_dictionary = video_finder_dictionary
# First: Download video
# Second: Download audio
# Third: Mux video and audio
def run(self):
self.video_completed = self.video_finder_dictionary['video_completed']
self.audio_completed = self.video_finder_dictionary['audio_completed']
self.muxing = 'no'
self.checking = 'no'
self.active = 'yes'
video_gid = self.video_finder_dictionary['video_gid']
audio_gid = self.video_finder_dictionary['audio_gid']
# find category
dictionary = self.main_window.persepolis_db.searchGidInDownloadTable(video_gid)
category = dictionary['category']
# VideoFinder handles downloads by itself, if category is "Single Downloads"
if category == 'Single Downloads':
# create an item for this thread in temp_db if not exists!
try:
video_finder_plus_gid = 'video_finder_' + str(video_gid)
self.main_window.temp_db.insertInQueueTable(video_finder_plus_gid)
except Exception:
# release lock
self.main_window.temp_db.lock = False
# check start time and end time
add_link_dictionary = self.main_window.persepolis_db.searchGidInAddLinkTable(video_gid)
start_time = add_link_dictionary['start_time']
if self.video_completed == 'no' and start_time:
# set start time only for video and cancel start time for audio.
# because video will downloaded first and start time must be set for first link! not second one
self.main_window.persepolis_db.setDefaultGidInAddlinkTable(audio_gid, start_time=True)
# update checking status in data base for starting the job!
self.checking = 'yes'
self.video_finder_dictionary['checking'] = 'yes'
self.main_window.persepolis_db.updateVideoFinderTable([self.video_finder_dictionary])
# if category "Single Downloads" >> manage download yourself.
# if category is not "Single Download" >> just check the status time to time and wait until download ends!
if self.video_completed == 'no':
if category == "Single Downloads":
# start video downloading
# get add_link_dictionary for video
add_link_dictionary = self.main_window.persepolis_db.searchGidInAddLinkTable(video_gid)
# create download_session
video_download_session = ytdlp_downloader.Ytdp_Download(add_link_dictionary, self.main_window, video_gid)
# add download_session and gid to download_session_dict
download_session_dict = {'gid': video_gid,
'download_session': video_download_session}
# append download_session_dict to download_sessions_list
self.main_window.download_sessions_list.append(download_session_dict)
# strat download in thread
new_download = DownloadLink(video_gid, video_download_session, self.main_window)
self.main_window.threadPool.append(new_download)
self.main_window.threadPool[-1].start()
# check the download status
# continue loop and check the download status
# if checking == 'no' >> problem has been occurred and download has been canceled.
while self.video_completed != 'yes' and self.checking == 'yes':
sleep(1)
if self.video_completed == 'yes':
if self.video_finder_dictionary['video_completed'] == 'no':
# update data base
self.video_finder_dictionary['video_completed'] = 'yes'
self.main_window.persepolis_db.updateVideoFinderTable([self.video_finder_dictionary])
# video is downloaded completely!
# let's start audio downloading
if self.audio_completed == 'no':
# if category "Single Downloads" >> start download yourself.
# if category is not "Single Download" >> just check the status time to time
if category == "Single Downloads":
# get add_link_dictionary for video
add_link_dictionary = self.main_window.persepolis_db.searchGidInAddLinkTable(audio_gid)
# create download_session
audio_download_session = ytdlp_downloader.Ytdp_Download(add_link_dictionary, self.main_window, audio_gid)
# add download_session and gid to download_session_dict
download_session_dict = {'gid': audio_gid,
'download_session': audio_download_session}
# append download_session_dict to download_sessions_list
self.main_window.download_sessions_list.append(download_session_dict)
# set speed limitation of video_download_session for audio_download_session
# audio_download_session.sleep_for_speed_limiting = video_download_session.sleep_for_speed_limiting
# strat download in thread
new_download = DownloadLink(audio_gid, audio_download_session, self.main_window)
self.main_window.threadPool.append(new_download)
self.main_window.threadPool[-1].start()
# check the download status
# continue loop and check the download status
# if checking == 'no' >> problem occurred and downloading canceled.
while self.audio_completed != 'yes' and self.checking == 'yes':
sleep(1)
self.checking = 'no'
# lets start muxing!
if self.video_completed == 'yes' and self.audio_completed == 'yes':
audio_file_exists = False
video_file_exists = False
# wait until the data_base is updated
while not (audio_file_exists) or not (video_file_exists):
sleep(0.5)
# checking for file existance
# find file path
video_file_dictionary = self.main_window.persepolis_db.searchGidInAddLinkTable(video_gid)
audio_file_dictionary = self.main_window.persepolis_db.searchGidInAddLinkTable(audio_gid)
# find inputs and output file path for ffmpeg
video_file_path = video_file_dictionary['download_path']
audio_file_path = audio_file_dictionary['download_path']
video_file_exists = os.path.isfile(video_file_path)
audio_file_exists = os.path.isfile(audio_file_path)
self.video_finder_dictionary['audio_completed'] = 'yes'
self.video_finder_dictionary['checking'] = 'no'
self.video_finder_dictionary['muxing_status'] = 'started'
self.muxing = 'started'
# update data base
self.main_window.persepolis_db.updateVideoFinderTable([self.video_finder_dictionary])
# audio and video files are downloaded completely.
# lets start muxing
result_dictionary = muxer(self.main_window, self.video_finder_dictionary)
error_message = result_dictionary['error']
ffmpeg_error_message = result_dictionary['ffmpeg_error_message']
if ffmpeg_error_message:
logger.sendToLog('ffmpeg error: ' + str(ffmpeg_error_message), 'DOWNLOAD ERROR')
if error_message == 'no error':
self.video_finder_dictionary['muxing_status'] = 'complete'
self.muxing = 'complete'
else:
self.video_finder_dictionary['muxing_status'] = 'error'
self.muxing = 'error'
# update data base
self.main_window.persepolis_db.updateVideoFinderTable([self.video_finder_dictionary])
complete_dictionary = {'error': error_message,
'final_path': result_dictionary['final_path'],
'final_size': result_dictionary['final_size'],
'video_gid': self.video_finder_dictionary['video_gid'],
'audio_gid': self.video_finder_dictionary['audio_gid'],
'download_path': self.video_finder_dictionary['download_path'],
'category': category}
# emit error_message
self.VIDEOFINDERCOMPLETED.emit(complete_dictionary)
self.active = 'no'
if category == 'Single Downloads':
# check if user selected shutdown after download in progress window.
shutdown_dict = self.main_window.temp_db.returnCategory(video_finder_plus_gid)
shutdown_status = shutdown_dict['shutdown']
if shutdown_status == 'wait':
# it means user want to persepolis shutdown system after download.
# write 'shutdown' value for this category in temp_db
shutdown_dict = {'category': video_finder_plus_gid,
'shutdown': 'shutdown'}
self.main_window.temp_db.updateQueueTable(shutdown_dict)
================================================
FILE: persepolis/scripts/video_finder_addlink.py
================================================
# -*- coding: utf-8 -*-
"""
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
try:
from PySide6.QtWidgets import QCheckBox, QPushButton, QTextEdit, QFrame, QLabel, QComboBox, QHBoxLayout, QApplication
from PySide6.QtCore import QThread, Signal, QCoreApplication, QTranslator, QLocale, QPoint, QSize
from PySide6.QtGui import QPixmap
except ImportError:
from PyQt5.QtWidgets import QCheckBox, QPushButton, QTextEdit, QFrame, QLabel, QComboBox, QHBoxLayout, QApplication
from PyQt5.QtCore import QThread, QCoreApplication, QTranslator, QLocale, QPoint, QSize
from PyQt5.QtGui import QPixmap
from PyQt5.QtCore import pyqtSignal as Signal
from persepolis.scripts.useful_tools import determineConfigFolder, dictToHeader
from persepolis.scripts.addlink import AddLinkWindow
from persepolis.scripts import logger, osCommands
from persepolis.scripts.spider import spider
from persepolis.constants import VERSION, OS
from functools import partial
from time import time
from random import random, randint
from copy import deepcopy
import yt_dlp as youtube_dl
import urllib
import re
import os
import glob
import platform
# write youtube_dl version in log
logger.sendToLog('yt-dlp version: ' + str(youtube_dl.version.__version__),
'INITIALIZATION')
# download manager config folder .
config_folder = determineConfigFolder()
# persepolis tmp folder path
persepolis_tmp = os.path.join(config_folder, 'persepolis_tmp')
os_type = platform.system()
home_address = os.path.expanduser("~")
if os_type is OS.WINDOWS:
tmp_folder_system = os.path.join(home_address, "AppData", "Local", "Temp")
# make tmp folder if not exists
osCommands.makeDirs(tmp_folder_system)
else:
tmp_folder_system = '/tmp'
class MediaListFetcherThread(QThread):
RESULT = Signal(dict)
cookies = '# HTTP cookie file.\n' # We shall write it in a file when thread starts.
LOADCOOKIEFILESIGNAL = Signal(str)
THUMBNAILSIGNAL = Signal(str)
def __init__(self, receiver_slot, video_dict, main_window):
super().__init__()
self.RESULT.connect(receiver_slot)
self.video_dict = video_dict
self.cookie_path = os.path.join(persepolis_tmp, '.{}{}'.format(time(), random()))
# check certificate
if str(main_window.persepolis_setting.value('settings/dont-check-certificate')) == 'yes':
self.dont_check_certificate = True
else:
self.dont_check_certificate = False
# youtube options must be added to youtube_dl_options_dict in dictionary format
self.youtube_dl_options_dict = {'dump_single_json': True,
'quiet': True,
'noplaylist': True,
'no_warnings': True,
'no-check-certificates': self.dont_check_certificate
}
# create random name for thumbnail
self.thumbnail_file_name = str(randint(0, 1500))
# file must be downloaded in temp folder of system
self.thumbnail_path = os.path.join(tmp_folder_system, self.thumbnail_file_name)
self.youtube_dl_options_dict_thumbnails = {'skip_download': True,
'quiet': True,
'writethumbnail': True,
'outtmpl': self.thumbnail_path,
}
# cookies
self.youtube_dl_options_dict['cookies'] = str(self.cookie_path)
self.youtube_dl_options_dict_thumbnails['cookies'] = str(self.cookie_path)
# referer
if 'referer' in video_dict.keys() and video_dict['referer']:
self.youtube_dl_options_dict['referer'] = str(video_dict['referer'])
self.youtube_dl_options_dict_thumbnails['referer'] = str(video_dict['referer'])
# user_agent
if 'user_agent' in video_dict.keys() and video_dict['user_agent']:
self.youtube_dl_options_dict['user-agent'] = str(video_dict['user_agent'])
self.youtube_dl_options_dict_thumbnails['user-agent'] = str(video_dict['user_agent'])
else:
# set PersepolisDM user agent
video_dict['user_agent'] = 'PersepolisDM/' + str(VERSION.version_str)
self.youtube_dl_options_dict['user-agent'] = 'PersepolisDM/' + str(VERSION.version_str)
self.youtube_dl_options_dict_thumbnails['user-agent'] = 'PersepolisDM/' + str(VERSION.version_str)
# load_cookies
if 'load_cookies' in video_dict.keys() and video_dict['load_cookies']:
# We need to convert raw cookies to http cookie file to use with youtube-dl.
self.cookies = self.makeHttpCookie(video_dict['load_cookies'])
# Proxy
if video_dict['ip']:
# ip + port
ip_port = '{}:{}'.format(video_dict['ip'], str(video_dict['port']))
if video_dict['proxy_user']:
proxy_argument = '{}://{}:{}@{}'.format(video_dict['proxy_type'],
video_dict['proxy_user'], video_dict['proxy_passwd'], ip_port)
else:
proxy_argument = '{}://{}'.format(video_dict['proxy_type'], ip_port)
self.youtube_dl_options_dict['proxy'] = str(proxy_argument)
self.youtube_dl_options_dict_thumbnails['proxy'] = str(proxy_argument)
if video_dict['download_user']:
self.youtube_dl_options_dict['username'] = str(video_dict['download_user'])
self.youtube_dl_options_dict['password'] = str(video_dict['download_passwd'])
self.youtube_dl_options_dict_thumbnails['username'] = str(video_dict['download_user'])
self.youtube_dl_options_dict_thumbnails['password'] = str(video_dict['download_passwd'])
if video_dict['link']:
self.youtube_link = str(video_dict['link'])
def run(self):
ret_val = {}
try: # Create cookie file
cookie_file = open(self.cookie_path, 'w')
cookie_file.write(self.cookies)
cookie_file.close()
ydl = youtube_dl.YoutubeDL(self.youtube_dl_options_dict)
with ydl:
result = ydl.extract_info(
self.youtube_link,
download=False
)
# write new cookies to cookie file
ydl.cookiejar.save(filename=self.cookie_path)
error = "error" # Or comment out this line to show full stderr.
if result:
ret_val = result
else:
ret_val = {'error': str(error)}
try:
osCommands.remove(self.cookie_path)
except Exception as ex:
logger.sendToLog(ex, "DOWNLOAD ERROR")
except Exception as ex:
ret_val = {'error': str(ex)}
try:
osCommands.remove(self.cookie_path)
except Exception as ex:
logger.sendToLog(ex, "DOWNLOAD ERROR")
try: # get video thumbnail
ydl_thumbnail = youtube_dl.YoutubeDL(self.youtube_dl_options_dict_thumbnails)
with ydl_thumbnail:
result = ydl_thumbnail.download(
self.youtube_link
)
# find thumbnail name with file extension
# we know the file name but we don't know file extension.
# create pattern
file_pattern = self.thumbnail_path + '*'
# search for pattern and return first item in the list
files_list = glob.glob(file_pattern)
self.THUMBNAILSIGNAL.emit(files_list[0])
except Exception as ex:
ex = 'Thumbnail error:' + str(ex)
logger.sendToLog(ex, "DOWNLOAD ERROR")
self.LOADCOOKIEFILESIGNAL.emit(self.cookie_path)
self.RESULT.emit(ret_val)
def makeHttpCookie(self, raw_cookie, host_name='.youtube.com'):
cookies = '# HTTP cookie file.\n'
if raw_cookie:
try:
raw_cookies = re.split(';\\s*', str(raw_cookie))
# Format all cookie values as netscape cookie.
for c in raw_cookies:
key, val = c.split('=', 1)
cookies = cookies + '{}\tTRUE\t/\tFALSE\t{}\t{}\t{}\n'. \
format(host_name, int(time()) + 259200, key, val) # Expires after 3 days.
except Exception:
pass
return cookies
class FileSizeFetcherThread(QThread):
FOUND = Signal(dict)
def __init__(self, dictionary, text, combobox_type, index):
super().__init__()
self.dictionary = dictionary
self.text = text
self.combobox_type = combobox_type
self.index = index
def run(self):
spider_file_size = spider(self.dictionary)[1]
self.FOUND.emit({'text': self.text,
'file_size': spider_file_size,
'combobox_type': self.combobox_type,
'index': self.index})
class VideoFinderAddLink(AddLinkWindow):
running_thread = None
threadPool = {}
def __init__(self, parent, receiver_slot, settings, video_dict={}):
super().__init__(parent, receiver_slot, settings, video_dict)
self.setWindowTitle(QCoreApplication.translate("ytaddlink_src_ui_tr", 'Video Finder'))
self.size_label.hide()
# empty lists for no_audio and no_video and video_audio files
self.no_audio_list = []
self.no_video_list = []
self.video_audio_list = []
self.cookie_path = None
self.thumbnail_path = ''
self.media_title = ''
# add support for other languages
locale = str(self.persepolis_setting.value('settings/locale'))
QLocale.setDefault(QLocale(locale))
self.translator = QTranslator()
if self.translator.load(':/translations/locales/ui_' + locale, 'ts'):
QCoreApplication.installTranslator(self.translator)
# extension_label
self.extension_label = QLabel(self.link_frame)
self.change_name_horizontalLayout.addWidget(self.extension_label)
# Fetch Button
self.url_submit_pushButtontton = QPushButton(self.link_frame)
self.link_horizontalLayout.addWidget(self.url_submit_pushButtontton)
# Status Box
self.status_box_textEdit = QTextEdit(self.link_frame)
self.status_box_textEdit.setMaximumHeight(150)
self.link_verticalLayout.addWidget(self.status_box_textEdit)
# Select format horizontal layout
select_format_horizontalLayout = QHBoxLayout()
# Selection Label
self.select_format_label = QLabel(self.link_frame)
select_format_horizontalLayout.addWidget(self.select_format_label)
# Selection combobox
self.media_comboBox = QComboBox(self.link_frame)
self.media_comboBox.setMinimumWidth(200)
select_format_horizontalLayout.addWidget(self.media_comboBox)
# Duration label
self.duration_label = QLabel(self.link_frame)
select_format_horizontalLayout.addWidget(self.duration_label)
self.format_selection_frame = QFrame(self)
self.format_selection_frame.setLayout(select_format_horizontalLayout)
self.link_verticalLayout.addWidget(self.format_selection_frame)
# advanced_format_selection_checkBox
self.advanced_format_selection_checkBox = QCheckBox(self)
self.link_verticalLayout.addWidget(self.advanced_format_selection_checkBox)
# advanced_format_selection_frame
self.advanced_format_selection_frame = QFrame(self)
self.link_verticalLayout.addWidget(self.advanced_format_selection_frame)
advanced_format_selection_horizontalLayout = QHBoxLayout(self.advanced_format_selection_frame)
# video_format_selection
self.video_format_selection_label = QLabel(self.advanced_format_selection_frame)
self.video_format_selection_comboBox = QComboBox(self.advanced_format_selection_frame)
# audio_format_selection
self.audio_format_selection_label = QLabel(self.advanced_format_selection_frame)
self.audio_format_selection_comboBox = QComboBox(self.advanced_format_selection_frame)
for widget in [self.video_format_selection_label,
self.video_format_selection_comboBox,
self.audio_format_selection_label,
self.audio_format_selection_comboBox]:
advanced_format_selection_horizontalLayout.addWidget(widget)
# thumbnail picture
self.thumbnail_label = QLabel(self.link_frame)
self.change_name_horizontalLayout.addWidget(self.thumbnail_label)
# Set Texts
self.url_submit_pushButtontton.setText(QCoreApplication.translate("ytaddlink_src_ui_tr", 'Fetch Media List'))
self.select_format_label.setText(QCoreApplication.translate("ytaddlink_src_ui_tr", 'Select a format'))
self.video_format_selection_label.setText(QCoreApplication.translate("ytaddlink_src_ui_tr", 'Video format:'))
self.audio_format_selection_label.setText(QCoreApplication.translate("ytaddlink_src_ui_tr", 'Audio format:'))
self.advanced_format_selection_checkBox.setText(
QCoreApplication.translate("ytaddlink_src_ui_tr", 'Advanced options'))
# Add Slot Connections
self.url_submit_pushButtontton.setEnabled(False)
self.change_name_lineEdit.setEnabled(False)
self.ok_pushButton.setEnabled(False)
self.download_later_pushButton.setEnabled(False)
self.format_selection_frame.setEnabled(True)
self.advanced_format_selection_frame.setEnabled(False)
self.advanced_format_selection_checkBox.toggled.connect(self.advancedFormatFrame)
self.url_submit_pushButtontton.clicked.connect(self.submitClicked)
self.media_comboBox.activated.connect(
partial(self.mediaSelectionChanged, 'video_audio'))
self.video_format_selection_comboBox.activated.connect(
partial(self.mediaSelectionChanged, 'video'))
self.audio_format_selection_comboBox.activated.connect(
partial(self.mediaSelectionChanged, 'audio'))
self.link_lineEdit.textChanged.disconnect(super().linkLineChanged) # Should be disconnected.
self.link_lineEdit.textChanged.connect(self.linkLineChangedHere)
self.setMinimumSize(650, 480)
# set window size and position
size = self.persepolis_setting.value(
'VideoFinderAddLinkWindow/size', QSize(652, 480))
position = self.persepolis_setting.value(
'VideoFinderAddLinkWindow/position', QPoint(300, 300))
self.resize(size)
self.move(position)
self.status_box_textEdit.hide()
self.format_selection_frame.hide()
self.advanced_format_selection_frame.hide()
self.advanced_format_selection_checkBox.hide()
if 'link' in video_dict.keys() and video_dict['link']:
self.link_lineEdit.setText(video_dict['link'])
self.url_submit_pushButtontton.setEnabled(True)
else:
# check clipboard
clipboard = QApplication.clipboard()
text = clipboard.text()
if (("tp:/" in text[2:6]) or ("tps:/" in text[2:7])):
self.link_lineEdit.setText(str(text))
self.url_submit_pushButtontton.setEnabled(True)
def advancedFormatFrame(self, button):
if self.advanced_format_selection_checkBox.isChecked():
self.advanced_format_selection_frame.setEnabled(True)
self.format_selection_frame.setEnabled(False)
self.mediaSelectionChanged('video', int(self.video_format_selection_comboBox.currentIndex()))
else:
self.advanced_format_selection_frame.setEnabled(False)
self.format_selection_frame.setEnabled(True)
self.mediaSelectionChanged('video_audio', int(self.media_comboBox.currentIndex()))
def getReadableSize(self, size):
try:
return '{:1.2f} MB'.format(int(size) / 1048576)
except Exception:
return str(size)
def getReadableDuration(self, seconds):
try:
seconds = int(seconds)
hours = seconds // 3600
seconds = seconds % 3600
minutes = seconds // 60
seconds = seconds % 60
return '{:02d}:{:02d}:{:02d}'.format(hours, minutes, seconds)
except Exception:
return str(seconds)
# Define native slots
def urlChanged(self, value):
if ' ' in value or value == '':
self.url_submit_pushButtontton.setEnabled(False)
self.url_submit_pushButtontton.setToolTip(QCoreApplication.translate(
"ytaddlink_src_ui_tr", 'Please enter a valid video link'))
else:
self.url_submit_pushButtontton.setEnabled(True)
self.url_submit_pushButtontton.setToolTip('')
def submitClicked(self, button=None):
# Clear media list
self.media_comboBox.clear()
self.format_selection_frame.hide()
self.advanced_format_selection_checkBox.hide()
self.advanced_format_selection_frame.hide()
self.video_format_selection_comboBox.clear()
self.audio_format_selection_comboBox.clear()
self.change_name_lineEdit.clear()
self.threadPool.clear()
self.change_name_checkBox.setChecked(False)
self.video_audio_list.clear()
self.no_video_list.clear()
self.no_audio_list.clear()
self.url_submit_pushButtontton.setEnabled(False)
self.status_box_textEdit.setText(QCoreApplication.translate("ytaddlink_src_ui_tr", 'Fetching Media Info...'))
self.status_box_textEdit.show()
self.ok_pushButton.setEnabled(False)
self.download_later_pushButton.setEnabled(False)
dictionary_to_send = deepcopy(self.plugin_add_link_dictionary)
# More options
more_options = self.collectMoreOptions()
for k in more_options.keys():
dictionary_to_send[k] = more_options[k]
dictionary_to_send['link'] = self.link_lineEdit.text()
dictionary_to_send['socket-timeout'] = '5'
fetcher_thread = MediaListFetcherThread(self.fetchedResult, dictionary_to_send, self.parent)
self.parent.threadPool.append(fetcher_thread)
self.parent.threadPool[-1].start()
self.parent.threadPool[-1].LOADCOOKIEFILESIGNAL.connect(self.setLoadCookie)
self.parent.threadPool[-1].THUMBNAILSIGNAL.connect(self.showThumbnail)
# Thumbnail is available! So show it!
def showThumbnail(self, thumbnail_path):
self.thumbnail_path = thumbnail_path
pixmap = QPixmap(self.thumbnail_path)
# Thumbnail height must be 1/5 height of window.
height_of_window = self.height()
self.thumbnail_label.setPixmap(pixmap.scaledToHeight(height_of_window // 5))
def setLoadCookie(self, str):
if os.path.isfile(str):
self.cookie_path = str
self.load_cookies_lineEdit.setText(str)
def fileNameChanged(self, value):
if value.strip() == '':
self.ok_pushButton.setEnabled(False)
def mediaSelectionChanged(self, combobox, index):
try:
if combobox == 'video_audio':
if self.media_comboBox.currentText() == 'Best quality':
self.change_name_lineEdit.setText(self.media_title)
self.extension_label.setText('.' + self.no_audio_list[-1]['ext'])
else:
self.change_name_lineEdit.setText(self.media_title)
self.extension_label.setText('.' + self.video_audio_list[index]['ext'])
self.change_name_checkBox.setChecked(True)
elif combobox == 'video':
if self.video_format_selection_comboBox.currentText() != 'No video':
self.change_name_lineEdit.setText(self.media_title)
self.extension_label.setText('.' + self.no_audio_list[index - 1]['ext'])
self.change_name_checkBox.setChecked(True)
else:
if self.audio_format_selection_comboBox.currentText() != 'No audio':
self.change_name_lineEdit.setText(self.media_title)
self.extension_label.setText('.' + self.no_video_list[int(self.audio_format_selection_comboBox.currentIndex()) - 1]['ext'])
self.change_name_checkBox.setChecked(True)
else:
self.change_name_lineEdit.setChecked(False)
elif combobox == 'audio':
if self.audio_format_selection_comboBox.currentText() != 'No audio' and self.video_format_selection_comboBox.currentText() == 'No video':
self.change_name_lineEdit.setText(self.media_title)
self.extension_label.setText('.' + self.no_video_list[index - 1]['ext'])
self.change_name_checkBox.setChecked(True)
elif (self.audio_format_selection_comboBox.currentText() == 'No audio' and self.video_format_selection_comboBox.currentText() != 'No video') or (self.audio_format_selection_comboBox.currentText() != 'No audio' and self.video_format_selection_comboBox.currentText() != 'No video'):
self.change_name_lineEdit.setText(self.media_title)
self.extension_label.setText('.' + self.no_audio_list[int(self.video_format_selection_comboBox.currentIndex()) - 1]['ext'])
self.change_name_checkBox.setChecked(True)
elif self.audio_format_selection_comboBox.currentText() == 'No audio' and self.video_format_selection_comboBox.currentText() == 'No video':
self.change_name_checkBox.setChecked(False)
except Exception as ex:
logger.sendToLog(ex, "DOWNLOAD ERROR")
# Return the filename extension from url, or ''.
def getFileExtension(self, url):
parsed = urllib.parse.urlparse(url)
root, ext = os.path.splitext(parsed.path)
return ext[1:]
def fetchedResult(self, media_dict):
if 'error' in media_dict.keys():
self.status_box_textEdit.setText('' + str(media_dict['error']) + '')
self.status_box_textEdit.show()
# enable fetch media button again
self.url_submit_pushButtontton.setEnabled(True)
else: # Show the media list
# add no audio and no video options to the comboboxes
self.video_format_selection_comboBox.addItem('No video')
self.audio_format_selection_comboBox.addItem('No audio')
# set first 20 characters of media_title for file name
if 'title' in media_dict.keys():
if len(media_dict['title']) > 20:
self.media_title = media_dict['title'][0:20]
else:
self.media_title = 'Video finder'
if 'formats' not in media_dict.keys() and 'entries' in media_dict.keys():
formats = media_dict['entries']
formats = formats[0]
media_dict['formats'] = formats['formats']
elif 'formats' not in media_dict.keys() and 'format' in media_dict.keys():
media_dict['formats'] = [media_dict.copy()]
try:
i = 0
media_dict_lenght = len(media_dict['formats'])
for f in media_dict['formats']:
url_ext = self.getFileExtension(f['url'])
no_audio = False
no_video = False
text = ''
# set http_headers
if 'http_headers' in f.keys():
header_dict = f['http_headers']
if 'User-Agent' in header_dict.keys():
self.user_agent_lineEdit.setText(header_dict['User-Agent'])
self.plugin_add_link_dictionary['user-agent'] = header_dict['User-Agent']
header_dict.pop('User-Agent')
if 'Referer' in header_dict.keys():
self.referer_lineEdit.setText(header_dict['Referer'])
self.plugin_add_link_dictionary['referer'] = header_dict['Referer']
header_dict.pop('Referer')
self.plugin_add_link_dictionary['header'] = dictToHeader(header_dict)
self.header_lineEdit.setText(self.plugin_add_link_dictionary['header'])
if 'acodec' in f.keys():
# only video, no audio
if f['acodec'] == 'none':
no_audio = True
# resolution
if 'height' in f.keys():
text = text + ' ' + '{}p'.format(f['height'])
if 'vcodec' in f.keys():
# if f['vcodec'] == 'none' and f['acodec'] != 'none':
# continue
# No video, show audio bit rate
if f['vcodec'] == 'none':
text = text + '{}kbps'.format(f['abr'])
no_video = True
if 'ext' in f.keys():
text = text + ' ' + '.{}'.format(f['ext'])
if 'filesize' in f.keys() and f['filesize']:
# Youtube api does not supply file size for some formats, so check it.
text = text + ' ' + '{}'.format(self.getReadableSize(f['filesize']))
size_available = True
else: # Start spider to find file size
size_available = False
input_dict = deepcopy(self.plugin_add_link_dictionary)
input_dict['link'] = f['url']
more_options = self.collectMoreOptions()
for key in more_options.keys():
input_dict[key] = more_options[key]
if url_ext == 'm3u8':
text = text + ' m3u8'
# Add current format to the related comboboxes
if no_audio:
combobox_type = 'video'
self.no_audio_list.append(f)
self.video_format_selection_comboBox.addItem(text)
index = self.video_format_selection_comboBox.count() - 1
elif no_video:
combobox_type = 'audio'
self.no_video_list.append(f)
self.audio_format_selection_comboBox.addItem(text)
index = self.audio_format_selection_comboBox.count() - 1
else:
if i == 0 and (media_dict_lenght > 1):
text = 'Worst quality: ' + text
elif i == (media_dict_lenght - 1):
text = 'Best quality: ' + text
combobox_type = 'media'
self.video_audio_list.append(f)
self.media_comboBox.addItem(text)
index = self.media_comboBox.count() - 1
# we can't get size of file from m3u8 format.
if not (size_available) and url_ext != 'm3u8':
size_fetcher = FileSizeFetcherThread(input_dict, text, combobox_type, index)
self.threadPool[str(i)] = {'thread': size_fetcher, 'item_id': i}
self.parent.threadPool.append(size_fetcher)
self.parent.threadPool[-1].start()
self.parent.threadPool[-1].FOUND.connect(self.findFileSize)
i = i + 1
self.status_box_textEdit.hide()
if 'duration' in media_dict.keys():
self.duration_label.setText('Duration ' + self.getReadableDuration(media_dict['duration']))
self.format_selection_frame.show()
self.advanced_format_selection_checkBox.show()
self.advanced_format_selection_frame.show()
self.ok_pushButton.setEnabled(True)
self.download_later_pushButton.setEnabled(True)
# if we have no options for separate audio and video, then hide advanced_format_selection...
if len(self.no_audio_list) == 0 and len(self.no_video_list) == 0:
self.advanced_format_selection_checkBox.hide()
self.advanced_format_selection_frame.hide()
# set index of comboboxes on best available quality.
# we have both audio and video
if len(self.no_audio_list) != 0 and len(self.no_video_list) != 0:
self.media_comboBox.addItem('Best quality')
self.media_comboBox.setCurrentIndex(len(self.video_audio_list))
self.change_name_lineEdit.setText(self.media_title)
self.extension_label.setText('.' + self.no_audio_list[-1]['ext'])
self.change_name_checkBox.setChecked(True)
# video and audio are not separate
elif len(self.video_audio_list) != 0:
self.media_comboBox.setCurrentIndex(len(self.video_audio_list) - 1)
self.mediaSelectionChanged('video_audio', int(self.media_comboBox.currentIndex()))
if len(self.no_audio_list) != 0:
self.video_format_selection_comboBox.setCurrentIndex(len(self.no_audio_list))
if len(self.no_video_list) != 0:
self.audio_format_selection_comboBox.setCurrentIndex(len(self.no_video_list))
# if we have only audio or we have only video then hide media_comboBox
if len(self.video_audio_list) == 0:
self.media_comboBox.hide()
self.select_format_label.hide()
# only video
if len(self.no_video_list) != 0 and len(self.no_audio_list) == 0:
self.mediaSelectionChanged('video', int(self.video_format_selection_comboBox.currentIndex()))
self.advanced_format_selection_checkBox.setChecked(True)
self.advanced_format_selection_checkBox.hide()
# only audio
elif len(self.no_video_list) == 0 and len(self.no_audio_list) != 0:
self.mediaSelectionChanged('audio', int(self.audio_format_selection_comboBox.currentIndex()))
self.advanced_format_selection_checkBox.setChecked(True)
self.advanced_format_selection_checkBox.hide()
# audio and video
else:
self.mediaSelectionChanged('video_audio', int(self.media_comboBox.currentIndex()))
except Exception as ex:
logger.sendToLog(ex, "DOWNLOAD ERROR")
def findFileSize(self, result):
try:
index = result['index']
text = result['text']
if result['file_size'] and result['file_size'] != '0':
if result['combobox_type'] == 'audio':
self.audio_format_selection_comboBox.setItemText(index, '{} - {}'.format(text, result['file_size']))
elif result['combobox_type'] == 'video':
self.video_format_selection_comboBox.setItemText(index, '{} - {}'.format(text, result['file_size']))
else:
self.media_comboBox.setItemText(index, '{} - {}'.format(text, result['file_size']))
self.video_audio_list[index]['file_size'] = result['file_size']
except Exception as ex:
logger.sendToLog(ex, "DOWNLOAD ERROR")
def linkLineChangedHere(self, lineEdit):
if str(lineEdit) == '':
self.url_submit_pushButtontton.setEnabled(False)
else:
self.url_submit_pushButtontton.setEnabled(True)
def closeEvent(self, event):
self.persepolis_setting.setValue('VideoFinderAddLinkWindow/size', self.size())
self.persepolis_setting.setValue('VideoFinderAddLinkWindow/position', self.pos())
self.persepolis_setting.sync()
osCommands.remove(self.thumbnail_path)
event.accept()
# This method collects additional information like proxy ip, user, password etc.
def collectMoreOptions(self):
options = {'ip': None, 'port': None, 'proxy_user': None, 'proxy_passwd': None, 'download_user': None,
'download_passwd': None, 'proxy_type': None, 'load_cookies': None}
if self.proxy_checkBox.isChecked():
options['ip'] = self.ip_lineEdit.text()
options['port'] = self.port_spinBox.value()
options['proxy_user'] = self.proxy_user_lineEdit.text()
options['proxy_passwd'] = self.proxy_pass_lineEdit.text()
# http, https or socks5 proxy
if self.http_radioButton.isChecked() is True:
options['proxy_type'] = 'http'
elif self.https_radioButton.isChecked() is True:
options['proxy_type'] = 'https'
else:
options['proxy_type'] = 'socks5'
if self.download_checkBox.isChecked():
options['download_user'] = self.download_user_lineEdit.text()
options['download_passwd'] = self.download_pass_lineEdit.text()
if self.load_cookies_lineEdit.text() != '':
options['load_cookies'] = self.load_cookies_lineEdit.text()
# These info (keys) are required for spider to find file size, because spider() does not check if key exists.
additional_info = ['header', 'user_agent', 'referer', 'out']
for i in additional_info:
if i not in self.plugin_add_link_dictionary.keys():
options[i] = None
return options
# user submitted information by pressing ok_pushButton, so get information
# from VideoFinderAddLink window and return them to the mainwindow with callback!
def okButtonPressed(self, download_later, button=None): # noqa
link_list = []
# separate audio format and video format is selected.
if self.advanced_format_selection_checkBox.isChecked():
if self.video_format_selection_comboBox.currentText() == 'No video' and self.audio_format_selection_comboBox.currentText() != 'No audio':
# only audio link must be added to the link_list
audio_link = self.no_video_list[self.audio_format_selection_comboBox.currentIndex() - 1]['url']
link_list.append(audio_link)
elif self.video_format_selection_comboBox.currentText() != 'No video' and self.audio_format_selection_comboBox.currentText() == 'No audio':
# only video link must be added to the link_list
video_link = self.no_audio_list[self.video_format_selection_comboBox.currentIndex() - 1]['url']
link_list.append(video_link)
elif self.video_format_selection_comboBox.currentText() != 'No video' and self.audio_format_selection_comboBox.currentText() != 'No audio':
# video and audio links must be added to the link_list
audio_link = self.no_video_list[self.audio_format_selection_comboBox.currentIndex() - 1]['url']
video_link = self.no_audio_list[self.video_format_selection_comboBox.currentIndex() - 1]['url']
link_list = [video_link, audio_link]
elif self.video_format_selection_comboBox.currentText() == 'No video' and self.audio_format_selection_comboBox.currentText() == 'No audio':
# no video and audio is selected! REALLY?!. user is DRUNK! close the window! :))
self.close()
else:
if self.media_comboBox.currentText() == 'Best quality':
# the last item in no_video_list and no_audio_list are the best.
video_link = self.no_audio_list[-1]['url']
audio_link = self.no_video_list[-1]['url']
link_list = [video_link, audio_link]
else:
audio_and_video_link = self.video_audio_list[self.media_comboBox.currentIndex()]['url']
link_list.append(audio_and_video_link)
# write user's new inputs in persepolis_setting for next time :)
self.persepolis_setting.setValue(
'add_link_initialization/ip', self.ip_lineEdit.text())
self.persepolis_setting.setValue(
'add_link_initialization/port', self.port_spinBox.value())
self.persepolis_setting.setValue(
'add_link_initialization/proxy_user', self.proxy_user_lineEdit.text())
self.persepolis_setting.setValue(
'add_link_initialization/download_user', self.download_user_lineEdit.text())
# http, https or socks5 proxy
if self.http_radioButton.isChecked() is True:
proxy_type = 'http'
self.persepolis_setting.setValue(
'add_link_initialization/proxy_type', 'http')
elif self.https_radioButton.isChecked() is True:
proxy_type = 'https'
self.persepolis_setting.setValue(
'add_link_initialization/proxy_type', 'https')
else:
proxy_type = 'socks5'
self.persepolis_setting.setValue(
'add_link_initialization/proxy_type', 'socks5')
# get proxy information
if not (self.proxy_checkBox.isChecked()):
ip = None
port = None
proxy_user = None
proxy_passwd = None
proxy_type = None
else:
ip = self.ip_lineEdit.text()
if not (ip):
ip = None
port = self.port_spinBox.value()
if not (port):
port = None
proxy_user = self.proxy_user_lineEdit.text()
if not (proxy_user):
proxy_user = None
proxy_passwd = self.proxy_pass_lineEdit.text()
if not (proxy_passwd):
proxy_passwd = None
# get download username and password information
if not (self.download_checkBox.isChecked()):
download_user = None
download_passwd = None
else:
download_user = self.download_user_lineEdit.text()
if not (download_user):
download_user = None
download_passwd = self.download_pass_lineEdit.text()
if not (download_passwd):
download_passwd = None
# get start time for download if user set that.
if not (self.start_checkBox.isChecked()):
start_time = None
else:
start_time = self.start_time_qDataTimeEdit.text()
# get end time for download if user set that.
if not (self.end_checkBox.isChecked()):
end_time = None
else:
end_time = self.end_time_qDateTimeEdit.text()
# set name for file(s)
if self.change_name_checkBox.isChecked():
name = str(self.change_name_lineEdit.text())
if name == '':
name = 'video_finder_file'
else:
name = 'video_finder_file'
# video finder always finds extension
# but if it can't find file extension
# use mp4 for extension.
if str(self.extension_label.text()) == '':
extension = '.mp4'
else:
extension = str(self.extension_label.text())
# did user select separate audio and video?
if len(link_list) == 2:
video_name = name + "_video" + extension
audio_name = name + "_audio" + '.' + \
str(self.no_video_list[self.audio_format_selection_comboBox.currentIndex() - 1]['ext'])
name_list = [video_name, audio_name]
else:
name_list = [name + extension]
# get number of connections
connections = self.connections_spinBox.value()
# get download_path
download_path = self.download_folder_lineEdit.text()
# referer
if self.referer_lineEdit.text() != '':
referer = self.referer_lineEdit.text()
else:
referer = self.link_lineEdit.text()
# header
if self.header_lineEdit.text() != '':
header = self.header_lineEdit.text()
else:
header = None
# user_agent
if self.user_agent_lineEdit.text() != '':
user_agent = self.user_agent_lineEdit.text()
else:
user_agent = None
# load_cookies
if self.load_cookies_lineEdit.text() != '':
load_cookies = self.load_cookies_lineEdit.text()
else:
load_cookies = None
add_link_dictionary_list = []
if len(link_list) == 1:
# save information in a dictionary(add_link_dictionary).
add_link_dictionary = {'referer': referer, 'header': header, 'user_agent': user_agent, 'load_cookies': load_cookies,
'out': name_list[0], 'start_time': start_time, 'end_time': end_time, 'link': link_list[0], 'ip': ip,
'port': port, 'proxy_user': proxy_user, 'proxy_passwd': proxy_passwd, 'proxy_type': proxy_type,
'download_user': download_user, 'download_passwd': download_passwd,
'connections': connections, 'limit_value': 10, 'download_path': download_path}
add_link_dictionary_list.append(add_link_dictionary)
else:
video_add_link_dictionary = {'referer': referer, 'header': header, 'user_agent': user_agent, 'load_cookies': load_cookies,
'out': name_list[0], 'start_time': start_time, 'end_time': end_time, 'link': link_list[0], 'ip': ip,
'port': port, 'proxy_user': proxy_user, 'proxy_passwd': proxy_passwd, 'proxy_type': proxy_type,
'download_user': download_user, 'download_passwd': download_passwd,
'connections': connections, 'limit_value': 10, 'download_path': download_path}
audio_add_link_dictionary = {'referer': referer, 'header': header, 'user_agent': user_agent, 'load_cookies': load_cookies,
'out': name_list[1], 'start_time': None, 'end_time': end_time, 'link': link_list[1], 'ip': ip,
'port': port, 'proxy_user': proxy_user, 'proxy_passwd': proxy_passwd, 'proxy_type': proxy_type,
'download_user': download_user, 'download_passwd': download_passwd,
'connections': connections, 'limit_value': 10, 'download_path': download_path}
add_link_dictionary_list = [video_add_link_dictionary, audio_add_link_dictionary]
# get category of download
category = str(self.add_queue_comboBox.currentText())
del self.plugin_add_link_dictionary
# return information to mainwindow
self.callback(add_link_dictionary_list, download_later, category)
# close window
self.close()
================================================
FILE: persepolis/scripts/video_finder_progress.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
try:
from PySide6.QtCore import Qt, QSize, QPoint, QThread, QTranslator, QCoreApplication, QLocale
from PySide6.QtWidgets import QLineEdit, QInputDialog
from PySide6.QtGui import QIcon
except ImportError:
from PyQt5.QtCore import Qt, QSize, QPoint, QThread, QTranslator, QCoreApplication, QLocale
from PyQt5.QtWidgets import QLineEdit, QInputDialog
from PyQt5.QtGui import QIcon
from persepolis.constants import OS
from persepolis.gui.video_finder_progress_ui import VideoFinderProgressWindow_Ui
from persepolis.scripts.shutdown import shutDown
import subprocess
import platform
os_type = platform.system()
class ShutDownThread(QThread):
def __init__(self, parent, category, password=None):
QThread.__init__(self)
self.category = category
self.password = password
self.main_window = parent
def run(self):
shutDown(self.main_window, category=self.category, password=self.password)
class VideoFinderProgressWindow(VideoFinderProgressWindow_Ui):
def __init__(self, parent, gid_list, persepolis_setting):
super().__init__(persepolis_setting, parent)
self.persepolis_setting = persepolis_setting
self.main_window = parent
# first item in the gid_list is related to video's link and second item is related to audio's link.
self.gid_list = gid_list
# this variable can be changed by checkDownloadInfo method in mainwindow.py
# self.gid defines that which gid is downloaded.
self.gid = gid_list[0]
# this variable used as category name in ShutDownThread
self.video_finder_plus_gid = 'video_finder_' + str(gid_list[0])
# connect signals and sluts
self.resume_pushButton.clicked.connect(self.resumePushButtonPressed)
self.stop_pushButton.clicked.connect(self.stopPushButtonPressed)
self.pause_pushButton.clicked.connect(self.pausePushButtonPressed)
self.download_progressBar.setValue(0)
self.after_frame.setEnabled(False)
self.after_checkBox.toggled.connect(self.afterCheckBoxToggled)
self.after_pushButton.clicked.connect(self.afterPushButtonPressed)
# add support for other languages
locale = str(self.persepolis_setting.value('settings/locale'))
QLocale.setDefault(QLocale(locale))
self.translator = QTranslator()
if self.translator.load(':/translations/locales/ui_' + locale, 'ts'):
QCoreApplication.installTranslator(self.translator)
self.after_comboBox.currentIndexChanged.connect(self.afterComboBoxChanged)
# speed limit
self.limit_dial.setValue(10)
self.limit_dial.sliderReleased.connect(self.limitDialIsReleased)
self.limit_dial.valueChanged.connect(self.limitDialIsChanged)
self.limit_label.setText('Speed : Maximum')
# set window size and position
size = self.persepolis_setting.value(
'ProgressWindow/size', QSize(595, 274))
position = self.persepolis_setting.value(
'ProgressWindow/position', QPoint(300, 300))
self.resize(size)
self.move(position)
# close window with ESC key
def keyPressEvent(self, event):
if event.key() == Qt.Key_Escape:
self.close()
def closeEvent(self, event):
# save window size and position
self.persepolis_setting.setValue('ProgressWindow/size', self.size())
self.persepolis_setting.setValue('ProgressWindow/position', self.pos())
self.persepolis_setting.sync()
self.hide()
def resumePushButtonPressed(self, button):
if self.status == "paused":
# search gid in download_sessions_list
for download_session_dict in self.main_window.download_sessions_list:
if download_session_dict['gid'] == self.gid:
# unpause download
download_session_dict['download_session'].downloadUnpause()
break
def pausePushButtonPressed(self, button):
if self.status == "downloading":
# search gid in download_sessions_list
for download_session_dict in self.main_window.download_sessions_list:
if download_session_dict['gid'] == self.gid:
# unpause download
download_session_dict['download_session'].downloadPause()
break
def stopPushButtonPressed(self, button):
# cancel shut down progress
dictionary = {'category': self.video_finder_plus_gid,
'shutdown': 'canceled'}
self.main_window.temp_db.updateQueueTable(dictionary)
if self.status == "downloading":
# search gid in download_sessions_list
for download_session_dict in self.main_window.download_sessions_list:
if download_session_dict['gid'] == self.gid:
# unpause download
download_session_dict['download_session'].downloadStop()
break
def afterComboBoxChanged(self, connect):
self.after_pushButton.setEnabled(True)
def afterCheckBoxToggled(self, checkBoxes):
if self.after_checkBox.isChecked():
self.after_frame.setEnabled(True)
self.after_pushButton.setEnabled(True)
else:
# so user canceled shutdown after download
# write cancel value in data_base for this gid
dictionary = {'category': self.video_finder_plus_gid,
'shutdown': 'canceled'}
self.main_window.temp_db.updateQueueTable(dictionary)
def afterPushButtonPressed(self, button):
self.after_pushButton.setEnabled(False)
# For Linux and Mac OSX and FreeBSD and OpenBSD
if os_type != OS.WINDOWS:
# get root password
passwd, ok = QInputDialog.getText(
self, 'PassWord', 'Please enter root password:', QLineEdit.Password)
if ok:
# check password is true or not!
pipe = subprocess.Popen(['sudo', '-S', 'echo', 'hello'],
stdout=subprocess.DEVNULL,
stdin=subprocess.PIPE,
stderr=subprocess.DEVNULL,
shell=False)
pipe.communicate(passwd.encode())
answer = pipe.wait()
# Wrong password
while answer != 0:
passwd, ok = QInputDialog.getText(
self, 'PassWord', 'Wrong Password!\nPlease try again.', QLineEdit.Password)
if ok:
pipe = subprocess.Popen(['sudo', '-S', 'echo', 'hello'],
stdout=subprocess.DEVNULL,
stdin=subprocess.PIPE,
stderr=subprocess.DEVNULL,
shell=False)
pipe.communicate(passwd.encode())
answer = pipe.wait()
else:
ok = False
self.after_pushButton.setEnabled(True)
break
if ok is not False:
# if user selects shutdown option after download progress,
# value of 'shutdown' will changed in temp_db for this progress
# and "wait" word will be written for this value.
# (see ShutDownThread and shutdown.py for more information)
# shutDown method will check that value in a loop .
# when "wait" changes to "shutdown" then shutdown.py script
# will shut down the system.
shutdown_enable = ShutDownThread(self.main_window, self.video_finder_plus_gid, passwd)
self.main_window.threadPool.append(shutdown_enable)
self.main_window.threadPool[-1].start()
else:
self.after_checkBox.setChecked(False)
else:
self.after_checkBox.setChecked(False)
else:
# for Windows
for gid in self.gid_list:
shutdown_enable = ShutDownThread(self.main_window, self.video_finder_plus_gid)
self.main_window.threadPool.append(shutdown_enable)
self.main_window.threadPool[-1].start()
def limitDialIsReleased(self):
limit_value = self.limit_dial.value()
# set speed limit value
for download_session_dict in self.main_window.download_sessions_list:
if download_session_dict['gid'] == self.gid:
# limit download speed
download_session_dict['download_session'].limitSpeed(limit_value)
break
def limitDialIsChanged(self, button):
if self.limit_dial.value() == 10:
self.limit_label.setText('Speed : Maximum')
elif self.limit_dial.value() == 0:
self.limit_label.setText('Speed : Minimum')
else:
self.limit_label.setText('Speed')
def changeIcon(self, icons):
icons = ':/' + str(icons) + '/'
self.resume_pushButton.setIcon(QIcon(icons + 'play'))
self.pause_pushButton.setIcon(QIcon(icons + 'pause'))
self.stop_pushButton.setIcon(QIcon(icons + 'stop'))
================================================
FILE: persepolis/scripts/ytdlp_downloader.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import yt_dlp
import time
import threading
from persepolis.constants import VERSION
from persepolis.scripts import logger
from persepolis.scripts.useful_tools import humanReadableSize, convertTime, returnNewFileName
from persepolis.scripts.osCommands import makeDirs, moveFile
from urllib.parse import urlparse, unquote
from pathlib import Path
import os
# This class gets yt-dlp log messages.
# see yt-dlp man page for more information.
class Ytdp_Logger():
def debug(self, msg):
# For compatibility with youtube-dl, both debug and info are passed into debug
# You can distinguish them by the prefix '[debug] '
if msg.startswith('[debug] '):
logger.sendToLog(msg, type="DEBUG")
else:
self.info(msg)
# Normally, It's not necessary!
def info(self, msg):
pass
def warning(self, msg):
logger.sendToLog(msg, type="WARNING")
def error(self, msg):
logger.sendToLog(msg, type="ERROR")
# This class downloads m3u8 format files.
class Ytdp_Download():
def __init__(self, add_link_dictionary, main_window, gid, single_video_link=False):
self.downloaded_size = 0
self.finished_threads = 0
self.eta = "0"
self.resume = False
self.main_window = main_window
self.download_speed_str = "0"
self.gid = gid
self.link = add_link_dictionary['link']
self.name = add_link_dictionary['out']
self.download_path = add_link_dictionary['download_path']
self.ip = add_link_dictionary['ip']
self.port = add_link_dictionary['port']
self.proxy_user = add_link_dictionary['proxy_user']
self.proxy_passwd = add_link_dictionary['proxy_passwd']
self.proxy_type = add_link_dictionary['proxy_type']
self.download_user = add_link_dictionary['download_user']
self.download_passwd = add_link_dictionary['download_passwd']
self.header = add_link_dictionary['header']
self.user_agent = add_link_dictionary['user_agent']
self.load_cookies = add_link_dictionary['load_cookies']
self.referer = add_link_dictionary['referer']
self.start_time = add_link_dictionary['start_time']
self.end_time = add_link_dictionary['end_time']
self.number_of_parts = 0
self.process = None
self.file_name = None
self.file_size = None
self.timeout = int(main_window.persepolis_setting.value('settings/timeout'))
self.retry = int(main_window.persepolis_setting.value('settings/max-tries'))
self.retry_wait = int(main_window.persepolis_setting.value('settings/retry-wait'))
self.python_request_chunk_size = int(main_window.persepolis_setting.value('settings/chunk-size'))
self.lock = False
self.sleep_for_speed_limiting = 0
self.not_converted_download_speed = 0
self.download_percent = 0
self.error_message = ''
self.single_video_link = single_video_link
self.yt_dlp_exited = False
# this flag notify that download finished(stopped, complete or error)
# in this situation download status must be written to the database
# None means, Download not finished yet.
# False meanse, Download has been finished, but download status must be written to the database
# True meanse, Download status has been written to the database
self.write_it_to_the_database = None
# check certificate
if str(main_window.persepolis_setting.value('settings/dont-check-certificate')) == 'yes':
self.dont_check_certificate = True
else:
self.dont_check_certificate = False
# number_of_threads can't be more that 64
self.number_of_threads = int(add_link_dictionary['connections'])
self.fragments = '0/0'
self.thread_list = []
# download_status can be in waiting, downloading, stop, error, eaused
self.download_status = 'waiting'
# update data_base
dict_ = {'gid': self.gid,
'download_status': self.download_status}
self.main_window.persepolis_db.updateVideoFinderTable2(dict_)
# get file name if available
# if file name is not available, then set a file name
def getFileName(self):
# set default file name
parsed_linkd = urlparse(self.link)
self.file_name = Path(parsed_linkd.path).name
# URL might contain percent-encoded characters
# for example farsi characters in link
if self.file_name.find('%'):
self.file_name = unquote(self.file_name)
# check if user set file name or not
if self.name:
self.file_name = self.name
# create yt-dlp session
def createSession(self):
self.getFileName()
self.file_path = os.path.join(self.download_path, self.file_name)
# youtube options must be added to youtube_dl_options_dict in dictionary format
self.youtube_dl_options_dict = {'dump_single_json': True,
'logger': Ytdp_Logger(),
'quiet': True,
'noplaylist': True,
'no_warnings': True,
'no-check-certificates': self.dont_check_certificate,
'retries': self.retry,
'socket_timeout': self.timeout,
'outtmpl': self.file_path,
'continue_dl': True
}
# cookies
self.youtube_dl_options_dict['cookies'] = str(self.load_cookies)
# referer
if self.referer:
self.youtube_dl_options_dict['referer'] = self.referer
# user_agent
if self.user_agent:
self.youtube_dl_options_dict['user-agent'] = self.user_agent
else:
# set PersepolisDM user agent
self.youtube_dl_options_dict['user-agent'] = 'PersepolisDM/' + str(VERSION.version_str)
# load_cookies
if self.load_cookies:
# We need to convert raw cookies to http cookie file to use with youtube-dl.
self.youtube_dl_options_dict['cookies'] = self.load_cookies
# Proxy
if self.ip:
# ip + port
ip_port = '{}:{}'.format(self.ip, str(self.port))
if self.proxy_user:
proxy_argument = '{}://{}:{}@{}'.format(self.proxy_type, self.proxy_user, self.proxy_passwd, ip_port)
else:
proxy_argument = '{}://{}'.format(self.proxy_type, ip_port)
self.youtube_dl_options_dict['proxy'] = str(proxy_argument)
if self.download_user:
self.youtube_dl_options_dict['username'] = str(self.download_user)
self.youtube_dl_options_dict['password'] = str(self.download_passwd)
self.youtube_dl_options_dict['progress_hooks'] = [self.getStatus]
# crete yt_dlp session
self.ytdl_session = yt_dlp.YoutubeDL(self.youtube_dl_options_dict)
# This method returns data and time in string format
# for example >> 2017/09/09 , 13:12:26
def nowDate(self):
date = time.strftime("%Y/%m/%d , %H:%M:%S")
return date
def sigmaTime(self, time):
hour, minute = time.split(":")
return (int(hour) * 60 + int(minute))
# nowTime returns now time in HH:MM format!
def nowTime(self):
now_time = time.strftime("%H:%M")
return self.sigmaTime(now_time)
# this method creates sleep time,if user sets "start time" for download.
def startTime(self):
# write some messages
logger.sendToLog("Download starts at " + self.start_time + ' - GID: ' + self.gid, "DOWNLOADS")
# start_time that specified by user
sigma_start = self.sigmaTime(self.start_time)
# get current time
sigma_now = self.nowTime()
# this loop is continuing until download time arrival!
while sigma_start != sigma_now and self.download_status == 'scheduled':
time.sleep(2.1)
sigma_now = self.nowTime()
# This method will stop the download when the end_time is reached.
def endTime(self):
logger.sendToLog("End time is activated: " + self.end_time + ' - GID: ' + self.gid, "DOWNLOADS")
sigma_end = self.sigmaTime(self.end_time)
# get current time
sigma_now = self.nowTime()
# while current time is not equal to end_time, continue the loop
while sigma_end != sigma_now and (self.download_status == 'downloading' or self.download_status == 'paused'):
# get current time
sigma_now = self.nowTime()
time.sleep(2.1)
# Time is up!
if (self.download_status == 'downloading' or self.download_status == 'paused'):
logger.sendToLog("Time is up! - GID:" + self.gid, "DOWNLOADS")
# stop download
self.downloadStop()
# job is done so change end_time value to None in data_base
self.main_window.persepolis_db.setDefaultGidInAddlinkTable(self.gid, end_time=True)
# this method runs endTime in a thread.
def runEndTimeThread(self):
end_time_thread = threading.Thread(
target=self.endTime)
end_time_thread.setDaemon(True)
end_time_thread.start()
self.thread_list.append(end_time_thread)
def getStatus(self, data):
# Download stopped by user!
# raise and exception for stopping download!
if self.download_status == 'stopped':
raise Exception('Download stopped')
return
if 'filename' in data.keys():
download_path_pluse_name = data['filename']
self.file_name = Path(download_path_pluse_name).name
if 'eta' in data.keys():
if data['eta']:
self.eta = convertTime(float(data['eta']))
if 'speed' in data.keys():
if data['speed']:
download_speed, speed_unit = humanReadableSize(float(data['speed']), 'speed')
self.download_speed_str = (str(download_speed) + " " + speed_unit + "/s")
if 'downloaded_bytes' in data.keys():
if data['downloaded_bytes']:
self.downloaded_size = float(data['downloaded_bytes'])
if 'total_bytes_estimate' in data.keys():
if data['total_bytes_estimate']:
self.file_size = float(data['total_bytes_estimate'])
# some times file_size is not available
elif 'downloaded_bytes' in data.keys():
self.file_size = self.downloaded_size
try:
if 'total_bytes_estimate' in data.keys():
# Calculate download percent
self.download_percent = int((self.downloaded_size / self.file_size) * 100)
else:
self.download_percent = 0
except Exception:
pass
if ('fragment_index' in data.keys()) and ('fragment_count' in data.keys()):
try:
self.fragments = str(data['fragment_index']) + '/' + str(data['fragment_count'])
except Exception:
self.fragments = 0
if 'status' in data.keys():
# download complete
if data['status'] == 'finished':
self.download_status = 'complete'
# some times file_size is not available
self.file_size = self.downloaded_size
self.fragments = 0
self.download_percent = 100
elif data['status'] == 'downloading':
self.download_status = 'downloading'
download_info_dict = {'gid': self.gid,
'download_status': self.download_status,
'file_name': self.file_name,
'eta': self.eta,
'download_speed_str': self.download_speed_str,
'downloaded_size': self.downloaded_size,
'file_size': self.file_size,
'download_percent': self.download_percent,
'fragments': self.fragments}
# update data_base
self.main_window.persepolis_db.updateVideoFinderTable2(download_info_dict)
def tellStatus(self):
# read from data_base
download_info_dict = self.main_window.persepolis_db.searchGidInVideoFinderTable2(self.gid)
if download_info_dict is None:
download_info_dict = {'gid': self.gid,
'download_status': self.download_status,
'file_name': self.file_name,
'eta': self.eta,
'download_speed_str': self.download_speed_str,
'downloaded_size': self.downloaded_size,
'file_size': self.file_size,
'download_percent': self.download_percent,
'fragments': self.fragments,
'error_message': self.error_message}
else:
self.file_size = download_info_dict['file_size']
self.file_name = download_info_dict['file_name']
self.file_path = os.path.join(self.download_path, self.file_name)
self.download_status = download_info_dict['download_status']
self.eta = download_info_dict['eta']
self.download_speed_str = download_info_dict['download_speed_str']
self.downloaded_size = download_info_dict['downloaded_size']
self.download_percent = download_info_dict['download_percent']
self.fragments = download_info_dict['fragments']
self.error_message = download_info_dict['error_message']
downloaded_size, downloaded_size_unit = humanReadableSize(download_info_dict['downloaded_size'])
if self.file_size:
file_size, file_size_unit = humanReadableSize(self.file_size)
else:
file_size = ''
file_size_unit = ''
# return information in dictionary format
download_info = {
'gid': self.gid,
'file_name': self.file_name,
'status': self.download_status,
'size': str(file_size) + ' ' + file_size_unit,
'downloaded_size': str(downloaded_size) + ' ' + downloaded_size_unit,
'percent': str(self.download_percent) + '%',
'connections': self.fragments,
'rate': self.download_speed_str,
'estimate_time_left': self.eta,
'link': self.link,
'error': self.error_message
}
return download_info
# this method checks and manages download progress.
def checkDownloadProgress(self):
logger.sendToLog("Download starts! - GID:" + self.gid, "DOWNLOADS")
# Run this loop until the download is finished.
while (self.download_status == 'downloading'):
time.sleep(1)
# If the downloaded size is the same as the file size, then the download has been completed successfully.
if self.download_status == 'complete':
logger.sendToLog('Download complete. - GID: ' + self.gid, 'DOWNLOADS')
# If the download is not complete and the user has not stopped the download, then the download has encountered an error.
elif self.download_status != 'stopped':
self.download_status = 'error'
logger.sendToLog('Download Error - GID: ' + self.gid, 'DOWNLOAD ERROR')
elif self.download_status == 'stopped':
logger.sendToLog('Download stopped. - GID: ' + self.gid, 'DOWNLOADS')
def download(self):
try:
self.ytdl_session.download([self.link])
except Exception as e:
if self.download_status != 'stopped':
# So download didn't stop by user!
self.error_message = str(e)
self.download_status = 'error'
# update data_base
dict_ = {'gid': self.gid,
'download_status': self.download_status,
'error_message': self.error_message}
self.main_window.persepolis_db.updateVideoFinderTable2(dict_)
self.yt_dlp_exited = True
def start(self):
# Create download_path if not existed
try:
makeDirs(self.download_path)
except Exception:
pass
self.createSession()
# update status and last_try_date in data_base
if self.start_time:
self.download_status = "scheduled"
else:
self.download_status = "waiting"
# update data_base
dict_ = {'gid': self.gid,
'download_status': self.download_status}
self.main_window.persepolis_db.updateVideoFinderTable2(dict_)
# get last_try_date
now_date = self.nowDate()
# update data_base
dict_ = {'gid': self.gid, 'status': self.download_status, 'last_try_date': now_date}
self.main_window.persepolis_db.updateDownloadTable([dict_])
# call startTime if start_time is available
# startTime creates sleep loop if user set start_time
# see startTime method for more information.
if self.start_time:
self.startTime()
# now startTime work is done! update data_base
# if download stopped by user don't update data_base
if self.download_status == "scheduled":
# set start_time value to None in data_base!
self.main_window.persepolis_db.setDefaultGidInAddlinkTable(self.gid, start_time=True)
if self.download_status != 'stopped':
self.download_status = 'downloading'
# update data_base
dict_ = {'gid': self.gid,
'download_status': self.download_status}
self.main_window.persepolis_db.updateVideoFinderTable2(dict_)
# if user set end_time
if self.end_time:
self.runEndTimeThread()
# Start the download thread
download_thread = threading.Thread(target=self.download)
download_thread.setDaemon(True)
download_thread.start()
self.checkDownloadProgress()
self.close()
else:
# if start_time_status is "stopped" it means download Canceled by user
logger.sendToLog("Download Canceled", "DOWNLOADS")
def close(self):
# if download complete, so delete control file
if self.download_status == 'complete':
# if user specified download_path is equal to persepolis_setting download_path,
# then subfolder must added to download path.
if self.main_window.persepolis_setting.value('settings/download_path') == self.download_path and self.single_video_link:
# return new download_path
if self.main_window.persepolis_setting.value('settings/subfolder') == 'yes':
new_download_path = os.path.join(self.download_path, 'Videos')
file_path = self.downloadCompleteAction(new_download_path)
else:
# keep user specified download_path
file_path = self.file_path
# update download_path in addlink_db_table
add_link_dictionary = self.main_window.persepolis_db.searchGidInAddLinkTable(self.gid)
add_link_dictionary['download_path'] = file_path
self.main_window.persepolis_db.updateAddLinkTable([add_link_dictionary])
# ask threads for exiting.
for thread in self.thread_list:
thread.join()
self.write_it_to_the_database = False
logger.sendToLog("ytdlp_downloader is closed!", 'DOWNLOADS')
# remove it from download_sessions_list when download status has been written to the database.
for download_session_dict in self.main_window.download_sessions_list:
if download_session_dict['gid'] == self.gid:
# Wait until the information is written to the database.
while self.write_it_to_the_database is False:
time.sleep(0.1)
# remove item
self.main_window.download_sessions_list.remove(download_session_dict)
# remove gid from single_video_link_gid_list
if self.gid in self.main_window.single_video_link_gid_list:
self.main_window.single_video_link_gid_list.remove(self.gid)
def downloadCompleteAction(self, new_download_path):
# rename file if file already existed
self.file_name = returnNewFileName(new_download_path, self.file_name)
new_file_path = os.path.join(new_download_path, self.file_name)
# move the file to the download folder but first make sure yt-dlp has finished.
while not (self.yt_dlp_exited):
time.sleep(0.1)
move_answer = moveFile(str(self.file_path), str(new_file_path), 'file')
if not (move_answer):
# write error message in log
logger.sendToLog('Persepolis can not move file' + ' - GID: ' + self.gid, "ERROR")
new_file_path = self.file_path
return str(new_file_path)
def downloadPause(self):
pass
def downloadUnpause(self):
pass
def downloadStop(self):
# Change download status to stopped and infrom self.getStatus method
self.download_status = 'stopped'
# update data_base
dict_ = {'gid': self.gid,
'download_status': self.download_status}
self.main_window.persepolis_db.updateVideoFinderTable2(dict_)
# This method limits download speed
def limitSpeed(self, limit_value):
pass
================================================
FILE: requirements.md
================================================
## You can find requirements and installation instructions in [Persepolis Wiki](https://github.com/persepolisdm/persepolis/wiki/git-installation-instruction)
================================================
FILE: requirements.txt
================================================
PySide6_Essentials;
requests;
urllib3;
setproctitle;
psutil;
yt-dlp;
================================================
FILE: resources/PersepolisBI.py
================================================
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import logging
import json
import subprocess
from subprocess import CREATE_NO_WINDOW, DETACHED_PROCESS, CREATE_NEW_PROCESS_GROUP, CREATE_BREAKAWAY_FROM_JOB
import platform
import os
import sys
import struct
import argparse
import random
import sqlite3
from copy import deepcopy
from time import sleep
from PySide6.QtCore import QSettings
# a "PersepolisBI.exe" file must be created from this script.
# "PersepolisBI.exe" and "Persepolis Download Manager.exe" must be in same directory.
# "PersepolisBI.exe" act as intermediary between browser(FireFox, Chrome, ...) and "Persepolis Download Manager.exe"
# find operating system
os_type = platform.system()
# user home address
home_address = os.path.expanduser("~")
# persepolis config folder in M.S Windows
config_folder = os.path.join(
home_address, 'AppData', 'Local', 'persepolis_download_manager')
# create folder if it's not exist.
os.makedirs(config_folder, exist_ok=True)
# persepolis tmp folder path
persepolis_tmp = os.path.join(config_folder, 'persepolis_tmp')
# load persepolis_settings
persepolis_setting = QSettings('persepolis_download_manager', 'persepolis')
# plugins.db is store links, when browser plugins are send new links.
# This class is managing plugin.db
class PluginsDB():
def __init__(self):
# plugins.db file path
plugins_db_path = os.path.join(persepolis_tmp, 'plugins.db')
# plugins_db_connection
self.plugins_db_connection = sqlite3.connect(plugins_db_path, check_same_thread=False)
# plugins_db_cursor
self.plugins_db_cursor = self.plugins_db_connection.cursor()
# create a lock for data base
self.lock = False
# this method locks data base.
# this is pervent accessing data base simultaneously.
def lockCursor(self):
while self.lock:
rand_float = random.uniform(0, 0.5)
sleep(rand_float)
self.lock = True
# plugins_db_table contains links that sends by browser plugins.
# insert new items in plugins_db_table
def insertInPluginsTable(self, list_):
# lock data base
self.lockCursor()
for dict_ in list_:
self.plugins_db_cursor.execute("""INSERT INTO plugins_db_table VALUES(
NULL,
:link,
:referer,
:load_cookies,
:user_agent,
:header,
:out,
'new'
)""", dict_)
self.plugins_db_connection.commit()
# release lock
self.lock = False
# close connections
def closeConnections(self):
# lock data base
self.lockCursor()
self.plugins_db_cursor.close()
self.plugins_db_connection.close()
# release lock
self.lock = False
# log file address
log_file = os.path.join(str(config_folder), 'persepolisbi.log')
# create log file if it's not exist.
if not os.path.isfile(log_file):
f = open(log_file, 'w')
f.close()
# define logging object
logObj = logging.getLogger("Persepolis")
logObj.setLevel(logging.INFO)
# don't show log in console
logObj.propagate = False
# create a file handler
handler = logging.FileHandler(log_file)
handler.setLevel(logging.INFO)
# create a logging format
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
# add the handlers to the logger
logObj.addHandler(handler)
def sendToLog(text="", type="INFO"):
if type == "INFO":
logObj.info(text)
elif type == "ERROR":
logObj.error(text)
else:
logObj.warning(text)
# find exeternal application execution path
def findExternalAppPath(app_name):
# alongside of the bundle path
cwd = sys.argv[0]
current_directory = os.path.dirname(cwd)
app_alongside = os.path.join(current_directory, app_name)
# for Mac OSX and MicroSoft Windows
app_command = app_alongside
log_list = ["{}'s file is detected alongside of bundle.".format(app_name), "INFO"]
return app_command, log_list
# create terminal arguments
parser = argparse.ArgumentParser(description='PersepolisBI')
parser.add_argument('--tray', action='store_true',
help="Persepolis is starting in tray icon. It's useful when you want to put persepolis in system's startup.")
parser.add_argument('--parent-window', action='store', nargs=1,
help='this switch is used for chrome native messaging in Windows')
parser.add_argument('--version', action='version', version='PersepolisBI 1.0.0')
# Clears unwanted args ( like args from Browers via NHM )
# unknown arguments (may sent by browser) will save in unknownargs.
args, unknownargs = parser.parse_known_args()
browser_url = True
plugin_list = []
browser_plugin_dict = {'link': None,
'referer': None,
'load_cookies': None,
'user_agent': None,
'header': None,
'out': None
}
# This dirty trick will show Persepolis version when there are unknown args
# Unknown args are sent by Browsers for NHM
if args.parent_window or unknownargs:
# Platform specific configuration
if os_type == 'Windows':
# Set the default I/O mode to O_BINARY in windows
import msvcrt
msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
# Send message to browsers plugin
message = '{"enable": true, "version": "1.85"}'.encode('utf-8')
sys.stdout.buffer.write((struct.pack('i', len(message))))
sys.stdout.buffer.write(message)
sys.stdout.flush()
text_length_bytes = sys.stdin.buffer.read(4)
# Unpack message length as 4 byte integer.
text_length = struct.unpack('@I', text_length_bytes)[0]
# Read the text (JSON object) of the message.
text = sys.stdin.buffer.read(text_length).decode("utf-8")
if text:
new_dict = json.loads(text)
if 'url_links' in new_dict:
# new_dict is sended by persepolis browser add-on.
# new_dict['url_links'] contains some lists.
# every list contains link information.
for item in new_dict['url_links']:
copy_dict = deepcopy(browser_plugin_dict)
if 'url' in item.keys():
copy_dict['link'] = str(item['url'])
if 'header' in item.keys() and item['header'] != '':
copy_dict['header'] = item['header']
if 'referrer' in item.keys() and item['referrer'] != '':
copy_dict['referer'] = item['referrer']
if 'filename' in item.keys() and item['filename'] != '':
copy_dict['out'] = os.path.basename(str(item['filename']))
if 'useragent' in item.keys() and item['useragent'] != '':
copy_dict['user_agent'] = item['useragent']
if 'cookies' in item.keys() and item['cookies'] != '':
copy_dict['load_cookies'] = item['cookies']
plugin_list.append(copy_dict)
else:
browser_url = False
# when browsers plugin calls persepolis or user runs persepolis by terminal arguments,
# then persepolis creates a request file in persepolis_tmp folder and link information added to
# plugins_db.db file(see data_base.py for more information).
# persepolis mainwindow checks persepolis_tmp for plugins request file every 2 seconds (see CheckingThread class in mainwindow.py)
# when request received in CheckingThread, a popup window (AddLinkWindow) comes up and window gets additional download information
# from user (port , proxy , ...) and download starts and request file deleted
if len(plugin_list) != 0:
# create an object for PluginsDB
plugins_db = PluginsDB()
# add plugin_list to plugins_table in plugins.db file.
plugins_db.insertInPluginsTable(plugin_list)
# Job is done! close connections.
plugins_db.closeConnections()
# notify that a link is added!
plugin_ready = os.path.join(persepolis_tmp, 'persepolis-plugin-ready')
f = open(plugin_ready, 'w')
f.close()
# start persepolis in system tray if browser executed
# and if user select this option in preferences window.
if str(persepolis_setting.value('settings/browser-persepolis')) == 'yes' and (args.parent_window or unknownargs):
start_persepolis_if_browser_executed = True
else:
start_persepolis_if_browser_executed = False
# find "Persepolis Download Manager.exe" file path
app_command, log_list = findExternalAppPath('Persepolis Download Manager.exe')
sendToLog(log_list[0], log_list[1])
# call persepolis
try:
creationflags = DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_BREAKAWAY_FROM_JOB | CREATE_NO_WINDOW
if browser_url:
subprocess.Popen([app_command],
creationflags=creationflags,
shell=False)
sendToLog("Download link(s) is sended to persepolis", "INFO")
elif start_persepolis_if_browser_executed:
subprocess.Popen([app_command, '--tray'],
creationflags=creationflags,
shell=False)
sendToLog("Browser is executed and persepolis called by browser.")
except Exception as e:
sendToLog(str(e), "ERROR")
================================================
FILE: resources/dark_style.qss
================================================
/*
* BreezeDark stylesheet.
*
* :author: Colin Duquesnoy
* :editor: Alex Huszagh
* :license: MIT, see LICENSE.md
*
* This is originally a fork of QDarkStyleSheet, and is based on Breeze/
* BreezeDark color scheme, but is in no way affiliated with KDE.
*
* ---------------------------------------------------------------------
* The MIT License (MIT)
*
* Copyright (c) <2013-2014>
* Copyright (c) <2015-2021>
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
* ---------------------------------------------------------------------
*/
/**
* MAIN STYLESHEET
* ---------------
*/
QToolTip
{
/* 0.2ex is the smallest value that's not ignored on Windows. */
border: 0.04em solid #eff0f1;
background-image: none;
background-color: #31363b;
alternate-background-color: #31363b;
color: #eff0f1;
padding: 0.1em;
opacity: 200;
}
QWidget
{
color: #eff0f1;
background-color: #31363b;
selection-background-color: #b83232;
selection-color: #eff0f1;
background-clip: border;
border-image: none;
/* QDialogButtonBox icons */
dialog-cancel-icon: url(:/dark fusion/dialog_cancel.svg);
dialog-close-icon: url(:/dark fusion/dialog_close.svg);
dialog-ok-icon: url(:/dark fusion/dialog_ok.svg);
dialog-open-icon: url(:/dark fusion/dialog_open.svg);
dialog-reset-icon: url(:/dark fusion/dialog_reset.svg);
dialog-save-icon: url(:/dark fusion/dialog_save.svg);
dialog-yes-icon: url(:/dark fusion/dialog_ok.svg);
dialog-help-icon: url(:/dark fusion/dialog_help.svg);
dialog-no-icon: url(:/dark fusion/dialog_no.svg);
dialog-apply-icon: url(:/dark fusion/dialog_ok.svg);
dialog-discard-icon: url(:/dark fusion/dialog_discard.svg);
/* File icons */
filedialog-backward-icon: url(:/dark fusion/left_arrow.svg);
filedialog-contentsview-icon: url(:/dark fusion/file_dialog_contents.svg);
filedialog-detailedview-icon: url(:/dark fusion/file_dialog_detailed.svg);
filedialog-end-icon: url(:/dark fusion/file_dialog_end.svg);
filedialog-infoview-icon: url(:/dark fusion/file_dialog_info.svg);
filedialog-listview-icon: url(:/dark fusion/file_dialog_list.svg);
filedialog-new-directory-icon: url(:/dark fusion/folder.svg);
filedialog-parent-directory-icon: url(:/dark fusion/up_arrow.svg);
filedialog-start-icon: url(:/dark fusion/file_dialog_start.svg);
directory-closed-icon: url(:/dark fusion/folder.svg);
directory-icon: url(:/dark fusion/folder.svg);
directory-link-icon: url(:/dark fusion/folder_link.svg);
directory-open-icon: url(:/dark fusion/folder_open.svg);
file-icon: url(:/dark fusion/file.svg);
file-link-icon: url(:/dark fusion/file_link.svg);
home-icon: url(:/dark fusion/home_directory.svg);
/* QMessageBox icons */
messagebox-critical-icon: url(:/dark fusion/message_critical.svg);
messagebox-information-icon: url(:/dark fusion/message_information.svg);
messagebox-question-icon: url(:/dark fusion/message_question.svg);
messagebox-warning-icon: url(:/dark fusion/message_warning.svg);
/* Computer icons */
computer-icon: url(:/dark fusion/computer.svg);
desktop-icon: url(:/dark fusion/desktop.svg);
cd-icon: url(:/dark fusion/disc_drive.svg);
dvd-icon: url(:/dark fusion/disc_drive.svg);
floppy-icon: url(:/dark fusion/floppy_drive.svg);
harddisk-icon: url(:/dark fusion/hard_drive.svg);
network-icon: url(:/dark fusion/network_drive.svg);
trash-icon: url(:/dark fusion/trash.svg);
/* Arrow icons */
uparrow-icon: url(:/dark fusion/up_arrow.svg);
downarrow-icon: url(:/dark fusion/down_arrow.svg);
leftarrow-icon: url(:/dark fusion/left_arrow.svg);
rightarrow-icon: url(:/dark fusion/right_arrow.svg);
backward-icon: url(:/dark fusion/left_arrow.svg);
forward-icon: url(:/dark fusion/right_arrow.svg);
/* Titlebar icons */
titlebar-close-icon: url(:/dark fusion/window_close.svg);
titlebar-contexthelp-icon: url(:/dark fusion/help.svg);
titlebar-maximize-icon: url(:/dark fusion/maximize.svg);
titlebar-menu-icon: url(:/dark fusion/menu.svg);
titlebar-minimize-icon: url(:/dark fusion/minimize.svg);
titlebar-normal-icon: url(:/dark fusion/restore.svg);
titlebar-shade-icon: url(:/dark fusion/shade.svg);
titlebar-unshade-icon: url(:/dark fusion/unshade.svg);
/* Other icons */
dockwidget-close-icon: url(:/dark fusion/close.svg);
/**
* Only available in Qt6, and causes other issues. See #62.
* lineedit-clear-button-icon: url(:/dark fusion/clear_text.svg);
*/
}
QWidget:disabled
{
color: #454545;
background-color: #31363b;
}
QCheckBox
{
spacing: 0.23em;
outline: none;
color: #eff0f1;
margin-bottom: 0.09em;
opacity: 200;
}
QCheckBox:disabled
{
color: #b0b0b0;
}
QGroupBox
{
/* Need to make sure the groupbox doesn't compress below the title. */
min-height: 1.2em;
border: 0.04em solid #76797c;
border-radius: 0.09em;
/**
* This gives us enough space at the top to ensure we can move the
* title to be inside the guidelines, and the padding at the top
* ensures we have space below the title.
*/
margin-top: 0.5em;
padding-top: 1em;
}
QGroupBox:focus
{
border: 0.04em solid #76797c;
border-radius: 0.09em;
}
QGroupBox::title
{
/* We need to move 0.6em up to be inside the lines, +1em for padding. */
top: -1.6em;
subcontrol-origin: content;
subcontrol-position: top center;
background: #31363b;
padding-left: 0.2em;
padding-right: 0.2em;
}
QGroupBox:flat
{
border-top: 0.04em solid #626568;
border-left: 0.04em transparent #76797c;
border-right: 0.04em transparent #76797c;
border-bottom: 0.04em transparent #76797c;
}
QCheckBox::indicator,
QTreeView::indicator,
QTableWidget::indicator,
QGroupBox::indicator
{
width: 1em;
height: 1em;
}
QGroupBox::indicator:unchecked,
QGroupBox::indicator:unchecked:focus,
QCheckBox::indicator:unchecked,
QCheckBox::indicator:unchecked:focus,
QTreeView::indicator:unchecked,
QTreeView::indicator:unchecked:focus
{
border-image: url(:/dark fusion/checkbox_unchecked_disabled.svg);
}
QGroupBox::indicator:unchecked,
QCheckBox::indicator:unchecked:hover,
QCheckBox::indicator:unchecked:pressed,
QTreeView::indicator:unchecked:hover,
QTreeView::indicator:unchecked:pressed,
QGroupBox::indicator:unchecked:hover,
QGroupBox::indicator:unchecked:pressed
{
border: none;
border-image: url(:/dark fusion/checkbox_unchecked.svg);
}
QCheckBox::indicator:checked,
QTreeView::indicator:checked,
QGroupBox::indicator:checked
{
border-image: url(:/dark fusion/checkbox_checked.svg);
}
QCheckBox::indicator:checked:hover,
QCheckBox::indicator:checked:focus,
QCheckBox::indicator:checked:pressed,
QTreeView::indicator:checked:hover,
QTreeView::indicator:checked:focus,
QTreeView::indicator:checked:pressed,
QGroupBox::indicator:checked:hover,
QGroupBox::indicator:checked:focus,
QGroupBox::indicator:checked:pressed
{
border: none;
border-image: url(:/dark fusion/checkbox_checked.svg);
}
QCheckBox::indicator:indeterminate,
QTreeView::indicator:indeterminate
{
border-image: url(:/dark fusion/checkbox_indeterminate.svg);
}
QCheckBox::indicator:indeterminate:focus,
QCheckBox::indicator:indeterminate:hover,
QCheckBox::indicator:indeterminate:pressed,
QTreeView::indicator:indeterminate:focus,
QTreeView::indicator:indeterminate:hover,
QTreeView::indicator:indeterminate:pressed
{
border-image: url(:/dark fusion/checkbox_indeterminate.svg);
}
QCheckBox::indicator:indeterminate:disabled,
QTreeView::indicator:indeterminate:disabled
{
border-image: url(:/dark fusion/checkbox_indeterminate_disabled.svg);
}
QCheckBox::indicator:checked:disabled,
QTreeView::indicator:checked:disabled,
QGroupBox::indicator:checked:disabled
{
border-image: url(:/dark fusion/checkbox_checked_disabled.svg);
}
QCheckBox::indicator:unchecked:disabled,
QTreeView::indicator:unchecked:disabled,
QGroupBox::indicator:unchecked:disabled
{
border-image: url(:/dark fusion/checkbox_unchecked_disabled.svg);
}
QRadioButton
{
spacing: 0.23em;
outline: none;
color: #eff0f1;
margin-bottom: 0.09em;
}
QRadioButton:disabled
{
color: #76797c;
}
QRadioButton::indicator
{
width: 1em;
height: 1em;
}
QRadioButton::indicator:unchecked,
QRadioButton::indicator:unchecked:focus
{
border-image: url(:/dark fusion/radio_unchecked_disabled.svg);
}
QRadioButton::indicator:unchecked:hover,
QRadioButton::indicator:unchecked:pressed
{
border: none;
outline: none;
border-image: url(:/dark fusion/radio_unchecked.svg);
}
QRadioButton::indicator:checked
{
border: none;
outline: none;
border-image: url(:/dark fusion/radio_checked.svg);
}
QRadioButton::indicator:checked:hover,
QRadioButton::indicator:checked:focus,
QRadioButton::indicator:checked:pressed
{
border: none;
outline: none;
border-image: url(:/dark fusion/radio_checked.svg);
}
QRadioButton::indicator:checked:disabled
{
outline: none;
border-image: url(:/dark fusion/radio_checked_disabled.svg);
}
QRadioButton::indicator:unchecked:disabled
{
border-image: url(:/dark fusion/radio_unchecked_disabled.svg);
}
QMenuBar
{
background-color: #31363b;
color: #eff0f1;
}
QMenuBar::item
{
background: transparent;
}
QMenuBar::item:selected
{
background: transparent;
border: 0.04em solid #b83232;
}
QMenuBar::item:disabled
{
color: #76797c;
}
QMenuBar::item:pressed
{
background-color: #b83232;
color: #eff0f1;
margin-bottom: -0.09em;
padding-bottom: 0.09em;
}
QMenu
{
color: #eff0f1;
margin: 0.09em;
}
QMenu::icon
{
margin: 0.23em;
}
QMenu::item
{
/* Add extra padding on the right for the QMenu arrow */
padding: 0.23em 1.5em 0.23em 1.3em;
border: 0.09em solid transparent;
background: transparent;
}
QMenu::item:selected
{
color: #eff0f1;
background-color: #b83232;
}
QMenu::item:selected:disabled
{
background-color: #31363b;
}
QMenu::item:disabled
{
color: #76797c;
}
QMenu::indicator
{
width: 0.8em;
height: 0.8em;
/* To align with QMenu::icon, which has a 0.23em margin. */
margin-left: 0.3em;
subcontrol-position: center left;
}
QMenu::indicator:non-exclusive:unchecked
{
border-image: url(:/dark fusion/checkbox_unchecked_disabled.svg);
}
QMenu::indicator:non-exclusive:unchecked:selected
{
border-image: url(:/dark fusion/checkbox_unchecked_disabled.svg);
}
QMenu::indicator:non-exclusive:checked
{
border-image: url(:/dark fusion/checkbox_checked.svg);
}
QMenu::indicator:non-exclusive:checked:selected
{
border-image: url(:/dark fusion/checkbox_checked.svg);
}
QMenu::indicator:exclusive:unchecked
{
border-image: url(:/dark fusion/radio_unchecked_disabled.svg);
}
QMenu::indicator:exclusive:unchecked:selected
{
border-image: url(:/dark fusion/radio_unchecked_disabled.svg);
}
QMenu::indicator:exclusive:checked
{
border-image: url(:/dark fusion/radio_checked.svg);
}
QMenu::indicator:exclusive:checked:selected
{
border-image: url(:/dark fusion/radio_checked.svg);
}
QMenu::right-arrow
{
margin: 0.23em;
border-image: url(:/dark fusion/right_arrow.svg);
width: 0.5em;
height: 0.8em;
}
QMenu::right-arrow:disabled
{
border-image: url(:/dark fusion/right_arrow_disabled.svg);
}
QAbstractItemView
{
alternate-background-color: #31363b;
color: #eff0f1;
border: 0.09em solid #31363b;
border-radius: 0.09em;
}
QTabWidget:focus,
QCheckBox:focus,
QRadioButton:focus,
QSlider:focus
{
border: none;
}
QLineEdit
{
background-color: #1d2023;
padding: 0.23em;
border-style: solid;
border: 0.04em solid #76797c;
border-radius: 0.09em;
color: #eff0f1;
}
QAbstractScrollArea
{
border-radius: 0.09em;
border: 0.09em solid #76797c;
background-color: transparent;
}
/**
* This is the background for the box in the bottom-right corner
* whene both scrollbars are active.
*/
QAbstractScrollArea::corner
{
background: #31363b;
}
/**
* Can't do the KDE style of where the scrollbar handle
* becomes light on the hover, and only when the handle
* is hovered does it become stylized. This is because
* both the handle and the background events are treated
* together.
*/
QScrollBar:horizontal
{
background-color: #1d2023;
height: 0.65em;
margin: 0.13em 0.65em 0.13em 0.65em;
border: 0.04em transparent #1d2023;
border-radius: 0.17em;
}
QScrollBar:horizontal:hover
{
background-color: #76797c;
}
QScrollBar::handle:horizontal
{
background-color: #b83232;
border: 0.04em solid #b83232;
min-width: 0.5em;
border-radius: 0.17em;
}
QScrollBar::handle:horizontal:hover
{
background-color: #b83232;
border: 0.04em solid #b83232;
}
QScrollBar::add-line:horizontal
{
margin: 0em 0.13em 0em 0.13em;
border-image: url(:/dark fusion/transparent.svg);
width: 0.41em;
height: 0.41em;
subcontrol-position: right;
subcontrol-origin: margin;
}
QScrollBar::sub-line:horizontal
{
margin: 0em 0.13em 0em 0.13em;
border-image: url(:/dark fusion/transparent.svg);
width: 0.41em;
height: 0.41em;
subcontrol-position: left;
subcontrol-origin: margin;
}
QScrollBar::add-line:horizontal:hover,
QScrollBar::add-line:horizontal:on
{
border-image: url(:/dark fusion/transparent.svg);
width: 0.41em;
height: 0.41em;
subcontrol-position: right;
subcontrol-origin: margin;
}
QScrollBar::sub-line:horizontal:hover,
QScrollBar::sub-line:horizontal:on
{
border-image: url(:/dark fusion/transparent.svg);
width: 0.41em;
height: 0.41em;
subcontrol-position: left;
subcontrol-origin: margin;
}
QScrollBar::up-arrow:horizontal,
QScrollBar::down-arrow:horizontal
{
background: none;
}
QScrollBar::add-page:horizontal,
QScrollBar::sub-page:horizontal
{
background: none;
}
QScrollBar:vertical
{
background-color: #1d2023;
width: 0.65em;
margin: 0.65em 0.13em 0.65em 0.13em;
border: 0.04em transparent #1d2023;
border-radius: 0.17em;
}
QScrollBar:vertical:hover
{
background-color: #76797c;
}
QScrollBar::handle:vertical
{
background-color: #b83232;
border: 0.04em solid #b83232;
min-height: 0.5em;
border-radius: 0.17em;
}
QScrollBar::handle:vertical:hover
{
background-color: #b83232;
border: 0.04em solid #b83232;
}
QScrollBar::sub-line:vertical
{
margin: 0.13em 0em 0.13em 0em;
border-image: url(:/dark fusion/transparent.svg);
height: 0.41em;
width: 0.41em;
subcontrol-position: top;
subcontrol-origin: margin;
}
QScrollBar::add-line:vertical
{
margin: 0.13em 0em 0.13em 0em;
border-image: url(:/dark fusion/transparent.svg);
height: 0.41em;
width: 0.41em;
subcontrol-position: bottom;
subcontrol-origin: margin;
}
QScrollBar::sub-line:vertical:hover,
QScrollBar::sub-line:vertical:on
{
border-image: url(:/dark fusion/transparent.svg);
height: 0.41em;
width: 0.41em;
subcontrol-position: top;
subcontrol-origin: margin;
}
QScrollBar::add-line:vertical:hover,
QScrollBar::add-line:vertical:on
{
border-image: url(:/dark fusion/transparent.svg);
height: 0.41em;
width: 0.41em;
subcontrol-position: bottom;
subcontrol-origin: margin;
}
QScrollBar::up-arrow:vertical,
QScrollBar::down-arrow:vertical
{
background: none;
}
QScrollBar::add-page:vertical,
QScrollBar::sub-page:vertical
{
background: none;
}
QTextEdit
{
background-color: #1d2023;
color: #eff0f1;
border: 0.04em solid #76797c;
}
QPlainTextEdit
{
background-color: #1d2023;
color: #eff0f1;
border-radius: 0.09em;
border: 0.04em solid #76797c;
}
QSizeGrip
{
border-image: url(:/dark fusion/sizegrip.svg);
width: 0.5em;
height: 0.5em;
}
/**
* Set the separator to be transparent, since the dock has a border.
* On PyQt6, neither the border nor the background seem to be respected.
*/
QMainWindow::separator
{
border: 0.09em transparent #76797c;
background: transparent;
}
QMenu::separator
{
height: 0.09em;
background-color: #76797c;
padding-left: 0.2em;
margin-top: 0.2em;
margin-bottom: 0.2em;
margin-left: 0.41em;
margin-right: 0.41em;
}
QFrame[frameShape="2"], /* QFrame::Panel == 0x0003 */
QFrame[frameShape="3"], /* QFrame::WinPanel == 0x0003 */
QFrame[frameShape="4"], /* QFrame::HLine == 0x0004 */
QFrame[frameShape="5"], /* QFrame::VLine == 0x0005 */
QFrame[frameShape="6"] /* QFrame::StyledPanel == 0x0006 */
{
border-width: 0.04em;
padding: 0.09em;
border-style: solid;
border-color: #31363b;
background-color: #76797c;
border-radius: 0.23em;
}
/* Provide highlighting for frame objects. */
QFrame[frameShape="2"]:hover,
QFrame[frameShape="3"]:hover,
QFrame[frameShape="4"]:hover,
QFrame[frameShape="5"]:hover,
QFrame[frameShape="6"]:hover
{
border: 0.04em solid #b83232;
}
/* Don't provide an outline if we have a widget that takes up the space. */
QFrame[frameShape] QAbstractItemView:hover
{
border: 0em solid black;
}
/**
* Note: I can't really change the background of the toolbars
* independently, since KDE Breeze has different colors for the
* window bar and the rest of the UI. The top toolbar uses
* the window style, and the rest use the application style,
* which we can't do.
*/
QToolBar
{
font-weight: bold;
}
QToolBar:horizontal
{
background: 0.09em solid #31363b;
}
QToolBar:vertical
{
background: 0.09em solid #31363b;
}
QToolBar::handle:horizontal
{
border-image: url(:/dark fusion/hmovetoolbar.svg);
}
QToolBar::handle:vertical
{
border-image: url(:/dark fusion/vmovetoolbar.svg);
}
QToolBar::separator:horizontal
{
border-image: url(:/dark fusion/hseptoolbar.svg);
}
QToolBar::separator:vertical
{
border-image: url(:/dark fusion/vseptoolbar.svg);
}
QToolBar QToolButton
{
font-weight: bold;
border: 0.04em transparent black;
padding-left: 0.2em;
padding-right: 0.3em;
}
QToolBar QToolButton:hover
{
border: 0.04em solid #b83232;
}
QToolBar QToolButton:pressed
{
border: 0.04em solid #b83232;
/* The padding doesn't inherit from `QToolBar QToolButton`, so leave it in. */
padding-left: 0.2em;
padding-right: 0.3em;
}
/**
* Special rules for a QFileDialog.
*
* Due to the widgets, we get rid of the min sizes to allow them
* to pack closer together, and ensure we have enough padding for
* the drop-down menu in the popup.
*/
QDialog QToolBar QToolButton[popupMode="0"],
QDialog QToolBar QToolButton[popupMode="1"]
{
padding-left: 0.1em;
padding-right: 0.1em;
}
QDialog QToolBar QToolButton[popupMode="2"]
{
padding-left: 0.1em;
padding-right: 0.7em;
}
QPushButton
{
color: #eff0f1;
background-color: #31363b;
border: 0.04em solid #76797c;
padding: 0.23em;
border-radius: 0.09em;
outline: none;
min-height: 1.1em;
}
QPushButton:flat,
QPushButton:flat:hover
{
border: 0.04em transparent #76797c;
}
QComboBox:open,
QPushButton:open
{
border-width: 0.04em;
border-color: #76797c;
}
QComboBox:closed,
QPushButton:closed
{
border-width: 0.04em;
border-color: #76797c;
}
QPushButton:disabled
{
background-color: #31363b;
border-width: 0.04em;
border-color: #76797c;
border-style: solid;
padding-top: 0.23em;
padding-bottom: 0.23em;
padding-left: 1ex;
padding-right: 1ex;
border-radius: 0.04em;
color: #454545;
}
QPushButton:focus
{
color: #eff0f1;
}
QPushButton:pressed
{
background-color: #454a4f;
padding-top: -0.65em;
padding-bottom: -0.74em;
color: #eff0f1;
}
QComboBox
{
border: 0.04em solid #76797c;
border-radius: 0.09em;
padding: 0.23em;
min-width: 2.5em;
}
QComboBox:editable
{
background-color: #1d2023;
}
QPushButton:checked
{
background-color: #626568;
border: 0.04em solid #76797c;
color: #eff0f1;
}
QPushButton:hover
{
background-color: #31363b;
border: 0.04em solid #b83232;
color: #eff0f1;
}
QPushButton:checked:hover
{
background-color: #626568;
border: 0.04em solid #b83232;
color: #eff0f1;
}
QComboBox:hover,
QComboBox:focus,
QAbstractSpinBox:hover,
QAbstractSpinBox:focus,
QLineEdit:hover,
QLineEdit:focus,
QTextEdit:hover,
QTextEdit:focus,
QPlainTextEdit:hover,
QPlainTextEdit:focus,
QAbstractView:hover,
QTreeView:hover,
QTreeView:focus
{
border: 0.04em solid #b83232;
color: #eff0f1;
}
QComboBox:hover:pressed:!editable,
QPushButton:hover:pressed,
QAbstractSpinBox:hover:pressed,
QLineEdit:hover:pressed,
QTextEdit:hover:pressed,
QPlainTextEdit:hover:pressed,
QAbstractView:hover:pressed,
QTreeView:hover:pressed
{
background-color: #31363b;
}
QColumnView
{
border: 0.04em transparent #31363b;
}
QColumnViewGrip
{
border-image: url(:/dark fusion/sizegrip.svg);
}
/* Each column in the view is a QAbstractItemView. */
QColumnView QAbstractItemView
{
border: 0.04em transparent #b83232;
}
/**
* In order to set consistency between Qt5 and Qt6, we need
* to ensure that we do the following steps:
* 1. Set a consistent `max-height` in the item. Anything
* below `0.8em` will cause clipping, so set that value
* to ensure the icon isn't larger.
* 2. Set padding to ensure the item is properly padded.
* 3. Set `0.2em` margins on the top and bottom of the arrows,
* and `0.1em` on the left and right to ensure the arrows
* are properly padded and have the same size.
*
* The size consistency only works if both the `::item` subcontrol
* `max-height` and the `::*-arrow` subcontrol `margin` is set.
*/
QColumnView QAbstractItemView::item
{
padding: 0.23em;
max-width: 0.5em;
max-height: 0.8em;
}
QColumnView QAbstractItemView::right-arrow
{
image: url(:/dark fusion/right_arrow.svg);
margin: 0.2em 0.1em 0.2em 0.1em;
}
QColumnView QAbstractItemView::right-arrow:selected,
QColumnView QAbstractItemView::right-arrow:hover
{
image: url(:/dark fusion/right_arrow_hover.svg);
}
QColumnView QAbstractItemView::left-arrow
{
image: url(:/dark fusion/left_arrow.svg);
margin: 0.2em 0.1em 0.2em 0.1em;
}
QColumnView QAbstractItemView::left-arrow:selected,
QColumnView QAbstractItemView::left-arrow:hover
{
image: url(:/dark fusion/left_arrow_hover.svg);
}
QComboBox:hover:pressed:editable
{
background-color: #1d2023;
}
QComboBox QAbstractItemView
{
/* This happens for the drop-down menu always, whether editable or not.*/
background-color: #1d2023;
selection-background-color: #972b2b;
outline-color: 0em;
border-radius: 0.09em;
}
QComboBox::drop-down
{
subcontrol-origin: padding;
subcontrol-position: top right;
width: 0.65em;
border-left-width: 0em;
border-left-style: solid;
border-top-right-radius: 0.13em;
border-bottom-right-radius: 0.13em;
}
QComboBox::down-arrow
{
border-image: url(:/dark fusion/down_arrow_disabled.svg);
width: 0.8em;
height: 0.5em;
margin-right: 0.41em;
}
QComboBox::down-arrow:on,
QComboBox::down-arrow:hover,
QComboBox::down-arrow:focus
{
border-image: url(:/dark fusion/down_arrow.svg);
width: 0.8em;
height: 0.5em;
margin-right: 0.41em;
}
QAbstractSpinBox
{
padding: 0.23em;
border: 0.09em solid #76797c;
background-color: #1d2023;
color: #eff0f1;
border-radius: 0.09em;
min-width: 3em;
min-height: 1em;
}
QAbstractSpinBox:hover
{
border: 0.09em solid #b83232;
}
QAbstractSpinBox:up-button,
QAbstractSpinBox:up-button:hover
{
background-color: transparent;
subcontrol-origin: padding;
subcontrol-position: center right;
padding-right: 0.1em;
width: 0.8em;
height: 0.5em;
}
QAbstractSpinBox:down-button,
QAbstractSpinBox:down-button:hover
{
background-color: transparent;
subcontrol-origin: padding;
subcontrol-position: center left;
padding-left: 0.1em;
width: 0.8em;
height: 0.5em;
}
/**
* Bug fixes for elongated items in QSpinBoxes.
* By default, the items are bounded by `down-button`
* and `up-button`, so this doesn't actually affect the styling.
*
* This does however affect some custom styling using
* QStyle.CC_ComboBox, which affects QDateEdit. This cannot
* be selected using QDateEdit, since it uses a global style.
* This sounds nonsensical, because CC_ComboBox isn't a spin box,
* but through trial and error, this is in fact the case.
*
* Affects #40.
*/
QAbstractSpinBox::up-arrow,
QAbstractSpinBox::up-arrow:disabled,
QAbstractSpinBox::up-arrow:off,
QAbstractSpinBox::up-arrow:!off:!disabled:hover,
QAbstractSpinBox::down-arrow,
QAbstractSpinBox::down-arrow:disabled,
QAbstractSpinBox::down-arrow:off,
QAbstractSpinBox::down-arrow:!off:!disabled:hover
{
border-image: none;
width: 0.8em;
height: 0.5em;
}
QAbstractSpinBox::up-arrow
{
image: url(:/dark fusion/up_arrow.svg);
}
QAbstractSpinBox::up-arrow:disabled,
QAbstractSpinBox::up-arrow:off
{
image: url(:/dark fusion/up_arrow_disabled.svg);
}
QAbstractSpinBox::up-arrow:hover
{
image: url(:/dark fusion/up_arrow_hover.svg);
}
QAbstractSpinBox::down-arrow
{
image: url(:/dark fusion/down_arrow.svg);
}
QAbstractSpinBox::down-arrow:disabled,
QAbstractSpinBox::down-arrow:off
{
image: url(:/dark fusion/down_arrow_disabled.svg);
}
QAbstractSpinBox::down-arrow:!off:!disabled:hover
{
image: url(:/dark fusion/down_arrow_hover.svg);
}
QDoubleSpinBox
{
min-width: 4em;
}
/**
* `QCalendarWidget QAbstractItemView:enabled` sets the color, background
* color, and selection color for active dates in the view.
* `QCalendarWidget QAbstractItemView:enabled` sets the disabled dates.
*/
QCalendarWidget QAbstractItemView:enabled
{
color: #eff0f1;
selection-color: #eff0f1;
selection-background-color: #b83232;
}
/* Won't take hover events. */
QPrevNextCalButton
{
min-width: 0.8em;
min-height: 1.2em;
qproperty-iconSize: 0px 0px;
}
QPrevNextCalButton#qt_calendar_nextmonth
{
image: url(:/dark fusion/calendar_next.svg);
}
QPrevNextCalButton#qt_calendar_prevmonth
{
image: url(:/dark fusion/calendar_previous.svg);
}
/**
* Setting for the month and year displays and drop-down menu for the
* month select. We style this separately because we want a drop-down
* indicator in the bottom right, unlike the normal QToolButton.
*/
QCalendarWidget QToolButton
{
background-color: transparent;
border: 0.04em solid #76797c;
border-radius: 0.09em;
margin: 0.23em;
padding: 0.23em;
padding-top: 0.1em;
padding-right: 1.2em;
min-height: 1.1em;
}
QCalendarWidget QToolButton:hover
{
border: 0.04em solid #b83232;
}
QCalendarWidget QToolButton:checked,
QCalendarWidget QToolButton:pressed
{
background-color: #b83232;
padding: 0.23em;
padding-right: 1.2em;
min-height: 1.3em;
outline: none;
}
/**
* The QCalendarWidget for QDateTimeEdit seems to improperly
* style the year QToolButton, which has an object name
* `qt_datetimedit_calendar`, so ensure we style it as well.
*/
QCalendarWidget QToolButton::menu-indicator,
#qt_datetimedit_calendar QCalendarWidget QToolButton::menu-indicator
{
border-image: none;
image: url(:/dark fusion/down_arrow.svg);
width: 0.8em;
height: 0.5em;
top: -0.7ex;
left: -0.09em;
padding-right: -1.11em;
subcontrol-origin: content;
subcontrol-position: bottom right;
}
QCalendarWidget QToolButton::menu-arrow,
#qt_datetimedit_calendar QCalendarWidget QToolButton::menu-arrow
{
border-image: none;
image: url(:/dark fusion/down_arrow.svg);
width: 0.8em;
height: 0.5em;
padding-right: 0.09em;
subcontrol-origin: content;
subcontrol-position: bottom right;
}
/**
* Setting for the year button. Both the month select and the year
* select are QToolButtons, and both are auto-raised. The year
* button however has the popup mode set to `DelayedPopup`.
*/
QCalendarWidget QToolButton[autoRaise="true"][popupMode="0"]
{
padding: 0.23em;
}
QCalendarWidget QSpinBox
{
max-height: 1.5em;
min-width: 3.5em;
margin: 0em;
margin-top: 0.2em;
padding: 0em;
outline: 0em;
padding-left: 0.5em;
}
QLabel
{
border: 0em solid black;
}
/* BORDERS */
QTabWidget::pane
{
padding: 0.23em;
margin: 0.04em;
}
QTabWidget::pane:top
{
border: 0.04em solid #76797c;
top: -0.04em;
}
QTabWidget::pane:bottom
{
border: 0.04em solid #76797c;
bottom: -0.04em;
}
QTabWidget::pane:left
{
border: 0.04em solid #76797c;
left: -0.04em;
}
QTabWidget::pane:right
{
border: 0.04em solid #76797c;
right: -0.04em;
}
QTabBar
{
qproperty-drawBase: 0;
left: 0.23em;
border-radius: 0.13em;
/**
* Note: this is the underline for each tab title. It's not
* documented, and this took forever to track down. At least
* 10 hours have been wasted trying to turn off this line,
* do not deleted this comment.
*/
selection-color: transparent;
}
QTabBar:focus
{
border: 0em transparent black;
}
QTabBar::close-button
{
/* Doesn't seem possible to resize these buttons */
border-image: url(:/dark fusion/transparent.svg);
image: url(:/dark fusion/close.svg);
background: transparent;
}
QTabBar::close-button:hover
{
image: url(:/dark fusion/close_hover.svg);
}
QTabBar::close-button:pressed
{
image: url(:/dark fusion/close_pressed.svg);
}
/* TOP TABS */
QTabBar::tab:top,
QTabBar::tab:top:last,
QTabBar::tab:top:only-one
{
color: #eff0f1;
border: 0.04em transparent black;
border-left: 0.04em solid #76797c;
border-right: 0.04em solid #76797c;
border-top: 0.09em solid #b83232;
background-color: #31363b;
padding: 0.23em;
min-width: 50px;
border-radius: 0.09em;
border-bottom-left-radius: 0em;
border-bottom-right-radius: 0em;
}
QTabBar::tab:top:first,
QTabBar::tab:top:only-one
{
margin-left: 0.04em;
}
QTabBar::tab:top:last,
QTabBar::tab:top:only-one
{
margin-right: 0.04em;
}
QTabBar::tab:top:!selected
{
color: #eff0f1;
background-color: #2c3034;
border: 0.04em transparent black;
border-right: 0.04em solid #76797c;
border-bottom: 0.04em solid #76797c;
border-radius: 0.09em;
border-bottom-left-radius: 0em;
border-bottom-right-radius: 0em;
}
QTabBar::tab:top:next-selected
{
border-right: 0.04em transparent #2c3034;
border-bottom-left-radius: 0em;
border-bottom-right-radius: 0em;
}
QTabBar::tab:top:!selected:hover
{
background-color: rgba(255, 0, 20, 0.1);
border-radius: 0.09em;
border-bottom-left-radius: 0em;
border-bottom-right-radius: 0em;
}
QTabBar::tab:top:!selected:first:hover
{
background-color: rgba(255, 0, 20, 0.1);
border-radius: 0.09em;
border-bottom-left-radius: 0em;
border-bottom-right-radius: 0em;
}
/* BOTTOM TABS */
QTabBar::tab:bottom,
QTabBar::tab:bottom:last,
QTabBar::tab:bottom:only-one
{
color: #eff0f1;
border: 0.04em transparent black;
border-left: 0.04em solid #76797c;
border-right: 0.04em solid #76797c;
border-bottom: 0.09em solid #b83232;
background-color: #31363b;
padding: 0.23em;
min-width: 50px;
border-radius: 0.09em;
border-top-left-radius: 0em;
border-top-right-radius: 0em;
}
QTabBar::tab:bottom:first,
QTabBar::tab:bottom:only-one
{
margin-left: 0.08em;
}
QTabBar::tab:bottom:last,
QTabBar::tab:bottom:only-one
{
margin-right: 0.08em;
}
QTabBar::tab:bottom:!selected
{
color: #eff0f1;
background-color: #2c3034;
border: 0.04em transparent black;
border-top: 0.04em solid #76797c;
border-right: 0.04em solid #76797c;
border-radius: 0.09em;
border-top-left-radius: 0em;
border-top-right-radius: 0em;
}
QTabBar::tab:bottom:next-selected
{
border-right: 0.04em transparent #2c3034;
border-top-left-radius: 0em;
border-top-right-radius: 0em;
}
QTabBar::tab:bottom:!selected:hover
{
background-color: rgba(255, 0, 20, 0.1);
border-radius: 0.09em;
border-top-left-radius: 0em;
border-top-right-radius: 0em;
}
QTabBar::tab:bottom:!selected:first:hover
{
background-color: rgba(255, 0, 20, 0.1);
border-radius: 0.09em;
border-top-left-radius: 0em;
border-top-right-radius: 0em;
}
/* LEFT TABS */
QTabBar::tab:left,
QTabBar::tab:left:last,
QTabBar::tab:left:only-one
{
color: #eff0f1;
border: 0.04em transparent black;
border-top: 0.09em solid #b83232;
border-bottom: 0.04em solid #76797c;
border-left: 0.04em solid #76797c;
background-color: #31363b;
padding: 0.23em;
min-height: 50px;
border-radius: 0.09em;
border-top-right-radius: 0em;
border-bottom-right-radius: 0em;
}
QTabBar::tab:left:first,
QTabBar::tab:left:only-one
{
margin-top: 0.08em;
}
QTabBar::tab:left:last,
QTabBar::tab:left:only-one
{
margin-bottom: 0.08em;
}
QTabBar::tab:left:!selected
{
color: #eff0f1;
background-color: #2c3034;
border: 0.04em transparent black;
border-top: 0.04em solid #76797c;
border-right: 0.04em solid #76797c;
border-radius: 0.09em;
border-top-right-radius: 0em;
border-bottom-right-radius: 0em;
}
QTabBar::tab:left:previous-selected
{
border-top: 0.04em transparent #2c3034;
border-top-right-radius: 0em;
border-bottom-right-radius: 0em;
}
QTabBar::tab:left:!selected:hover
{
background-color: rgba(255, 0, 20, 0.1);
border-radius: 0.09em;
border-top-right-radius: 0em;
border-bottom-right-radius: 0em;
}
QTabBar::tab:left:!selected:first:hover
{
background-color: rgba(255, 0, 20, 0.1);
border-radius: 0.09em;
border-top-right-radius: 0em;
border-bottom-right-radius: 0em;
}
/* RIGHT TABS */
QTabBar::tab:right,
QTabBar::tab:right:last,
QTabBar::tab:right:only-one
{
color: #eff0f1;
border: 0.04em transparent black;
border-top: 0.09em solid #b83232;
border-bottom: 0.04em solid #76797c;
border-right: 0.04em solid #76797c;
background-color: #31363b;
padding: 0.23em;
min-height: 50px;
border-radius: 0.09em;
border-top-left-radius: 0em;
border-bottom-left-radius: 0em;
}
QTabBar::tab:right:first,
QTabBar::tab:right:only-one
{
margin-top: 0.08em;
}
QTabBar::tab:right:last,
QTabBar::tab:right:only-one
{
margin-bottom: 0.08em;
}
QTabBar::tab:right:!selected
{
color: #eff0f1;
background-color: #2c3034;
border: 0.04em transparent black;
border-top: 0.04em solid #76797c;
border-left: 0.04em solid #76797c;
border-radius: 0.09em;
border-top-left-radius: 0em;
border-bottom-left-radius: 0em;
}
QTabBar::tab:right:previous-selected
{
border-top: 0.04em transparent #2c3034;
border-top-left-radius: 0em;
border-bottom-left-radius: 0em;
}
QTabBar::tab:right:!selected:hover
{
background-color: rgba(255, 0, 20, 0.1);
border-radius: 0.09em;
border-top-left-radius: 0em;
border-bottom-left-radius: 0em;
}
QTabBar::tab:right:!selected:first:hover
{
background-color: rgba(255, 0, 20, 0.1);
border-radius: 0.09em;
border-top-left-radius: 0em;
border-bottom-left-radius: 0em;
}
/**
* Special styles for triangular QTabWidgets.
* These ignore the border attributes, and the border and
* text color seems to be set via the `QTabBar::tab` color
* property. This seemingly cannot be changed.
*
* The rounded shapes are 0-3, and the triangular ones are 4-7.
*
* The QTabBar outline doesn't respect on QTabBar::tab:
* border-color
* outline-color
*/
QTabBar[shape="4"]::tab,
QTabBar[shape="5"]::tab,
QTabBar[shape="6"]::tab,
QTabBar[shape="7"]::tab,
QTabBar[shape="4"]::tab:last,
QTabBar[shape="5"]::tab:last,
QTabBar[shape="6"]::tab:last,
QTabBar[shape="7"]::tab:last,
QTabBar[shape="4"]::tab:only-one,
QTabBar[shape="5"]::tab:only-one,
QTabBar[shape="6"]::tab:only-one,
QTabBar[shape="7"]::tab:only-one
{
/* Need a dark color without alpha channel since it affects the text. */
color: #b83232;
background-color: #31363b;
padding: 0.23em;
}
QTabBar[shape="4"]::tab,
QTabBar[shape="5"]::tab,
QTabBar[shape="4"]::tab:last,
QTabBar[shape="5"]::tab:last,
QTabBar[shape="4"]::tab:only-one,
QTabBar[shape="5"]::tab:only-one
{
min-width: 50px;
}
QTabBar[shape="6"]::tab,
QTabBar[shape="7"]::tab,
QTabBar[shape="6"]::tab:last,
QTabBar[shape="7"]::tab:last,
QTabBar[shape="6"]::tab:only-one,
QTabBar[shape="7"]::tab:only-one
{
min-height: 50px;
}
QTabBar[shape="4"]::tab:!selected,
QTabBar[shape="5"]::tab:!selected,
QTabBar[shape="6"]::tab:!selected,
QTabBar[shape="7"]::tab:!selected
{
color: #eff0f1;
background-color: #2c3034;
}
/**
* Increase padding on the opposite side of the icon to avoid text clipping.
*
* BUG: The padding works for North, West, and East in Qt5, South does not
* work. All tab positions work for triangular tabs in Qt6.
*/
QTabBar[shape="4"][tabsClosable="true"]::tab,
QTabBar[shape="5"][tabsClosable="true"]::tab
{
padding-left: 0.5em;
}
QTabBar[shape="6"][tabsClosable="true"]::tab
{
padding-bottom: 0.5em;
}
QTabBar[shape="7"][tabsClosable="true"]::tab
{
padding-top: 0.5em;
}
/**
* Undo the padding for the tab.
*
* Enumerated values are North, South, West, East in that order,
* from 4-7.
*
* NOTE: Any higher padding will clip the icon.
*/
QTabBar[shape="4"]::close-button,
QTabBar[shape="5"]::close-button
{
padding-left: -0.12em;
}
QTabBar[shape="6"]::close-button
{
padding-bottom: -0.18em;
}
QTabBar[shape="7"]::close-button
{
padding-top: -0.18em;
}
QDockWidget
{
background: #31363b;
/**
* It doesn't seem possible to change the border of the
* QDockWidget without changing the content margins.
*/
/**
* This is a bug fix so we can handle hover, pressed, and other events.
* Reference: https://stackoverflow.com/questions/32145080/qdockwidget-float-close-button-hover-images
*/
titlebar-close-icon: url(:/dark fusion/transparent.svg);
titlebar-normal-icon: url(:/dark fusion/transparent.svg);
}
/**
* Don't style the title, since it gives a weird, missing border
* around the rest of the dock widget, which the remaining border
* cannot be removed.
*
* There is a bug in non-Breeze styles, where the icons are small. It
* doesn't change if we use `image` instead of `border-image`, nor if
* we use `qproperty-icon`, etc. The icon seem to be half the size
* of our desired values.
*/
QDockWidget::close-button,
QDockWidget::float-button
{
border: 0.04em solid transparent;
border-radius: 0.09em;
background: transparent;
/* Maximum icon size for buttons */
icon-size: 14px;
}
QDockWidget::float-button
{
border-image: url(:/dark fusion/transparent.svg);
image: url(:/dark fusion/undock.svg);
}
QDockWidget::float-button:hover
{
image: url(:/dark fusion/undock_hover.svg);
}
/* The :pressed events don't register, seems to be a Qt bug. */
QDockWidget::float-button:pressed
{
image: url(:/dark fusion/undock_hover.svg);
}
QDockWidget::close-button
{
border-image: url(:/dark fusion/transparent.svg);
image: url(:/dark fusion/close.svg);
}
QDockWidget::close-button:hover
{
image: url(:/dark fusion/close_hover.svg);
}
/* The :pressed events don't register, seems to be a Qt bug. */
QDockWidget::close-button:pressed
{
image: url(:/dark fusion/close_pressed.svg);
}
QTreeView,
QListView
{
background-color: #1d2023;
border: 0em solid black;
}
QTreeView:selected,
QTreeView:!selected,
QListView:selected,
QListView:!selected
{
border: 0em solid black;
}
QTreeView::branch:has-siblings
{
border-image: url(:/dark fusion/vline.svg);
image: none;
}
/* These branch indicators don't scale */
QTreeView::branch:!has-siblings
{
border-image: none;
image: none;
}
QTreeView::branch:has-siblings:adjoins-item
{
border-image: url(:/dark fusion/branch_more.svg);
}
QTreeView::branch:!has-children:!has-siblings:adjoins-item
{
border-image: url(:/dark fusion/branch_end.svg);
}
QTreeView::branch:has-children:!has-siblings:closed,
QTreeView::branch:closed:has-children:has-siblings
{
image: url(:/dark fusion/branch_closed.svg);
}
QTreeView::branch:has-children:!has-siblings:closed:hover,
QTreeView::branch:closed:has-children:has-siblings:hover
{
image: url(:/dark fusion/branch_closed_hover.svg);
}
QTreeView::branch:has-children:!has-siblings:closed,
QTreeView::branch:open:has-children:!has-siblings
{
border-image: url(:/dark fusion/branch_end_arrow.svg);
}
QTreeView::branch:closed:has-children:has-siblings,
QTreeView::branch:open:has-children:has-siblings
{
border-image: url(:/dark fusion/branch_more_arrow.svg);
}
QTreeView::branch:open:has-children:!has-siblings,
QTreeView::branch:open:has-children:has-siblings
{
image: url(:/dark fusion/branch_open.svg);
}
QTreeView::branch:open:has-children:!has-siblings:hover,
QTreeView::branch:open:has-children:has-siblings:hover
{
image: url(:/dark fusion/branch_open_hover.svg);
}
QListView
{
/* Give space for elements aligned left or right. */
padding: 0.2em;
}
QTableView::item,
QListView::item,
QTreeView::item
{
padding: 0.13em;
color: #eff0f1;
}
QTreeView::item
{
/**
* Need to set the background color in Qt6, or else
* the QTreeView indicators use the style defaults,
* along with the box model, which conflicts with our
* theme (except with hover/focus/selected pseudostates).
*
* Affects issue #51.
*/
background-color: #1d2023;
outline: 0;
}
QTableView::item:!selected:hover,
QListView::item:!selected:hover,
QTreeView::item:!selected:hover
{
background-color: rgba(255, 0, 20, 0.1);
outline: 0;
color: #eff0f1;
padding: 0.13em;
}
QAbstractItemView::item QLineEdit
{
border: 0em transparent black;
/*
* The top/bottom padding causes the editable widget to conceal text.
* https://github.com/Alexhuszagh/BreezeStyleSheets/issues/69
*/
padding: 0em;
}
QSlider::handle:horizontal,
QSlider::handle:vertical
{
background: #1d2023;
border: 0.04em solid #626568;
width: 0.7em;
height: 0.7em;
border-radius: 0.35em;
}
QSlider:horizontal
{
height: 2em;
}
QSlider:vertical
{
width: 2em;
}
QSlider::handle:horizontal
{
margin: -0.23em 0;
}
QSlider::handle:vertical
{
margin: 0 -0.23em;
}
QSlider::groove:horizontal,
QSlider::groove:vertical
{
background: #2c3034;
border: 0em solid #31363b;
border-radius: 0.19em;
}
QSlider::groove:horizontal
{
height: 0.4em;
}
QSlider::groove:vertical
{
width: 0.4em;
}
QSlider::handle:horizontal:hover,
QSlider::handle:horizontal:focus,
QSlider::handle:vertical:hover,
QSlider::handle:vertical:focus
{
border: 0.04em solid #b83232;
}
QSlider::handle:horizontal:!focus:!hover,
QSlider::handle:vertical:!focus:!hover
{
border: 0.04em solid #626568;
}
QSlider::sub-page:horizontal,
QSlider::add-page:vertical
{
background: #b83232;
border-radius: 0.19em;
}
QSlider::add-page:horizontal,
QSlider::sub-page:vertical
{
background: #626568;
border-radius: 0.19em;
}
/* QToolButton */
/**
* QToolButton's that have a push button need to be styled differently,
* depending on whether there are actions (a menu) associated with it.
* This is signaled by a drop-down arrow on the right of the push button.
* Unfortunately, there's no good property to determine this. The property
* we need is `QWidget::actions`, however, it's a method and not a
* property.Note that the drop-down menu is not signaled by any of the
* following:
* popupMode: any pop-up mode does not affect the right arrow style.
* arrowType: only replaces the icon.
* toolButtonStyle: this is almost always set to icon only, even with text.
* text: can have a drop-down menu with or without text.
*
* Notably, we need to ensure we don't pad the widgets in the following
* cases:
* 1. If the QToolButton is auto-raised.
* This adds undesired padding in`QFileDialog`. These widgets
* have text, even though no text is visible. This is not the
* default, so it won't affect most situations.
* 2. If the QToolButton does not have text.
* Normally, text-less buttons do not have a menu, and this
* is required for #47, since the padding affects the scroll
* bar icons in QTabBar. This causes major issues in the
* UI, so disable the padding by default.
*
* The padding can affect the placement of icons and other things
* inside the toolbutton: near the menu-button in QFileDialog,
* the clear text icon is misplaced vertically, making it nearly
* illegible.
*
* We provide special styles for a custom, dynamic property to
* override the padding decisions with or without a menu.
* To force styling as if there is a menu, set the `hasMenu` property
* to true. Setting `hasMenu` to false will style as if there is no menu.
* You can use `QWidget::setProperty` to set this property dynamically.
*
* The affected issues are #22, #28, #47.
* https://github.com/Alexhuszagh/BreezeStyleSheets/issues/22
* https://github.com/Alexhuszagh/BreezeStyleSheets/issues/28
* https://github.com/Alexhuszagh/BreezeStyleSheets/issues/47
*/
/**
* Use an overly specific selector here to ensure no margins,
* or for the default QToolButton. We must have `autoRaise="false"`
* and `text` to have padding, so just add a `hasMenu="false"` to
* undo the padding in that case. Also add selectors for QDialog
* if a menu is explicitly forbidden.
*/
QToolButton,
QToolButton[hasMenu="false"][autoRaise="false"][text],
QDialog QToolBar QToolButton[hasMenu="false"][popupMode="0"],
QDialog QToolBar QToolButton[hasMenu="false"][popupMode="1"],
QDialog QToolBar QToolButton[hasMenu="false"][popupMode="2"]
{
margin: 0em;
padding: 0em;
}
QToolButton[autoRaise="false"]
{
background-color: #31363b;
border: 0.04em solid #76797c;
border-radius: 0.09em;
}
QToolButton[autoRaise="true"]
{
background-color: #31363b;
border: 0.04em solid transparent;
}
/* Add selectors for the QDialog if a menu is explicitly requested. */
QToolButton[hasMenu="true"],
QToolButton[autoRaise="false"][text],
QDialog QToolBar QToolButton[hasMenu="true"][popupMode="0"],
QDialog QToolBar QToolButton[hasMenu="true"][popupMode="1"],
QDialog QToolBar QToolButton[hasMenu="true"][popupMode="2"]
{
margin: 0.23em;
padding: 0.23em;
padding-top: 0.1em;
padding-right: 1.2em;
}
QToolButton:hover
{
border: 0.04em solid #b83232;
}
QToolButton:checked,
QToolButton:pressed
{
border: 0.04em solid #b83232;
background-color: #b83232;
}
QToolButton::right-arrow,
QToolButton::left-arrow,
QToolButton::up-arrow,
QToolButton::down-arrow
{
/**
* Do not set the arrow width/height here. It causes
* small icons in Qt6, and doesn't affect the styling
* in Qt5. Both look ideal without manually specified sizes.
*/
subcontrol-origin: content;
subcontrol-position: center;
margin: 0em;
padding: 0em;
}
QToolButton::right-arrow:enabled
{
image: url(:/dark fusion/right_arrow.svg);
}
QToolButton::left-arrow:enabled
{
image: url(:/dark fusion/left_arrow.svg);
}
QToolButton::up-arrow:enabled
{
image: url(:/dark fusion/up_arrow.svg);
}
QToolButton::down-arrow:enabled
{
image: url(:/dark fusion/down_arrow.svg);
}
QToolButton::right-arrow:disabled
{
image: url(:/dark fusion/right_arrow_disabled.svg);
}
QToolButton::left-arrow:disabled
{
image: url(:/dark fusion/left_arrow_disabled.svg);
}
QToolButton::up-arrow:disabled
{
image: url(:/dark fusion/up_arrow_disabled.svg);
}
QToolButton::down-arrow:disabled
{
image: url(:/dark fusion/down_arrow_disabled.svg);
}
QToolButton::menu-indicator
{
border-image: none;
image: url(:/dark fusion/down_arrow.svg);
width: 0.8em;
height: 0.5em;
left: -0.09em;
/* -1.2em + 0.09em */
padding-right: -1.11em;
/**
* Qt5 and Qt6 differ if the subcontrol-origin is set to
* the default, AKA, padding. Setting it to the content,
* which we adjust the padding to, makes it uniform between
* both.
*/
subcontrol-origin: content;
subcontrol-position: right;
}
/**
* Special rule for the drop-down indicator in a QFileDialog.
* We want these to be more compact, hence the smaller padding.
*/
QDialog QToolBar QToolButton[popupMode="2"]::menu-indicator
{
padding-right: -0.7em;
}
QToolButton::menu-arrow
{
border-image: none;
image: url(:/dark fusion/down_arrow.svg);
width: 0.8em;
height: 0.5em;
padding-right: 0.09em;
subcontrol-position: right;
}
QToolButton::menu-button
{
border-top-right-radius: 0.5em;
border-bottom-right-radius: 0.5em;
/* 1ex width + 0.4ex for border + no text = 2ex allocated above */
width: 1.3em;
padding: 0.23em;
outline: none;
}
QToolButton::menu-button::menu-arrow
{
left: -0.09em;
subcontrol-position: right;
}
QToolButton::menu-button:hover
{
background-color: transparent;
}
QToolButton::menu-button:pressed
{
background-color: transparent;
padding: 0.23em;
outline: none;
}
QTableView
{
border: 0em solid black;
gridline-color: #31363b;
background-color: #1d2023;
}
QTableView:!selected,
QTableView:selected
{
border: 0em solid black;
}
QTableView
{
border-radius: 0em;
}
QAbstractItemView::item
{
color: #eff0f1;
}
QAbstractItemView::item:pressed
{
background: #972b2b;
color: #eff0f1;
}
QAbstractItemView::item:selected:!active
{
background: rgba(255, 0, 20, 0.1);
}
/* Use background with qlineargradient to avoid ugly border on widget. */
QAbstractItemView::item:selected:active
{
background: qlineargradient(
x1: 0.5, y1: 0.5
x2: 0.5, y2: 1,
stop: 0 #972b2b,
stop: 1 #972b2b
);
color: #eff0f1;
}
QAbstractItemView::item:selected:hover
{
background: qlineargradient(
x1: 0.5, y1: 0.5
x2: 0.5, y2: 1,
stop: 0 #981f1f,
stop: 1 #981f1f
);
color: #eff0f1;
}
QHeaderView
{
background-color: #31363b;
border: 0.04em transparent;
border-radius: 0em;
margin: 0em;
padding: 0em;
}
QHeaderView::section
{
background-color: #31363b;
border: 0.04em solid #76797c;
color: #eff0f1;
border-radius: 0em;
padding: 0em 0.23em 0em 0.23em;
text-align: center;
}
QHeaderView::section::vertical::first,
QHeaderView::section::vertical::only-one
{
border-top: 0.04em solid #76797c;
}
QHeaderView::section::vertical
{
border-top: transparent;
}
QHeaderView::section::horizontal::first,
QHeaderView::section::horizontal::only-one
{
border-left: 0.04em solid #76797c;
}
QHeaderView::section::horizontal
{
border-left: transparent;
}
QHeaderView[showSortIndicator="true"]::section::horizontal
{
/* Same as the width of the arrow subcontrols below. */
padding-right: 0.8em;
}
QHeaderView::section:checked
{
color: #ffffff;
background-color: #682727;
}
/* Note that this doesn't work for QTreeView unless the header is clickable */
QHeaderView::section:hover,
QHeaderView::section::horizontal::first:hover,
QHeaderView::section::horizontal::only-one:hover,
QHeaderView::section::vertical::first:hover,
QHeaderView::section::vertical::only-one:hover
{
border: 0.04em solid #b83232;
}
QHeaderView[showSortIndicator="true"]::down-arrow
{
image: url(:/dark fusion/down_arrow.svg);
/**
* Qt5 and Qt6 differ if the subcontrol-origin is set to
* the default, AKA, padding. Setting it to the content,
* which we adjust the padding to, makes it uniform between
* both.
*/
subcontrol-origin: content;
subcontrol-position: center right;
width: 0.8em;
height: 0.5em;
/**
* Qt5 and Qt6 have different ideas of the padding of the
* arrow subcontrols: using `padding-left` to ensure that
* the width is undone fixes the padding of the content by
* an extra `0.8em` in Qt6, but doesn't affect Qt5.
*/
padding-right: 0.09em;
padding-left: -0.8em;
}
QHeaderView[showSortIndicator="true"]::up-arrow
{
image: url(:/dark fusion/up_arrow.svg);
subcontrol-origin: content;
subcontrol-position: center right;
width: 0.8em;
height: 0.5em;
padding-right: 0.09em;
padding-left: -0.8em;
}
QTableView QTableCornerButton::section
{
background-color: #31363b;
border: 0.04em transparent #76797c;
border-top: 0.04em solid #76797c;
border-left: 0.04em solid #76797c;
border-radius: 0em;
}
/* No hover event */
QTableView QTableCornerButton:hover
{
border: 0.04em transparent #76797c;
}
QTableView QTableCornerButton::section:pressed
{
border: 0.04em solid #b83232;
border-radius: 0em;
}
QToolBox
{
padding: 0.23em;
border: 0.09em transparent black;
}
QToolBox::tab
{
border-bottom: 0.09em solid #76797c;
margin-left: 1.5em;
}
QToolBox::tab:selected,
QToolBox::tab:hover
{
border-bottom: 0.09em solid #b83232;
}
QSplitter::handle
{
border: 0.09em solid #2c3034;
background: -0.5em solid #2c3034;
max-width: 0em;
max-height: 0em;
}
/**
* It's not possible to get satisfactory rounded borders here.
* If you set the border to be negative, while adjusting the
* widths, you get an asymmetrical curve which produces an
* unappealing border.
*/
QProgressBar:horizontal,
QProgressBar:vertical
{
background-color: #626568;
border: 0.9em solid #31363b;
border-radius: 0.13em;
padding: 0em;
}
QProgressBar:horizontal
{
height: 0.2em;
min-width: 6em;
text-align: right;
padding-left: -0.03em;
padding-right: -0.03em;
margin-top: 0.2em;
margin-bottom: 0.2em;
margin-right: 1.3em;
}
QProgressBar:vertical
{
width: 0.2em;
min-height: 6em;
text-align: bottom;
padding-top: -0.03em;
padding-bottom: -0.03em;
margin-left: 0.2em;
margin-right: 0.2em;
margin-bottom: 0.41em;
}
QProgressBar::chunk:horizontal,
QProgressBar::chunk:vertical
{
background-color: #b83232;
border: 0.9em transparent;
border-radius: 0.08em;
}
QScrollArea,
QScrollArea:focus,
QScrollArea:hover
{
border: 0em solid black;
}
/* ICONS */
/**
* Qt's built-in icons can look pretty bad if the system theme
* is a different color than the current one. For example, when
* using a dark theme, with a light UI, the `Ok` button is greyed
* out for an about dialog.
*
* QDialogButtonBox will apply for all standard buttons in all standard
* widgets, such as QMessageBox, etc. However, we do need to override
* standard icons elsewhere.
*
* The rest of the icons make little sense to implement:
* Qt uses native window decorations.
* Qt normally uses native file dialogs, which look nicer.
* Media controls are used in custom widgets, which aren't standard.
*/
QDialogButtonBox
{
dialogbuttonbox-buttons-have-icons: true;
dialog-cancel-icon: url(:/dark fusion/dialog_cancel.svg);
dialog-close-icon: url(:/dark fusion/dialog_close.svg);
dialog-ok-icon: url(:/dark fusion/dialog_ok.svg);
dialog-open-icon: url(:/dark fusion/dialog_open.svg);
dialog-reset-icon: url(:/dark fusion/dialog_reset.svg);
dialog-save-icon: url(:/dark fusion/dialog_save.svg);
/**
* No support yet for overriding saveall.
* dialog-saveall-icon: url(:/dark fusion/dialog_saveall.svg);
*/
dialog-yes-icon: url(:/dark fusion/dialog_ok.svg);
dialog-help-icon: url(:/dark fusion/dialog_help.svg);
dialog-no-icon: url(:/dark fusion/dialog_no.svg);
dialog-apply-icon: url(:/dark fusion/dialog_ok.svg);
dialog-discard-icon: url(:/dark fusion/dialog_discard.svg);
}
/* Set some styles for these custom dialog buttons */
QDialogButtonBox QPushButton,
QMessageBox QPushButton
{
min-height: 1.1em;
min-width: 5em;
}
/**
* Special rules for creating a custom titlebar. This can only work
* when setting the Qt property `isTitlebar` to `true`.
*/
QWidget[isTitlebar="true"],
QWidget[isTitlebar="true"] *
{
background-color: #2c3034;
}
/**
* Special rules for creating a border around a top-level frame of a window.
* This can only work when setting the Qt property `isWindow` to `true`.
* We've manually enumerated border widths from 1-5 below.
*/
QFrame[isWindow="true"],
QFrame[frameShape][isWindow="true"]
{
border: 0px transparent #2c3034;
}
QFrame[isWindow="true"][windowFrame="1"],
QFrame[frameShape][isWindow="true"][windowFrame="1"]
{
border: 1px solid #2c3034;
border-radius: 3px;
}
QFrame[isWindow="true"][windowFrame="2"],
QFrame[frameShape][isWindow="true"][windowFrame="2"]
{
border: 2px solid #2c3034;
border-radius: 3px;
}
QFrame[isWindow="true"][windowFrame="3"],
QFrame[frameShape][isWindow="true"][windowFrame="3"]
{
border: 3px solid #2c3034;
border-radius: 3px;
}
QFrame[isWindow="true"][windowFrame="4"],
QFrame[frameShape][isWindow="true"][windowFrame="4"]
{
border: 4px solid #2c3034;
border-radius: 3px;
}
QFrame[isWindow="true"][windowFrame="5"],
QFrame[frameShape][isWindow="true"][windowFrame="5"]
{
border: 5px solid #2c3034;
border-radius: 3px;
}
/**
* ADVANCED DOCKING SYSTEM STYLESHEET
* ----------------------------------
*/
/**
* The general approach is as follows:
* 1. Turn `qproperty-icon` off.
* This avoids having a weird background, preventing our desired icon.
* This is presumably because ADS uses `qproperty-icon`.
* 2. Ensure the background is set.
* This is to avoid any QToolButton styling hints when the
* widget is clicked.
* 3. Set the QTOolButton width and height.
* This gives us consistent icon sizes without compression.
* 4. Undo the border.
* Must keep the border width identical to before (0.04em),
* to avoid moving the widgets on pressed/hover events.
*/
#tabCloseButton,
#dockAreaCloseButton,
#tabsMenuButton,
#detachGroupButton,
#floatingTitleCloseButton,
#floatingTitleMaximizeButton
{
qproperty-icon: url(:/dark fusion/transparent.svg);
background: #31363b;
width: 1.2em;
height: 1.2em;
padding: 0em;
margin: 0em;
border: 0.04em transparent black;
}
#tabsMenuButton,
#floatingTitleMaximizeButton
{
/* Need to make the icon smaller, or else it's unusually large. */
width: 0.8em;
}
#tabCloseButton:hover,
#dockAreaCloseButton:hover,
#tabsMenuButton:hover,
#detachGroupButton:hover,
#floatingTitleCloseButton:hover,
#floatingTitleMaximizeButton:hover,
#tabCloseButton:pressed,
#dockAreaCloseButton:pressed,
#tabsMenuButton:pressed,
#detachGroupButton:pressed,
#floatingTitleCloseButton:pressed,
#floatingTitleMaximizeButton:pressed
{
background: #31363b;
}
#tabCloseButton,
#dockAreaCloseButton,
#floatingTitleCloseButton
{
image: url(:/dark fusion/ads_close.svg);
}
#tabCloseButton:hover,
#dockAreaCloseButton:hover,
#floatingTitleCloseButton:hover
{
image: url(:/dark fusion/ads_close_hover.svg);
}
#tabCloseButton:pressed,
#dockAreaCloseButton:pressed,
#floatingTitleCloseButton:pressed
{
image: url(:/dark fusion/ads_close_pressed.svg);
}
#tabsMenuButton
{
image: url(:/dark fusion/ads_menu_button.svg);
}
#tabsMenuButton:hover
{
image: url(:/dark fusion/ads_menu_button_hover.svg);
}
#tabsMenuButton:pressed
{
image: url(:/dark fusion/ads_menu_button_pressed.svg);
}
#tabsMenuButton::menu-indicator
{
image: none;
}
#detachGroupButton
{
image: url(:/dark fusion/ads_detach.svg);
}
#detachGroupButton:hover
{
image: url(:/dark fusion/ads_detach_hover.svg);
}
#detachGroupButton:pressed
{
image: url(:/dark fusion/ads_detach_hover_pressed.svg);
}
/* FLOATING */
/* Disable the default icons when the dock is floating. */
ads--CFloatingWidgetTitleBar
{
qproperty-maximizeIcon: url(:/dark fusion/transparent.svg);
qproperty-normalIcon: url(:/dark fusion/transparent.svg);
}
#floatingTitleMaximizeButton
{
image: url(:/dark fusion/ads_maximize.svg);
}
#floatingTitleMaximizeButton:hover
{
image: url(:/dark fusion/ads_maximize_hover.svg);
}
#floatingTitleMaximizeButton:pressed
{
image: url(:/dark fusion/ads_maximize_pressed.svg);
}
/**
* Using the `maximized="true"`, `isMaximized="true"`, or other attribute
* selectors don't work, and since the maximize button and minimize
* button are always the same...
*
* To get a nicer looking UI, just use the same maximize and restore
* buttons.
*/
/* TABS */
ads--CDockWidgetTab
{
border: 0.04em solid #76797c;
border-top: 0.09em solid #76797c;
background-color: #2c3034;
padding: 0.23em;
min-width: 50px;
border-radius: 0.09em;
border-bottom-left-radius: 0em;
border-bottom-right-radius: 0em;
}
ads--CDockWidgetTab[activeTab="true"]
{
background-color: #31363b;
border-top: 0.09em solid #b83232;
border-left: 0.04em solid #76797c;
border-right: 0.04em solid #76797c;
border-bottom: 0.04em transparent #76797c;
}
ads--CDockWidgetTab QLabel
{
background-color: #2c3034;
}
ads--CDockWidgetTab[activeTab="true"] QLabel
{
background-color: #31363b;
}
/**
* CDockWidgetTab doesn't seem to have the concept of `::next-selected`
* and `::previous-selected`, so we just draw the borders for everything.
* It's not nearly as pretty, but it's not bad either.
*/
/* OVERLAY */
ads--CDockOverlayCross
{
qproperty-iconFrameColor: #b83232;
qproperty-iconBackgroundColor: #1d2023;
qproperty-iconOverlayColor: #b83232;
qproperty-iconArrowColor: #eff0f1;
qproperty-iconShadowColor: transparent;
}
/**
* This adds support for the focus highlighting feature of the ADS.
* https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System/blob/master/doc/user-guide.md#focushighlighting
*/
ads--CDockWidgetTab[focused="true"]
{
background-color: rgba(218, 60, 66, 0.2);
border-color: rgba(218, 60, 66, 0.25);
border-top-color: #b83232;
}
ads--CDockWidgetTab[focused="true"] QLabel,
ads--CDockWidgetTab[focused="true"] #tabCloseButton
{
background-color: transparent;
}
/**
* QDOCKWIDGET TOOLTIP STYLESHEET
* ------------------------------
*/
QAbstractButton#qt_dockwidget_closebutton
{
qproperty-toolTip: "Close";
}
QAbstractButton#qt_dockwidget_floatbutton
{
qproperty-toolTip: "Detach";
}
================================================
FILE: resources/light_style.qss
================================================
/*
* BreezeDark stylesheet.
*
* :author: Colin Duquesnoy
* :editor: Alex Huszagh
* :license: MIT, see LICENSE.md
*
* This is originally a fork of QDarkStyleSheet, and is based on Breeze/
* BreezeDark color scheme, but is in no way affiliated with KDE.
*
* ---------------------------------------------------------------------
* The MIT License (MIT)
*
* Copyright (c) <2013-2014>
* Copyright (c) <2015-2021>
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
* ---------------------------------------------------------------------
*/
/**
* MAIN STYLESHEET
* ---------------
*/
QToolTip
{
/* 0.2ex is the smallest value that's not ignored on Windows. */
border: 0.04em solid #31363b;
background-image: none;
background-color: #eff0f1;
alternate-background-color: #eaebec;
color: #31363b;
padding: 0.1em;
opacity: 200;
}
QWidget
{
color: #31363b;
background-color: #eff0f1;
selection-background-color: rgba(223, 52, 52, 0.5);
selection-color: #31363b;
background-clip: border;
border-image: none;
/* QDialogButtonBox icons */
dialog-cancel-icon: url(:/light fusion/dialog_cancel.svg);
dialog-close-icon: url(:/light fusion/dialog_close.svg);
dialog-ok-icon: url(:/light fusion/dialog_ok.svg);
dialog-open-icon: url(:/light fusion/dialog_open.svg);
dialog-reset-icon: url(:/light fusion/dialog_reset.svg);
dialog-save-icon: url(:/light fusion/dialog_save.svg);
dialog-yes-icon: url(:/light fusion/dialog_ok.svg);
dialog-help-icon: url(:/light fusion/dialog_help.svg);
dialog-no-icon: url(:/light fusion/dialog_no.svg);
dialog-apply-icon: url(:/light fusion/dialog_ok.svg);
dialog-discard-icon: url(:/light fusion/dialog_discard.svg);
/* File icons */
filedialog-backward-icon: url(:/light fusion/left_arrow.svg);
filedialog-contentsview-icon: url(:/light fusion/file_dialog_contents.svg);
filedialog-detailedview-icon: url(:/light fusion/file_dialog_detailed.svg);
filedialog-end-icon: url(:/light fusion/file_dialog_end.svg);
filedialog-infoview-icon: url(:/light fusion/file_dialog_info.svg);
filedialog-listview-icon: url(:/light fusion/file_dialog_list.svg);
filedialog-new-directory-icon: url(:/light fusion/folder.svg);
filedialog-parent-directory-icon: url(:/light fusion/up_arrow.svg);
filedialog-start-icon: url(:/light fusion/file_dialog_start.svg);
directory-closed-icon: url(:/light fusion/folder.svg);
directory-icon: url(:/light fusion/folder.svg);
directory-link-icon: url(:/light fusion/folder_link.svg);
directory-open-icon: url(:/light fusion/folder_open.svg);
file-icon: url(:/light fusion/file.svg);
file-link-icon: url(:/light fusion/file_link.svg);
home-icon: url(:/light fusion/home_directory.svg);
/* QMessageBox icons */
messagebox-critical-icon: url(:/light fusion/message_critical.svg);
messagebox-information-icon: url(:/light fusion/message_information.svg);
messagebox-question-icon: url(:/light fusion/message_question.svg);
messagebox-warning-icon: url(:/light fusion/message_warning.svg);
/* Computer icons */
computer-icon: url(:/light fusion/computer.svg);
desktop-icon: url(:/light fusion/desktop.svg);
cd-icon: url(:/light fusion/disc_drive.svg);
dvd-icon: url(:/light fusion/disc_drive.svg);
floppy-icon: url(:/light fusion/floppy_drive.svg);
harddisk-icon: url(:/light fusion/hard_drive.svg);
network-icon: url(:/light fusion/network_drive.svg);
trash-icon: url(:/light fusion/trash.svg);
/* Arrow icons */
uparrow-icon: url(:/light fusion/up_arrow.svg);
downarrow-icon: url(:/light fusion/down_arrow.svg);
leftarrow-icon: url(:/light fusion/left_arrow.svg);
rightarrow-icon: url(:/light fusion/right_arrow.svg);
backward-icon: url(:/light fusion/left_arrow.svg);
forward-icon: url(:/light fusion/right_arrow.svg);
/* Titlebar icons */
titlebar-close-icon: url(:/light fusion/window_close.svg);
titlebar-contexthelp-icon: url(:/light fusion/help.svg);
titlebar-maximize-icon: url(:/light fusion/maximize.svg);
titlebar-menu-icon: url(:/light fusion/menu.svg);
titlebar-minimize-icon: url(:/light fusion/minimize.svg);
titlebar-normal-icon: url(:/light fusion/restore.svg);
titlebar-shade-icon: url(:/light fusion/shade.svg);
titlebar-unshade-icon: url(:/light fusion/unshade.svg);
/* Other icons */
dockwidget-close-icon: url(:/light fusion/close.svg);
/**
* Only available in Qt6, and causes other issues. See #62.
* lineedit-clear-button-icon: url(:/light fusion/clear_text.svg);
*/
}
QWidget:disabled
{
color: #b4b4b4;
background-color: #eff0f1;
}
QCheckBox
{
spacing: 0.23em;
outline: none;
color: #31363b;
margin-bottom: 0.09em;
opacity: 200;
}
QCheckBox:disabled
{
color: #bab9b8;
}
QGroupBox
{
/* Need to make sure the groupbox doesn't compress below the title. */
min-height: 1.2em;
border: 0.04em solid #bab9b8;
border-radius: 0.09em;
/**
* This gives us enough space at the top to ensure we can move the
* title to be inside the guidelines, and the padding at the top
* ensures we have space below the title.
*/
margin-top: 0.5em;
padding-top: 1em;
}
QGroupBox:focus
{
border: 0.04em solid #bab9b8;
border-radius: 0.09em;
}
QGroupBox::title
{
/* We need to move 0.6em up to be inside the lines, +1em for padding. */
top: -1.6em;
subcontrol-origin: content;
subcontrol-position: top center;
background: #eff0f1;
padding-left: 0.2em;
padding-right: 0.2em;
}
QGroupBox:flat
{
border-top: 0.04em solid rgba(106, 105, 105, 0.7);
border-left: 0.04em transparent #bab9b8;
border-right: 0.04em transparent #bab9b8;
border-bottom: 0.04em transparent #bab9b8;
}
QCheckBox::indicator,
QTreeView::indicator,
QTableWidget::indicator,
QGroupBox::indicator
{
width: 1em;
height: 1em;
}
QGroupBox::indicator:unchecked,
QGroupBox::indicator:unchecked:focus,
QCheckBox::indicator:unchecked,
QCheckBox::indicator:unchecked:focus,
QTreeView::indicator:unchecked,
QTreeView::indicator:unchecked:focus
{
border-image: url(:/light fusion/checkbox_unchecked_disabled.svg);
}
QGroupBox::indicator:unchecked,
QCheckBox::indicator:unchecked:hover,
QCheckBox::indicator:unchecked:pressed,
QTreeView::indicator:unchecked:hover,
QTreeView::indicator:unchecked:pressed,
QGroupBox::indicator:unchecked:hover,
QGroupBox::indicator:unchecked:pressed
{
border: none;
border-image: url(:/light fusion/checkbox_unchecked.svg);
}
QCheckBox::indicator:checked,
QTreeView::indicator:checked,
QGroupBox::indicator:checked
{
border-image: url(:/light fusion/checkbox_checked.svg);
}
QCheckBox::indicator:checked:hover,
QCheckBox::indicator:checked:focus,
QCheckBox::indicator:checked:pressed,
QTreeView::indicator:checked:hover,
QTreeView::indicator:checked:focus,
QTreeView::indicator:checked:pressed,
QGroupBox::indicator:checked:hover,
QGroupBox::indicator:checked:focus,
QGroupBox::indicator:checked:pressed
{
border: none;
border-image: url(:/light fusion/checkbox_checked.svg);
}
QCheckBox::indicator:indeterminate,
QTreeView::indicator:indeterminate
{
border-image: url(:/light fusion/checkbox_indeterminate.svg);
}
QCheckBox::indicator:indeterminate:focus,
QCheckBox::indicator:indeterminate:hover,
QCheckBox::indicator:indeterminate:pressed,
QTreeView::indicator:indeterminate:focus,
QTreeView::indicator:indeterminate:hover,
QTreeView::indicator:indeterminate:pressed
{
border-image: url(:/light fusion/checkbox_indeterminate.svg);
}
QCheckBox::indicator:indeterminate:disabled,
QTreeView::indicator:indeterminate:disabled
{
border-image: url(:/light fusion/checkbox_indeterminate_disabled.svg);
}
QCheckBox::indicator:checked:disabled,
QTreeView::indicator:checked:disabled,
QGroupBox::indicator:checked:disabled
{
border-image: url(:/light fusion/checkbox_checked_disabled.svg);
}
QCheckBox::indicator:unchecked:disabled,
QTreeView::indicator:unchecked:disabled,
QGroupBox::indicator:unchecked:disabled
{
border-image: url(:/light fusion/checkbox_unchecked_disabled.svg);
}
QRadioButton
{
spacing: 0.23em;
outline: none;
color: #31363b;
margin-bottom: 0.09em;
}
QRadioButton:disabled
{
color: #bab9b8;
}
QRadioButton::indicator
{
width: 1em;
height: 1em;
}
QRadioButton::indicator:unchecked,
QRadioButton::indicator:unchecked:focus
{
border-image: url(:/light fusion/radio_unchecked_disabled.svg);
}
QRadioButton::indicator:unchecked:hover,
QRadioButton::indicator:unchecked:pressed
{
border: none;
outline: none;
border-image: url(:/light fusion/radio_unchecked.svg);
}
QRadioButton::indicator:checked
{
border: none;
outline: none;
border-image: url(:/light fusion/radio_checked.svg);
}
QRadioButton::indicator:checked:hover,
QRadioButton::indicator:checked:focus,
QRadioButton::indicator:checked:pressed
{
border: none;
outline: none;
border-image: url(:/light fusion/radio_checked.svg);
}
QRadioButton::indicator:checked:disabled
{
outline: none;
border-image: url(:/light fusion/radio_checked_disabled.svg);
}
QRadioButton::indicator:unchecked:disabled
{
border-image: url(:/light fusion/radio_unchecked_disabled.svg);
}
QMenuBar
{
background-color: #eff0f1;
color: #31363b;
}
QMenuBar::item
{
background: transparent;
}
QMenuBar::item:selected
{
background: transparent;
border: 0.04em solid rgba(223, 52, 52, 0.5);
}
QMenuBar::item:disabled
{
color: #bab9b8;
}
QMenuBar::item:pressed
{
background-color: rgba(223, 52, 52, 0.5);
color: #31363b;
margin-bottom: -0.09em;
padding-bottom: 0.09em;
}
QMenu
{
color: #31363b;
margin: 0.09em;
}
QMenu::icon
{
margin: 0.23em;
}
QMenu::item
{
/* Add extra padding on the right for the QMenu arrow */
padding: 0.23em 1.5em 0.23em 1.3em;
border: 0.09em solid transparent;
background: transparent;
}
QMenu::item:selected
{
color: #31363b;
background-color: rgba(223, 52, 52, 0.5);
}
QMenu::item:selected:disabled
{
background-color: #eff0f1;
}
QMenu::item:disabled
{
color: #bab9b8;
}
QMenu::indicator
{
width: 0.8em;
height: 0.8em;
/* To align with QMenu::icon, which has a 0.23em margin. */
margin-left: 0.3em;
subcontrol-position: center left;
}
QMenu::indicator:non-exclusive:unchecked
{
border-image: url(:/light fusion/checkbox_unchecked_disabled.svg);
}
QMenu::indicator:non-exclusive:unchecked:selected
{
border-image: url(:/light fusion/checkbox_unchecked_disabled.svg);
}
QMenu::indicator:non-exclusive:checked
{
border-image: url(:/light fusion/checkbox_checked.svg);
}
QMenu::indicator:non-exclusive:checked:selected
{
border-image: url(:/light fusion/checkbox_checked.svg);
}
QMenu::indicator:exclusive:unchecked
{
border-image: url(:/light fusion/radio_unchecked_disabled.svg);
}
QMenu::indicator:exclusive:unchecked:selected
{
border-image: url(:/light fusion/radio_unchecked_disabled.svg);
}
QMenu::indicator:exclusive:checked
{
border-image: url(:/light fusion/radio_checked.svg);
}
QMenu::indicator:exclusive:checked:selected
{
border-image: url(:/light fusion/radio_checked.svg);
}
QMenu::right-arrow
{
margin: 0.23em;
border-image: url(:/light fusion/right_arrow.svg);
width: 0.5em;
height: 0.8em;
}
QMenu::right-arrow:disabled
{
border-image: url(:/light fusion/right_arrow_disabled.svg);
}
QAbstractItemView
{
alternate-background-color: #eff0f1;
color: #31363b;
border: 0.09em solid #bab9b8;
border-radius: 0.09em;
}
QTabWidget:focus,
QCheckBox:focus,
QRadioButton:focus,
QSlider:focus
{
border: none;
}
QLineEdit
{
background-color: #eff0f1;
padding: 0.23em;
border-style: solid;
border: 0.04em solid #bab9b8;
border-radius: 0.09em;
color: #31363b;
}
QAbstractScrollArea
{
border-radius: 0.09em;
border: 0.09em solid #bab9b8;
background-color: transparent;
}
/**
* This is the background for the box in the bottom-right corner
* whene both scrollbars are active.
*/
QAbstractScrollArea::corner
{
background: #eff0f1;
}
/**
* Can't do the KDE style of where the scrollbar handle
* becomes light on the hover, and only when the handle
* is hovered does it become stylized. This is because
* both the handle and the background events are treated
* together.
*/
QScrollBar:horizontal
{
background-color: #eff0f1;
height: 0.65em;
margin: 0.13em 0.65em 0.13em 0.65em;
border: 0.04em transparent #eff0f1;
border-radius: 0.17em;
}
QScrollBar:horizontal:hover
{
background-color: #c7c7c6;
}
QScrollBar::handle:horizontal
{
background-color: rgba(223, 52, 52, 0.8);
border: 0.04em solid rgba(223, 52, 52, 0.8);
min-width: 0.5em;
border-radius: 0.17em;
}
QScrollBar::handle:horizontal:hover
{
background-color: rgba(223, 52, 52, 0.8);
border: 0.04em solid rgba(223, 52, 52, 0.8);
}
QScrollBar::add-line:horizontal
{
margin: 0em 0.13em 0em 0.13em;
border-image: url(:/light fusion/transparent.svg);
width: 0.41em;
height: 0.41em;
subcontrol-position: right;
subcontrol-origin: margin;
}
QScrollBar::sub-line:horizontal
{
margin: 0em 0.13em 0em 0.13em;
border-image: url(:/light fusion/transparent.svg);
width: 0.41em;
height: 0.41em;
subcontrol-position: left;
subcontrol-origin: margin;
}
QScrollBar::add-line:horizontal:hover,
QScrollBar::add-line:horizontal:on
{
border-image: url(:/light fusion/transparent.svg);
width: 0.41em;
height: 0.41em;
subcontrol-position: right;
subcontrol-origin: margin;
}
QScrollBar::sub-line:horizontal:hover,
QScrollBar::sub-line:horizontal:on
{
border-image: url(:/light fusion/transparent.svg);
width: 0.41em;
height: 0.41em;
subcontrol-position: left;
subcontrol-origin: margin;
}
QScrollBar::up-arrow:horizontal,
QScrollBar::down-arrow:horizontal
{
background: none;
}
QScrollBar::add-page:horizontal,
QScrollBar::sub-page:horizontal
{
background: none;
}
QScrollBar:vertical
{
background-color: #eff0f1;
width: 0.65em;
margin: 0.65em 0.13em 0.65em 0.13em;
border: 0.04em transparent #eff0f1;
border-radius: 0.17em;
}
QScrollBar:vertical:hover
{
background-color: #c7c7c6;
}
QScrollBar::handle:vertical
{
background-color: rgba(223, 52, 52, 0.8);
border: 0.04em solid rgba(223, 52, 52, 0.8);
min-height: 0.5em;
border-radius: 0.17em;
}
QScrollBar::handle:vertical:hover
{
background-color: rgba(223, 52, 52, 0.8);
border: 0.04em solid rgba(223, 52, 52, 0.8);
}
QScrollBar::sub-line:vertical
{
margin: 0.13em 0em 0.13em 0em;
border-image: url(:/light fusion/transparent.svg);
height: 0.41em;
width: 0.41em;
subcontrol-position: top;
subcontrol-origin: margin;
}
QScrollBar::add-line:vertical
{
margin: 0.13em 0em 0.13em 0em;
border-image: url(:/light fusion/transparent.svg);
height: 0.41em;
width: 0.41em;
subcontrol-position: bottom;
subcontrol-origin: margin;
}
QScrollBar::sub-line:vertical:hover,
QScrollBar::sub-line:vertical:on
{
border-image: url(:/light fusion/transparent.svg);
height: 0.41em;
width: 0.41em;
subcontrol-position: top;
subcontrol-origin: margin;
}
QScrollBar::add-line:vertical:hover,
QScrollBar::add-line:vertical:on
{
border-image: url(:/light fusion/transparent.svg);
height: 0.41em;
width: 0.41em;
subcontrol-position: bottom;
subcontrol-origin: margin;
}
QScrollBar::up-arrow:vertical,
QScrollBar::down-arrow:vertical
{
background: none;
}
QScrollBar::add-page:vertical,
QScrollBar::sub-page:vertical
{
background: none;
}
QTextEdit
{
background-color: #eff0f1;
color: #31363b;
border: 0.04em solid #bab9b8;
}
QPlainTextEdit
{
background-color: #eff0f1;
color: #31363b;
border-radius: 0.09em;
border: 0.04em solid #bab9b8;
}
QSizeGrip
{
border-image: url(:/light fusion/sizegrip.svg);
width: 0.5em;
height: 0.5em;
}
/**
* Set the separator to be transparent, since the dock has a border.
* On PyQt6, neither the border nor the background seem to be respected.
*/
QMainWindow::separator
{
border: 0.09em transparent #bab9b8;
background: transparent;
}
QMenu::separator
{
height: 0.09em;
background-color: #bab9b8;
padding-left: 0.2em;
margin-top: 0.2em;
margin-bottom: 0.2em;
margin-left: 0.41em;
margin-right: 0.41em;
}
QFrame[frameShape="2"], /* QFrame::Panel == 0x0003 */
QFrame[frameShape="3"], /* QFrame::WinPanel == 0x0003 */
QFrame[frameShape="4"], /* QFrame::HLine == 0x0004 */
QFrame[frameShape="5"], /* QFrame::VLine == 0x0005 */
QFrame[frameShape="6"] /* QFrame::StyledPanel == 0x0006 */
{
border-width: 0.04em;
padding: 0.09em;
border-style: solid;
border-color: #eff0f1;
background-color: #bab9b8;
border-radius: 0.23em;
}
/* Provide highlighting for frame objects. */
QFrame[frameShape="2"]:hover,
QFrame[frameShape="3"]:hover,
QFrame[frameShape="4"]:hover,
QFrame[frameShape="5"]:hover,
QFrame[frameShape="6"]:hover
{
border: 0.04em solid rgba(223, 52, 52, 0.5);
}
/* Don't provide an outline if we have a widget that takes up the space. */
QFrame[frameShape] QAbstractItemView:hover
{
border: 0em solid black;
}
/**
* Note: I can't really change the background of the toolbars
* independently, since KDE Breeze has different colors for the
* window bar and the rest of the UI. The top toolbar uses
* the window style, and the rest use the application style,
* which we can't do.
*/
QToolBar
{
font-weight: bold;
}
QToolBar:horizontal
{
background: 0.09em solid #eff0f1;
}
QToolBar:vertical
{
background: 0.09em solid #eff0f1;
}
QToolBar::handle:horizontal
{
border-image: url(:/light fusion/hmovetoolbar.svg);
}
QToolBar::handle:vertical
{
border-image: url(:/light fusion/vmovetoolbar.svg);
}
QToolBar::separator:horizontal
{
border-image: url(:/light fusion/hseptoolbar.svg);
}
QToolBar::separator:vertical
{
border-image: url(:/light fusion/vseptoolbar.svg);
}
QToolBar QToolButton
{
font-weight: bold;
border: 0.04em transparent black;
padding-left: 0.2em;
padding-right: 0.3em;
}
QToolBar QToolButton:hover
{
border: 0.04em solid rgba(223, 52, 52, 0.5);
}
QToolBar QToolButton:pressed
{
border: 0.04em solid rgba(223, 52, 52, 0.5);
/* The padding doesn't inherit from `QToolBar QToolButton`, so leave it in. */
padding-left: 0.2em;
padding-right: 0.3em;
}
/**
* Special rules for a QFileDialog.
*
* Due to the widgets, we get rid of the min sizes to allow them
* to pack closer together, and ensure we have enough padding for
* the drop-down menu in the popup.
*/
QDialog QToolBar QToolButton[popupMode="0"],
QDialog QToolBar QToolButton[popupMode="1"]
{
padding-left: 0.1em;
padding-right: 0.1em;
}
QDialog QToolBar QToolButton[popupMode="2"]
{
padding-left: 0.1em;
padding-right: 0.7em;
}
QPushButton
{
color: #31363b;
background-color: #eaebec;
border: 0.04em solid #bab9b8;
padding: 0.23em;
border-radius: 0.09em;
outline: none;
min-height: 1.1em;
}
QPushButton:flat,
QPushButton:flat:hover
{
border: 0.04em transparent #bab9b8;
}
QComboBox:open,
QPushButton:open
{
border-width: 0.04em;
border-color: #bab9b8;
}
QComboBox:closed,
QPushButton:closed
{
border-width: 0.04em;
border-color: #bab9b8;
}
QPushButton:disabled
{
background-color: #eff0f1;
border-width: 0.04em;
border-color: #bab9b8;
border-style: solid;
padding-top: 0.23em;
padding-bottom: 0.23em;
padding-left: 1ex;
padding-right: 1ex;
border-radius: 0.04em;
color: #b4b4b4;
}
QPushButton:focus
{
color: #31363b;
}
QPushButton:pressed
{
background-color: #bedfec;
padding-top: -0.65em;
padding-bottom: -0.74em;
color: #31363b;
}
QComboBox
{
border: 0.04em solid #bab9b8;
border-radius: 0.09em;
padding: 0.23em;
min-width: 2.5em;
}
QComboBox:editable
{
background-color: #eff0f1;
}
QPushButton:checked
{
background-color: #c7c7c6;
border: 0.04em solid #bab9b8;
color: #31363b;
}
QPushButton:hover
{
background-color: #eaebec;
border: 0.04em solid rgba(223, 52, 52, 0.5);
color: #31363b;
}
QPushButton:checked:hover
{
background-color: #c7c7c6;
border: 0.04em solid rgba(223, 52, 52, 0.5);
color: #31363b;
}
QComboBox:hover,
QComboBox:focus,
QAbstractSpinBox:hover,
QAbstractSpinBox:focus,
QLineEdit:hover,
QLineEdit:focus,
QTextEdit:hover,
QTextEdit:focus,
QPlainTextEdit:hover,
QPlainTextEdit:focus,
QAbstractView:hover,
QTreeView:hover,
QTreeView:focus
{
border: 0.04em solid rgba(223, 52, 52, 0.5);
color: #31363b;
}
QComboBox:hover:pressed:!editable,
QPushButton:hover:pressed,
QAbstractSpinBox:hover:pressed,
QLineEdit:hover:pressed,
QTextEdit:hover:pressed,
QPlainTextEdit:hover:pressed,
QAbstractView:hover:pressed,
QTreeView:hover:pressed
{
background-color: #eff0f1;
}
QColumnView
{
border: 0.04em transparent #eff0f1;
}
QColumnViewGrip
{
border-image: url(:/light fusion/sizegrip.svg);
}
/* Each column in the view is a QAbstractItemView. */
QColumnView QAbstractItemView
{
border: 0.04em transparent rgba(223, 52, 52, 0.5);
}
/**
* In order to set consistency between Qt5 and Qt6, we need
* to ensure that we do the following steps:
* 1. Set a consistent `max-height` in the item. Anything
* below `0.8em` will cause clipping, so set that value
* to ensure the icon isn't larger.
* 2. Set padding to ensure the item is properly padded.
* 3. Set `0.2em` margins on the top and bottom of the arrows,
* and `0.1em` on the left and right to ensure the arrows
* are properly padded and have the same size.
*
* The size consistency only works if both the `::item` subcontrol
* `max-height` and the `::*-arrow` subcontrol `margin` is set.
*/
QColumnView QAbstractItemView::item
{
padding: 0.23em;
max-width: 0.5em;
max-height: 0.8em;
}
QColumnView QAbstractItemView::right-arrow
{
image: url(:/light fusion/right_arrow.svg);
margin: 0.2em 0.1em 0.2em 0.1em;
}
QColumnView QAbstractItemView::right-arrow:selected,
QColumnView QAbstractItemView::right-arrow:hover
{
image: url(:/light fusion/right_arrow_hover.svg);
}
QColumnView QAbstractItemView::left-arrow
{
image: url(:/light fusion/left_arrow.svg);
margin: 0.2em 0.1em 0.2em 0.1em;
}
QColumnView QAbstractItemView::left-arrow:selected,
QColumnView QAbstractItemView::left-arrow:hover
{
image: url(:/light fusion/left_arrow_hover.svg);
}
QComboBox:hover:pressed:editable
{
background-color: #eff0f1;
}
QComboBox QAbstractItemView
{
/* This happens for the drop-down menu always, whether editable or not.*/
background-color: #eff0f1;
selection-background-color: rgba(200, 45, 45, 0.5);
outline-color: 0em;
border-radius: 0.09em;
}
QComboBox::drop-down
{
subcontrol-origin: padding;
subcontrol-position: top right;
width: 0.65em;
border-left-width: 0em;
border-left-style: solid;
border-top-right-radius: 0.13em;
border-bottom-right-radius: 0.13em;
}
QComboBox::down-arrow
{
border-image: url(:/light fusion/down_arrow_disabled.svg);
width: 0.8em;
height: 0.5em;
margin-right: 0.41em;
}
QComboBox::down-arrow:on,
QComboBox::down-arrow:hover,
QComboBox::down-arrow:focus
{
border-image: url(:/light fusion/down_arrow.svg);
width: 0.8em;
height: 0.5em;
margin-right: 0.41em;
}
QAbstractSpinBox
{
padding: 0.23em;
border: 0.09em solid #bab9b8;
background-color: #eff0f1;
color: #31363b;
border-radius: 0.09em;
min-width: 3em;
min-height: 1em;
}
QAbstractSpinBox:hover
{
border: 0.09em solid rgba(223, 52, 52, 0.5);
}
QAbstractSpinBox:up-button,
QAbstractSpinBox:up-button:hover
{
background-color: transparent;
subcontrol-origin: padding;
subcontrol-position: center right;
padding-right: 0.1em;
width: 0.8em;
height: 0.5em;
}
QAbstractSpinBox:down-button,
QAbstractSpinBox:down-button:hover
{
background-color: transparent;
subcontrol-origin: padding;
subcontrol-position: center left;
padding-left: 0.1em;
width: 0.8em;
height: 0.5em;
}
/**
* Bug fixes for elongated items in QSpinBoxes.
* By default, the items are bounded by `down-button`
* and `up-button`, so this doesn't actually affect the styling.
*
* This does however affect some custom styling using
* QStyle.CC_ComboBox, which affects QDateEdit. This cannot
* be selected using QDateEdit, since it uses a global style.
* This sounds nonsensical, because CC_ComboBox isn't a spin box,
* but through trial and error, this is in fact the case.
*
* Affects #40.
*/
QAbstractSpinBox::up-arrow,
QAbstractSpinBox::up-arrow:disabled,
QAbstractSpinBox::up-arrow:off,
QAbstractSpinBox::up-arrow:!off:!disabled:hover,
QAbstractSpinBox::down-arrow,
QAbstractSpinBox::down-arrow:disabled,
QAbstractSpinBox::down-arrow:off,
QAbstractSpinBox::down-arrow:!off:!disabled:hover
{
border-image: none;
width: 0.8em;
height: 0.5em;
}
QAbstractSpinBox::up-arrow
{
image: url(:/light fusion/up_arrow.svg);
}
QAbstractSpinBox::up-arrow:disabled,
QAbstractSpinBox::up-arrow:off
{
image: url(:/light fusion/up_arrow_disabled.svg);
}
QAbstractSpinBox::up-arrow:hover
{
image: url(:/light fusion/up_arrow_hover.svg);
}
QAbstractSpinBox::down-arrow
{
image: url(:/light fusion/down_arrow.svg);
}
QAbstractSpinBox::down-arrow:disabled,
QAbstractSpinBox::down-arrow:off
{
image: url(:/light fusion/down_arrow_disabled.svg);
}
QAbstractSpinBox::down-arrow:!off:!disabled:hover
{
image: url(:/light fusion/down_arrow_hover.svg);
}
QDoubleSpinBox
{
min-width: 4em;
}
/**
* `QCalendarWidget QAbstractItemView:enabled` sets the color, background
* color, and selection color for active dates in the view.
* `QCalendarWidget QAbstractItemView:enabled` sets the disabled dates.
*/
QCalendarWidget QAbstractItemView:enabled
{
color: #31363b;
selection-color: #31363b;
selection-background-color: rgba(223, 52, 52, 0.5);
}
/* Won't take hover events. */
QPrevNextCalButton
{
min-width: 0.8em;
min-height: 1.2em;
qproperty-iconSize: 0px 0px;
}
QPrevNextCalButton#qt_calendar_nextmonth
{
image: url(:/light fusion/calendar_next.svg);
}
QPrevNextCalButton#qt_calendar_prevmonth
{
image: url(:/light fusion/calendar_previous.svg);
}
/**
* Setting for the month and year displays and drop-down menu for the
* month select. We style this separately because we want a drop-down
* indicator in the bottom right, unlike the normal QToolButton.
*/
QCalendarWidget QToolButton
{
background-color: transparent;
border: 0.04em solid #bab9b8;
border-radius: 0.09em;
margin: 0.23em;
padding: 0.23em;
padding-top: 0.1em;
padding-right: 1.2em;
min-height: 1.1em;
}
QCalendarWidget QToolButton:hover
{
border: 0.04em solid rgba(223, 52, 52, 0.5);
}
QCalendarWidget QToolButton:checked,
QCalendarWidget QToolButton:pressed
{
background-color: rgba(223, 52, 52, 0.5);
padding: 0.23em;
padding-right: 1.2em;
min-height: 1.3em;
outline: none;
}
/**
* The QCalendarWidget for QDateTimeEdit seems to improperly
* style the year QToolButton, which has an object name
* `qt_datetimedit_calendar`, so ensure we style it as well.
*/
QCalendarWidget QToolButton::menu-indicator,
#qt_datetimedit_calendar QCalendarWidget QToolButton::menu-indicator
{
border-image: none;
image: url(:/light fusion/down_arrow.svg);
width: 0.8em;
height: 0.5em;
top: -0.7ex;
left: -0.09em;
padding-right: -1.11em;
subcontrol-origin: content;
subcontrol-position: bottom right;
}
QCalendarWidget QToolButton::menu-arrow,
#qt_datetimedit_calendar QCalendarWidget QToolButton::menu-arrow
{
border-image: none;
image: url(:/light fusion/down_arrow.svg);
width: 0.8em;
height: 0.5em;
padding-right: 0.09em;
subcontrol-origin: content;
subcontrol-position: bottom right;
}
/**
* Setting for the year button. Both the month select and the year
* select are QToolButtons, and both are auto-raised. The year
* button however has the popup mode set to `DelayedPopup`.
*/
QCalendarWidget QToolButton[autoRaise="true"][popupMode="0"]
{
padding: 0.23em;
}
QCalendarWidget QSpinBox
{
max-height: 1.5em;
min-width: 3.5em;
margin: 0em;
margin-top: 0.2em;
padding: 0em;
outline: 0em;
padding-left: 0.5em;
}
QLabel
{
border: 0em solid black;
}
/* BORDERS */
QTabWidget::pane
{
padding: 0.23em;
margin: 0.04em;
}
QTabWidget::pane:top
{
border: 0.04em solid #bab9b8;
top: -0.04em;
}
QTabWidget::pane:bottom
{
border: 0.04em solid #bab9b8;
bottom: -0.04em;
}
QTabWidget::pane:left
{
border: 0.04em solid #bab9b8;
left: -0.04em;
}
QTabWidget::pane:right
{
border: 0.04em solid #bab9b8;
right: -0.04em;
}
QTabBar
{
qproperty-drawBase: 0;
left: 0.23em;
border-radius: 0.13em;
/**
* Note: this is the underline for each tab title. It's not
* documented, and this took forever to track down. At least
* 10 hours have been wasted trying to turn off this line,
* do not deleted this comment.
*/
selection-color: transparent;
}
QTabBar:focus
{
border: 0em transparent black;
}
QTabBar::close-button
{
/* Doesn't seem possible to resize these buttons */
border-image: url(:/light fusion/transparent.svg);
image: url(:/light fusion/close.svg);
background: transparent;
}
QTabBar::close-button:hover
{
image: url(:/light fusion/close_hover.svg);
}
QTabBar::close-button:pressed
{
image: url(:/light fusion/close_pressed.svg);
}
/* TOP TABS */
QTabBar::tab:top,
QTabBar::tab:top:last,
QTabBar::tab:top:only-one
{
color: #31363b;
border: 0.04em transparent black;
border-left: 0.04em solid #bab9b8;
border-right: 0.04em solid #bab9b8;
border-top: 0.09em solid rgba(223, 52, 52, 0.5);
background-color: #eff0f1;
padding: 0.23em;
min-width: 50px;
border-radius: 0.09em;
border-bottom-left-radius: 0em;
border-bottom-right-radius: 0em;
}
QTabBar::tab:top:first,
QTabBar::tab:top:only-one
{
margin-left: 0.04em;
}
QTabBar::tab:top:last,
QTabBar::tab:top:only-one
{
margin-right: 0.04em;
}
QTabBar::tab:top:!selected
{
color: #31363b;
background-color: #d9d8d7;
border: 0.04em transparent black;
border-right: 0.04em solid #bab9b8;
border-bottom: 0.04em solid #bab9b8;
border-radius: 0.09em;
border-bottom-left-radius: 0em;
border-bottom-right-radius: 0em;
}
QTabBar::tab:top:next-selected
{
border-right: 0.04em transparent #d9d8d7;
border-bottom-left-radius: 0em;
border-bottom-right-radius: 0em;
}
QTabBar::tab:top:!selected:hover
{
background-color: rgba(255, 0, 20, 0.1);
border-radius: 0.09em;
border-bottom-left-radius: 0em;
border-bottom-right-radius: 0em;
}
QTabBar::tab:top:!selected:first:hover
{
background-color: rgba(255, 0, 20, 0.1);
border-radius: 0.09em;
border-bottom-left-radius: 0em;
border-bottom-right-radius: 0em;
}
/* BOTTOM TABS */
QTabBar::tab:bottom,
QTabBar::tab:bottom:last,
QTabBar::tab:bottom:only-one
{
color: #31363b;
border: 0.04em transparent black;
border-left: 0.04em solid #bab9b8;
border-right: 0.04em solid #bab9b8;
border-bottom: 0.09em solid rgba(223, 52, 52, 0.5);
background-color: #eff0f1;
padding: 0.23em;
min-width: 50px;
border-radius: 0.09em;
border-top-left-radius: 0em;
border-top-right-radius: 0em;
}
QTabBar::tab:bottom:first,
QTabBar::tab:bottom:only-one
{
margin-left: 0.08em;
}
QTabBar::tab:bottom:last,
QTabBar::tab:bottom:only-one
{
margin-right: 0.08em;
}
QTabBar::tab:bottom:!selected
{
color: #31363b;
background-color: #d9d8d7;
border: 0.04em transparent black;
border-top: 0.04em solid #bab9b8;
border-right: 0.04em solid #bab9b8;
border-radius: 0.09em;
border-top-left-radius: 0em;
border-top-right-radius: 0em;
}
QTabBar::tab:bottom:next-selected
{
border-right: 0.04em transparent #d9d8d7;
border-top-left-radius: 0em;
border-top-right-radius: 0em;
}
QTabBar::tab:bottom:!selected:hover
{
background-color: rgba(255, 0, 20, 0.1);
border-radius: 0.09em;
border-top-left-radius: 0em;
border-top-right-radius: 0em;
}
QTabBar::tab:bottom:!selected:first:hover
{
background-color: rgba(255, 0, 20, 0.1);
border-radius: 0.09em;
border-top-left-radius: 0em;
border-top-right-radius: 0em;
}
/* LEFT TABS */
QTabBar::tab:left,
QTabBar::tab:left:last,
QTabBar::tab:left:only-one
{
color: #31363b;
border: 0.04em transparent black;
border-top: 0.09em solid rgba(223, 52, 52, 0.5);
border-bottom: 0.04em solid #bab9b8;
border-left: 0.04em solid #bab9b8;
background-color: #eff0f1;
padding: 0.23em;
min-height: 50px;
border-radius: 0.09em;
border-top-right-radius: 0em;
border-bottom-right-radius: 0em;
}
QTabBar::tab:left:first,
QTabBar::tab:left:only-one
{
margin-top: 0.08em;
}
QTabBar::tab:left:last,
QTabBar::tab:left:only-one
{
margin-bottom: 0.08em;
}
QTabBar::tab:left:!selected
{
color: #31363b;
background-color: #d9d8d7;
border: 0.04em transparent black;
border-top: 0.04em solid #bab9b8;
border-right: 0.04em solid #bab9b8;
border-radius: 0.09em;
border-top-right-radius: 0em;
border-bottom-right-radius: 0em;
}
QTabBar::tab:left:previous-selected
{
border-top: 0.04em transparent #d9d8d7;
border-top-right-radius: 0em;
border-bottom-right-radius: 0em;
}
QTabBar::tab:left:!selected:hover
{
background-color: rgba(255, 0, 20, 0.1);
border-radius: 0.09em;
border-top-right-radius: 0em;
border-bottom-right-radius: 0em;
}
QTabBar::tab:left:!selected:first:hover
{
background-color: rgba(255, 0, 20, 0.1);
border-radius: 0.09em;
border-top-right-radius: 0em;
border-bottom-right-radius: 0em;
}
/* RIGHT TABS */
QTabBar::tab:right,
QTabBar::tab:right:last,
QTabBar::tab:right:only-one
{
color: #31363b;
border: 0.04em transparent black;
border-top: 0.09em solid rgba(223, 52, 52, 0.5);
border-bottom: 0.04em solid #bab9b8;
border-right: 0.04em solid #bab9b8;
background-color: #eff0f1;
padding: 0.23em;
min-height: 50px;
border-radius: 0.09em;
border-top-left-radius: 0em;
border-bottom-left-radius: 0em;
}
QTabBar::tab:right:first,
QTabBar::tab:right:only-one
{
margin-top: 0.08em;
}
QTabBar::tab:right:last,
QTabBar::tab:right:only-one
{
margin-bottom: 0.08em;
}
QTabBar::tab:right:!selected
{
color: #31363b;
background-color: #d9d8d7;
border: 0.04em transparent black;
border-top: 0.04em solid #bab9b8;
border-left: 0.04em solid #bab9b8;
border-radius: 0.09em;
border-top-left-radius: 0em;
border-bottom-left-radius: 0em;
}
QTabBar::tab:right:previous-selected
{
border-top: 0.04em transparent #d9d8d7;
border-top-left-radius: 0em;
border-bottom-left-radius: 0em;
}
QTabBar::tab:right:!selected:hover
{
background-color: rgba(255, 0, 20, 0.1);
border-radius: 0.09em;
border-top-left-radius: 0em;
border-bottom-left-radius: 0em;
}
QTabBar::tab:right:!selected:first:hover
{
background-color: rgba(255, 0, 20, 0.1);
border-radius: 0.09em;
border-top-left-radius: 0em;
border-bottom-left-radius: 0em;
}
/**
* Special styles for triangular QTabWidgets.
* These ignore the border attributes, and the border and
* text color seems to be set via the `QTabBar::tab` color
* property. This seemingly cannot be changed.
*
* The rounded shapes are 0-3, and the triangular ones are 4-7.
*
* The QTabBar outline doesn't respect on QTabBar::tab:
* border-color
* outline-color
*/
QTabBar[shape="4"]::tab,
QTabBar[shape="5"]::tab,
QTabBar[shape="6"]::tab,
QTabBar[shape="7"]::tab,
QTabBar[shape="4"]::tab:last,
QTabBar[shape="5"]::tab:last,
QTabBar[shape="6"]::tab:last,
QTabBar[shape="7"]::tab:last,
QTabBar[shape="4"]::tab:only-one,
QTabBar[shape="5"]::tab:only-one,
QTabBar[shape="6"]::tab:only-one,
QTabBar[shape="7"]::tab:only-one
{
/* Need a dark color without alpha channel since it affects the text. */
color: #df3434;
background-color: #eff0f1;
padding: 0.23em;
}
QTabBar[shape="4"]::tab,
QTabBar[shape="5"]::tab,
QTabBar[shape="4"]::tab:last,
QTabBar[shape="5"]::tab:last,
QTabBar[shape="4"]::tab:only-one,
QTabBar[shape="5"]::tab:only-one
{
min-width: 50px;
}
QTabBar[shape="6"]::tab,
QTabBar[shape="7"]::tab,
QTabBar[shape="6"]::tab:last,
QTabBar[shape="7"]::tab:last,
QTabBar[shape="6"]::tab:only-one,
QTabBar[shape="7"]::tab:only-one
{
min-height: 50px;
}
QTabBar[shape="4"]::tab:!selected,
QTabBar[shape="5"]::tab:!selected,
QTabBar[shape="6"]::tab:!selected,
QTabBar[shape="7"]::tab:!selected
{
color: #31363b;
background-color: #d9d8d7;
}
/**
* Increase padding on the opposite side of the icon to avoid text clipping.
*
* BUG: The padding works for North, West, and East in Qt5, South does not
* work. All tab positions work for triangular tabs in Qt6.
*/
QTabBar[shape="4"][tabsClosable="true"]::tab,
QTabBar[shape="5"][tabsClosable="true"]::tab
{
padding-left: 0.5em;
}
QTabBar[shape="6"][tabsClosable="true"]::tab
{
padding-bottom: 0.5em;
}
QTabBar[shape="7"][tabsClosable="true"]::tab
{
padding-top: 0.5em;
}
/**
* Undo the padding for the tab.
*
* Enumerated values are North, South, West, East in that order,
* from 4-7.
*
* NOTE: Any higher padding will clip the icon.
*/
QTabBar[shape="4"]::close-button,
QTabBar[shape="5"]::close-button
{
padding-left: -0.12em;
}
QTabBar[shape="6"]::close-button
{
padding-bottom: -0.18em;
}
QTabBar[shape="7"]::close-button
{
padding-top: -0.18em;
}
QDockWidget
{
background: #eaebec;
/**
* It doesn't seem possible to change the border of the
* QDockWidget without changing the content margins.
*/
/**
* This is a bug fix so we can handle hover, pressed, and other events.
* Reference: https://stackoverflow.com/questions/32145080/qdockwidget-float-close-button-hover-images
*/
titlebar-close-icon: url(:/light fusion/transparent.svg);
titlebar-normal-icon: url(:/light fusion/transparent.svg);
}
/**
* Don't style the title, since it gives a weird, missing border
* around the rest of the dock widget, which the remaining border
* cannot be removed.
*
* There is a bug in non-Breeze styles, where the icons are small. It
* doesn't change if we use `image` instead of `border-image`, nor if
* we use `qproperty-icon`, etc. The icon seem to be half the size
* of our desired values.
*/
QDockWidget::close-button,
QDockWidget::float-button
{
border: 0.04em solid transparent;
border-radius: 0.09em;
background: transparent;
/* Maximum icon size for buttons */
icon-size: 14px;
}
QDockWidget::float-button
{
border-image: url(:/light fusion/transparent.svg);
image: url(:/light fusion/undock.svg);
}
QDockWidget::float-button:hover
{
image: url(:/light fusion/undock_hover.svg);
}
/* The :pressed events don't register, seems to be a Qt bug. */
QDockWidget::float-button:pressed
{
image: url(:/light fusion/undock_hover.svg);
}
QDockWidget::close-button
{
border-image: url(:/light fusion/transparent.svg);
image: url(:/light fusion/close.svg);
}
QDockWidget::close-button:hover
{
image: url(:/light fusion/close_hover.svg);
}
/* The :pressed events don't register, seems to be a Qt bug. */
QDockWidget::close-button:pressed
{
image: url(:/light fusion/close_pressed.svg);
}
QTreeView,
QListView
{
background-color: #eff0f1;
border: 0em solid black;
}
QTreeView:selected,
QTreeView:!selected,
QListView:selected,
QListView:!selected
{
border: 0em solid black;
}
QTreeView::branch:has-siblings
{
border-image: url(:/light fusion/vline.svg);
image: none;
}
/* These branch indicators don't scale */
QTreeView::branch:!has-siblings
{
border-image: none;
image: none;
}
QTreeView::branch:has-siblings:adjoins-item
{
border-image: url(:/light fusion/branch_more.svg);
}
QTreeView::branch:!has-children:!has-siblings:adjoins-item
{
border-image: url(:/light fusion/branch_end.svg);
}
QTreeView::branch:has-children:!has-siblings:closed,
QTreeView::branch:closed:has-children:has-siblings
{
image: url(:/light fusion/branch_closed.svg);
}
QTreeView::branch:has-children:!has-siblings:closed:hover,
QTreeView::branch:closed:has-children:has-siblings:hover
{
image: url(:/light fusion/branch_closed_hover.svg);
}
QTreeView::branch:has-children:!has-siblings:closed,
QTreeView::branch:open:has-children:!has-siblings
{
border-image: url(:/light fusion/branch_end_arrow.svg);
}
QTreeView::branch:closed:has-children:has-siblings,
QTreeView::branch:open:has-children:has-siblings
{
border-image: url(:/light fusion/branch_more_arrow.svg);
}
QTreeView::branch:open:has-children:!has-siblings,
QTreeView::branch:open:has-children:has-siblings
{
image: url(:/light fusion/branch_open.svg);
}
QTreeView::branch:open:has-children:!has-siblings:hover,
QTreeView::branch:open:has-children:has-siblings:hover
{
image: url(:/light fusion/branch_open_hover.svg);
}
QListView
{
/* Give space for elements aligned left or right. */
padding: 0.2em;
}
QTableView::item,
QListView::item,
QTreeView::item
{
padding: 0.13em;
color: #31363b;
}
QTreeView::item
{
/**
* Need to set the background color in Qt6, or else
* the QTreeView indicators use the style defaults,
* along with the box model, which conflicts with our
* theme (except with hover/focus/selected pseudostates).
*
* Affects issue #51.
*/
background-color: #eff0f1;
outline: 0;
}
QTableView::item:!selected:hover,
QListView::item:!selected:hover,
QTreeView::item:!selected:hover
{
background-color: rgba(255, 0, 20, 0.1);
outline: 0;
color: #31363b;
padding: 0.13em;
}
QAbstractItemView::item QLineEdit
{
border: 0em transparent black;
/*
* The top/bottom padding causes the editable widget to conceal text.
* https://github.com/Alexhuszagh/BreezeStyleSheets/issues/69
*/
padding: 0em;
}
QSlider::handle:horizontal,
QSlider::handle:vertical
{
background: #eff0f1;
border: 0.04em solid rgba(106, 105, 105, 0.7);
width: 0.7em;
height: 0.7em;
border-radius: 0.35em;
}
QSlider:horizontal
{
height: 2em;
}
QSlider:vertical
{
width: 2em;
}
QSlider::handle:horizontal
{
margin: -0.23em 0;
}
QSlider::handle:vertical
{
margin: 0 -0.23em;
}
QSlider::groove:horizontal,
QSlider::groove:vertical
{
background: #d9d8d7;
border: 0em solid #eff0f1;
border-radius: 0.19em;
}
QSlider::groove:horizontal
{
height: 0.4em;
}
QSlider::groove:vertical
{
width: 0.4em;
}
QSlider::handle:horizontal:hover,
QSlider::handle:horizontal:focus,
QSlider::handle:vertical:hover,
QSlider::handle:vertical:focus
{
border: 0.04em solid #df3434;
}
QSlider::handle:horizontal:!focus:!hover,
QSlider::handle:vertical:!focus:!hover
{
border: 0.04em solid rgba(106, 105, 105, 0.7);
}
QSlider::sub-page:horizontal,
QSlider::add-page:vertical
{
background: #df3434;
border-radius: 0.19em;
}
QSlider::add-page:horizontal,
QSlider::sub-page:vertical
{
background: rgba(106, 105, 105, 0.7);
border-radius: 0.19em;
}
/* QToolButton */
/**
* QToolButton's that have a push button need to be styled differently,
* depending on whether there are actions (a menu) associated with it.
* This is signaled by a drop-down arrow on the right of the push button.
* Unfortunately, there's no good property to determine this. The property
* we need is `QWidget::actions`, however, it's a method and not a
* property.Note that the drop-down menu is not signaled by any of the
* following:
* popupMode: any pop-up mode does not affect the right arrow style.
* arrowType: only replaces the icon.
* toolButtonStyle: this is almost always set to icon only, even with text.
* text: can have a drop-down menu with or without text.
*
* Notably, we need to ensure we don't pad the widgets in the following
* cases:
* 1. If the QToolButton is auto-raised.
* This adds undesired padding in`QFileDialog`. These widgets
* have text, even though no text is visible. This is not the
* default, so it won't affect most situations.
* 2. If the QToolButton does not have text.
* Normally, text-less buttons do not have a menu, and this
* is required for #47, since the padding affects the scroll
* bar icons in QTabBar. This causes major issues in the
* UI, so disable the padding by default.
*
* The padding can affect the placement of icons and other things
* inside the toolbutton: near the menu-button in QFileDialog,
* the clear text icon is misplaced vertically, making it nearly
* illegible.
*
* We provide special styles for a custom, dynamic property to
* override the padding decisions with or without a menu.
* To force styling as if there is a menu, set the `hasMenu` property
* to true. Setting `hasMenu` to false will style as if there is no menu.
* You can use `QWidget::setProperty` to set this property dynamically.
*
* The affected issues are #22, #28, #47.
* https://github.com/Alexhuszagh/BreezeStyleSheets/issues/22
* https://github.com/Alexhuszagh/BreezeStyleSheets/issues/28
* https://github.com/Alexhuszagh/BreezeStyleSheets/issues/47
*/
/**
* Use an overly specific selector here to ensure no margins,
* or for the default QToolButton. We must have `autoRaise="false"`
* and `text` to have padding, so just add a `hasMenu="false"` to
* undo the padding in that case. Also add selectors for QDialog
* if a menu is explicitly forbidden.
*/
QToolButton,
QToolButton[hasMenu="false"][autoRaise="false"][text],
QDialog QToolBar QToolButton[hasMenu="false"][popupMode="0"],
QDialog QToolBar QToolButton[hasMenu="false"][popupMode="1"],
QDialog QToolBar QToolButton[hasMenu="false"][popupMode="2"]
{
margin: 0em;
padding: 0em;
}
QToolButton[autoRaise="false"]
{
background-color: #eff0f1;
border: 0.04em solid #bab9b8;
border-radius: 0.09em;
}
QToolButton[autoRaise="true"]
{
background-color: #eff0f1;
border: 0.04em solid transparent;
}
/* Add selectors for the QDialog if a menu is explicitly requested. */
QToolButton[hasMenu="true"],
QToolButton[autoRaise="false"][text],
QDialog QToolBar QToolButton[hasMenu="true"][popupMode="0"],
QDialog QToolBar QToolButton[hasMenu="true"][popupMode="1"],
QDialog QToolBar QToolButton[hasMenu="true"][popupMode="2"]
{
margin: 0.23em;
padding: 0.23em;
padding-top: 0.1em;
padding-right: 1.2em;
}
QToolButton:hover
{
border: 0.04em solid rgba(223, 52, 52, 0.5);
}
QToolButton:checked,
QToolButton:pressed
{
border: 0.04em solid rgba(223, 52, 52, 0.5);
background-color: rgba(223, 52, 52, 0.5);
}
QToolButton::right-arrow,
QToolButton::left-arrow,
QToolButton::up-arrow,
QToolButton::down-arrow
{
/**
* Do not set the arrow width/height here. It causes
* small icons in Qt6, and doesn't affect the styling
* in Qt5. Both look ideal without manually specified sizes.
*/
subcontrol-origin: content;
subcontrol-position: center;
margin: 0em;
padding: 0em;
}
QToolButton::right-arrow:enabled
{
image: url(:/light fusion/right_arrow.svg);
}
QToolButton::left-arrow:enabled
{
image: url(:/light fusion/left_arrow.svg);
}
QToolButton::up-arrow:enabled
{
image: url(:/light fusion/up_arrow.svg);
}
QToolButton::down-arrow:enabled
{
image: url(:/light fusion/down_arrow.svg);
}
QToolButton::right-arrow:disabled
{
image: url(:/light fusion/right_arrow_disabled.svg);
}
QToolButton::left-arrow:disabled
{
image: url(:/light fusion/left_arrow_disabled.svg);
}
QToolButton::up-arrow:disabled
{
image: url(:/light fusion/up_arrow_disabled.svg);
}
QToolButton::down-arrow:disabled
{
image: url(:/light fusion/down_arrow_disabled.svg);
}
QToolButton::menu-indicator
{
border-image: none;
image: url(:/light fusion/down_arrow.svg);
width: 0.8em;
height: 0.5em;
left: -0.09em;
/* -1.2em + 0.09em */
padding-right: -1.11em;
/**
* Qt5 and Qt6 differ if the subcontrol-origin is set to
* the default, AKA, padding. Setting it to the content,
* which we adjust the padding to, makes it uniform between
* both.
*/
subcontrol-origin: content;
subcontrol-position: right;
}
/**
* Special rule for the drop-down indicator in a QFileDialog.
* We want these to be more compact, hence the smaller padding.
*/
QDialog QToolBar QToolButton[popupMode="2"]::menu-indicator
{
padding-right: -0.7em;
}
QToolButton::menu-arrow
{
border-image: none;
image: url(:/light fusion/down_arrow.svg);
width: 0.8em;
height: 0.5em;
padding-right: 0.09em;
subcontrol-position: right;
}
QToolButton::menu-button
{
border-top-right-radius: 0.5em;
border-bottom-right-radius: 0.5em;
/* 1ex width + 0.4ex for border + no text = 2ex allocated above */
width: 1.3em;
padding: 0.23em;
outline: none;
}
QToolButton::menu-button::menu-arrow
{
left: -0.09em;
subcontrol-position: right;
}
QToolButton::menu-button:hover
{
background-color: transparent;
}
QToolButton::menu-button:pressed
{
background-color: transparent;
padding: 0.23em;
outline: none;
}
QTableView
{
border: 0em solid black;
gridline-color: #bab9b8;
background-color: #eff0f1;
}
QTableView:!selected,
QTableView:selected
{
border: 0em solid black;
}
QTableView
{
border-radius: 0em;
}
QAbstractItemView::item
{
color: #31363b;
}
QAbstractItemView::item:pressed
{
background: rgba(200, 45, 45, 0.5);
color: #31363b;
}
QAbstractItemView::item:selected:!active
{
background: rgba(255, 0, 20, 0.1);
}
/* Use background with qlineargradient to avoid ugly border on widget. */
QAbstractItemView::item:selected:active
{
background: qlineargradient(
x1: 0.5, y1: 0.5
x2: 0.5, y2: 1,
stop: 0 rgba(200, 45, 45, 0.5),
stop: 1 rgba(200, 45, 45, 0.5)
);
color: #31363b;
}
QAbstractItemView::item:selected:hover
{
background: qlineargradient(
x1: 0.5, y1: 0.5
x2: 0.5, y2: 1,
stop: 0 rgba(243, 73, 73, 0.6),
stop: 1 rgba(243, 73, 73, 0.6)
);
color: #31363b;
}
QHeaderView
{
background-color: #eff0f1;
border: 0.04em transparent;
border-radius: 0em;
margin: 0em;
padding: 0em;
}
QHeaderView::section
{
background-color: #eff0f1;
border: 0.04em solid #bab9b8;
color: #31363b;
border-radius: 0em;
padding: 0em 0.23em 0em 0.23em;
text-align: center;
}
QHeaderView::section::vertical::first,
QHeaderView::section::vertical::only-one
{
border-top: 0.04em solid #bab9b8;
}
QHeaderView::section::vertical
{
border-top: transparent;
}
QHeaderView::section::horizontal::first,
QHeaderView::section::horizontal::only-one
{
border-left: 0.04em solid #bab9b8;
}
QHeaderView::section::horizontal
{
border-left: transparent;
}
QHeaderView[showSortIndicator="true"]::section::horizontal
{
/* Same as the width of the arrow subcontrols below. */
padding-right: 0.8em;
}
QHeaderView::section:checked
{
color: #272b2f;
background-color: #da7c7c;
}
/* Note that this doesn't work for QTreeView unless the header is clickable */
QHeaderView::section:hover,
QHeaderView::section::horizontal::first:hover,
QHeaderView::section::horizontal::only-one:hover,
QHeaderView::section::vertical::first:hover,
QHeaderView::section::vertical::only-one:hover
{
border: 0.04em solid rgba(223, 52, 52, 0.5);
}
QHeaderView[showSortIndicator="true"]::down-arrow
{
image: url(:/light fusion/down_arrow.svg);
/**
* Qt5 and Qt6 differ if the subcontrol-origin is set to
* the default, AKA, padding. Setting it to the content,
* which we adjust the padding to, makes it uniform between
* both.
*/
subcontrol-origin: content;
subcontrol-position: center right;
width: 0.8em;
height: 0.5em;
/**
* Qt5 and Qt6 have different ideas of the padding of the
* arrow subcontrols: using `padding-left` to ensure that
* the width is undone fixes the padding of the content by
* an extra `0.8em` in Qt6, but doesn't affect Qt5.
*/
padding-right: 0.09em;
padding-left: -0.8em;
}
QHeaderView[showSortIndicator="true"]::up-arrow
{
image: url(:/light fusion/up_arrow.svg);
subcontrol-origin: content;
subcontrol-position: center right;
width: 0.8em;
height: 0.5em;
padding-right: 0.09em;
padding-left: -0.8em;
}
QTableView QTableCornerButton::section
{
background-color: #eff0f1;
border: 0.04em transparent #bab9b8;
border-top: 0.04em solid #bab9b8;
border-left: 0.04em solid #bab9b8;
border-radius: 0em;
}
/* No hover event */
QTableView QTableCornerButton:hover
{
border: 0.04em transparent #bab9b8;
}
QTableView QTableCornerButton::section:pressed
{
border: 0.04em solid rgba(223, 52, 52, 0.5);
border-radius: 0em;
}
QToolBox
{
padding: 0.23em;
border: 0.09em transparent black;
}
QToolBox::tab
{
border-bottom: 0.09em solid #bab9b8;
margin-left: 1.5em;
}
QToolBox::tab:selected,
QToolBox::tab:hover
{
border-bottom: 0.09em solid rgba(223, 52, 52, 0.5);
}
QSplitter::handle
{
border: 0.09em solid #d9d8d7;
background: -0.5em solid #d9d8d7;
max-width: 0em;
max-height: 0em;
}
/**
* It's not possible to get satisfactory rounded borders here.
* If you set the border to be negative, while adjusting the
* widths, you get an asymmetrical curve which produces an
* unappealing border.
*/
QProgressBar:horizontal,
QProgressBar:vertical
{
background-color: rgba(106, 105, 105, 0.7);
border: 0.9em solid #eff0f1;
border-radius: 0.13em;
padding: 0em;
}
QProgressBar:horizontal
{
height: 0.2em;
min-width: 6em;
text-align: right;
padding-left: -0.03em;
padding-right: -0.03em;
margin-top: 0.2em;
margin-bottom: 0.2em;
margin-right: 1.3em;
}
QProgressBar:vertical
{
width: 0.2em;
min-height: 6em;
text-align: bottom;
padding-top: -0.03em;
padding-bottom: -0.03em;
margin-left: 0.2em;
margin-right: 0.2em;
margin-bottom: 0.41em;
}
QProgressBar::chunk:horizontal,
QProgressBar::chunk:vertical
{
background-color: #df3434;
border: 0.9em transparent;
border-radius: 0.08em;
}
QScrollArea,
QScrollArea:focus,
QScrollArea:hover
{
border: 0em solid black;
}
/* ICONS */
/**
* Qt's built-in icons can look pretty bad if the system theme
* is a different color than the current one. For example, when
* using a dark theme, with a light UI, the `Ok` button is greyed
* out for an about dialog.
*
* QDialogButtonBox will apply for all standard buttons in all standard
* widgets, such as QMessageBox, etc. However, we do need to override
* standard icons elsewhere.
*
* The rest of the icons make little sense to implement:
* Qt uses native window decorations.
* Qt normally uses native file dialogs, which look nicer.
* Media controls are used in custom widgets, which aren't standard.
*/
QDialogButtonBox
{
dialogbuttonbox-buttons-have-icons: true;
dialog-cancel-icon: url(:/light fusion/dialog_cancel.svg);
dialog-close-icon: url(:/light fusion/dialog_close.svg);
dialog-ok-icon: url(:/light fusion/dialog_ok.svg);
dialog-open-icon: url(:/light fusion/dialog_open.svg);
dialog-reset-icon: url(:/light fusion/dialog_reset.svg);
dialog-save-icon: url(:/light fusion/dialog_save.svg);
/**
* No support yet for overriding saveall.
* dialog-saveall-icon: url(:/light fusion/dialog_saveall.svg);
*/
dialog-yes-icon: url(:/light fusion/dialog_ok.svg);
dialog-help-icon: url(:/light fusion/dialog_help.svg);
dialog-no-icon: url(:/light fusion/dialog_no.svg);
dialog-apply-icon: url(:/light fusion/dialog_ok.svg);
dialog-discard-icon: url(:/light fusion/dialog_discard.svg);
}
/* Set some styles for these custom dialog buttons */
QDialogButtonBox QPushButton,
QMessageBox QPushButton
{
min-height: 1.1em;
min-width: 5em;
}
/**
* Special rules for creating a custom titlebar. This can only work
* when setting the Qt property `isTitlebar` to `true`.
*/
QWidget[isTitlebar="true"],
QWidget[isTitlebar="true"] *
{
background-color: #d9d8d7;
}
/**
* Special rules for creating a border around a top-level frame of a window.
* This can only work when setting the Qt property `isWindow` to `true`.
* We've manually enumerated border widths from 1-5 below.
*/
QFrame[isWindow="true"],
QFrame[frameShape][isWindow="true"]
{
border: 0px transparent #d9d8d7;
}
QFrame[isWindow="true"][windowFrame="1"],
QFrame[frameShape][isWindow="true"][windowFrame="1"]
{
border: 1px solid #d9d8d7;
border-radius: 3px;
}
QFrame[isWindow="true"][windowFrame="2"],
QFrame[frameShape][isWindow="true"][windowFrame="2"]
{
border: 2px solid #d9d8d7;
border-radius: 3px;
}
QFrame[isWindow="true"][windowFrame="3"],
QFrame[frameShape][isWindow="true"][windowFrame="3"]
{
border: 3px solid #d9d8d7;
border-radius: 3px;
}
QFrame[isWindow="true"][windowFrame="4"],
QFrame[frameShape][isWindow="true"][windowFrame="4"]
{
border: 4px solid #d9d8d7;
border-radius: 3px;
}
QFrame[isWindow="true"][windowFrame="5"],
QFrame[frameShape][isWindow="true"][windowFrame="5"]
{
border: 5px solid #d9d8d7;
border-radius: 3px;
}
/**
* ADVANCED DOCKING SYSTEM STYLESHEET
* ----------------------------------
*/
/**
* The general approach is as follows:
* 1. Turn `qproperty-icon` off.
* This avoids having a weird background, preventing our desired icon.
* This is presumably because ADS uses `qproperty-icon`.
* 2. Ensure the background is set.
* This is to avoid any QToolButton styling hints when the
* widget is clicked.
* 3. Set the QTOolButton width and height.
* This gives us consistent icon sizes without compression.
* 4. Undo the border.
* Must keep the border width identical to before (0.04em),
* to avoid moving the widgets on pressed/hover events.
*/
#tabCloseButton,
#dockAreaCloseButton,
#tabsMenuButton,
#detachGroupButton,
#floatingTitleCloseButton,
#floatingTitleMaximizeButton
{
qproperty-icon: url(:/light fusion/transparent.svg);
background: #eff0f1;
width: 1.2em;
height: 1.2em;
padding: 0em;
margin: 0em;
border: 0.04em transparent black;
}
#tabsMenuButton,
#floatingTitleMaximizeButton
{
/* Need to make the icon smaller, or else it's unusually large. */
width: 0.8em;
}
#tabCloseButton:hover,
#dockAreaCloseButton:hover,
#tabsMenuButton:hover,
#detachGroupButton:hover,
#floatingTitleCloseButton:hover,
#floatingTitleMaximizeButton:hover,
#tabCloseButton:pressed,
#dockAreaCloseButton:pressed,
#tabsMenuButton:pressed,
#detachGroupButton:pressed,
#floatingTitleCloseButton:pressed,
#floatingTitleMaximizeButton:pressed
{
background: #eff0f1;
}
#tabCloseButton,
#dockAreaCloseButton,
#floatingTitleCloseButton
{
image: url(:/light fusion/ads_close.svg);
}
#tabCloseButton:hover,
#dockAreaCloseButton:hover,
#floatingTitleCloseButton:hover
{
image: url(:/light fusion/ads_close_hover.svg);
}
#tabCloseButton:pressed,
#dockAreaCloseButton:pressed,
#floatingTitleCloseButton:pressed
{
image: url(:/light fusion/ads_close_pressed.svg);
}
#tabsMenuButton
{
image: url(:/light fusion/ads_menu_button.svg);
}
#tabsMenuButton:hover
{
image: url(:/light fusion/ads_menu_button_hover.svg);
}
#tabsMenuButton:pressed
{
image: url(:/light fusion/ads_menu_button_pressed.svg);
}
#tabsMenuButton::menu-indicator
{
image: none;
}
#detachGroupButton
{
image: url(:/light fusion/ads_detach.svg);
}
#detachGroupButton:hover
{
image: url(:/light fusion/ads_detach_hover.svg);
}
#detachGroupButton:pressed
{
image: url(:/light fusion/ads_detach_hover_pressed.svg);
}
/* FLOATING */
/* Disable the default icons when the dock is floating. */
ads--CFloatingWidgetTitleBar
{
qproperty-maximizeIcon: url(:/light fusion/transparent.svg);
qproperty-normalIcon: url(:/light fusion/transparent.svg);
}
#floatingTitleMaximizeButton
{
image: url(:/light fusion/ads_maximize.svg);
}
#floatingTitleMaximizeButton:hover
{
image: url(:/light fusion/ads_maximize_hover.svg);
}
#floatingTitleMaximizeButton:pressed
{
image: url(:/light fusion/ads_maximize_pressed.svg);
}
/**
* Using the `maximized="true"`, `isMaximized="true"`, or other attribute
* selectors don't work, and since the maximize button and minimize
* button are always the same...
*
* To get a nicer looking UI, just use the same maximize and restore
* buttons.
*/
/* TABS */
ads--CDockWidgetTab
{
border: 0.04em solid #bab9b8;
border-top: 0.09em solid #bab9b8;
background-color: #d9d8d7;
padding: 0.23em;
min-width: 50px;
border-radius: 0.09em;
border-bottom-left-radius: 0em;
border-bottom-right-radius: 0em;
}
ads--CDockWidgetTab[activeTab="true"]
{
background-color: #eff0f1;
border-top: 0.09em solid rgba(223, 52, 52, 0.5);
border-left: 0.04em solid #bab9b8;
border-right: 0.04em solid #bab9b8;
border-bottom: 0.04em transparent #bab9b8;
}
ads--CDockWidgetTab QLabel
{
background-color: #d9d8d7;
}
ads--CDockWidgetTab[activeTab="true"] QLabel
{
background-color: #eff0f1;
}
/**
* CDockWidgetTab doesn't seem to have the concept of `::next-selected`
* and `::previous-selected`, so we just draw the borders for everything.
* It's not nearly as pretty, but it's not bad either.
*/
/* OVERLAY */
ads--CDockOverlayCross
{
qproperty-iconFrameColor: rgba(223, 52, 52, 0.5);
qproperty-iconBackgroundColor: #eff0f1;
qproperty-iconOverlayColor: rgba(223, 52, 52, 0.5);
qproperty-iconArrowColor: #31363b;
qproperty-iconShadowColor: transparent;
}
/**
* This adds support for the focus highlighting feature of the ADS.
* https://github.com/githubuser0xFFFF/Qt-Advanced-Docking-System/blob/master/doc/user-guide.md#focushighlighting
*/
ads--CDockWidgetTab[focused="true"]
{
background-color: rgba(232, 63, 63, 0.2);
border-color: rgba(232, 63, 63, 0.25);
border-top-color: rgba(223, 52, 52, 0.5);
}
ads--CDockWidgetTab[focused="true"] QLabel,
ads--CDockWidgetTab[focused="true"] #tabCloseButton
{
background-color: transparent;
}
/**
* QDOCKWIDGET TOOLTIP STYLESHEET
* ------------------------------
*/
QAbstractButton#qt_dockwidget_closebutton
{
qproperty-toolTip: "Close";
}
QAbstractButton#qt_dockwidget_floatbutton
{
qproperty-toolTip: "Detach";
}
================================================
FILE: resources/locales/ui.qm
================================================
about_ui_trAbout PersepolisPersepolis Download Manager<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!DevelopersTranslatorsLicenseOK
AliReza AmirSamimi
Mohammadreza Abdollahzadeh
Sadegh Alirezaie
Mostafa Asadi
Jafar Akhondali
Kia Hamedi
H.Rostami
Ehsan Titish
MohammadAmin VahediniaTRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!Acknowledgments<a href=https://github.com/yt-dlp/yt-dlp>YT-DLP project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/ffmpeg/ffmpeg>FFmpeg project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://wiki.qt.io/Qt_for_Python>Pyside project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/psf/requests>Requests project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!addlink_ui_trAdd to category: ProxyIP: Port:Change Download FolderDownload Folder: CancelOKLinkMore OptionsAdvanced OptionsAdd Download LinkDownload link: Change file name: Detect System Proxy SettingsProxy password: Proxy username: Download username and passwordDownload username: Download password: Remember this pathStart timeEnd timeNumber of connections:Download LaterHTTPHTTPSSOCKS5after_download_src_ui_tr<b>File name</b>: <b>Size</b>: after_download_ui_trPersepolis Download Manager Open File Open Download Folder OK Don't show this message again.<b>Download Completed!</b><b>Save as</b>: <b>Link</b>: log_window_ui_trReport IssueInitialization and informationDownloadsErrors and warningsmainwindow_src_ui_trError: <b>Link</b>: <b>Downloaded</b>: <b>Transfer rate</b>: <b>Estimated time left</b>: <b>Connections</b>: <b>Status</b>: Download StoppedError - Download Complete<b><center>This link has been added before! Are you sure you want to add it again?</center></b>Download StartsOperation was not successful.Please resume the following category: Not enough free space in:muxing erroran error occurredPlease update Persepolis.<b>Fragments</b>: mainwindow_ui_trPersepolis Download ManagerShow/Hide system tray iconAdd New Download LinkResume DownloadPause DownloadStop/Cancel DownloadPropertiesProgressExitStart TimeEnd TimeDownload bottom of
the list firstApplyAfter downloadShut Down<b>Video file status: </b><b>Audio file status: </b><b>Status: </b><b>Muxing status: </b> downloadedMinimize to System TrayHide OptionsKeep System Awake!<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Start Mixing<b>Video File Status: </b><b>Audio File Status: </b><b>Mixing status: </b>progress_ui_trPersepolis Download ManagerStatus: Downloaded:Transfer rate: Estimated time left:Number of connections: Download InformationAfter downloadApplyShut DownDownload OptionsResumePauseStopLink: setting_src_ui_tr<b><center>This shortcut has been used before! Use another one!</center></b>setting_ui_trPreferencesNumber of tries: <html><head/><body><p>Set timeout in seconds. </p></body></html>Timeout (seconds): <html><head/><body><p>Using multiple connections can help speed up your download.</p></body></html>Number of connections: ChangeDownload OptionsVolume: NotificationsStyle: Color scheme: Icons: Notification type: Font: Size: Run Persepolis at startupKeep system awake!<html><head/><body><p>Format HH:MM</p></body></html>StatusVideo Finder OptionsDefaultsCancelOKPress new keysShortcutsHide main window if close button clicked.<html><head/><body><p>This feature may not work in your operating system.</p></body></html>Language: Wait period between retries (seconds): Don't use certificate to verify the peers<html><head/><body><p>This option avoids SSL/TLS handshake failure. But use it at your own risk!</p></body></html>Download folder: Create subfolders for Music,Videos, ... in default download folderSave AsEnable Notification SoundsToolbar icons size: Enable system tray iconShow download complete dialog when download is finishedShow menubarShow side panelShow download progress window<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Columns CustomizationCheck system clipboard for copied links<html><head/><body><p>The program will automatically check the clipboard for copied links. </p></body></html>Download requests from the browser will be executed immediately.<html><head/><body><p>When a download request is sent from the browser extension, the download will start without showing the Add Link window. </p></body></html><html><head/><body><p>Set number of retries if download failed.</p></body></html><html><head/><body><p>Set the seconds to wait between retries.</p></body></html>Chunk size(KiB): It is python requests library chunk size. Do not change this If you are not familiar with it.text_ui_trPersepolis Download ManagerIP:Port:Change Download FolderProxy password: Proxy username: Download username and passwordDownload username: Download password: Download folder: Number of connections:video_finder_progress_ui_tr<b>Video file status: </b><b>Audio file status: </b><b>Muxing status: </b><b>Mixing status: </b>
================================================
FILE: resources/locales/ui_ar.ts
================================================
about_ui_trAbout Persepolisحول بيرسيبولسPersepolis Download Managerمدير التحميل بيرسيبولس<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a><a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a><a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>DevelopersالمطورونTranslatorsالمترجمونLicenseالرخصةOKتم
AliReza AmirSamimi
Mohammadreza Abdollahzadeh
Sadegh Alirezaie
Mostafa Asadi
Jafar Akhondali
Kia Hamedi
H.Rostami
Ehsan Titish
MohammadAmin VahediniaTRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!Acknowledgments<a href=https://github.com/yt-dlp/yt-dlp>YT-DLP project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/ffmpeg/ffmpeg>FFmpeg project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://wiki.qt.io/Qt_for_Python>Pyside project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/psf/requests>Requests project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!addlink_ui_trAdd to category: إضافة إلى فئة:ProxyالبروكسيIP: عنوان الآي بي:Port:المنفذ:Change Download Folderتعديل مكان تحميل الملفات:Download Folder: مجلد التحميل:CancelإلغاءOKتمLinkالرابطMore Optionsخيارات إضافيةAdvanced Optionsخيارات متقدمةAdd Download Linkإضافة رابط تحميلDownload link: رابط التحميل:Change file name: تعديل اسم الملف:Detect System Proxy Settingsاكتشاف إعدادات البروكسي للنظامProxy password: كلمة سر البروكسي:Proxy username: اسم مستخدم للبروكسي:Download username and passwordDownload username: Download password: Remember this pathStart timeEnd timeNumber of connections:Download LaterHTTPHTTPSSOCKS5after_download_src_ui_tr<b>File name</b>: <b>اسم الملف</b>:<b>Size</b>: <b>الحجم</b>:after_download_ui_trPersepolis Download Managerمدير التحميل بيرسيبولس Open File فتح ملفOpen Download Folderفتح مجلد التحميل OK تمDon't show this message again.لا تعرض هذه الرسالة مرة أخرى.<b>Download Completed!</b><b>اكتمل التحميل!</b><b>Save as</b>: <b>حفظ باسم</b>:<b>Link</b>: <b>الرابط</b>:log_window_ui_trReport Issueالإبلاغ عن مشكلةInitialization and informationDownloadsErrors and warningsmainwindow_src_ui_trError: خطأ:<b>Link</b>: <b>الرابط</b>:<b>Downloaded</b>: <b>المحمل</b>:<b>Transfer rate</b>: <b>معدل النقل</b>:<b>Estimated time left</b>: <b>الوقت المتبقي</b>:<b>Connections</b>: <b>الاتصالات</b>:<b>Status</b>: <b>الحالة</b>:Download Stoppedتوقف التحميلError - خطأ - Download Completeاكتمل التحميل<b><center>This link has been added before! Are you sure you want to add it again?</center></b><b><center>لقد تم إضافة هذا الرابط من قبل! هل تريد إضافته مرة أخرى؟</center></b>Download Startsبدء التحميلOperation was not successful.لم تتم العملية بنجاح.Please resume the following category: يرجى استكمال الفئة التالية:Not enough free space in:لا يوجد مساحة كافية في:muxing errorخطأ بالمزجan error occurredPlease update Persepolis.<b>Fragments</b>: mainwindow_ui_trPersepolis Download Managerمدير التحميل بيرسيبولسShow/Hide system tray iconإظهار/إخفاء أيقونة علبة النظامAdd New Download Linkإضافة رابط تحميل جديدResume Downloadاستئناف التحميلPause Downloadإيقاف التحميل مؤقتاَStop/Cancel Downloadإيقاف/إلغاء التحميلPropertiesالخصائصProgressالتقدمExitخروجStart Timeوقت البدءEnd Timeوقت الانتهاءDownload bottom of
the list firstالتحميل من أسفل
القائمة أولاَApplyتطبيقAfter downloadبعد التحميلShut Downإيقاف التشغيل<b>Video file status: </b><b>حالة ملف الفيديو: </b><b>Audio file status: </b><b>حالة ملف الصوت: </b><b>Status: </b><b>الحالة: </b><b>Muxing status: </b><b>حالة المزج: </b> downloadedتحملMinimize to System TrayHide OptionsKeep System Awake!<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Start Mixing<b>Video File Status: </b><b>Audio File Status: </b><b>Mixing status: </b>progress_ui_trPersepolis Download Managerمدير التحميل بيرسيبولسStatus: الحالة:Downloaded:المحمل:Transfer rate: معدل النقل:Estimated time left:الوقت المتبقي:Number of connections: عدد الاتصالات:Download Informationمعلومات التحميلAfter downloadبعد التحميلApplyتطبيقShut Downإيقاف التشغيلDownload Optionsخيارات التحميلResumeاستئنافPauseإيقاف مؤقتStopإيقافLink: setting_src_ui_tr<b><center>This shortcut has been used before! Use another one!</center></b><b><center>لقد تم استخدام هذا الاختصار مسبقا! استخدم اختصار آخر!</center></b>setting_ui_trPreferencesالتفضيلاتNumber of tries: عدد مرات المحاولة:<html><head/><body><p>Set timeout in seconds. </p></body></html><html><head/><body><p> اضبط وقت الانتهاء بالثواني. </p></body></html>Timeout (seconds): وقت الانتهاء (بالثواني):<html><head/><body><p>Using multiple connections can help speed up your download.</p></body></html><html><head/><body><p> استخدام اتصالات متعددة يمكن أن يساعد في زيادة سرعة التحميل. </p></body></html>Number of connections: عدد الاتصالات:ChangeتغييرDownload Optionsخيارات التحميلVolume: الحجم:NotificationsالتنبيهاتStyle: النمط:Color scheme: مخطط الألوان:Icons: الأيقونات:Notification type: نوع التنبيهات:Font: الخط:Size: الحجم:Run Persepolis at startupتشغيل بيرسيبولس عند بدء النظامKeep system awake!ابقاء الجهاز منتبه!<html><head/><body><p>Format HH:MM</p></body></html><html><head/><body><p> الصيغة د د:س س </p></body></html>StatusالحالةVideo Finder Optionsخيارات ملتقط الفيديوDefaultsالافتراضياتCancelإلغاءOKتمPress new keysاضغط مفاتيح جديدةShortcutsالاختصاراتHide main window if close button clicked.إخفاء النافذة الرئيسية عند النقر على زر الإغلاق.<html><head/><body><p>This feature may not work in your operating system.</p></body></html><html><head/><body><p>قد لا تعمل هذه الخاصية في نظام التشغيل الخاص بك.</p></body></html>Language: Wait period between retries (seconds): Don't use certificate to verify the peers<html><head/><body><p>This option avoids SSL/TLS handshake failure. But use it at your own risk!</p></body></html>Download folder: Create subfolders for Music,Videos, ... in default download folderSave AsEnable Notification SoundsToolbar icons size: Enable system tray iconShow download complete dialog when download is finishedShow menubarShow side panelShow download progress window<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Columns CustomizationCheck system clipboard for copied links<html><head/><body><p>The program will automatically check the clipboard for copied links. </p></body></html>Download requests from the browser will be executed immediately.<html><head/><body><p>When a download request is sent from the browser extension, the download will start without showing the Add Link window. </p></body></html><html><head/><body><p>Set number of retries if download failed.</p></body></html><html><head/><body><p>Set the seconds to wait between retries.</p></body></html>Chunk size(KiB): It is python requests library chunk size. Do not change this If you are not familiar with it.text_ui_trPersepolis Download Managerمدير التحميل بيرسيبولسIP:عنوان الآي بي:Port:المنفذ:Change Download Folderتغيير مجلد التحميلProxy password: Proxy username: Download username and passwordDownload username: Download password: Download folder: Number of connections:video_finder_progress_ui_tr<b>Video file status: </b><b>حالة ملف الفيديو: </b><b>Audio file status: </b><b>حالة ملف الصوت: </b><b>Muxing status: </b><b>حالة المزج: </b><b>Mixing status: </b>
================================================
FILE: resources/locales/ui_de.ts
================================================
about_ui_trAbout PersepolisÜber PersepolisPersepolis Download ManagerPersepolis Download Manager<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a><a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a><a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>DevelopersEntwicklerTranslatorsÜbersetzerLicenseLizenzOKOK
AliReza AmirSamimi
Mohammadreza Abdollahzadeh
Sadegh Alirezaie
Mostafa Asadi
Jafar Akhondali
Kia Hamedi
H.Rostami
Ehsan Titish
MohammadAmin VahediniaTRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!Acknowledgments<a href=https://github.com/yt-dlp/yt-dlp>YT-DLP project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/ffmpeg/ffmpeg>FFmpeg project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://wiki.qt.io/Qt_for_Python>Pyside project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/psf/requests>Requests project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!addlink_ui_trAdd to category: Zu Kategorie hinzufügen:ProxyProxyIP: IP: Port:Port:Change Download FolderDownload Ordner wechselnDownload Folder: Download Ordner:CancelAbbruchOKOKLinkLinkMore OptionsMehr OptionenAdvanced OptionsFortgeschrittene OptionenAdd Download LinkDownload link: Change file name: Detect System Proxy SettingsProxy password: Proxy username: Download username and passwordDownload username: Download password: Remember this pathStart timeEnd timeNumber of connections:Download LaterHTTPHTTPSSOCKS5after_download_src_ui_tr<b>File name</b>: <b>Dateiname</b>: <b>Size</b>: <b>Dateigröße</b>: after_download_ui_trPersepolis Download ManagerPersepolis Download Manager Open File Datei öffnenOpen Download FolderDownload Ordner öffnen OK OKDon't show this message again.Diese Nachricht nicht mehr anzeigen.<b>Download Completed!</b><b>Download beendet!</b><b>Save as</b>: <b>Speichern als</b>: <b>Link</b>: <b>Link</b>: log_window_ui_trReport IssueProblem berichtenInitialization and informationDownloadsErrors and warningsmainwindow_src_ui_trError: Fehler:<b>Link</b>: <b>Link</b>: <b>Downloaded</b>: <b>Hertuntergeladen</b>: <b>Transfer rate</b>: <b>Übertragungsrate</b>: <b>Estimated time left</b>: <b>Geschätzte Übertragungszeit</b>: <b>Connections</b>: <b>Verbindungen</b>: <b>Status</b>: <b>Status</b>: Download StoppedDownload gestopptError - Fehler -Download CompleteDownload abgeschlossen<b><center>This link has been added before! Are you sure you want to add it again?</center></b><b><center>Dieser Link wurde bereits hinzugefügt! Wollen Sie Ihn erneut hinzufügen?</center></b>Download StartsDownload startetOperation was not successful.Der Vorgang war nicht erfolgreich.Please resume the following category: Bitte mit folgender Kategorie fortfahren:Not enough free space in:Nicht genug freier Speicher in:muxing errorMuxing-Fehleran error occurredPlease update Persepolis.<b>Fragments</b>: mainwindow_ui_trPersepolis Download ManagerPersepolis Download ManagerShow/Hide system tray iconSymbol in der Systemleiste anzeigen/verbergenAdd New Download LinkNeuen Download Link hinzufügenResume DownloadDownload fortfahrenPause DownloadDownload pausierenStop/Cancel DownloadDownload stoppen/abbrechenPropertiesEigenschaftenProgressFortschrittExitSchließenStart TimeStartzeitEnd TimeAbschlusszeitDownload bottom of
the list firstEnde der Liste
zuerst herunterladenApplyAnwendenAfter downloadNach dem DownloadShut DownHerunterfahren<b>Video file status: </b>Status der Videodatei:<b>Audio file status: </b>Audiodatei Status:<b>Status: </b>Status:<b>Muxing status: </b>Muxing Status: downloadedheruntergeladenMinimize to System TrayHide OptionsKeep System Awake!<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Start Mixing<b>Video File Status: </b><b>Audio File Status: </b><b>Mixing status: </b>progress_ui_trPersepolis Download ManagerPersepolis Download ManagerStatus: Status: Downloaded:Heruntergeladen:Transfer rate: Übertragungsgeschwindigkeit:Estimated time left:Geschätzte Übertragungszeit:Number of connections: Verbindungen:Download InformationDownload InformationAfter downloadNach dem DownloadApplyAndwendenShut DownHerunterfahrenDownload OptionsDownload OptionenResumeFortfahrenPausePauseStopStopLink: setting_src_ui_tr<b><center>This shortcut has been used before! Use another one!</center></b>Dieser Shortcut wurde bereits verwendet. - Bitte verwende einen anderen.setting_ui_trPreferencesEinstellungenNumber of tries: Anzahl der Versuche:<html><head/><body><p>Set timeout in seconds. </p></body></html><html><head/><body><p>Zeitüberschreitung in Sekunden. </p></body></html>Timeout (seconds): Zeitüberschreitung (Sekunden):<html><head/><body><p>Using multiple connections can help speed up your download.</p></body></html><html><head/><body><p>Mehrfache gleichzeitige Verbindungen können einen Download beschleunigen. </p></body></html>Number of connections: Anzahl der Verbindungen:ChangeÄndernDownload OptionsDownload OptionenVolume: Lautstärke:NotificationsBenachrichtigungenStyle: Stil:Color scheme: Farbschema:Icons: Symbole:Notification type: Benachrichtigungsart:Font: Schriftart:Size: Größe:Run Persepolis at startupPersepolis beim Systemstart ladenKeep system awake!Ruhezustand des Systems verhindern!<html><head/><body><p>Format HH:MM</p></body></html>Zeitformat HH:MMStatusStatusVideo Finder OptionsOptionen zur Video SucheDefaultsStandardCancelAbbruchOKOKPress new keysDrücke eine neue TasteShortcutsShortcutsHide main window if close button clicked.Verstecke das Hauptfenster wenn der Schließen-Knopf geklickt wurde.<html><head/><body><p>This feature may not work in your operating system.</p></body></html>Diese Funktion funktioniert möglicherweise nicht in Ihrem Betriebssystem.Language: Wait period between retries (seconds): Don't use certificate to verify the peers<html><head/><body><p>This option avoids SSL/TLS handshake failure. But use it at your own risk!</p></body></html>Download folder: Create subfolders for Music,Videos, ... in default download folderSave AsEnable Notification SoundsToolbar icons size: Enable system tray iconShow download complete dialog when download is finishedShow menubarShow side panelShow download progress window<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Columns CustomizationCheck system clipboard for copied links<html><head/><body><p>The program will automatically check the clipboard for copied links. </p></body></html>Download requests from the browser will be executed immediately.<html><head/><body><p>When a download request is sent from the browser extension, the download will start without showing the Add Link window. </p></body></html><html><head/><body><p>Set number of retries if download failed.</p></body></html><html><head/><body><p>Set the seconds to wait between retries.</p></body></html>Chunk size(KiB): It is python requests library chunk size. Do not change this If you are not familiar with it.text_ui_trPersepolis Download ManagerPersepolis Download ManagerIP:IP:Port:Port:Change Download FolderDownload Ordner ändernProxy password: Proxy username: Download username and passwordDownload username: Download password: Download folder: Number of connections:video_finder_progress_ui_tr<b>Video file status: </b>Status der Videodatei:<b>Audio file status: </b>Status der Audiodatei:<b>Muxing status: </b>Muxing Status:<b>Mixing status: </b>
================================================
FILE: resources/locales/ui_es_ES.ts
================================================
about_ui_trAbout PersepolisAcerca de PersepolisPersepolis Download ManagerGestor de descargas Persepolis<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a><a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a><a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>DevelopersDesarrolladores:TranslatorsTraductoresLicenseLicenciaOKAceptar
AliReza AmirSamimi
Mohammadreza Abdollahzadeh
Sadegh Alirezaie
Mostafa Asadi
Jafar Akhondali
Kia Hamedi
H.Rostami
Ehsan Titish
MohammadAmin VahediniaTRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!Acknowledgments<a href=https://github.com/yt-dlp/yt-dlp>YT-DLP project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/ffmpeg/ffmpeg>FFmpeg project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://wiki.qt.io/Qt_for_Python>Pyside project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/psf/requests>Requests project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!addlink_ui_trAdd to category: Añadir a una categoría:ProxyProxyIP: IP: Port:Puerto:Change Download FolderCambiar la carpeta de descargasDownload Folder: Carpeta de descargas:CancelCancelarOKAceptarLinkEnlaceMore OptionsMás opcionesAdvanced OptionsOpciones avanzadasAdd Download LinkDownload link: Change file name: Detect System Proxy SettingsProxy password: Proxy username: Download username and passwordDownload username: Download password: Remember this pathStart timeEnd timeNumber of connections:Download LaterHTTPHTTPSSOCKS5after_download_src_ui_tr<b>File name</b>: <b>Nombre del archivo</b>: <b>Size</b>: <b>Tamaño</b>: after_download_ui_trPersepolis Download ManagerGestor de descargas Persepolis Open File Abrir un archivoOpen Download FolderAbrir la carpeta de de descargas OK Aceptar Don't show this message again.No mostrar este mensaje nuevamente.<b>Download Completed!</b><b>¡Descarga completada!</b><b>Save as</b>: <b>Guardar como</b>: <b>Link</b>: <b>Enlace</b>: log_window_ui_trReport IssueInformar de un problemaInitialization and informationDownloadsErrors and warningsmainwindow_src_ui_trError: Error: <b>Link</b>: <b>Enlace</b>: <b>Downloaded</b>: <b>Descargado</b><b>Transfer rate</b>: <b>Tasa de transferencia</b>:<b>Estimated time left</b>: <b>Tiempo restante estimado</b>: <b>Connections</b>: <b>Conexiones</b>: <b>Status</b>: <b>Estado</b>: Download StoppedDescarga detenidaError - Error - Download CompleteDescarga completa<b><center>This link has been added before! Are you sure you want to add it again?</center></b><b><center>Este enlace ha sido añadido anteriormente. ¿Seguro de que quiere añadirlo de nuevo?</center></b>Download StartsDescarga iniciadaOperation was not successful.La operación no tuvo éxito.Please resume the following category: Reanude la categoría siguiente:Not enough free space in:No hay suficiente espacio disponible en:muxing errorError de multiplexaciónan error occurredPlease update Persepolis.<b>Fragments</b>: mainwindow_ui_trPersepolis Download ManagerGestor de descargas PersepolisShow/Hide system tray iconMostrar/ocultar el icono de notificaciónAdd New Download LinkAñadir un nuevo enlace de descargaResume DownloadReanudar la descargaPause DownloadPausar la descargaStop/Cancel DownloadDetener/cancelar la descargaPropertiesPropiedadesProgressProgresoExitSalirStart TimeHora de inicioEnd TimeHora de finalizaciónDownload bottom of
the list firstDescargar primero
el final de la listaApplyAplicarAfter downloadDespués de la descargaShut DownApagar el equipo<b>Video file status: </b><b>Estado del archivo de vídeo: </b><b>Audio file status: </b><b>Estado del archivo de audio: </b><b>Status: </b><b>Estado: </b><b>Muxing status: </b><b>Estado de la multiplexación: </b> downloaded descargadoMinimize to System TrayHide OptionsKeep System Awake!<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Start Mixing<b>Video File Status: </b><b>Audio File Status: </b><b>Mixing status: </b>progress_ui_trPersepolis Download ManagerGestor de descargas PersepolisStatus: Estado:Downloaded:Descargado:Transfer rate: Tasa de transferencia:Estimated time left:Tiempo restante estimado:Number of connections: Número de conexiones:Download InformationInformación de la descargaAfter downloadDespués de la descargaApplyAplicarShut DownApagarDownload OptionsOpciones de descargaResumeReanudarPausePausarStopDetenerLink: setting_src_ui_tr<b><center>This shortcut has been used before! Use another one!</center></b><center><b>Este atajo ya se ha usado antes. ¡Use otro!</b>setting_ui_trPreferencesPreferenciasNumber of tries: Número de intentos:<html><head/><body><p>Set timeout in seconds. </p></body></html><html><head/><body><p>Configura el tiempo de espera en segundos. </p></body></html>Timeout (seconds): Tiempo de espera (segundos):<html><head/><body><p>Using multiple connections can help speed up your download.</p></body></html><html><head/><body><p>El uso de conexiones múltiples puede acelerar la descarga.</p></body></html>Number of connections: Número de conexiones:ChangeCambiarDownload OptionsOpciones de descargaVolume: Volumen:NotificationsNotificacionesStyle: Estilo:Color scheme: Esquema de colores:Icons: Iconos:Notification type: Tipo de notificación:Font: Tipografía:Size: Tamaño:Run Persepolis at startupEjecutar Persepolis al inicioKeep system awake!¡Mantenga el sistema activo!<html><head/><body><p>Format HH:MM</p></body></html><html><head/><body><p>Formato HH:MM</p></body></html>StatusEstadoVideo Finder OptionsOpciones de la búsqueda de vídeosDefaultsValores por defectoCancelCancelarOKAceptarPress new keysPulsar teclas nuevasShortcutsAtajosHide main window if close button clicked.Ocultar la ventana principal al hacer clic en el botón de cerrar<html><head/><body><p>This feature may not work in your operating system.</p></body></html><html><head/><body><p>Es posible que esta característica no funcione en su sistema operativo.</p></body></html>Language: Wait period between retries (seconds): Don't use certificate to verify the peers<html><head/><body><p>This option avoids SSL/TLS handshake failure. But use it at your own risk!</p></body></html>Download folder: Create subfolders for Music,Videos, ... in default download folderSave AsEnable Notification SoundsToolbar icons size: Enable system tray iconShow download complete dialog when download is finishedShow menubarShow side panelShow download progress window<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Columns CustomizationCheck system clipboard for copied links<html><head/><body><p>The program will automatically check the clipboard for copied links. </p></body></html>Download requests from the browser will be executed immediately.<html><head/><body><p>When a download request is sent from the browser extension, the download will start without showing the Add Link window. </p></body></html><html><head/><body><p>Set number of retries if download failed.</p></body></html><html><head/><body><p>Set the seconds to wait between retries.</p></body></html>Chunk size(KiB): It is python requests library chunk size. Do not change this If you are not familiar with it.text_ui_trPersepolis Download ManagerGestor de descargas PersepolisIP:Dirección IP:Port:Puerto:Change Download FolderCambiar la carpeta de descargasProxy password: Proxy username: Download username and passwordDownload username: Download password: Download folder: Number of connections:video_finder_progress_ui_tr<b>Video file status: </b><b>Estado del archivo de vídeo: </b><b>Audio file status: </b><b>Estado del archivo de audio: </b><b>Muxing status: </b><b>Estado de la multiplexación: </b><b>Mixing status: </b>
================================================
FILE: resources/locales/ui_fa_IR.ts
================================================
about_ui_trAbout Persepolisدرباره پرسپولیسPersepolis Download Managerدانلود منیجر پرسپولیس<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!https://persepolisdm.github.io<a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!https://telegram.me/persepolisdm<a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!https://twitter.com/persepolisdmDevelopersتوسعه دهندگانTranslatorsترجمهکنندگانLicenseپروانهOKتایید
AliReza AmirSamimi
Mohammadreza Abdollahzadeh
Sadegh Alirezaie
Mostafa Asadi
Jafar Akhondali
Kia Hamedi
H.Rostami
Ehsan Titish
MohammadAmin VahediniaTRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!
علیرضا امیرصمیمی
محمدرضا عبداللهزاده
صادق علیرضایی
مصطفی اسدی
جعفر آخوندعلی
کیا حامدی
ح. رستمی
احسان تیتیش
محمدامین واحدینیاAcknowledgmentsقدردانیها<a href=https://github.com/yt-dlp/yt-dlp>YT-DLP project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/yt-dlp/yt-dlp>پروژه YT-DLP</a><a href=https://github.com/ffmpeg/ffmpeg>FFmpeg project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/ffmpeg/ffmpeg>پروژه FFmpeg</a><a href=https://wiki.qt.io/Qt_for_Python>Pyside project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://wiki.qt.io/Qt_for_Python>پروژه Pyside</a><a href=https://github.com/psf/requests>Requests project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/psf/requests>پروژه Requests</a>addlink_ui_trAdd to category: افزودن به دسته:ProxyپراکسیIP: آیپی:Port:پورت : Change Download Folderتغییر پوشه دانلودDownload Folder: پوشه دانلود: CancelلغوOKتاییدLinkپیوندMore Optionsگزینههای بیشترAdvanced Optionsگزینههای پیشرفتهAdd Download Linkافزودن لینک دانلودDownload link: لینک دانلود:Change file name: ویرایش نام پرونده:Detect System Proxy Settingsتشخیص تنظیمات پراکسی سیستمProxy password: گذرواژه پراکسی:Proxy username: نام کاربری پراکسی:Download username and passwordنامکاربری و گذرواژه دانلودDownload username: نامکاربری دانلود:Download password: گذرواژه دانلود: Remember this pathاین مسیر را به خاطر بسپارStart timeزمان شروعEnd timeزمان پایانNumber of connections:شمار اتصالها:Download Laterبعدا دانلودکنHTTPHTTPHTTPSHTTPSSOCKS5SOCKS5after_download_src_ui_tr<b>File name</b>: <b>نام پرونده</b>: <b>Size</b>: <b>حجم پرونده</b>:after_download_ui_trPersepolis Download Managerدانلود منیجر پرسپولیس Open File گشودن پروندهOpen Download Folderگشودن پوشه دانلود OK تاییدDon't show this message again.این پیام را دیگر نشان نده.<b>Download Completed!</b><b>دانلود به پایان رسید</b><b>Save as</b>: <b>ذخیره شده در</b>:<b>Link</b>: <b>پیوند</b>:log_window_ui_trReport IssueگزارشInitialization and informationاطلاعات و دادههای اولیهDownloadsدانلودهاErrors and warningsخطاها و هشدارهاmainwindow_src_ui_trError: خطا:<b>Link</b>: <b>پیوند</b>:<b>Downloaded</b>: <b>دانلود شده:</b><b>Transfer rate</b>: <b>سرعت:</b><b>Estimated time left</b>: <b>زمان باقیمانده:</b><b>Connections</b>: <b>تعداد اتصال ها</b><b>Status</b>: <b>وضعیت:</b>Download Stoppedدانلود متوقف شدError - خطا - Download Completeدانلود به پایان رسید<b><center>This link has been added before! Are you sure you want to add it again?</center></b><b><center>این پیوند قبلا اضافه شده است ! آیا مطمئنید میخواهید دوباره آن را بیافزایید ؟</center></b>Download Startsدانلود آغاز شدOperation was not successful.عملیات ناموفق بود.Please resume the following category: لطفا این دسته را ادامه دهید: Not enough free space in:فضای خالی کافی وجود ندارد: muxing errorخطای میکسan error occurredخطایی رخ دادPlease update Persepolis.لطفا پرسپولیس را بروزرسانی کنید.<b>Fragments</b>: <b>قطعات</b>:mainwindow_ui_trPersepolis Download Managerدانلود منیجر پرسپولیسShow/Hide system tray iconنمایش/عدم نمایش نمایه در سینی سیستمAdd New Download Linkافزودن پیوند دانلود نوResume Downloadادامه دانلودPause Downloadمکث در دانلودStop/Cancel Downloadتوقف و لغو دانلودPropertiesویژگی هاProgressپیشرفتExitخروجStart Timeزمان شروعEnd Timeزمان پایانDownload bottom of
the list firstابتدای فهرست را
دانلودکنApplyاعمالAfter downloadپس از دانلودShut Downخاموش کردن<b>Video file status: </b><b>وضعیت پرونده ویدئو</b><b>Audio file status: </b><b>وضعیت پرونده صدا: </b>,<b>Status: </b><b>وضعیت: </b><b>Muxing status: </b><b>وضعیت میکس: </b> downloadedبارگیری شدMinimize to System Trayفرستادن برنامه به سینی نمایهHide Optionsگزینههای مخفیKeep System Awake!سیستم را روشن نگهدار!<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html><html><head/><body><p>این گزینه از به خواب رفتن سیستم جلوگیری میکند
این گزینه اگر سیستم شما بصورت خودکار به خواب می رود ضروری است.</p></body></html>Start Mixingشروع میکس<b>Video File Status: </b><b>وضعیت پرونده ویدئو</b><b>Audio File Status: </b><b>وضعیت پرونده صدا: </b>,<b>Mixing status: </b><b>وضعیت میکس: </b>progress_ui_trPersepolis Download Managerدانلود منیجر پرسپولیسStatus: وضعیت:Downloaded:دانلود شده:Transfer rate: سرعت:Estimated time left:زمان باقیمانده:Number of connections: تعداد اتصالات:Download Informationاطلاعات دانلودAfter downloadپس از دانلودApplyاعمالShut Downخاموش کردنDownload Optionsگزینههای دانلودResumeادامهPauseدرنگStopتوقفLink: پیوند:setting_src_ui_tr<b><center>This shortcut has been used before! Use another one!</center></b><b><center>این میانبر قبلا استفاده شده است! میانبر دیگری استفاده کنید!</center></b>setting_ui_trPreferencesشخصیسازیNumber of tries: تعداد تلاش های دوباره:<html><head/><body><p>Set timeout in seconds. </p></body></html><html><head/><body><p>زمان وقفه را مشخص کنید. </p></body></html>Timeout (seconds): وقفه زمانی (ثانیه):<html><head/><body><p>Using multiple connections can help speed up your download.</p></body></html><html><head/><body><p>استفاده از چند اتصال میتواند به سرعت دانلود شما کمک کند..</p></body></html>Number of connections: تعداد اتصالات:ChangeتغییرDownload Optionsگزینههای دانلودVolume: میزان بلندی صدا:Notificationsآگاهسازی هاStyle: سبکColor scheme: ساختار رنگIcons: آیکن:Notification type: نوع آگاهسازیFont: فونت:Size: اندازه :Run Persepolis at startupاجرای پرسپولیس در ابتدای نشستKeep system awake!سیستم را روشن نگهدار!<html><head/><body><p>Format HH:MM</p></body></html><html><head/><body><p>قالب ساعت:دقیقه</p></body></html>StatusوضعیتVideo Finder Optionsگزینههای یابنده ویدئوDefaultsپیشفرضCancelلغوOKتاییدPress new keysکلیدهای جدیدی را بفشاریدShortcutsمیانبرهاHide main window if close button clicked.مخفی کردن پنجره اصلی هنگام انتخاب دکمه بستن<html><head/><body><p>This feature may not work in your operating system.</p></body></html><html><head/><body><p>این ویژگی شاید در سیستم عامل شما کار نکند.</p></body></html>Language: زبان:Wait period between retries (seconds): درنگ در بین تلاش های دوباره(به ثانیه):Don't use certificate to verify the peersاز گواهی برای تأیید همتایان استفاده نکنید<html><head/><body><p>This option avoids SSL/TLS handshake failure. But use it at your own risk!</p></body></html><html><head/><body><p>این گزینه از هندشیک ناموفق SSL/TLS جلوگیری میکند، اما با مسئولیت خود از آن استفاده کنید </p></body></html>Download folder: پوشه دانلود: Create subfolders for Music,Videos, ... in default download folderبرای ویدئو ، موسیقی و... در پوشه پیشفرض دانلود پوشههای مجزا بسازSave Asذخیره درEnable Notification Soundsفعال کردن صدای آگاهسازیToolbar icons size: اندازه نمایهها در نواز ابزار : Enable system tray iconفعال کردن نمایه در سینی نمایهShow download complete dialog when download is finishedنمایش پنجره پایان دانلود پس از اتمام دانلودShow menubarنمایش منوبارShow side panelنمایش پنل کناریShow download progress windowنمایش پنجره پیشرفت دانلود<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html><html><head/><body><p>این گزینه از به خواب رفتن سیستم جلوگیری میکند
این گزینه اگر سیستم شما بصورت خودکار به خواب می رود ضروری است.</p></body></html>Columns Customizationشخصیسازی ستونهاCheck system clipboard for copied linksبررسی کلیپبورد برای پیوندهای کپیشده<html><head/><body><p>The program will automatically check the clipboard for copied links. </p></body></html><html><head/><body><p>برنامه بصورت خودکار کلیپبورد را برای پیوندهای کپیشده بررسی میکند.Download requests from the browser will be executed immediately.درخواست های دانلود از مرورگر بلافاصله اجرا می شود.<html><head/><body><p>When a download request is sent from the browser extension, the download will start without showing the Add Link window. </p></body></html><html><head/><body><p>پس از ارسال درخواست دانلود از افزونه مرورگر، دانلود بدون نمایش پنجره Add Link شروع می شود.</p></body></html><html><head/><body><p>Set number of retries if download failed.</p></body></html><html><head/><body><p>تعداد تلاشها پس از خطا در دانلود را مشخص کنید</p></body></html><html><head/><body><p>Set the seconds to wait between retries.</p></body></html><html><head/><body><p>زمان وقفه را مشخص کنید. </p></body></html>Chunk size(KiB): اندازه قطعه (کیلوبایت):It is python requests library chunk size. Do not change this If you are not familiar with it.این اندازه قطعه در کتابخانه requests پایتون است. اگر با آن آشنایی ندارید، آن را تغییر ندهید.text_ui_trPersepolis Download Managerدانلود منیجر پرسپولیسIP:آیپی:Port:پورت:Change Download Folderتغییر پوشه دانلودProxy password: گذرواژه پراکسی:Proxy username: نام کاربری پراکسی:Download username and passwordنامکاربری و گذرواژه دانلودDownload username: نامکاربری دانلود:Download password: گذرواژه دانلود: Download folder: پوشه دانلود: Number of connections:شمار اتصالها:video_finder_progress_ui_tr<b>Video file status: </b><b>وضعیت پروند ویدئو: </b><b>Audio file status: </b><b>وضعیت پرونده صدا: </b><b>Muxing status: </b><b>وضعیت میکس: </b><b>Mixing status: </b><b>وضعیت میکس: </b>
================================================
FILE: resources/locales/ui_fr_FR.ts
================================================
about_ui_trAbout PersepolisÀ propos de PersepolisPersepolis Download ManagerGestionnaire de téléchargement Persepolis<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a><a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a><a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>DevelopersDéveloppeursTranslatorsTraducteursLicenseLicenceOKOK
AliReza AmirSamimi
Mohammadreza Abdollahzadeh
Sadegh Alirezaie
Mostafa Asadi
Jafar Akhondali
Kia Hamedi
H.Rostami
Ehsan Titish
MohammadAmin VahediniaTRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!
AliReza AmirSamimi
Mohammadreza Abdollahzadeh
Sadegh Alirezaie
Mostafa Asadi
Jafar Akhondali
Kia Hamedi
H.Rostami
Ehsan Titish
MohammadAmin VahediniaAcknowledgments<a href=https://github.com/yt-dlp/yt-dlp>YT-DLP project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/ffmpeg/ffmpeg>FFmpeg project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://wiki.qt.io/Qt_for_Python>Pyside project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/psf/requests>Requests project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!addlink_ui_trAdd to category: Ajouter à la catégorie :ProxyProxyIP: IP : Port:Port :Change Download FolderChanger le dossier de téléchargementDownload Folder: Dossier de téléchargement :CancelAnnulerOKOKLinkLienMore OptionsPlus d'optionsAdvanced OptionsOptions avancéesAdd Download LinkAjouter un lien de téléchargementDownload link: Lien de téléchargement :Change file name: Changer le nom de fichier :Detect System Proxy SettingsDétecter les paramètres système de proxyProxy password: Mot de passe du proxy :Proxy username: Nom d'utilisateur du proxy :Download username and passwordNom d'utilisateur et mot de passe du téléchargementDownload username: Nom d'utilisateur du téléchargement :Download password: Mot de passe du téléchargement :Remember this pathSe souvenir de ce cheminStart timeHeure de débutEnd timeHeure de finNumber of connections:Nombre de connexions :Download LaterTélécharger plus tardHTTPHTTPSSOCKS5after_download_src_ui_tr<b>File name</b>: <b>Nom de fichier</b> : <b>Size</b>: <b>Taille</b> : after_download_ui_trPersepolis Download ManagerGestionnaire de téléchargement Persepolis Open File Ouvrir fichierOpen Download FolderOuvrir le dossier de téléchargement OK OK Don't show this message again.Ne plus afficher ce message.<b>Download Completed!</b><b>Téléchargement terminé !</b><b>Save as</b>: <b>Enregistrer sous</b> : <b>Link</b>: <b>Lien</b> : log_window_ui_trReport IssueRapporter un problèmeInitialization and informationDownloadsErrors and warningsmainwindow_src_ui_trError: Erreur :<b>Link</b>: <b>Lien</b> : <b>Downloaded</b>: <b>Téléchargés</b> : <b>Transfer rate</b>: <b>Taux de transfert</b> : <b>Estimated time left</b>: <b>Temps restant estimé</b> : <b>Connections</b>: <b>Connexions</b> : <b>Status</b>: <b>Statut</b> : Download StoppedTéléchargement interrompuError - Erreur - Download CompleteTéléchargement terminé<b><center>This link has been added before! Are you sure you want to add it again?</center></b><b><center>Ce lien a été ajouté auparavant ! Êtes-vous sûr que vous voulez l'ajouter à nouveau ?</center></b>Download StartsDébut du téléchargementOperation was not successful.L'opération n'a pas réussiPlease resume the following category: Veuillez reprendre le téléchargement de la catégorie suivante :Not enough free space in:Pas assez d'espace libre dans :muxing errorerreur de multiplexagean error occurredune erreur est s'est produitePlease update Persepolis.<b>Fragments</b>: mainwindow_ui_trPersepolis Download ManagerGestionnaire de téléchargement PersepolisShow/Hide system tray iconAfficher/cacher l'icône dans la zone de notificationAdd New Download LinkAjouter un nouveau lien de téléchargementResume DownloadReprendre le téléchargementPause DownloadMettre le téléchargement en pauseStop/Cancel DownloadArrêter/Annuler le téléchargementPropertiesPropriétésProgressProgressionExitQuitterStart TimeHeure de débutEnd TimeHeure de finDownload bottom of
the list firstTélécharger d'abord le bas de
la listeApplyAppliquerAfter downloadAprès le téléchargementShut DownArrêter<b>Video file status: </b><b>Statut du fichier vidéo : </b><b>Audio file status: </b><b>Statut du fichier audio : </b><b>Status: </b><b>Statut : </b><b>Muxing status: </b><b>Statut du multiplexage : </b> downloadedtéléchargésMinimize to System TrayRéduire dans la zone de notificationHide OptionsCacher les optionsKeep System Awake!Garder le système éveillé !<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html><html><head/><body><p>Cette option empêchera votre système de se mettre en veille. Ceci est nécessaire si votre gestionnaire d'alimentation met le système en veille automatiquement. </p></body></html>Start MixingDémarrer le multiplexage<b>Video File Status: </b><b>Statut du fichier vidéo : </b><b>Audio File Status: </b><b>Statut du fichier audio : </b><b>Mixing status: </b><b>Statut du multiplexage : </b>progress_ui_trPersepolis Download ManagerGestionnaire de téléchargement PersepolisStatus: Statut : Downloaded:Téléchargés :Transfer rate: Taux de transfert :Estimated time left:Temps restant estimé :Number of connections: Nombre de connexions :Download InformationInformation de téléchargementAfter downloadAprès le téléchargementApplyAppliquerShut DownArrêterDownload OptionsOptions de téléchargementResumeReprendrePauseMettre en pauseStopArrêterLink: Lien :setting_src_ui_tr<b><center>This shortcut has been used before! Use another one!</center></b><b><center>Ce raccourci a été utilisé auparavant ! Utilisez-en un autre !</center></b>setting_ui_trPreferencesPréférencesNumber of tries: Nombre de tentatives :<html><head/><body><p>Set timeout in seconds. </p></body></html><html><head/><body><p>Définir la temporisation en secondes. </p></body></html>Timeout (seconds): Temporisation (secondes) :<html><head/><body><p>Using multiple connections can help speed up your download.</p></body></html><html><head/><body><p>Utiliser plusieurs connexion peut aider à accélérer votre téléchargement.</p></body></html>Number of connections: Nombre de connexions :ChangeModifierDownload OptionsOptions de téléchargementVolume: Volume : NotificationsNotificationsStyle: Style :Color scheme: Thème de couleur :Icons: Icônes :Notification type: Type de notification : Font: Police :Size: Taille :Run Persepolis at startupLancer Persepolis au démarrageKeep system awake!Garder le système éveillé !<html><head/><body><p>Format HH:MM</p></body></html><html><head/><body><p>Format HH:MM</p></body></html>StatusStatutVideo Finder OptionsOption de la recherche de vidéosDefaultsPar défautCancelAnnulerOKOKPress new keysAppuyer sur de nouvelles touchesShortcutsRaccourcisHide main window if close button clicked.Cacher la fenêtre principale si le bouton fermer est cliqué<html><head/><body><p>This feature may not work in your operating system.</p></body></html><html><head/><body><p>Cette fonctionnalité pourrait ne pas fonctionner dans votre système d'exploitation.</p></body></html>Language: Langue :Wait period between retries (seconds): Temps d'attente entre les tentatives (secondes) :Don't use certificate to verify the peersNe pas utiliser de certificats pour vérifier les pairs<html><head/><body><p>This option avoids SSL/TLS handshake failure. But use it at your own risk!</p></body></html><html><head/><body><p>Cette option évite les échecs de handshake SSL/TLS. Utilisez-la à vos risques et périls !</p></body></html>Download folder: Dossier de téléchargement :Create subfolders for Music,Videos, ... in default download folderCréer des sous-dossiers pour Musique, Vidéos, etc. dans le dossier de téléchargement par défautSave AsEnregistrer sousEnable Notification SoundsActiver les sons de notificationToolbar icons size: Taille des icônes de la barre d'outils :Enable system tray iconActiver l'icône dans la zone de notification.Show download complete dialog when download is finishedAfficher une fenêtre d'annonce lorsque le téléchargement est terminéShow menubarAfficher la barre de menusShow side panelAfficher le panneau latéralShow download progress windowAfficher la fenêtre de progression des téléchargements<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html><html><head/><body><p>Cette option empêchera votre système de se mettre en veille. Ceci est nécessaire si votre gestionnaire d'alimentation met le système en veille automatiquement. </p></body></html>Columns CustomizationPersonnalisation des colonnesCheck system clipboard for copied links<html><head/><body><p>The program will automatically check the clipboard for copied links. </p></body></html>Download requests from the browser will be executed immediately.<html><head/><body><p>When a download request is sent from the browser extension, the download will start without showing the Add Link window. </p></body></html><html><head/><body><p>Set number of retries if download failed.</p></body></html><html><head/><body><p>Set the seconds to wait between retries.</p></body></html>Chunk size(KiB): It is python requests library chunk size. Do not change this If you are not familiar with it.text_ui_trPersepolis Download ManagerGestionnaire de téléchargement PersepolisIP:IP :Port:Port :Change Download FolderChanger le dossier de téléchargementProxy password: Mot de passe du proxy :Proxy username: Nom d'utilisateur du proxy :Download username and passwordNom d'utilisateur et mot de passe du téléchargementDownload username: Nom d'utilisateur du téléchargement :Download password: Mot de passe du téléchargement :Download folder: Dossier de téléchargement :Number of connections:Nombre de connexions :video_finder_progress_ui_tr<b>Video file status: </b><b>Statut du fichier vidéo : </b><b>Audio file status: </b><b>Statut du fichier audio : </b><b>Muxing status: </b><b>Statut du multiplexage : </b><b>Mixing status: </b>
================================================
FILE: resources/locales/ui_hu.ts
================================================
about_ui_trAbout PersepolisA Persepolis névjegyePersepolis Download ManagerPersepolis Download Manager<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a><a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a><a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>DevelopersKészítőkTranslatorsFordítókLicenseLicenszOKOK
AliReza AmirSamimi
Mohammadreza Abdollahzadeh
Sadegh Alirezaie
Mostafa Asadi
Jafar Akhondali
Kia Hamedi
H.Rostami
Ehsan Titish
MohammadAmin VahediniaTRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!Acknowledgments<a href=https://github.com/yt-dlp/yt-dlp>YT-DLP project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/ffmpeg/ffmpeg>FFmpeg project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://wiki.qt.io/Qt_for_Python>Pyside project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/psf/requests>Requests project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!addlink_ui_trAdd to category: Hozáadás kategóriához:ProxyProxyIP: IP: Port:Port:Change Download FolderLetöltés helyének módosításaDownload Folder: Letöltés helye:CancelMégseOKOKLinkLinkMore OptionsTovábbi opciókAdvanced OptionsHaladó opciókAdd Download LinkDownload link: Change file name: Detect System Proxy SettingsProxy password: Proxy username: Download username and passwordDownload username: Download password: Remember this pathStart timeEnd timeNumber of connections:Download LaterHTTPHTTPSSOCKS5after_download_src_ui_tr<b>File name</b>: <b>File name</b>: <b>Size</b>: <b>Size</b>: after_download_ui_trPersepolis Download ManagerPersepolis Download Manager Open File Fájl megnyitásaOpen Download FolderLetöltés helyének megnyitása OK OK Don't show this message again.Ne jelenjen meg újra ez az üzenet.<b>Download Completed!</b><b>Letöltés elkészült!</b><b>Save as</b>: <b>Mentés másként</b>: <b>Link</b>: <b>Link</b>: log_window_ui_trReport IssueHibajelentésInitialization and informationDownloadsErrors and warningsmainwindow_src_ui_trError: Hiba:<b>Link</b>: <b>Link</b>: <b>Downloaded</b>: <b>Letöltve</b>: <b>Transfer rate</b>: <b>Átviteli ráta</b>: <b>Estimated time left</b>: <b>Hátralévő idő</b>: <b>Connections</b>: <b>Kapcsolatok</b>: <b>Status</b>: <b>Állapot</b>: Download StoppedLetöltés megállítvaError - Hiba -Download CompleteLetöltés befejezve<b><center>This link has been added before! Are you sure you want to add it again?</center></b><b><center>Ezt a linket korábban már hozzáadtad! Biztos, hogy ismét hozzáadod?</center></b>Download StartsLetöltés indításaOperation was not successful.A művelet nem sikerült.Please resume the following category: Folytasd a következő kategóriát:Not enough free space in:Nincs elég hely itt:muxing errormuxing hibaan error occurredPlease update Persepolis.<b>Fragments</b>: mainwindow_ui_trPersepolis Download ManagerPersepolis Download ManagerShow/Hide system tray iconTálcaikon megjelenítése/elrejtéseAdd New Download LinkÚj letöltésilink hozzáadásaResume DownloadA letöltés folytatásaPause DownloadA letöltés felfüggesztéseStop/Cancel DownloadA letöltés leállítása / megszakításaPropertiesBeállításokProgressFolyamatExitKilépésStart TimeKezdésEnd TimeBefejezésDownload bottom of
the list firstLetöltés előszőr
a lista aljárólApplyHozzáadAfter downloadLetöltés utánShut DownKikapcsolás<b>Video file status: </b><b>Videófájl állapot: </b><b>Audio file status: </b><b>Audiófájl állapot: </b><b>Status: </b><b>Állapot: </b><b>Muxing status: </b><b>Muxing állapot: </b> downloadedletöltveMinimize to System TrayHide OptionsKeep System Awake!<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Start Mixing<b>Video File Status: </b><b>Audio File Status: </b><b>Mixing status: </b>progress_ui_trPersepolis Download ManagerPersepolis Download ManagerStatus: Állapot:Downloaded:Letöltve:Transfer rate: Átviteli ráta:Estimated time left:Hátralévő idő:Number of connections: Kapcsolatok száma:Download InformationLetöltés információAfter downloadLetöltés utánApplyHozzáadShut DownKikapcsolásDownload OptionsLetöltés információkResumeFolytatásPauseFelfüggesztésStopStopLink: setting_src_ui_tr<b><center>This shortcut has been used before! Use another one!</center></b><b><center>Ezt a parancsikont korábban már használtad! Válassz másikat!</center></b>setting_ui_trPreferencesBeállításokNumber of tries: Próbálkozások száma:<html><head/><body><p>Set timeout in seconds. </p></body></html><html><head/><body><p>Időtúllépés beállítása másodpercekben. </p></body></html>Timeout (seconds): Időtúllépés (sec):<html><head/><body><p>Using multiple connections can help speed up your download.</p></body></html><html><head/><body><p>Több kapcsolat használata felgyorsíthatja a letöltést.</p></body></html>Number of connections: Kapcsolatok száma:ChangeVáltoztatDownload OptionsLetöltési opciókVolume: Hangerő:NotificationsÉrtesítésekStyle: Stílus:Color scheme: Színséma:Icons: Ikonok:Notification type: Értesítés típusa:Font: Betű:Size: Méret:Run Persepolis at startupPersepolis automatikus indítása a rendszerrelKeep system awake!Rendszer ébrentartása<html><head/><body><p>Format HH:MM</p></body></html><html><head/><body><p>Formátum: ÓÓ:PP</p></body></html>StatusÁllapotVideo Finder OptionsVideókereső opciókDefaultsAlapértelmezésCancelMégseOKOKPress new keysNyomd meg az új billentyűketShortcutsParancsikonokHide main window if close button clicked.Főablak elrejtése a Bezárás gombra való kattintáskor<html><head/><body><p>This feature may not work in your operating system.</p></body></html><html><head/><body><p>Lehet, hogy ez a szolgáltatás nem működik ezen az operációs rendszeren.</p></body></html>Language: Wait period between retries (seconds): Don't use certificate to verify the peers<html><head/><body><p>This option avoids SSL/TLS handshake failure. But use it at your own risk!</p></body></html>Download folder: Create subfolders for Music,Videos, ... in default download folderSave AsEnable Notification SoundsToolbar icons size: Enable system tray iconShow download complete dialog when download is finishedShow menubarShow side panelShow download progress window<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Columns CustomizationCheck system clipboard for copied links<html><head/><body><p>The program will automatically check the clipboard for copied links. </p></body></html>Download requests from the browser will be executed immediately.<html><head/><body><p>When a download request is sent from the browser extension, the download will start without showing the Add Link window. </p></body></html><html><head/><body><p>Set number of retries if download failed.</p></body></html><html><head/><body><p>Set the seconds to wait between retries.</p></body></html>Chunk size(KiB): It is python requests library chunk size. Do not change this If you are not familiar with it.text_ui_trPersepolis Download ManagerPersepolis Download ManagerIP:IP:Port:Port:Change Download FolderLetöltés helyének módosításaProxy password: Proxy username: Download username and passwordDownload username: Download password: Download folder: Number of connections:video_finder_progress_ui_tr<b>Video file status: </b><b>Videófájl állapot: </b><b>Audio file status: </b><b>Audiófájl állapot: </b><b>Muxing status: </b><b>Muxing állapot: </b><b>Mixing status: </b>
================================================
FILE: resources/locales/ui_ko.ts
================================================
about_ui_trAbout PersepolisPersepolis 정보Persepolis Download ManagerPersepolis 다운로드 관리자<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a><a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a><a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>Developers개발자Translators번역자License라이선스OK확인
AliReza AmirSamimi
Mohammadreza Abdollahzadeh
Sadegh Alirezaie
Mostafa Asadi
Jafar Akhondali
Kia Hamedi
H.Rostami
Ehsan Titish
MohammadAmin VahediniaTRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!
AliReza AmirSamimi
Mohammadreza Abdollahzadeh
Sadegh Alirezaie
Mostafa Asadi
Jafar Akhondali
Kia Hamedi
H.Rostami
Ehsan Titish
MohammadAmin VahediniaAcknowledgments감사의 글<a href=https://github.com/yt-dlp/yt-dlp>YT-DLP project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/yt-dlp/yt-dlp>YT-DLP 프로젝트</a><a href=https://github.com/ffmpeg/ffmpeg>FFmpeg project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/ffmpeg/ffmpeg>FFmpeg 프로젝트</a><a href=https://wiki.qt.io/Qt_for_Python>Pyside project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://wiki.qt.io/Qt_for_Python>Pyside 프로젝트</a><a href=https://github.com/psf/requests>Requests project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/psf/requests>Requests 프로젝트</a>addlink_ui_trAdd to category: 카테고리에 추가하기:Proxy프록시IP: IP: Port:포트:Change Download Folder다운로드 폴더 변경하기Download Folder: 다운로드 폴더:Cancel취소OK확인Link링크More Options추가 옵션Advanced Options고급 옵션Add Download Link다운로드 링크 추가하기Download link: 다운로드 링크: Change file name: 파일 이름 변경하기:Detect System Proxy Settings시스템 프록시 설정 감지하기Proxy password: 프록시 비밀번호: Proxy username: 프록시 사용자이름: Download username and password사용자이름 및 비밀번호 다운로드Download username: 사용자이름 다운로드: Download password: 비밀번호 다운로드: Remember this path이 경로 기억하기Start time시작 시간End time종료 시간Number of connections:연결 수:Download Later나중에 다운로드HTTPHTTPHTTPSHTTPSSOCKS5SOCKS5after_download_src_ui_tr<b>File name</b>: <b>파일 이름</b>: <b>Size</b>: <b>크기</b>: after_download_ui_trPersepolis Download ManagerPersepolis 다운로드 관리자 Open File 파일 열기Open Download Folder다운로드 폴더 열기 OK 확인Don't show this message again.이 메시지를 다시 표시하지 않습니다.<b>Download Completed!</b><b>다운로드가 완료되었습니다!</b><b>Save as</b>: <b>다른 이름으로 저장</b>: <b>Link</b>: <b>링크</b>: log_window_ui_trReport Issue이슈 보고Initialization and informationDownloadsErrors and warningsmainwindow_src_ui_trError: 오류: <b>Link</b>: <b>링크</b>: <b>Downloaded</b>: <b>다운로드됨</b>: <b>Transfer rate</b>: <b>전송 속도</b>: <b>Estimated time left</b>: <b>남은 예상 시간</b>: <b>Connections</b>: <b>연결 수</b>: <b>Status</b>: <b>상태</b>: Download Stopped다운로드가 중지되었습니다Error - 오류 - Download Complete다운로드를 완료하였습니다<b><center>This link has been added before! Are you sure you want to add it again?</center></b><b><center>이 링크는 이전에 추가된 적이 있습니다! 다시 추가하시겠습니까?</center></b>Download Starts다운로드가 시작됩니다Operation was not successful.작업에 실패했습니다.Please resume the following category: 다음 카테고리를 재개해 주세요:Not enough free space in:사용 가능한 공간이 부족합니다:muxing error먹싱 오류an error occurred오류 발생됨Please update Persepolis.Persepolis를 업데이트해 주세요.<b>Fragments</b>: <b>조각</b>: mainwindow_ui_trPersepolis Download ManagerPersepolis 다운로드 관리자Show/Hide system tray icon시스템 트레이 아이콘 표시/숨김Add New Download Link새 다운로드 링크 추가하기Resume Download다운로드 이어받기Pause Download다운로드 일시정지Stop/Cancel Download다운로드 중지/취소Properties속성Progress진행률Exit종료Start Time시작 시간End Time종료 시간Download bottom of
the list first목록 하단에서
먼저 다운로드하기Apply적용하기After download다운로드 완료 후Shut Down컴퓨터 끄기<b>Video file status: </b><b>비디오 파일 상태: </b><b>Audio file status: </b><b>오디오 파일 상태: </b><b>Status: </b><b>상태: </b><b>Muxing status: </b><b>먹싱 상태: </b> downloaded다운로드됨Minimize to System Tray시스템 트레이로 최소화Hide Options옵션 숨기기Keep System Awake!시스템 작동 상태 유지!<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html><html><head/><body><p>이 옵션은 시스템이 절전 모드로 전환되는 것을 방지합니다. 전원 관리자가 시스템을 자동으로 절전모드로 전환하는 경우 필요합니다. </p></body></html>Start Mixing믹싱 시작하기<b>Video File Status: </b><b>비디오 파일 상태: </b><b>Audio File Status: </b><b>오디오 파일 상태: </b><b>Mixing status: </b><b>먹싱 상태: </b>progress_ui_trPersepolis Download ManagerPersepolis 다운로드 관리자Status: 상태: Downloaded:다운로드됨:Transfer rate: 전송 속도: Estimated time left:남은 예상 시간:Number of connections: 연결 수: Download Information다운로드 정보After download다운로드 완료 후Apply적용하기Shut Down컴퓨터 끄기Download Options다운로드 옵션Resume이어받기Pause일시정지Stop중지하기Link: 링크: setting_src_ui_tr<b><center>This shortcut has been used before! Use another one!</center></b><b><center>이 단축키는 이전에 사용한 적이 있습니다! 다른 단축키를 사용하세요!</center></b>setting_ui_trPreferences환경설정Number of tries: 시도 횟수: <html><head/><body><p>Set timeout in seconds. </p></body></html><html><head/><body><p>시간초과를 초 단위로 지정합니다. </p></body></html>Timeout (seconds): 시간 초과 (초): <html><head/><body><p>Using multiple connections can help speed up your download.</p></body></html><html><head/><body><p>다중 연결을 사용하면 다운로드 속도를 높일 수 있습니다..</p></body></html>Number of connections: 연결 수: Change변경하기Download Options다운로드 옵션Volume: 용량: Notifications알림Style: 스타일: Color scheme: 색 구성표: Icons: 아이콘: Notification type: 알림 유형: Font: 글꼴: Size: 크기: Run Persepolis at startup시작 시 Persepolis 실행하기Keep system awake!시스템 작동 상태 유지!<html><head/><body><p>Format HH:MM</p></body></html><html><head/><body><p>형식 HH:MM</p></body></html>Status상태Video Finder Options비디오 찾기도구 옵션Defaults기본값Cancel취소OK확인Press new keys새 키 누르기Shortcuts단축키Hide main window if close button clicked.닫기 버튼을 클릭하면 기본 창이 숨겨집니다.<html><head/><body><p>This feature may not work in your operating system.</p></body></html><html><head/><body><p>이 기능은 운영 체제에서 작동하지 않을 수 있습니다.</p></body></html>Language: 언어: Wait period between retries (seconds): 재시도 간 대기 시간 (초): Don't use certificate to verify the peers피어를 확인하기 위해 인증서를 사용하지 않음<html><head/><body><p>This option avoids SSL/TLS handshake failure. But use it at your own risk!</p></body></html><html><head/><body><p>이 옵션은 SSL/TLS 핸드셰이크 실패를 방지합니다. 하지만 사용에 따른 위험은 사용자 본인의 책임입니다!</p></body></html>Download folder: 다운로드 폴더: Create subfolders for Music,Videos, ... in default download folder기본 다운로드 폴더에 음악, 동영상 등에 대한 하위 폴더 만들기Save As다른 이름으로 저장Enable Notification Sounds알림 사운드 활성화Toolbar icons size: 도구모음 아이콘 크기:Enable system tray icon시스템 트레이 아이콘 활성화Show download complete dialog when download is finished다운로드가 완료되면 다운로드 완료 대화상자 표시하기Show menubar메뉴표시줄 표시하기Show side panel측면 패널 표시하기Show download progress window다운로드 진행률 창 표시하기<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html><html><head/><body><p>이 옵션은 시스템이 절전 모드로 전환되는 것을 방지합니다. 전원 관리자가 시스템을 자동으로 절전모드로 전환하는 경우 필요합니다. </p></body></html>Columns Customization열 사용자 정의Check system clipboard for copied links복사된 링크가 있는지 시스템 클립보드 확인<html><head/><body><p>The program will automatically check the clipboard for copied links. </p></body></html><html><head/><body><p>프로그램은 자동으로 클립보드에 복사된 링크를 확인합니다. </p></body></html>Download requests from the browser will be executed immediately.브라우저에서 다운로드 요청이 즉시 실행됩니다.<html><head/><body><p>When a download request is sent from the browser extension, the download will start without showing the Add Link window. </p></body></html><html><head/><body><p>브라우저 확장 프로그램에서 다운로드 요청이 전송되면 링크 추가 창이 표시되지 않고 다운로드가 시작됩니다. </p></body></html><html><head/><body><p>Set number of retries if download failed.</p></body></html><html><head/><body><p>다운로드에 실패할 경우 재시도 횟수를 지정합니다.</p></body></html><html><head/><body><p>Set the seconds to wait between retries.</p></body></html><html><head/><body><p>재시도 간 대기 시간을 초 단위로 지정합니다.</p></body></html>Chunk size(KiB): 블록 크기(KiB): It is python requests library chunk size. Do not change this If you are not familiar with it.파이썬 요청 라이브러리 블록 크기입니다. 익숙하지 않더라도 변경하지 마세요.text_ui_trPersepolis Download ManagerPersepolis 다운로드 관리자IP:IP:Port:포트:Change Download Folder다운로드 폴더 변경하기Proxy password: 프록시 비밀번호: Proxy username: 프록시 사용자이름: Download username and password사용자이름 및 비밀번호 다운로드Download username: 사용자이름 다운로드: Download password: 비밀번호 다운로드: Download folder: 다운로드 폴더: Number of connections:연결 수:video_finder_progress_ui_tr<b>Video file status: </b><b>비디오 파일 상태: </b><b>Audio file status: </b><b>오디오 파일 상태: </b><b>Muxing status: </b><b>먹싱 상태: </b><b>Mixing status: </b><b>믹싱 상태: </b>
================================================
FILE: resources/locales/ui_nl_NL.ts
================================================
about_ui_trAbout PersepolisOver PersepolisPersepolis Download ManagerPersepolis-downloadbeheer<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a><a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a><a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>DevelopersOntwikkelaarsTranslatorsVertalersLicenseLicentieOKOké
AliReza AmirSamimi
Mohammadreza Abdollahzadeh
Sadegh Alirezaie
Mostafa Asadi
Jafar Akhondali
Kia Hamedi
H.Rostami
Ehsan Titish
MohammadAmin VahediniaTRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!
AliReza AmirSamimi
Mohammadreza Abdollahzadeh
Sadegh Alirezaie
Mostafa Asadi
Jafar Akhondali
Kia Hamedi
H.Rostami
Ehsan Titish
MohammadAmin VahediniaAcknowledgmentsErkenningen<a href=https://github.com/yt-dlp/yt-dlp>YT-DLP project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/yt-dlp/yt-dlp>YT-DLP-project</a><a href=https://github.com/ffmpeg/ffmpeg>FFmpeg project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/ffmpeg/ffmpeg>FFmpeg-project</a><a href=https://wiki.qt.io/Qt_for_Python>Pyside project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://wiki.qt.io/Qt_for_Python>Pyside-project<a href=https://github.com/psf/requests>Requests project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/psf/requests>Requests-project</a>addlink_ui_trAdd to category: Toevoegen aan categorie: ProxyProxyIP: Ip-adres: Port:Poort:Change Download FolderAndere downloadmap kiezenDownload Folder: Downloadmap: CancelAnnulerenOKOkéLinkLinkMore OptionsMeer optiesAdvanced OptionsGeavanceerde optiesAdd Download LinkDownloadlink toevoegenDownload link: Downloadlink: Change file name: Bestandsnaam wijzigen: Detect System Proxy SettingsProxy-instellingen van systeem gebruikenProxy password: Proxywachtwoord: Proxy username: Proxy-gebruikersnaam:Download username and passwordGebruikersnaam en wachtwoord voor downloadenDownload username: Gebruikersnaam voor downloaden: Download password: Wachtwoord voor downloaden: Remember this pathLocatie onthoudenStart timeBegintijdEnd timeEindtijdNumber of connections:Aantal gelijktijdige verbindingen:Download LaterLater downloadenHTTPHttpHTTPSHttpsSOCKS5Socks5after_download_src_ui_tr<b>File name</b>: <b>Bestandsnaam</b>: <b>Size</b>: <b>Grootte</b>: after_download_ui_trPersepolis Download ManagerPersepolis-downloadbeheer Open File Bestand openen Open Download FolderDownloadmap openen OK OkéDon't show this message again.Bericht niet meer tonen<b>Download Completed!</b><b>Download afgerond!</b><b>Save as</b>: <b>Opslaan als</b>: <b>Link</b>: <b>Link</b>: log_window_ui_trReport IssueProbleem meldenInitialization and informationInstellen en informatieDownloadsDownloadsErrors and warningsFoutmeldingen en waarschuwingenmainwindow_src_ui_trError: Foutmelding:<b>Link</b>: <b>Link</b>: <b>Downloaded</b>: <b>Gedownload</b>: <b>Transfer rate</b>: <b>Snelheid</b>: <b>Estimated time left</b>: <b>Resterende tijd (geschat)</b>: <b>Connections</b>: <b>Aantal verbindingen</b>: <b>Status</b>: <b>Status</b>: Download StoppedDownload gestoptError - Foutmelding - Download CompleteDownload afgerond<b><center>This link has been added before! Are you sure you want to add it again?</center></b><b><center>Je hebt deze link al toegevoegd! Weet je zeker dat je deze opnieuw wilt toevoegen?</center></b>Download StartsDownload startOperation was not successful.Handeling mislukt.Please resume the following category: Hervat de volgende categorie: Not enough free space in:Onvoldoende vrije ruimte in:muxing errormuxing-foutan error occurreder is een fout opgetredenPlease update Persepolis.Werk Persepolis bij.<b>Fragments</b>: <b>Gedeelten</b>:mainwindow_ui_trPersepolis Download ManagerPersepolis-downloadbeheerShow/Hide system tray iconSysteemvakpictogram tonen/verbergenAdd New Download LinkNieuwe downloadlink toevoegenResume DownloadDownload hervattenPause DownloadDownload pauzerenStop/Cancel DownloadDownload afbreken/stopzettenPropertiesEigenschappenProgressVoortgangExitAfsluitenStart TimeBegintijdEnd TimeEindtijdDownload bottom of
the list firstOnderaan beginnen
met downloadenApplyToepassenAfter downloadActie na downloaden:Shut DownAfsluiten<b>Video file status: </b><b>Status van videobestand: </b><b>Audio file status: </b><b>Status van audiobestand: </b><b>Status: </b><b>Status: </b><b>Muxing status: </b><b>Muxing-status: </b> downloadedgedownloadMinimize to System TrayMinimaliseren naar systeemvakHide OptionsOpties verbergenKeep System Awake!Systeem actief houden<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html><html><head/><body><p>Deze optie voorkomt dat het systeem inactief wordt. Dit is nodig als energiebeheer je systeem automatisch in de pauze- of slaapstand zet.</p></body></html>Start MixingMengen starten<b>Video File Status: </b><b>Status van videobestand: </b><b>Audio File Status: </b><b>Status van audiobestand: </b><b>Mixing status: </b><b>Mengstatus: </b>progress_ui_trPersepolis Download ManagerPersepolis-downloadbeheerStatus: Status: Downloaded:Gedownload:Transfer rate: Snelheid:Estimated time left:Resterende tijd (geschat):Number of connections: Aantal gelijktijdige verbindingen:Download InformationDownloadinformatieAfter downloadActie na downloadenApplyToepassenShut DownAfsluitenDownload OptionsDownloadoptiesResumeHervattenPausePauzerenStopStoppenLink: Link: setting_src_ui_tr<b><center>This shortcut has been used before! Use another one!</center></b><b><center>Deze sneltoets wordt al gebruikt! Kies een andere!</center></b>setting_ui_trPreferencesVoorkeurenNumber of tries: Aantal pogingen:<html><head/><body><p>Set timeout in seconds. </p></body></html><html><head/><body><p>Stel de time-out in, in seconden.</p></body></html>Timeout (seconds): Time-out (in seconden):<html><head/><body><p>Using multiple connections can help speed up your download.</p></body></html><html><head/><body><p>Meerdere gelijktijdige verbindingen kunnen het downloaden versnellen.</p></body></html>Number of connections: Aantal gelijktijdige verbindingen:ChangeWijzigenDownload OptionsDownloadoptiesVolume: Volume: NotificationsMeldingenStyle: Stijl: Color scheme: Kleurenschema: Icons: Pictogrammen: Notification type: Soort melding: Font: Lettertype: Size: Grootte: Run Persepolis at startupAutomatisch opstartenKeep system awake!Systeem actief houden<html><head/><body><p>Format HH:MM</p></body></html><html><head/><body><p>Opmaak: UU:MM</p></body></html>StatusStatusVideo Finder OptionsVideo-zoekoptiesDefaultsStandaardwaardenCancelAnnulerenOKOkéPress new keysDruk op de gewenste toetsenShortcutsSneltoetsenHide main window if close button clicked.Hoofdvenster minimaliseren na klikken op sluitknop.<html><head/><body><p>This feature may not work in your operating system.</p></body></html><html><head/><body><p>Deze functie werkt mogelijk niet op je besturingssysteem.</p></body></html>Language: Taal:Wait period between retries (seconds): Tijd tussen pogingen (in seconden):Don't use certificate to verify the peersGeen certificaten gebruiken om peers te verifiëren<html><head/><body><p>This option avoids SSL/TLS handshake failure. But use it at your own risk!</p></body></html><html><head/><body><p>Let op: met deze optie omzeil je de ssl-/tls-overdrachtsfoutmelding. Gebruik is op eigen risico!</p></body></html>Download folder: Downloadmap: Create subfolders for Music,Videos, ... in default download folderSubmappen creëren in downloadmap voor muziek, video's, etc.Save AsOpslaan alsEnable Notification SoundsMeldingsgeluiden inschakelenToolbar icons size: Pictogramgrootte op werkbalk: Enable system tray iconSysteemvakpictogram tonenShow download complete dialog when download is finishedBevestigingsvenster tonen als het downloaden is afgerondShow menubarMenubalk tonenShow side panelZijpaneel tonenShow download progress windowVenster met downloadvoortgang tonen<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html><html><head/><body><p>Deze optie voorkomt dat het systeem inactief wordt. Dit is nodig als energiebeheer je systeem automatisch in de pauze- of slaapstand zet.</p></body></html>Columns CustomizationKolomaanpassingCheck system clipboard for copied linksControleer het klembord op gekopieerde links<html><head/><body><p>The program will automatically check the clipboard for copied links. </p></body></html><html><head/><body><p>Het programma zal het klembord automatisch scannen op gekopieerde links. </p></body></html>Download requests from the browser will be executed immediately.Downloadverzoeken van de browser worden onmiddellijk uitgevoerd.<html><head/><body><p>When a download request is sent from the browser extension, the download will start without showing the Add Link window. </p></body></html><html><head/><body><p>Als er een downloadverzoek wordt verstuurd vanuit de browserextensie, dan start het downloaden zonder het toevoegvenster te tonen. </p></body></html><html><head/><body><p>Set number of retries if download failed.</p></body></html><html><head/><body><p>Stel in hoe vaak opnieuw moet worden geprobeerd na een mislukking.</p></body></html><html><head/><body><p>Set the seconds to wait between retries.</p></body></html><html><head/><body><p>Stel in na hoelang opnieuw moet worden geprobeerd, in seconden.</p></body></html>Chunk size(KiB): Opgedeelde groottes (in KiB):It is python requests library chunk size. Do not change this If you are not familiar with it.Dit zijn de opgedeelde groottes zoals vastgesteld door de Python-bibliotheek ‘requests’. Pas dit alléén aan als je weet wat je doet.text_ui_trPersepolis Download ManagerPersepolis-downloadbeheerIP:Ip-adres:Port:Poort: Change Download FolderAndere downloadmap kiezenProxy password: Proxywachtwoord: Proxy username: Proxy-gebruikersnaam:Download username and passwordGebruikersnaam en wachtwoord voor downloadenDownload username: Gebruikersnaam voor downloaden: Download password: Wachtwoord voor downloaden: Download folder: Downloadmap: Number of connections:Aantal gelijktijdige verbindingen:video_finder_progress_ui_tr<b>Video file status: </b><b>Status van videobestand: </b><b>Audio file status: </b><b>Status van audiobestand: </b><b>Muxing status: </b><b>Muxing-status: </b><b>Mixing status: </b><b>Mengstatus: </b>
================================================
FILE: resources/locales/ui_pl_PL.ts
================================================
about_ui_trAbout PersepolisO PersepolisPersepolis Download ManagerPersepolis - menadżer pobierania<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a><a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a><a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>DevelopersDeweloperzyTranslatorsTłumaczeLicenseLicencjaOKOK
AliReza AmirSamimi
Mohammadreza Abdollahzadeh
Sadegh Alirezaie
Mostafa Asadi
Jafar Akhondali
Kia Hamedi
H.Rostami
Ehsan Titish
MohammadAmin VahediniaTRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!Acknowledgments<a href=https://github.com/yt-dlp/yt-dlp>YT-DLP project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/ffmpeg/ffmpeg>FFmpeg project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://wiki.qt.io/Qt_for_Python>Pyside project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/psf/requests>Requests project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!addlink_ui_trAdd to category: Dodaj do kategorii:ProxyProxyIP: IP:Port:Port:Change Download FolderZmień folderDownload Folder: Folder:CancelAnulujOKOKLinkLinkMore OptionsWięcej opcjiAdvanced OptionsOpcje zaawansowaneAdd Download LinkDownload link: Change file name: Detect System Proxy SettingsProxy password: Proxy username: Download username and passwordDownload username: Download password: Remember this pathStart timeEnd timeNumber of connections:Download LaterHTTPHTTPSSOCKS5after_download_src_ui_tr<b>File name</b>: <b>Nazwa pliku</b>:<b>Size</b>: <b>Rozmiar</b>:after_download_ui_trPersepolis Download ManagerPersepolis - menadżer pobierania Open File Otwórz plik Open Download FolderOtwórz folder pobierania OK OKDon't show this message again.Nie pokazuj tego komunikatu ponownie.<b>Download Completed!</b><b>Pobieranie zakończone!</b><b>Save as</b>: <b>Zapisz jako</b>:<b>Link</b>: <b>Link</b>:log_window_ui_trReport IssueZgłoś problemInitialization and informationDownloadsErrors and warningsmainwindow_src_ui_trError: Błąd:<b>Link</b>: <b>Link</b>: <b>Downloaded</b>: <b>Pobrano</b>:<b>Transfer rate</b>: <b>Szybkość pobierania</b>:<b>Estimated time left</b>: <b>Pozostało</b>:<b>Connections</b>: <b>Połączenia</b>:<b>Status</b>: <b>Status</b>: Download StoppedPobieranie zatrzymaneError - Błąd -Download CompletePobieranie ukończone<b><center>This link has been added before! Are you sure you want to add it again?</center></b><b><center>Ten link został już dodany! Czy na pewno chcesz go dodać ponownie?</center></b>Download StartsStatus pobieraniaOperation was not successful.Działanie nie powiodło się.Please resume the following category: Wznów następującą kategorię:Not enough free space in:Brak wystarczającej ilości wolnego miejsca w:muxing errorbłąd muxingan error occurredPlease update Persepolis.<b>Fragments</b>: mainwindow_ui_trPersepolis Download ManagerPersepolis - menadżer pobieraniaShow/Hide system tray iconPokarz/Ukryj ikonę na pasku zadańAdd New Download LinkDodaj nowy link do pobraniaResume DownloadWznów pobieraniePause DownloadZapauzuj pobieranieStop/Cancel DownloadZatrzymaj/Anuluj pobieraniePropertiesWłaściwościProgressPostępExitWyjścieStart TimeGodzina rozpoczęciaEnd TimeGodzina zakończeniaDownload bottom of
the list firstDownload bottom of
the list firstApplyZastosujAfter downloadPo pobraniuShut DownWyłącz<b>Video file status: </b><b>Status pliku wideo: </b><b>Audio file status: </b><b>Status pliku audio: </b><b>Status: </b><b>Status: </b><b>Muxing status: </b><b>Status muxing: </b> downloadedpobraneMinimize to System TrayHide OptionsKeep System Awake!<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Start Mixing<b>Video File Status: </b><b>Audio File Status: </b><b>Mixing status: </b>progress_ui_trPersepolis Download ManagerPersepolis - menadżer pobieraniaStatus: Status:Downloaded:Pobrane:Transfer rate: Szybkość pobierania:Estimated time left:Pozostało:Number of connections: Liczba połączeń:Download InformationInformacje o pobraniuAfter downloadPo pobraniuApplyZastosujShut DownWyłączDownload OptionsOpcje pobieraniaResumeWznówPausePauzaStopStopLink: setting_src_ui_tr<b><center>This shortcut has been used before! Use another one!</center></b><b><center>Ta kombinacja została jest już zajęta. Użyj innej </center></b>setting_ui_trPreferencesPreferencjeNumber of tries: Liczba prób pobrania:<html><head/><body><p>Set timeout in seconds. </p></body></html><html><head/><body><p>Ustaw timeout w sekundach. </p></body></html>Timeout (seconds): Timeout (sekundy): <html><head/><body><p>Using multiple connections can help speed up your download.</p></body></html>Korzystanie z wielu połączeń może przyspieszyć pobieranie.Number of connections: Liczba połączeń:ChangeZmieńDownload OptionsOpcje pobieraniaVolume: Głośność:NotificationsPowiadomieniaStyle: Styl:Color scheme: Schemat kolorów:Icons: Ikony:Notification type: Typ powiadomienia:Font: Czcionka:Size: Rozmiar:Run Persepolis at startupUruchom Persepolis przy starcie systemuKeep system awake!Nie usypiaj systemu!<html><head/><body><p>Format HH:MM</p></body></html><html><head/><body><p>Format HH:MM</p></body></html>StatusStatusVideo Finder OptionsOpcje wyszukiwarki wideoDefaultsDomyślneCancelAnulujOKOKPress new keysWciśnij nowe klawiszeShortcutsSkrótyHide main window if close button clicked.Schowaj okno główne po kliknięciu przycisku zamknij<html><head/><body><p>This feature may not work in your operating system.</p></body></html><html><head/><body><p>Ta funkcja może nie działać w twoim systemie operacyjnym.</p></body></html>Language: Wait period between retries (seconds): Don't use certificate to verify the peers<html><head/><body><p>This option avoids SSL/TLS handshake failure. But use it at your own risk!</p></body></html>Download folder: Create subfolders for Music,Videos, ... in default download folderSave AsEnable Notification SoundsToolbar icons size: Enable system tray iconShow download complete dialog when download is finishedShow menubarShow side panelShow download progress window<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Columns CustomizationCheck system clipboard for copied links<html><head/><body><p>The program will automatically check the clipboard for copied links. </p></body></html>Download requests from the browser will be executed immediately.<html><head/><body><p>When a download request is sent from the browser extension, the download will start without showing the Add Link window. </p></body></html><html><head/><body><p>Set number of retries if download failed.</p></body></html><html><head/><body><p>Set the seconds to wait between retries.</p></body></html>Chunk size(KiB): It is python requests library chunk size. Do not change this If you are not familiar with it.text_ui_trPersepolis Download ManagerPersepolis - menadżer pobieraniaIP:IP:Port:Port:Change Download FolderZmień folderProxy password: Proxy username: Download username and passwordDownload username: Download password: Download folder: Number of connections:video_finder_progress_ui_tr<b>Video file status: </b><b>Status pliku wideo: </b><b>Audio file status: </b><b>Status pliku audio: </b><b>Muxing status: </b><b>Status muxing: </b><b>Mixing status: </b>
================================================
FILE: resources/locales/ui_pt.ts
================================================
about_ui_trAbout PersepolisSobre o PersepolisPersepolis Download ManagerGerenciador de Downloads Persepolis<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a><a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a><a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>DevelopersDesenvolvedoresTranslatorsTradutoresLicenseLicençaOKOKSpecial thanks to:Agradecimentos especiais a:
AliReza AmirSamimi
Mohammadreza Abdollahzadeh
Sadegh Alirezaie
Mostafa Asadi
Jafar Akhondali
Kia Hamedi
H.Rostami
Ehsan Titish
MohammadAmin VahediniaTRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!Acknowledgments:<a href=https://github.com/yt-dlp/yt-dlp>YT-DLP project</a><a href=https://github.com/ffmpeg/ffmpeg>FFmpeg project</a>AcknowledgmentsVersion 4.3.0TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!addlink_ui_trAdd to category: Adicionar a categoria:ProxyProxyIP: IP:Port:Porta:Change Download FolderAlterar pasta de downloads:Download Folder: Pasta de downloadsCancelCancelarOKOKLinkLinkMore OptionsMais opçõesAdvanced OptionsOpções avançadasReferrer: Referenciador:Header: Cabeçalho:User agent: Agente de usuário:Add Download LinkDownload link: Change file name: Detect System Proxy SettingsProxy password: Proxy username: Download username and passwordDownload username: Download password: Remember this pathStart timeEnd timeNumber of connections:Download LaterHTTPHTTPSSOCKS5Cookies: after_download_src_ui_tr<b>File name</b>: <b>Nome do arquivo</b>:<b>Size</b>: <b>Tamanho</b>:after_download_ui_trPersepolis Download ManagerGerenciador de Downloads Persepolis Open File Abrir arquivoOpen Download FolderAbrir pasta OK OK Don't show this message again.Não mostre esta mensagem novamente.<b>Download Completed!</b><b>Download completo!</b><b>Save as</b>: <b>Salvar como</b>: <b>Link</b>: <b>Link</b>: log_window_ui_trPersepolis LogPersepolis LogReport IssueRelatar um problemaCloseCopy Selected to ClipboardRefresh Log MessagesClear Log Messagesmainwindow_src_ui_trPersepolisPersepolisQueue Stopped!Fila parada!Persepolis is shutting downPersepolis desligaráyour system in 20 secondsseu sistema em 20 segundosQueue completed!Fila completa!Show main WindowMostrar janela principalError: Erro:<b>Link</b>: <b>Link</b>: <b>Downloaded</b>: <b>Baixado</b>: <b>Transfer rate</b>: <b>Taxa de transferência</b>: <b>Estimated time left</b>: <b>Tempo restante estimado</b>: <b>Connections</b>: <b>Conexões</b>: <b>Status</b>: <b>Progresso</b>: Download StoppedDownload paradoError - Erro - Download CompleteDownload completo<b><center>This link has been added before! Are you sure you want to add it again?</center></b><b><center>Este link foi adicionado antes! Tem certeza de que deseja adicioná-lo novamente?</center></b>Download StartsIniciar downloadDownload ScheduledDownload agendadoOperation was not successful.A operação não foi bem sucedida.Please resume the following category: Por favor, retome a seguinte categoria:Please stop the following category: Por favor, pare a seguinte categoria:Minimize to system trayMinimizar para área de notificaçãoNot FoundNão encontradoOperation was not successful!A operação não foi bem sucedida.Operation was not successful! Please stop the following category first: A operação não foi bem sucedida! Por favor pare a seguinte categoria primeiro:Please stop the following download first: Por favor pare o seguinte download primeiro:<b><center>This operation will delete downloaded files from your hard disk<br>PERMANENTLY!</center></b><b><center>Esta operação deletará os arquivos baixados no seu disco rígido<br>PERMANENTEMENTE!</center></b><center>Do you want to continue?</center><center>Deseja continuar?</center>Please stop the following category first: Por favor pare a seguinte categoria primeiro:</b>" already exists!</b>"Já existe!Send selected downloads toEnvie o download selecionado paraSend toEnviar paraSort byOrdenar por<b><center>This operation will remove all download items in this queue<br>from "All Downloads" list!</center></b><b><center>Esta operação irá remover todos os itens nesta fila<br>de "Todas as listas de Downloads"!</center></b><b>Sorry! You can't remove default queue!</b><b>Desculpe! Você não pode remover a fila padrão!</b>Some items didn't transferred successfully!Alguns itens não foram transferidos com sucesso!Please stop download progress first.Por favor, primeiro pare o progresso do download.Hide optionsOcultar opçõesShow optionsMostrar opçõesStop all downloads first!Pare primeiro todos os downloads!Moving isfinished!Download is in progress by video finder!be patient!Stop the following download first: ffmpeg is not installed!Not enough free space in:muxing erroran error occurredPlease update Persepolis.There is not enough disk space available at the download folder! Please choose another one or clear some space.yt-dlp is not installed!mainwindow_ui_trFileArquivoEditEditarViewVisualizarDownloadDownloadQueueFilaVideo FinderVideo FinderHelpAjudaSort byOrdenar porPersepolis Download ManagerGerenciador de Downloads PersepolisCategoryCategoriaFile NameNome de arquivoStatusProgressoSizeTamanhoDownloadedBaixadoPercentagePorcentagemConnectionsConexõesLinkLink&File&Arquivo&Edit&Editar&View&Visualizar&Download&Download&Queue&Fila&Help&AjudaShow/Hide system tray iconMostrar/Ocultar área de notificaçãoAdd New Download LinkAdicionar novo link de downloadResume DownloadResumir DownloadPause DownloadPausar DownloadStop DownloadParar DownloadStop/Cancel DownloadParar/Cancelar DownloadPropertiesPropriedadesProgressProgressoExitSairClear all items in download listLimpar todos os itens na lista de downloadCreate new download queueCriar nova fila de downloadRemove this queueRemover esta filaStart this queueIniciar esta filaStop this queueParar esta filaMove currently selected items up by one rowMover itens selecionados uma linha acimaMove currently selected items down by one rowMover itens selecionados uma linha abaixoPreferencesPreferênciasAboutSobreReport an issueRelatar um problemaStart TimeTempo de inicioEnd TimeTempo de terminoDownload bottom of
the list firstBaixe o último arquivo
da lista primeiroApplyExecutarAfter downloadDepois o downloadShut DownDesligarV&ideo FinderV&ideo Finder<b>Video file status: </b><b>Audio file status: </b><b>Status: </b><b>Muxing status: </b> downloadedActiveNot ActiveStartedErrorCompleteTransfer RateEstimated Time LeftFirst Try DateLast Try DateFind Video Links...Download video or audio from Youtube, Vimeo, etc.Stop All Active DownloadsFile SizeDownload StatusShow System Tray IconShow MenubarShow Side PanelMinimize to System TrayAdd New Download Link...Import Links from Text File...Create a text file and put links in it, line by line!Open File...Open Download FolderOpen Default Download FolderClear Download ListRemove Selected Downloads from ListDelete Selected Download FilesMove Selected Download Files to Another Folder...Move Selected Download Files to Another FolderCreate New Queue...Remove QueueStart QueueStop QueueMove Selected Items UpMove Selected Items DownReport an IssueShow Log FileHide OptionsKeep System Awake!<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Start Mixing<b>Video File Status: </b><b>Audio File Status: </b><b>Mixing status: </b>Import Links from Clipboard...Import Links From Clipboardprogress_ui_trPersepolis Download ManagerGerenciador de Downloads PersepolisStatus: Progresso:Downloaded:Baixado:Transfer rate: Taxa de transferência:Estimated time left:Tempo restante estimado:Number of connections: Número de conexões:Download InformationInformações de DownloadAfter downloadApós o downloadApplyExecutarShut DownDesligarDownload OptionsOpções de DownloadResumeResumirPausePausarStopPararLink: setting_src_ui_tr<b><center>Restart Persepolis Please!</center></b><br><center>Some changes take effect after restarting Persepolis</center><b><center>Reinicie o Persepolis por favor!</center></b><br><center>Algumas alterações só têm efeito após reiniciar o software</center>Restart Persepolis!Reiniciar o Persepolis!<b><center>This shortcut has been used before! Use another one!</center></b>setting_ui_trPreferencesPreferênciasNumber of tries: Números de tentativas:<html><head/><body><p>Set timeout in seconds. </p></body></html><html><head/><body><p>Definir intervalo em segundos. </p></body></html>Timeout (seconds): Intervalo (segundos): <html><head/><body><p>Using multiple connections can help speed up your download.</p></body></html><html><head/><body><p>Usar várias conexões pode ajudar a acelerar o download.</p></body></html>Number of connections: Número de conexõesChangeAlterarDownload OptionsOpções de DownloadVolume: Volume: NotificationsNotificaçõesStyle: Estilo:Color scheme: Esquema de cores:Icons: Ícones:Notification type: Tipo da notificação:Font: Fonte:Size: Tamanho:Run Persepolis at startupExecutar Persepolis na inicializaçãoKeep system awake!Impedir sistema de entrar em modo suspenso!<html><head/><body><p>Format HH:MM</p></body></html><html><head/><body><p>Formato HH:MM</p></body></html>File NameNome de arquivoStatusStatusSizeTamanhoDownloadedBaixadoPercentagePorcentagemConnectionsConexõesCategoryCategoriaVideo Finder OptionsOpções do Video FinderMaximum number of links to capture:<br/><small>(If browser sends multiple video links at a time)</small>Número máximo de links a serem capturados:<br/><small>(se o browser envia vários links de vídeo de cada vez)</small>DefaultsPadrõesCancelCancelarOKOKPress new keysActionShortcutShortcutsQuitHide main window if close button clicked.<html><head/><body><p>This feature may not work in your operating system.</p></body></html>Language: Minimize to System TrayRemove Download ItemsDelete Download ItemsMove Selected Items UpMove Selected Items DownAdd New Download LinkAdd New Video LinkImport Links from Text FileWait period between retries (seconds): Wait period between each download in queue:Don't use certificate to verify the peers<html><head/><body><p>This option avoids SSL/TLS handshake failure. But use it at your own risk!</p></body></html>Download folder: Create subfolders for Music,Videos, ... in default download folderSave AsEnable Notification SoundsToolbar icons size: If browser is opened, start Persepolis in system trayEnable system tray iconShow download complete dialog when download is finishedShow menubarShow side panelShow download progress window<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Show these columns:Transfer RateEstimated Time LeftFirst Try DateLast Try DateColumns CustomizationCheck system clipboard for copied links<html><head/><body><p>The program will automatically check the clipboard for copied links. </p></body></html>Download requests from the browser will be executed immediately.<html><head/><body><p>When a download request is sent from the browser extension, the download will start without showing the Add Link window. </p></body></html><html><head/><body><p>Set number of retries if download failed.</p></body></html><html><head/><body><p>Set the seconds to wait between retries.</p></body></html>Chunk size(KiB): It is python requests library chunk size. Do not change this If you are not familiar with it.text_ui_trPersepolis Download ManagerGerenciador de Download PersepolisLinksLinksSelect AllSelecionar TodosDeselect AllDesselecionar TodosAdd to queue: Adicionar para a fila:ProxyProxyIP:IP:Port:PortaChange Download FolderAlterar pasta de downloadsOKOKCancelCancelarDownload OptionsProxy password: Proxy username: Download username and passwordDownload username: Download password: Download folder: Number of connections:video_finder_progress_ui_tr<b>Video file status: </b><b>Audio file status: </b><b>Muxing status: </b><b>Mixing status: </b>ytaddlink_src_ui_trVideo FinderVideo FinderFetch Media ListBuscar lista de mídiasSelect a formatSelecione um formatoPlease enter a valid video linkPor favor, insira um link de vídeo válidoFetching Media Info...Buscando informações sobre a mídia...Video format:Audio format:Advanced options
================================================
FILE: resources/locales/ui_pt_BR.ts
================================================
about_ui_trAbout PersepolisSobre PersépolisPersepolis Download ManagerGerenciador de Downloads Persépolis<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a><a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a><a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>DevelopersDesenvolvedoresTranslatorsTradutoresLicenseLicençaOKAceitar
AliReza AmirSamimi
Mohammadreza Abdollahzadeh
Sadegh Alirezaie
Mostafa Asadi
Jafar Akhondali
Kia Hamedi
H.Rostami
Ehsan Titish
MohammadAmin VahediniaTRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!Acknowledgments<a href=https://github.com/yt-dlp/yt-dlp>YT-DLP project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/ffmpeg/ffmpeg>FFmpeg project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://wiki.qt.io/Qt_for_Python>Pyside project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/psf/requests>Requests project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!addlink_ui_trAdd to category: Adicionar à categoria: ProxyProxyIP: IP: Port:Porta:Change Download FolderAlterar LocalDownload Folder: Baixar para: CancelCancelarOKAceitarLinkLinkMore OptionsMais OpçõesAdvanced OptionsOpções AvançadasAdd Download LinkDownload link: Change file name: Detect System Proxy SettingsProxy password: Proxy username: Download username and passwordDownload username: Download password: Remember this pathStart timeEnd timeNumber of connections:Download LaterHTTPHTTPSSOCKS5after_download_src_ui_tr<b>File name</b>: <b>Nome do Arquivo</b>: <b>Size</b>: <b>Tamanho</b>: after_download_ui_trPersepolis Download ManagerGerenciador de Downloads Persépolis Open File Abrir Arquivo Open Download FolderAbrir Pasta de Baixados OK Aceitar Don't show this message again.Não exibir essa mensagem novamente.<b>Download Completed!</b><b>Terminou de Baixar!</b><b>Save as</b>: <b>Salvar como</b>: <b>Link</b>: <b>Link</b>: log_window_ui_trReport IssueInformar um ProblemaInitialization and informationDownloadsErrors and warningsmainwindow_src_ui_trError: Erro: <b>Link</b>: <b>Link</b>: <b>Downloaded</b>: <b>Baixado</b>: <b>Transfer rate</b>: <b>Baixando a</b>: <b>Estimated time left</b>: <b>Termina em</b>: <b>Connections</b>: <b>Conexões</b>: <b>Status</b>: <b>Estado</b>: Download StoppedRecebimento ParadoError - Erro - Download CompleteTerminou de Baixar<b><center>This link has been added before! Are you sure you want to add it again?</center></b><b><center>Esse link já foi adicionado antes! Tem certeza que deseja adicioná-lo novamente?</center></b>Download StartsComeçou a BaixarOperation was not successful.A operação não obteve sucesso.Please resume the following category: Por favor, continue a seguinte categoria: Not enough free space in:Não há espaço livre suficiente em:muxing errorErro ao multiplexaran error occurredPlease update Persepolis.<b>Fragments</b>: mainwindow_ui_trPersepolis Download ManagerGerenciador de Downloads PersépolisShow/Hide system tray iconExibir/Ocultar para área de notificaçãoAdd New Download LinkAdicionar Novo Link para BaixarResume DownloadContinuarPause DownloadPausar Stop/Cancel DownloadParar/Cancelar RecebimentoPropertiesPropriedadesProgressProgressoExitSairStart TimeTempo InicialEnd TimeTempo FinalDownload bottom of
the list firstBaixar o final da
lista primeiroApplyAplicarAfter downloadBaixar depoisShut DownDesligar<b>Video file status: </b><b>Estado dos arquivos de vídeo: </b><b>Audio file status: </b><b>Estado dos arquivos de áudio: </b><b>Status: </b><b>Estado: </b><b>Muxing status: </b><b>Estado da multiplexação: </b> downloadedbaixadoMinimize to System TrayHide OptionsKeep System Awake!<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Start Mixing<b>Video File Status: </b><b>Audio File Status: </b><b>Mixing status: </b>progress_ui_trPersepolis Download ManagerGerenciador de Downloads PersépolisStatus: Estado:Downloaded:Baixado:Transfer rate: Taxa de transferência: Estimated time left:Tempo restante estimado:Number of connections: Número de Conexões: Download InformationInformação de RecebimentoAfter downloadBaixar depoisApplyAplicarShut DownDesligarDownload OptionsOpções ao BaixarResumeContinuarPausePausarStopPararLink: setting_src_ui_tr<b><center>This shortcut has been used before! Use another one!</center></b><b><center>Este atalho já está sendo usado! Tente usar outro!</center></b>setting_ui_trPreferencesPreferênciasNumber of tries: Número de tentativas: <html><head/><body><p>Set timeout in seconds. </p></body></html><html><head/><body><p>Define o tempo limite em segundos. </p></body></html>Timeout (seconds): Tempo limite (segundos): <html><head/><body><p>Using multiple connections can help speed up your download.</p></body></html><html><head/><body><p>Utilizar várias conexões pode ajudar a acelerar o recebimento.</p></body></html>Number of connections: Número de conexões: ChangeAlterarDownload OptionsOpções Para BaixarVolume: Volume: NotificationsNotificaçãoStyle: Estilo: Color scheme: Esquema de cor: Icons: Ícones: Notification type: Tipo de notificação: Font: Fonte: Size: Tamanho: Run Persepolis at startupExecutar o Persépolis na inicializaçãoKeep system awake!Manter o sistema ativo!<html><head/><body><p>Format HH:MM</p></body></html><html><head/><body><p>Formato HH:MM</p></body></html>StatusStatusVideo Finder OptionsOpções do Buscador de VídeoDefaultsPadrõesCancelCancelarOKAceitarPress new keysEscolha uma nova teclaShortcutsAtalhosHide main window if close button clicked.Ocultar janela ao clicar no botão fechar.<html><head/><body><p>This feature may not work in your operating system.</p></body></html><html><head/><body><p> Este recurso pode não funcionar no seu sistema operacional.</p></body></html>Language: Wait period between retries (seconds): Don't use certificate to verify the peers<html><head/><body><p>This option avoids SSL/TLS handshake failure. But use it at your own risk!</p></body></html>Download folder: Create subfolders for Music,Videos, ... in default download folderSave AsEnable Notification SoundsToolbar icons size: Enable system tray iconShow download complete dialog when download is finishedShow menubarShow side panelShow download progress window<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Columns CustomizationCheck system clipboard for copied links<html><head/><body><p>The program will automatically check the clipboard for copied links. </p></body></html>Download requests from the browser will be executed immediately.<html><head/><body><p>When a download request is sent from the browser extension, the download will start without showing the Add Link window. </p></body></html><html><head/><body><p>Set number of retries if download failed.</p></body></html><html><head/><body><p>Set the seconds to wait between retries.</p></body></html>Chunk size(KiB): It is python requests library chunk size. Do not change this If you are not familiar with it.text_ui_trPersepolis Download ManagerGerenciador de Downloads PersépolisIP:IP:Port:Porta:Change Download FolderAlterar Pasta PadrãoProxy password: Proxy username: Download username and passwordDownload username: Download password: Download folder: Number of connections:video_finder_progress_ui_tr<b>Video file status: </b><b>Estado do arquivo de vídeo: </b><b>Audio file status: </b><b>Estado do arquivo de áudio: </b><b>Muxing status: </b><b>Estado da Multiplexação: </b><b>Mixing status: </b>
================================================
FILE: resources/locales/ui_ru.ts
================================================
about_ui_trAbout PersepolisО PersepolisPersepolis Download ManagerМенеджер загрузок Persepolis<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a><a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a><a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>DevelopersРазработчикиTranslatorsПереводчикиLicenseЛицензияOKOK
AliReza AmirSamimi
Mohammadreza Abdollahzadeh
Sadegh Alirezaie
Mostafa Asadi
Jafar Akhondali
Kia Hamedi
H.Rostami
Ehsan Titish
MohammadAmin VahediniaTRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!AcknowledgmentsБлагодарности<a href=https://github.com/yt-dlp/yt-dlp>YT-DLP project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/yt-dlp/yt-dlp>Проект YT-DLP</a><a href=https://github.com/ffmpeg/ffmpeg>FFmpeg project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/ffmpeg/ffmpeg>Проект FFmpeg</a><a href=https://wiki.qt.io/Qt_for_Python>Pyside project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://wiki.qt.io/Qt_for_Python>Проект Pyside</a><a href=https://github.com/psf/requests>Requests project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/psf/requests>Запросы проекта</a>addlink_ui_trAdd to category: Добавить в категорию:ProxyПроксиIP: IP: Port:Порт:Change Download FolderИзменить папку для загрузкиDownload Folder: Папка для загрузки: CancelОтменитьOKOKLinkСсылкаMore OptionsБольше параметровAdvanced OptionsРасширенные параметрыAdd Download LinkДобавить ссылку для скачиванияDownload link: Ссылка для скачивания: Change file name: Изменить имя файла: Detect System Proxy SettingsОпределить настройки системного проксиProxy password: Пароль прокси: Proxy username: Имя пользователя прокси:Download username and passwordЗагрузить имя пользователя и парольDownload username: Загрузить имя пользователяDownload password: Загрузить парольRemember this pathЗапомнить этот путьStart timeВремя началаEnd timeВремя окончанияNumber of connections:Количество подключений:Download LaterЗагрузить позжеHTTPHTTPHTTPSHTTPSSOCKS5SOCKS5after_download_src_ui_tr<b>File name</b>: <b>Имя файла</b>: <b>Size</b>: <b>Размер</b>: after_download_ui_trPersepolis Download ManagerМенеджер загрузок Persepolis Open File Открыть файлOpen Download FolderОткрыть папку загрузки OK OK Don't show this message again.Не показывать это сообщение снова.<b>Download Completed!</b><b>Загрузка завершена!</b><b>Save as</b>: <b>Сохранить как</b>: <b>Link</b>: <b>Ссылка</b>: log_window_ui_trReport IssueСообщить о проблемеInitialization and informationDownloadsErrors and warningsmainwindow_src_ui_trError: Ошибка:<b>Link</b>: <b>Ссылка</b>: <b>Downloaded</b>: <b>Загружено</b>: <b>Transfer rate</b>: <b>Скорость передачи</b>: <b>Estimated time left</b>: <b>Ожидаемое время</b>: <b>Connections</b>: <b>Соединения</b>: <b>Status</b>: <b>Статус</b>: Download StoppedЗагрузка остановленаError - Ошибка -Download CompleteЗагрузка завершена<b><center>This link has been added before! Are you sure you want to add it again?</center></b><b><center>Эта ссылка была добавлена раньше! Вы уверены, что хотите добавить её ещё раз?</center></b>Download StartsЗагрузка началасьOperation was not successful.Операция не была успешной.Please resume the following category: Пожалуйста, возобновите следующую категорию:Not enough free space in:Недостаточно свободного места в:muxing errorошибка мультиплексированияan error occurredпроизошла ошибкаPlease update Persepolis.Пожалуйста, обновите Persepolis.<b>Fragments</b>: <b>Фрагменты</b>:mainwindow_ui_trPersepolis Download ManagerМенеджер загрузок PersepolisShow/Hide system tray iconПоказать / спрятать значок в системном трееAdd New Download LinkДобавить новую ссылку для скачиванияResume DownloadВозобновить загрузкуPause DownloadПауза загрузкиStop/Cancel DownloadОстановить/Отменить загрузкуPropertiesСвойстваProgressПрогрессExitВыходStart TimeВремя началаEnd TimeВремя окончанияDownload bottom of
the list firstСначала загрузите
нижнюю часть спискаApplyПринятьAfter downloadПосле загрузкиShut DownВыключить<b>Video file status: </b><b>Состояние видеофайла: </b><b>Audio file status: </b><b>Состояние аудиофайла: </b><b>Status: </b><b>Состояние: </b><b>Muxing status: </b><b>Состояние мультиплексирования: </b> downloadedзагруженоMinimize to System TrayHide OptionsСкрыть параметрыKeep System Awake!<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Start Mixing<b>Video File Status: </b><b>Audio File Status: </b><b>Mixing status: </b>progress_ui_trPersepolis Download ManagerМенеджер загрузок PersepolisStatus: Статус:Downloaded:Закачено:Transfer rate: Скорость передачи:Estimated time left:Ожидаемое время:Number of connections: Количество соединений:Download InformationИнформация о закачкеAfter downloadПосле закачкиApplyПринятьShut DownВыключитьDownload OptionsПараметры загрузкиResumeПродолжитьPauseПаузаStopОстановитьLink: Ссылка: setting_src_ui_tr<b><center>This shortcut has been used before! Use another one!</center></b><b><center>Это сочетание уже используется! Выберите другое!</center></b>setting_ui_trPreferencesПредпочтенияNumber of tries: Количество попыток:<html><head/><body><p>Set timeout in seconds. </p></body></html><html><head/><body><p>Установите тайм-аут в секундах. </p></body></html>Timeout (seconds): Тайм-аут (в секундах):<html><head/><body><p>Using multiple connections can help speed up your download.</p></body></html><html><head/><body><p>Использование нескольких соединений может помочь ускорить загрузку.</p></body></html>Number of connections: Количество соединений:ChangeИзменитьDownload OptionsПараметры загрузкиVolume: Громкость:NotificationsУведомленияStyle: Стиль:Color scheme: Цветовая схема:Icons: Иконки:Notification type: Тип уведомлений:Font: Шрифт:Size: Размер:Run Persepolis at startupЗапускать Persepolis при старте системыKeep system awake!Не позволять системе перейти в ждущий режим <html><head/><body><p>Format HH:MM</p></body></html><html><head/><body><p>Формат HH:MM</p></body></html>StatusСтатусVideo Finder OptionsПараметры Video FinderDefaultsЗначения по умолчаниюCancelОтменитьOKOKPress new keysНажмите новые клавишиShortcutsСочетания клавишHide main window if close button clicked.Скрывать главное окно, если нажата клавиша закрытия.<html><head/><body><p>This feature may not work in your operating system.</p></body></html><html><head/><body><p>Эта функция может не работать в вашей операционной системе.</p></body></html>Language: Язык: Wait period between retries (seconds): Don't use certificate to verify the peers<html><head/><body><p>This option avoids SSL/TLS handshake failure. But use it at your own risk!</p></body></html>Download folder: Create subfolders for Music,Videos, ... in default download folderSave AsEnable Notification SoundsВключить звуки уведомленийToolbar icons size: Размер значков на панели инструментов: Enable system tray iconВключить значок на панели задачShow download complete dialog when download is finishedПоказывать диалоговое окно после завершения загрузкиShow menubarShow side panelShow download progress window<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Columns CustomizationCheck system clipboard for copied links<html><head/><body><p>The program will automatically check the clipboard for copied links. </p></body></html>Download requests from the browser will be executed immediately.<html><head/><body><p>When a download request is sent from the browser extension, the download will start without showing the Add Link window. </p></body></html><html><head/><body><p>Set number of retries if download failed.</p></body></html><html><head/><body><p>Set the seconds to wait between retries.</p></body></html>Chunk size(KiB): It is python requests library chunk size. Do not change this If you are not familiar with it.text_ui_trPersepolis Download ManagerМенеджер загрузок PersepolisIP:IP: Port:Порт:Change Download FolderИзменить папку для загрузкиProxy password: Proxy username: Download username and passwordDownload username: Download password: Download folder: Number of connections:Количество подключений:video_finder_progress_ui_tr<b>Video file status: </b><b>Состояние видеофайла: </b><b>Audio file status: </b><b>Состояние аудиофайла: </b><b>Muxing status: </b><b>Состояние мультиплексирования: </b><b>Mixing status: </b>
================================================
FILE: resources/locales/ui_sv.ts
================================================
about_ui_trAbout PersepolisOm PersepolisPersepolis Download ManagerNerladdningshanteraren Persepolis<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a><a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a><a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>DevelopersUtvecklareTranslatorsÖversättareLicenseLicensOKOKSpecial thanks to:Speciellt tack till:
AliReza AmirSamimi
Mohammadreza Abdollahzadeh
Sadegh Alirezaie
Mostafa Asadi
Jafar Akhondali
Kia Hamedi
H.Rostami
Ehsan Titish
MohammadAmin VahediniaTRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!Acknowledgments:<a href=https://github.com/yt-dlp/yt-dlp>YT-DLP project</a><a href=https://github.com/ffmpeg/ffmpeg>FFmpeg project</a>AcknowledgmentsVersion 4.3.0TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!addlink_ui_trAdd to category: Lägg till i kategori:ProxyProxyIP: IP:Port:Port:Change Download FolderÄndra nerladdningsmappDownload Folder: Nerladdningsmapp:CancelAvbrytOKOKLinkLänkMore OptionsFler alternativAdvanced OptionsAvancerade alternativReferrer: Refererare:Header: Huvud:User agent: Användaragent:Add Download LinkDownload link: Change file name: Detect System Proxy SettingsProxy password: Proxy username: Download username and passwordDownload username: Download password: Remember this pathStart timeEnd timeNumber of connections:Download LaterHTTPHTTPSSOCKS5Cookies: after_download_src_ui_tr<b>File name</b>: <b>Filnamn</b>:<b>Size</b>: <b>Storlek</b>:after_download_ui_trPersepolis Download ManagerNerladdningshanteraren Persepolis Open File Öppna filOpen Download FolderÖppna nerladdningsmapp OK OKDon't show this message again.Visa inte det här meddelandet igen.<b>Download Completed!</b><b>Nerladdning slutförd!</b><b>Save as</b>: <b>Spara som</b>:<b>Link</b>: <b>Länk</b>:log_window_ui_trPersepolis LogLogg för PersepolisReport IssueRapportera felCloseCopy Selected to ClipboardRefresh Log MessagesClear Log Messagesmainwindow_src_ui_trPersepolisPersepolisQueue Stopped!Kön stannade!Persepolis is shutting downPersepolis stängs neryour system in 20 secondsditt system på 20 sekunderQueue completed!Kön slutfördes!Show main WindowVisa huvudfönstretError: Fel:<b>Link</b>: <b>Länk</b>:<b>Downloaded</b>: <b>Nerladdade</b>:<b>Transfer rate</b>: <b>Överföringshastighet</b>:<b>Estimated time left</b>: <b>Beräknad tid som återstår</b>:<b>Connections</b>: <b>Anslutningar</b>:<b>Status</b>: <b>Status</b>:Download StoppedNerladdningen stannadeError - Fel -Download CompleteNerladdningen färdig<b><center>This link has been added before! Are you sure you want to add it again?</center></b>Download StartsNerladdning påbörjasDownload ScheduledOperation was not successful.Operationen lyckades inte.Please resume the following category: Vänligen återuppta den följande kategorin:Please stop the following category: Vänligen stoppa den följande kategorin:Minimize to system trayMinimera till aktivitetsfältNot FoundHittades inteOperation was not successful!Åtgärden lyckades inte!Operation was not successful! Please stop the following category first: Operationen lyckades inte! Vänligen stoppa den följande kategorin först:Please stop the following download first: Vänligen stoppa den följande nerladdningen först:<b><center>This operation will delete downloaded files from your hard disk<br>PERMANENTLY!</center></b><center>Do you want to continue?</center><center>Vill du fortsätta?</center>Please stop the following category first: Vänligen stoppa den följande kategorin först:</b>" already exists!</b>" finns redan!Send selected downloads toSkicka valda nerladdningar tillSend toSkicka tillSort bySortera efter<b><center>This operation will remove all download items in this queue<br>from "All Downloads" list!</center></b><b>Sorry! You can't remove default queue!</b><b>Ledsen! Du kan inte ta bort standard-kön!</b>Some items didn't transferred successfully!Överföringen av några objekt lyckades inte!Please stop download progress first.Vänligen stoppa nerladdningsprocessen först.Hide optionsGöm alternativShow optionsVisa alternativStop all downloads first!Stoppa alla nerladdningar först!Moving isFlytten ärfinished!klar!Download is in progress by video finder!be patient!ha tålamod!Stop the following download first: Stoppa den följande nerladdningen först:ffmpeg is not installed!ffmpeg är inte installerad!Not enough free space in:Det finns inte tillräckligt med fritt utrymme i:muxing errormuxningsfelan error occurredPlease update Persepolis.There is not enough disk space available at the download folder! Please choose another one or clear some space.yt-dlp is not installed!mainwindow_ui_trFileArkivEditRedigeraViewVisaDownloadLadda nerQueueKöVideo FinderVideosökareHelpHjälpSort bySortera efterPersepolis Download ManagerNerladdningshanteraren PersepolisCategoryKategoriFile NameFilnamnStatusStatusSizeStorlekDownloadedNerladdadePercentageProcentConnectionsAnslutningarLinkLänk&File&Arkiv&Edit&Redigera&View&Visa&Download&Ladda ner&Queue&Kö&Help&HjälpShow/Hide system tray iconVisa/Göm ikonen i systemets aktivitetsfältAdd New Download LinkLägg till nerladdningslänkResume DownloadÅteruppta nerladdningPause DownloadPausa nerladdningStop DownloadStoppa nerladdningStop/Cancel DownloadStoppa/Avbryt nerladdningPropertiesEgenskaperProgressFramgångExitAvslutaClear all items in download listRensa alla objekt i nerladdninslistanCreate new download queueSkapa ny nerladdningsköRemove this queueTa bort den här könStart this queueStarta den här könStop this queueStoppa den här könMove currently selected items up by one rowMove currently selected items down by one rowPreferencesEgenskaperAboutOmReport an issueRapportera ett felStart TimeEnd TimeDownload bottom of
the list firstApplyVerkställAfter downloadEfter nerladdningShut DownStäng nerV&ideo FinderV&ideosökare<b>Video file status: </b><b>Audio file status: </b><b>Status: </b><b>Muxing status: </b> downloadedActiveNot ActiveStartedErrorCompleteTransfer RateEstimated Time LeftFirst Try DateLast Try DateFind Video Links...Download video or audio from Youtube, Vimeo, etc.Stop All Active DownloadsFile SizeDownload StatusShow System Tray IconShow MenubarShow Side PanelMinimize to System TrayAdd New Download Link...Import Links from Text File...Create a text file and put links in it, line by line!Open File...Open Download FolderOpen Default Download FolderClear Download ListRemove Selected Downloads from ListDelete Selected Download FilesMove Selected Download Files to Another Folder...Move Selected Download Files to Another FolderCreate New Queue...Remove QueueStart QueueStop QueueMove Selected Items UpMove Selected Items DownReport an IssueShow Log FileHide OptionsKeep System Awake!<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Start Mixing<b>Video File Status: </b><b>Audio File Status: </b><b>Mixing status: </b>Import Links from Clipboard...Import Links From Clipboardprogress_ui_trPersepolis Download ManagerNerladdningshanteraren PersepolisStatus: Status:Downloaded:Nerladdade:Transfer rate: Estimated time left:Number of connections: Antalet anslutningar:Download InformationAfter downloadEfter nerladdningApplyVerkställShut DownStäng nerDownload OptionsNerladdningsalternativResumeÅterupptaPausePausaStopStoppLink: setting_src_ui_tr<b><center>Restart Persepolis Please!</center></b><br><center>Some changes take effect after restarting Persepolis</center>Restart Persepolis!Starta om Persepolis!<b><center>This shortcut has been used before! Use another one!</center></b>setting_ui_trPreferencesEgenskaperNumber of tries: Antalet försök:<html><head/><body><p>Set timeout in seconds. </p></body></html>Timeout (seconds): <html><head/><body><p>Using multiple connections can help speed up your download.</p></body></html>Number of connections: Antalet anslutningar:ChangeÄndraDownload OptionsNerladdningsalternativVolume: Volym:NotificationsAviseringarStyle: Stil:Color scheme: Färgschema:Icons: Ikoner:Notification type: Typ av avisering:Font: Typsnitt:Size: Storlek:Run Persepolis at startupKör Persepolis vid uppstartKeep system awake!<html><head/><body><p>Format HH:MM</p></body></html>File NameFilnamnStatusStatusSizeStorlekDownloadedNerladdatPercentageProcentConnectionsAnslutningarCategoryKategoriVideo Finder OptionsMaximum number of links to capture:<br/><small>(If browser sends multiple video links at a time)</small>DefaultsCancelAvbrytOKOKPress new keysActionShortcutShortcutsQuitHide main window if close button clicked.<html><head/><body><p>This feature may not work in your operating system.</p></body></html>Language: Minimize to System TrayRemove Download ItemsDelete Download ItemsMove Selected Items UpMove Selected Items DownAdd New Download LinkAdd New Video LinkImport Links from Text FileWait period between retries (seconds): Wait period between each download in queue:Don't use certificate to verify the peers<html><head/><body><p>This option avoids SSL/TLS handshake failure. But use it at your own risk!</p></body></html>Download folder: Create subfolders for Music,Videos, ... in default download folderSave AsEnable Notification SoundsToolbar icons size: If browser is opened, start Persepolis in system trayEnable system tray iconShow download complete dialog when download is finishedShow menubarShow side panelShow download progress window<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Show these columns:Transfer RateEstimated Time LeftFirst Try DateLast Try DateColumns CustomizationCheck system clipboard for copied links<html><head/><body><p>The program will automatically check the clipboard for copied links. </p></body></html>Download requests from the browser will be executed immediately.<html><head/><body><p>When a download request is sent from the browser extension, the download will start without showing the Add Link window. </p></body></html><html><head/><body><p>Set number of retries if download failed.</p></body></html><html><head/><body><p>Set the seconds to wait between retries.</p></body></html>Chunk size(KiB): It is python requests library chunk size. Do not change this If you are not familiar with it.text_ui_trPersepolis Download ManagerNerladdningshanteraren PersepolisLinksLänkarSelect AllMarkera allaDeselect AllAvmarkera allaAdd to queue: Lägg till i kön:ProxyProxyIP:IP:Port:Port:Change Download FolderOKOKCancelAvbrytDownload OptionsProxy password: Proxy username: Download username and passwordDownload username: Download password: Download folder: Number of connections:video_finder_progress_ui_tr<b>Video file status: </b><b>Audio file status: </b><b>Muxing status: </b><b>Mixing status: </b>ytaddlink_src_ui_trVideo FinderFetch Media ListSelect a formatVälj ett formatPlease enter a valid video linkFetching Media Info...Hämtar media-info...Video format:Audio format:Advanced options
================================================
FILE: resources/locales/ui_tr.ts
================================================
about_ui_trAbout PersepolisPersepolis HakkındaPersepolis Download ManagerPersepolis İndirme Yöneticisi<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a><a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a><a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>DevelopersGeliştiricilerTranslatorsÇevirmenlerLicenseLisansOKTamam
AliReza AmirSamimi
Mohammadreza Abdollahzadeh
Sadegh Alirezaie
Mostafa Asadi
Jafar Akhondali
Kia Hamedi
H.Rostami
Ehsan Titish
MohammadAmin VahediniaTRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!Acknowledgments<a href=https://github.com/yt-dlp/yt-dlp>YT-DLP project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/ffmpeg/ffmpeg>FFmpeg project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://wiki.qt.io/Qt_for_Python>Pyside project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/psf/requests>Requests project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!addlink_ui_trAdd to category: Kategoriye ekle: ProxyProksiIP: IP: Port:Port:Change Download Folderİndirme Dizinini DeğiştirDownload Folder: İndirme Dizini: CancelVazgeçOKTamamLinkBağlantıMore OptionsDaha Fazla SeçenekAdvanced OptionsGelişmiş SeçeneklerAdd Download LinkDownload link: Change file name: Detect System Proxy SettingsProxy password: Proxy username: Download username and passwordDownload username: Download password: Remember this pathStart timeEnd timeNumber of connections:Download LaterHTTPHTTPSSOCKS5after_download_src_ui_tr<b>File name</b>: <b>Dosya adı</b>: <b>Size</b>: <b>Boyut</b>: after_download_ui_trPersepolis Download ManagerPersepolis İndirme Yöneticisi Open File Dosya Aç Open Download Folderİndirme Dizinini Aç OK Tamam Don't show this message again.Bu mesajı tekrar gösterme.<b>Download Completed!</b><b>İndirme Tamamlandı!</b><b>Save as</b>: <b>Farklı kaydet</b>: <b>Link</b>: <b>Bağlantı</b>: log_window_ui_trReport IssueSorun RaporlaInitialization and informationDownloadsErrors and warningsmainwindow_src_ui_trError: Hata: <b>Link</b>: <b>Bağlantı</b>: <b>Downloaded</b>: <b>İndirildi</b>: <b>Transfer rate</b>: <b>Transfer oranı</b>: <b>Estimated time left</b>: <b>Tahmini kalan süre</b>: <b>Connections</b>: <b>Bağlantılar</b>: <b>Status</b>: <b>Durum</b>: Download Stoppedİndrime DurdurulduError - Hata - Download Completeİndirme Tamamlandı<b><center>This link has been added before! Are you sure you want to add it again?</center></b><b><center>Bu bağlantı daha önce eklenmiştir! Tekrar eklemek istediğinizden emin misiniz?</center></b>Download Startsİndirme BaşladıOperation was not successful.İşlem başarılı değildi.Please resume the following category: Lütfen aşağıdaki kategoriye devam edin: Not enough free space in:İçinde yeterince boş alan yok:muxing errormuxing hatasıan error occurredPlease update Persepolis.<b>Fragments</b>: mainwindow_ui_trPersepolis Download ManagerPersepolis İndirme YöneticisiShow/Hide system tray iconSistem tepsisi simgesini Göster/GizleAdd New Download LinkYeni İndirme Bağlantısı EkleResume Downloadİndirmeye Devam etPause Downloadİndirmeyi DDuraklatStop/Cancel Downloadİndirmeyi Durdur/İptal etPropertiesÖzelliklerProgressİlerlemeExitÇıkışStart TimeBaşlama ZamanıEnd TimeBitiş ZamanıDownload bottom of
the list firstİlk önce listenin
altını indirApplyUygulaAfter downloadİndirmeden sonraShut DownKapat<b>Video file status: </b><b>Video dosyası durumu: </b><b>Audio file status: </b><b>Ses dosyası durumu: </b><b>Status: </b><b>Durum: </b><b>Muxing status: </b><b>Muxing durumu: </b> downloadedindirildiMinimize to System TrayHide OptionsKeep System Awake!<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Start Mixing<b>Video File Status: </b><b>Audio File Status: </b><b>Mixing status: </b>progress_ui_trPersepolis Download ManagerPersepolis İndirme YöneticisiStatus: Durum: Downloaded:İndirildi:Transfer rate: Transfer oranı: Estimated time left:Tahmini kalan süre:Number of connections: Bağlantıların Sayısı: Download Informationİndirme BilgisiAfter downloadİndirmeden sonraApplyUygulaShut DownKapatDownload Optionsİndirme SeçenekleriResumeDevam etPauseDuraklatStopDurdurLink: setting_src_ui_tr<b><center>This shortcut has been used before! Use another one!</center></b><b><center>Bu kısayol daha önce kullanılmış! Başka bir tane kullanın!</center></b>setting_ui_trPreferencesTercihlerNumber of tries: Deneme sayısı: <html><head/><body><p>Set timeout in seconds. </p></body></html><html><head/><body><p>Zaman aşımını saniye cinsinden ayarla. </p></body></html>Timeout (seconds): Zaman aşımı (saniye): <html><head/><body><p>Using multiple connections can help speed up your download.</p></body></html><html><head/><body><p>Birden fazla bağlantı kullanarak indirme işleminizi hızlandırabilirsiniz.</p></body></html>Number of connections: Bağlantıların Sayısı: ChangeDeğiştirDownload Optionsİndirme SeçenekleriVolume: Hacim: NotificationsBildirimStyle: Tarz: Color scheme: Renk şeması: Icons: Simgeler: Notification type: Bildirim türü: Font: Yazı tipi: Size: Boyut: Run Persepolis at startupBaşlangıçta Persepolis'i çalıştırKeep system awake!Sistemi uyanık tut!<html><head/><body><p>Format HH:MM</p></body></html><html><head/><body><p>Biçim SS:DD</p></body></html>StatusDurumVideo Finder OptionsVideo Bulucu SeçenekleriDefaultsVarsayılanlarCancelVazgeçOKTamamPress new keysYeni tuşlara basınShortcutsKısayollarHide main window if close button clicked.Kapat düğmesi tıklanırsa ana pencereyi gizle.<html><head/><body><p>This feature may not work in your operating system.</p></body></html><html><head/><body><p>Bu özellik işletim sisteminizde çalışmayabilir.</p></body></html>Language: Wait period between retries (seconds): Don't use certificate to verify the peers<html><head/><body><p>This option avoids SSL/TLS handshake failure. But use it at your own risk!</p></body></html>Download folder: Create subfolders for Music,Videos, ... in default download folderSave AsEnable Notification SoundsToolbar icons size: Enable system tray iconShow download complete dialog when download is finishedShow menubarShow side panelShow download progress window<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Columns CustomizationCheck system clipboard for copied links<html><head/><body><p>The program will automatically check the clipboard for copied links. </p></body></html>Download requests from the browser will be executed immediately.<html><head/><body><p>When a download request is sent from the browser extension, the download will start without showing the Add Link window. </p></body></html><html><head/><body><p>Set number of retries if download failed.</p></body></html><html><head/><body><p>Set the seconds to wait between retries.</p></body></html>Chunk size(KiB): It is python requests library chunk size. Do not change this If you are not familiar with it.text_ui_trPersepolis Download ManagerPersepolis İndirme YöneticisiIP:IP:Port:Port:Change Download Folderİndirme Dizinini DeğiştirProxy password: Proxy username: Download username and passwordDownload username: Download password: Download folder: Number of connections:video_finder_progress_ui_tr<b>Video file status: </b><b>Video dosyası durumu: </b><b>Audio file status: </b><b>Ses dosyası durumu: </b><b>Muxing status: </b><b>Muxing durumu: </b><b>Mixing status: </b>
================================================
FILE: resources/locales/ui_tr_TR.ts
================================================
about_ui_trAbout PersepolisPersepolis HakkındaPersepolis Download ManagerPersepolis İndirme Yöneticisi<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a><a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a><a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>DevelopersGeliştiricilerTranslatorsÇevirmenlerLicenseLisansOKTamam
AliReza AmirSamimi
Mohammadreza Abdollahzadeh
Sadegh Alirezaie
Mostafa Asadi
Jafar Akhondali
Kia Hamedi
H.Rostami
Ehsan Titish
MohammadAmin VahediniaTRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!Acknowledgments<a href=https://github.com/yt-dlp/yt-dlp>YT-DLP project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/ffmpeg/ffmpeg>FFmpeg project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://wiki.qt.io/Qt_for_Python>Pyside project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/psf/requests>Requests project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!addlink_ui_trAdd to category: Kategoriye ekle: ProxyAracı sunucuIP: IP: Port:Bağlantı noktası:Change Download Folderİndirme Dizinini DeğiştirDownload Folder: İndirme Dizini: CancelİptalOKTamamLinkBağlantıMore OptionsDaha Fazla SeçenekAdvanced OptionsGelişmiş SeçeneklerAdd Download LinkDownload link: Change file name: Detect System Proxy SettingsProxy password: Proxy username: Download username and passwordDownload username: Download password: Remember this pathStart timeEnd timeNumber of connections:Download LaterHTTPHTTPSSOCKS5after_download_src_ui_tr<b>File name</b>: <b>Dosya adı</b>: <b>Size</b>: <b>Boyut</b>: after_download_ui_trPersepolis Download ManagerPersepolis İndirme Yöneticisi Open File Dosya Aç Open Download Folderİndirme Dizinini Aç OK Tamam Don't show this message again.Bu iletiyi tekrar gösterme.<b>Download Completed!</b><b>İndirme Tamamlandı!</b><b>Save as</b>: <b>Farklı kaydet</b>: <b>Link</b>: <b>Bağlantı</b>: log_window_ui_trReport IssueSorun BildirInitialization and informationDownloadsErrors and warningsmainwindow_src_ui_trError: Hata: <b>Link</b>: <b>Bağlantı</b>: <b>Downloaded</b>: <b>İndirildi</b>: <b>Transfer rate</b>: <b>Aktarım hızı</b>: <b>Estimated time left</b>: <b>Tahmini kalan süre</b>: <b>Connections</b>: <b>Bağlantılar</b>: <b>Status</b>: <b>Durum</b>: Download Stoppedİndrime DurdurulduError - Hata - Download Completeİndirme Tamamlandı<b><center>This link has been added before! Are you sure you want to add it again?</center></b><b><center>Bu bağlantı daha önce eklenmiştir! Tekrar eklemek istediğinizden emin misiniz?</center></b>Download Startsİndirme BaşladıOperation was not successful.İşleyiş başarılı değildi.Please resume the following category: Lütfen sıradaki kategoriyi devam ettirin: Not enough free space in:Yeterli boş alan yokmuxing errorÇoğullama hatasıan error occurredPlease update Persepolis.<b>Fragments</b>: mainwindow_ui_trPersepolis Download ManagerPersepolis İndirme YöneticisiShow/Hide system tray iconGörev çubuğu simgesini Göster/GizleAdd New Download LinkYeni İndirme Bağlantısı EkleResume Downloadİndirmeye Kaldığı Yerden Devam EtPause Downloadİndirmeye Ara VerStop/Cancel Downloadİndirmeyi Durdur/İptal etPropertiesÖzelliklerProgressİlerleyişExitÇıkışStart TimeBaşlama ZamanıEnd TimeBitiş ZamanıDownload bottom of
the list firstİlk önce listenin
altındakini indirApplyUygulaAfter downloadİndirdikten sonraShut DownKapat<b>Video file status: </b><b>Video dosyası durumu: </b><b>Audio file status: </b><b>Ses dosyası durumu: </b><b>Status: </b><b>Durum: </b><b>Muxing status: </b><b>Çoğullama durumu: </b> downloadedindirildiMinimize to System TrayHide OptionsKeep System Awake!<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Start Mixing<b>Video File Status: </b><b>Audio File Status: </b><b>Mixing status: </b>progress_ui_trPersepolis Download ManagerPersepolis İndirme YöneticisiStatus: Durum: Downloaded:İndirildi:Transfer rate: Aktarım hızı: Estimated time left:Tahmini kalan süre:Number of connections: Bağlantıların Sayısı: Download Informationİndirme BilgisiAfter downloadİndirdikten sonraApplyUygulaShut DownKapatDownload Optionsİndirme SeçenekleriResumeKaldığı Yerden Devam EtPauseAra verStopDurdurLink: setting_src_ui_tr<b><center>This shortcut has been used before! Use another one!</center></b><b><center>Bu kısayol daha önce kullanılmıştır! Diğer birini kullanın!</center></b>setting_ui_trPreferencesTercihlerNumber of tries: Deneme sayısı: <html><head/><body><p>Set timeout in seconds. </p></body></html><html><head/><body><p>Zaman aşımını saniye olarak ayarla. </p></body></html>Timeout (seconds): Zaman aşımı (saniye): <html><head/><body><p>Using multiple connections can help speed up your download.</p></body></html><html><head/><body><p>Birden fazla bağlantı kullanmak, indirmenizi hızlandırmaya yardımcı olabilir</p></body></html>Number of connections: Bağlantıların Sayısı: ChangeDeğiştirDownload Optionsİndirme SeçenekleriVolume: Hacim: NotificationsBildirimlerStyle: Tarz: Color scheme: Renk şeması: Icons: Simgeler: Notification type: Bildirim türü: Font: Yazı tipi: Size: Boyut: Run Persepolis at startupBaşlangıçta Persepolis'i çalıştırKeep system awake!Sistemi uyanık tut!<html><head/><body><p>Format HH:MM</p></body></html><html><head/><body><p>Biçim SS:DD</p></body></html>StatusDurumVideo Finder OptionsVideo Bulucu SeçenekleriDefaultsVarsayılanlarCancelİptalOKTamamPress new keysYeni tuşlara basınShortcutsKısayollarHide main window if close button clicked.Kapat düğmesi tıklandığında ana pencereyi gizle.<html><head/><body><p>This feature may not work in your operating system.</p></body></html><html><head/><body><p>Bu özellik işletim sisteminizde çalışmayabilir.</p></body></html>Language: Wait period between retries (seconds): Don't use certificate to verify the peers<html><head/><body><p>This option avoids SSL/TLS handshake failure. But use it at your own risk!</p></body></html>Download folder: Create subfolders for Music,Videos, ... in default download folderSave AsEnable Notification SoundsToolbar icons size: Enable system tray iconShow download complete dialog when download is finishedShow menubarShow side panelShow download progress window<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Columns CustomizationCheck system clipboard for copied links<html><head/><body><p>The program will automatically check the clipboard for copied links. </p></body></html>Download requests from the browser will be executed immediately.<html><head/><body><p>When a download request is sent from the browser extension, the download will start without showing the Add Link window. </p></body></html><html><head/><body><p>Set number of retries if download failed.</p></body></html><html><head/><body><p>Set the seconds to wait between retries.</p></body></html>Chunk size(KiB): It is python requests library chunk size. Do not change this If you are not familiar with it.text_ui_trPersepolis Download ManagerPersepolis İndirme YöneticisiIP:IP:Port:Bağlantı noktası:Change Download Folderİndirme Dizinini DeğiştirProxy password: Proxy username: Download username and passwordDownload username: Download password: Download folder: Number of connections:video_finder_progress_ui_tr<b>Video file status: </b><b>Video dosyası durumu: </b><b>Audio file status: </b><b>Ses dosyası durumu: </b><b>Muxing status: </b><b>Çoğullama durumu: </b><b>Mixing status: </b>
================================================
FILE: resources/locales/ui_zh_CN.ts
================================================
about_ui_trAbout Persepolis关于 PersepolisPersepolis Download ManagerPersepolis 下载管理器<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a><a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a><a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>Developers开发人员Translators译者License许可协议OK确定
AliReza AmirSamimi
Mohammadreza Abdollahzadeh
Sadegh Alirezaie
Mostafa Asadi
Jafar Akhondali
Kia Hamedi
H.Rostami
Ehsan Titish
MohammadAmin VahediniaTRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!
AliReza AmirSamimi
Mohammadreza Abdollahzadeh
Sadegh Alirezaie
Mostafa Asadi
Jafar Akhondali
Kia Hamedi
H.Rostami
Ehsan Titish
MohammadAmin VahediniaAcknowledgments致谢<a href=https://github.com/yt-dlp/yt-dlp>YT-DLP project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/yt-dlp/yt-dlp>YT-DLP 项目</a><a href=https://github.com/ffmpeg/ffmpeg>FFmpeg project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/ffmpeg/ffmpeg>FFmpeg 项目</a><a href=https://wiki.qt.io/Qt_for_Python>Pyside project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://wiki.qt.io/Qt_for_Python>Pyside 项目</a><a href=https://github.com/psf/requests>Requests project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/psf/requests>Requests 项目</a>addlink_ui_trAdd to category: 添加到类别:Proxy代理IP: IP 地址:Port:端口:Change Download Folder更改下载文件夹Download Folder: 下载文件夹:Cancel取消OK确定Link链接More Options更多选项Advanced Options高级选项Add Download Link添加下载链接Download link: 下载链接:Change file name: 更改文件名:Detect System Proxy Settings检测系统代理设置Proxy password: 代理密码:Proxy username: 代理用户名:Download username and password下载用户名和密码Download username: 下载用户名:Download password: 下载密码:Remember this path记住此路径Start time开始时间End time结束时间Number of connections:连接数:Download Later稍后下载HTTPHTTPHTTPSHTTPSSOCKS5SOCKS5after_download_src_ui_tr<b>File name</b>: <b>文件名</b>:<b>Size</b>: <b>大小</b>:after_download_ui_trPersepolis Download ManagerPersepolis 下载管理器 Open File 打开文件Open Download Folder打开下载文件夹 OK 确定Don't show this message again.不再显示这条信息。<b>Download Completed!</b><b>下载完成!</b><b>Save as</b>: <b>另存为</b>:<b>Link</b>: <b>链接</b>:log_window_ui_trReport Issue上报问题Initialization and informationDownloadsErrors and warningsmainwindow_src_ui_trError: 错误...<b>Link</b>: <b>链接</b>:<b>Downloaded</b>: <b>已下载</b>:<b>Transfer rate</b>: <b>传输速度</b>:<b>Estimated time left</b>: <b>预计剩余时间</b>:<b>Connections</b>: <b>连接数</b>: <b>Status</b>: <b>状态</b>:Download Stopped下载已停止Error - 错误 - Download Complete下载完成<b><center>This link has been added before! Are you sure you want to add it again?</center></b><b><center>此链接之前已被添加!您确定要再次添加吗?</center></b>Download Starts下载状态Operation was not successful.操作失败。Please resume the following category: 请继续以下类别:Not enough free space in:此处剩余空间不足:muxing error复用错误an error occurred发生错误Please update Persepolis.请更新 Persepolis。<b>Fragments</b>: <b>分片</b>:mainwindow_ui_trPersepolis Download ManagerPersepolis 下载管理器Show/Hide system tray icon显示/隐藏系统托盘图标Add New Download Link新建下载链接Resume Download继续下载Pause Download暂停下载Stop/Cancel Download终止/取消下载Properties属性Progress进度Exit退出Start Time开始时间End Time结束时间Download bottom of
the list first优先下载列表
底部资源Apply应用After download下载完成后Shut Down关机<b>Video file status: </b><b>视频文件状态:</b><b>Audio file status: </b><b>音频文件状态:</b><b>Status: </b><b>状态:</b><b>Muxing status: </b><b>复用状态:</b> downloaded已下载Minimize to System Tray最小化到系统托盘Hide Options隐藏选项Keep System Awake!阻止系统睡眠!<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>此选项将阻止系统进入睡眠状态。 如果您的电源管理器自动暂停系统,请打开此选项。Start Mixing开始混合<b>Video File Status: </b><b>视频文件状态:</b><b>Audio File Status: </b><b>音频文件状态:</b><b>Mixing status: </b><b>混合状态: </b>progress_ui_trPersepolis Download ManagerPersepolis 下载管理器Status: 状态:Downloaded:已下载:Transfer rate: 传输速度:Estimated time left:预计剩余时间:Number of connections: 连接数:Download Information下载信息After download下载完成后Apply应用Shut Down关闭Download Options下载选项Resume继续Pause暂停Stop终止Link: 链接:setting_src_ui_tr<b><center>This shortcut has been used before! Use another one!</center></b><b><center>此快捷键已被占用,请使用其他快捷键!</center></b>setting_ui_trPreferences首选项Number of tries: 尝试次数:<html><head/><body><p>Set timeout in seconds. </p></body></html><html><head/><body><p>设置超时秒数。</p></body></html>Timeout (seconds): 超时 (秒):<html><head/><body><p>Using multiple connections can help speed up your download.</p></body></html><html><head/><body><p>使用多线程可以提升下载速度。</p></body></html>Number of connections: 连接数:Change更改Download Options下载选项Volume: 音量:Notifications通知Style: 风格:Color scheme: 配色方案:Icons: 图标:Notification type: 通知类型:Font: 字体:Size: 大小:Run Persepolis at startup开机自动运行 PersepolisKeep system awake!防止系统休眠!<html><head/><body><p>Format HH:MM</p></body></html><html><head/><body><p>格式:HH:MM</p></body></html>Status状态Video Finder Options视频嗅探选项Defaults默认Cancel取消OK确定Press new keys按下新的快捷键组合Shortcuts快捷键Hide main window if close button clicked.点击关闭按钮时隐藏主窗口<html><head/><body><p>This feature may not work in your operating system.</p></body></html><html><head/><body><p>此特性可能不支持当前操作系统</p></body></html>Language: 语言:Wait period between retries (seconds): 重试间隔 (秒):Don't use certificate to verify the peers不要使用证书来验证节点<html><head/><body><p>This option avoids SSL/TLS handshake failure. But use it at your own risk!</p></body></html><html><head/><body><p>此选项可避免 SSL/TLS 握手失败。使用时需自担风险!</p></body></html>Download folder: 下载文件夹:Create subfolders for Music,Videos, ... in default download folder在默认下载文件夹中创建音乐、视频等子文件夹Save As另存为Enable Notification Sounds打开通知音效Toolbar icons size: 工具栏图标尺寸:Enable system tray icon启用系统托盘图标Show download complete dialog when download is finished下载完成后显示下载完成对话框Show menubar显示菜单栏Show side panel显示侧边栏Show download progress window显示下载进度窗口<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html><html><head/><body><p>此选项将阻止系统进入睡眠状态。如果您的电源管理设置会自动挂起系统,则此设置是必需的。</p></body></html>Columns Customization自定义列Check system clipboard for copied links检查系统剪贴板中是否有复制的链接<html><head/><body><p>The program will automatically check the clipboard for copied links. </p></body></html><html><head/><body><p>该程序将自动检查剪贴板中复制的链接。 </p></body></html>Download requests from the browser will be executed immediately.来自浏览器的下载请求将被立即执行。<html><head/><body><p>When a download request is sent from the browser extension, the download will start without showing the Add Link window. </p></body></html><html><head/><body><p>当从浏览器扩展发送下载请求时,下载将开始而不显示添加链接窗口。</p></body></html><html><head/><body><p>Set number of retries if download failed.</p></body></html><html><head/><body><p>设置下载失败后的重试次数。</p></body></html><html><head/><body><p>Set the seconds to wait between retries.</p></body></html><html><head/><body><p>设置每次重试间间隔的秒数。</p></body></html>Chunk size(KiB): 块大小(KB):It is python requests library chunk size. Do not change this If you are not familiar with it.这是 python request 库的块大小。若不熟悉请不要更改。text_ui_trPersepolis Download ManagerPersepolis 下载管理器IP:IP:Port:端口:Change Download Folder更改下载文件夹Proxy password: 代理密码:Proxy username: 代理用户名:Download username and password下载用户名和密码Download username: 下载用户名:Download password: 下载密码:Download folder: 下载文件夹:Number of connections:连接数:video_finder_progress_ui_tr<b>Video file status: </b><b>视频文件状态:</b><b>Audio file status: </b><b>音频文件状态:</b><b>Muxing status: </b><b>复用状态:</b><b>Mixing status: </b><b>混合状态:</b>
================================================
FILE: resources/locales/ui_zh_TW.ts
================================================
about_ui_trAbout PersepolisAbout PersepolisPersepolis Download ManagerPersepolis 下載管理器<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://persepolisdm.github.io>https://persepolisdm.github.io</a><a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://telegram.me/persepolisdm>https://telegram.me/persepolisdm</a><a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://twitter.com/persepolisdm>https://twitter.com/persepolisdm</a>Developers開發者Translators譯者License授權條款OKOK
AliReza AmirSamimi
Mohammadreza Abdollahzadeh
Sadegh Alirezaie
Mostafa Asadi
Jafar Akhondali
Kia Hamedi
H.Rostami
Ehsan Titish
MohammadAmin VahediniaTRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!Acknowledgments<a href=https://github.com/yt-dlp/yt-dlp>YT-DLP project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/ffmpeg/ffmpeg>FFmpeg project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://wiki.qt.io/Qt_for_Python>Pyside project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!<a href=https://github.com/psf/requests>Requests project</a>TRANSLATORS NOTE: YOU REALLY DON'T NEED TO TRANSLATE THIS PART!addlink_ui_trAdd to category: 加入類別:ProxyProxyIP: IP 位址:Port:連接埠:Change Download Folder更改下載資料夾Download Folder: 下載資料夾:Cancel取消OKOKLink連結More Options更多選項Advanced Options進階選項Add Download LinkDownload link: Change file name: Detect System Proxy SettingsProxy password: Proxy username: Download username and passwordDownload username: Download password: Remember this pathStart timeEnd timeNumber of connections:Download LaterHTTPHTTPSSOCKS5after_download_src_ui_tr<b>File name</b>: <b>檔名</b>:<b>Size</b>: <b>大小</b>:after_download_ui_trPersepolis Download ManagerPersepolis 下載管理器 Open File 開啟檔案Open Download Folder開啟下載資料夾 OK OKDon't show this message again.不再顯示此訊息<b>Download Completed!</b><b>下載完成!</b><b>Save as</b>: <b>另存為</b>:<b>Link</b>: <b>連結</b>:log_window_ui_trReport Issue回報問題Initialization and informationDownloadsErrors and warningsmainwindow_src_ui_trError: 錯誤:<b>Link</b>: <b>連結</b>:<b>Downloaded</b>: <b>已下載</b>: <b>Transfer rate</b>: <b>傳輸速度</b>: <b>Estimated time left</b>: <b>預計剩餘時間</b>: <b>Connections</b>: <b>連接數</b>:<b>Status</b>: <b>狀態</b>: Download Stopped下載已停止Error - 錯誤 - Download Complete下載完成<b><center>This link has been added before! Are you sure you want to add it again?</center></b><b><center>已經新增過此連結了! 您確定要再加入一次嗎?</center></b>Download Starts下載開始Operation was not successful.操作失敗。Please resume the following category: 請繼續以下類別:Not enough free space in:空間不足:muxing error多工錯誤an error occurredPlease update Persepolis.<b>Fragments</b>: mainwindow_ui_trPersepolis Download ManagerPersepolis 下載管理器Show/Hide system tray icon顯示/隱藏系統匣圖示Add New Download Link加入新下載連結Resume Download繼續下載Pause Download暫停下載Stop/Cancel Download停止/取消下載Properties屬性Progress進度Exit離開Start Time開始時間End Time結束時間Download bottom of
the list first置底項目優先
下載Apply套用After download下載後Shut Down關機<b>Video file status: </b><b>視訊檔案狀態:</b><b>Audio file status: </b><b>音訊檔案狀態:</b><b>Status: </b><b>狀態:</b><b>Muxing status: </b><b>多工狀態:</b> downloaded已下載Minimize to System TrayHide OptionsKeep System Awake!<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Start Mixing<b>Video File Status: </b><b>Audio File Status: </b><b>Mixing status: </b>progress_ui_trPersepolis Download ManagerPersepolis 下載管理器Status: 狀態:Downloaded:已下載:Transfer rate: 傳輸速度:Estimated time left:預計剩餘時間:Number of connections: 連接數:Download Information下載資訊After download下載後Apply套用Shut Down關機Download Options下載選項Resume繼續Pause暫停Stop停止Link: setting_src_ui_tr<b><center>This shortcut has been used before! Use another one!</center></b>setting_ui_trPreferences選項Number of tries: 嘗試次數:<html><head/><body><p>Set timeout in seconds. </p></body></html><html><head/><body><p>設置逾時秒數。</p></body></html>Timeout (seconds): 逾時 (秒):<html><head/><body><p>Using multiple connections can help speed up your download.</p></body></html><html><head/><body><p>使用多個連線可以加速下載。</p></body></html>Number of connections: 連接數:Change更改Download Options下載選項Volume: 音量:Notifications通知Style: 樣式:Color scheme: 配色:Icons: 圖示:Notification type: 通知類型:Font: 字型:Size: 大小:Run Persepolis at startup開機時啟動 PersepolisKeep system awake!阻止系統進入睡眠模式!<html><head/><body><p>Format HH:MM</p></body></html><html><head/><body><p>格式 分:秒</p></body></html>Status狀態Video Finder Options影片搜尋工具選項Defaults預設Cancel取消OKOKPress new keys按下新按鍵Shortcuts快捷鍵Hide main window if close button clicked.若點擊關閉按鈕,將隱藏視窗。<html><head/><body><p>This feature may not work in your operating system.</p></body></html><html><head/><body><p>此功能在您的作業系統可能無法正常運作。</p></body></html>Language: Wait period between retries (seconds): Don't use certificate to verify the peers<html><head/><body><p>This option avoids SSL/TLS handshake failure. But use it at your own risk!</p></body></html>Download folder: Create subfolders for Music,Videos, ... in default download folderSave AsEnable Notification SoundsToolbar icons size: Enable system tray iconShow download complete dialog when download is finishedShow menubarShow side panelShow download progress window<html><head/><body><p>This option will prevent the system from going to sleep. It is necessary if your power manager is suspending the system automatically. </p></body></html>Columns CustomizationCheck system clipboard for copied links<html><head/><body><p>The program will automatically check the clipboard for copied links. </p></body></html>Download requests from the browser will be executed immediately.<html><head/><body><p>When a download request is sent from the browser extension, the download will start without showing the Add Link window. </p></body></html><html><head/><body><p>Set number of retries if download failed.</p></body></html><html><head/><body><p>Set the seconds to wait between retries.</p></body></html>Chunk size(KiB): It is python requests library chunk size. Do not change this If you are not familiar with it.text_ui_trPersepolis Download ManagerPersepolis 下載管理器IP:IP 位址:Port:連接埠:Change Download Folder更改下載資料夾Proxy password: Proxy username: Download username and passwordDownload username: Download password: Download folder: Number of connections:video_finder_progress_ui_tr<b>Video file status: </b><b>視訊檔案狀態:</b><b>Audio file status: </b><b>音訊檔案狀態:</b><b>Muxing status: </b><b>多工狀態:</b><b>Mixing status: </b>
================================================
FILE: resources/meson.build
================================================
icon_files = [
'com.github.persepolisdm.persepolis.svg',
'persepolis-tray.svg']
install_data(icon_files, install_dir: icondir)
================================================
FILE: resources/resources.qrc
================================================
translators.txtcom.github.persepolisdm.persepolis.svgpersepolis-tray.svgBreeze/about.svgBreeze/add.svgBreeze/exit.svgBreeze/folder.svgBreeze/minimize.svgBreeze/ok.svgBreeze/pause.svgBreeze/play.svgBreeze/preferences.svgBreeze/remove.svgBreeze/setting.svgBreeze/stop.svgBreeze/stop_all.svgBreeze/trash.svgBreeze/window.svgBreeze/file.svgBreeze/multi_remove.svgBreeze/multi_trash.svgBreeze/select_all.svgBreeze/add_queue.svgBreeze/start_queue.svgBreeze/stop_queue.svgBreeze/pause_queue.svgBreeze/remove_queue.svgBreeze/menu.svgBreeze/up.svgBreeze/down.svgBreeze/multi_up.svgBreeze/multi_down.svgBreeze/clipboard.svgBreeze/refresh.svgBreeze/video_finder.svgBreeze-Dark/about.svgBreeze-Dark/add.svgBreeze-Dark/exit.svgBreeze-Dark/folder.svgBreeze-Dark/minimize.svgBreeze-Dark/ok.svgBreeze-Dark/pause.svgBreeze-Dark/play.svgBreeze-Dark/preferences.svgBreeze-Dark/remove.svgBreeze-Dark/setting.svgBreeze-Dark/stop.svgBreeze-Dark/stop_all.svgBreeze-Dark/trash.svgBreeze-Dark/window.svgBreeze-Dark/file.svgBreeze-Dark/multi_remove.svgBreeze-Dark/multi_trash.svgBreeze-Dark/select_all.svgBreeze-Dark/add_queue.svgBreeze-Dark/start_queue.svgBreeze-Dark/stop_queue.svgBreeze-Dark/pause_queue.svgBreeze-Dark/remove_queue.svgBreeze-Dark/menu.svgBreeze-Dark/up.svgBreeze-Dark/down.svgBreeze-Dark/multi_up.svgBreeze-Dark/multi_down.svgBreeze-Dark/clipboard.svgBreeze-Dark/refresh.svgBreeze-Dark/video_finder.svgPapirus/about.svgPapirus/add.svgPapirus/exit.svgPapirus/folder.svgPapirus/minimize.svgPapirus/ok.svgPapirus/pause.svgPapirus/play.svgPapirus/preferences.svgPapirus/remove.svgPapirus/setting.svgPapirus/stop.svgPapirus/stop_all.svgPapirus/trash.svgPapirus/window.svgPapirus/file.svgPapirus/multi_remove.svgPapirus/multi_trash.svgPapirus/select_all.svgPapirus/add_queue.svgPapirus/start_queue.svgPapirus/stop_queue.svgPapirus/pause_queue.svgPapirus/remove_queue.svgPapirus/menu.svgPapirus/up.svgPapirus/down.svgPapirus/multi_up.svgPapirus/multi_down.svgPapirus/clipboard.svgPapirus/refresh.svgPapirus/video_finder.svgPapirus-Dark/about.svgPapirus-Dark/add.svgPapirus-Dark/exit.svgPapirus-Dark/folder.svgPapirus-Dark/minimize.svgPapirus-Dark/ok.svgPapirus-Dark/pause.svgPapirus-Dark/play.svgPapirus-Dark/preferences.svgPapirus-Dark/remove.svgPapirus-Dark/setting.svgPapirus-Dark/stop.svgPapirus-Dark/stop_all.svgPapirus-Dark/trash.svgPapirus-Dark/window.svgPapirus-Dark/file.svgPapirus-Dark/multi_remove.svgPapirus-Dark/multi_trash.svgPapirus-Dark/select_all.svgPapirus-Dark/add_queue.svgPapirus-Dark/start_queue.svgPapirus-Dark/stop_queue.svgPapirus-Dark/pause_queue.svgPapirus-Dark/remove_queue.svgPapirus-Dark/menu.svgPapirus-Dark/up.svgPapirus-Dark/down.svgPapirus-Dark/multi_up.svgPapirus-Dark/multi_down.svgPapirus-Dark/clipboard.svgPapirus-Dark/refresh.svgPapirus-Dark/video_finder.svgPapirus-Light/about.svgPapirus-Light/add.svgPapirus-Light/exit.svgPapirus-Light/folder.svgPapirus-Light/minimize.svgPapirus-Light/ok.svgPapirus-Light/pause.svgPapirus-Light/play.svgPapirus-Light/preferences.svgPapirus-Light/remove.svgPapirus-Light/setting.svgPapirus-Light/stop.svgPapirus-Light/stop_all.svgPapirus-Light/trash.svgPapirus-Light/window.svgPapirus-Light/file.svgPapirus-Light/multi_remove.svgPapirus-Light/multi_trash.svgPapirus-Light/select_all.svgPapirus-Light/add_queue.svgPapirus-Light/start_queue.svgPapirus-Light/stop_queue.svgPapirus-Light/pause_queue.svgPapirus-Light/remove_queue.svgPapirus-Light/menu.svgPapirus-Light/up.svgPapirus-Light/down.svgPapirus-Light/multi_up.svgPapirus-Light/multi_down.svgPapirus-Light/clipboard.svgPapirus-Light/refresh.svgPapirus-Light/video_finder.svglight_style.qsslight fusion/ads_close.svglight fusion/ads_close_hover.svglight fusion/ads_close_pressed.svglight fusion/ads_detach.svglight fusion/ads_detach_hover.svglight fusion/ads_detach_hover_pressed.svglight fusion/ads_maximize.svglight fusion/ads_maximize_hover.svglight fusion/ads_maximize_pressed.svglight fusion/ads_menu_button.svglight fusion/ads_menu_button_hover.svglight fusion/ads_menu_button_pressed.svglight fusion/branch_closed.svglight fusion/branch_closed_hover.svglight fusion/branch_end.svglight fusion/branch_end_arrow.svglight fusion/branch_more.svglight fusion/branch_more_arrow.svglight fusion/branch_open.svglight fusion/branch_open_hover.svglight fusion/browser_refresh.svglight fusion/browser_refresh_stop.svglight fusion/calendar_next.svglight fusion/calendar_previous.svglight fusion/checkbox_checked.svglight fusion/checkbox_checked_disabled.svglight fusion/checkbox_indeterminate.svglight fusion/checkbox_indeterminate_disabled.svglight fusion/checkbox_unchecked.svglight fusion/checkbox_unchecked_disabled.svglight fusion/clear_text.svglight fusion/close.svglight fusion/close_hover.svglight fusion/close_pressed.svglight fusion/computer.svglight fusion/desktop.svglight fusion/dialog_apply.svglight fusion/dialog_cancel.svglight fusion/dialog_close.svglight fusion/dialog_discard.svglight fusion/dialog_help.svglight fusion/dialog_ignore.svglight fusion/dialog_no.svglight fusion/dialog_ok.svglight fusion/dialog_open.svglight fusion/dialog_reset.svglight fusion/dialog_retry.svglight fusion/dialog_save.svglight fusion/dialog_save_all.svglight fusion/dialog_yes_to_all.svglight fusion/disc_drive.svglight fusion/down_arrow.svglight fusion/down_arrow_disabled.svglight fusion/down_arrow_hover.svglight fusion/file.svglight fusion/file_dialog_contents.svglight fusion/file_dialog_detailed.svglight fusion/file_dialog_end.svglight fusion/file_dialog_info.svglight fusion/file_dialog_list.svglight fusion/file_dialog_start.svglight fusion/file_link.svglight fusion/floppy_drive.svglight fusion/folder.svglight fusion/folder_link.svglight fusion/folder_open.svglight fusion/folder_open_link.svglight fusion/hard_drive.svglight fusion/help.svglight fusion/hmovetoolbar.svglight fusion/home_directory.svglight fusion/horizontal_extension.svglight fusion/hseptoolbar.svglight fusion/left_arrow.svglight fusion/left_arrow_disabled.svglight fusion/left_arrow_hover.svglight fusion/maximize.svglight fusion/menu.svglight fusion/message_critical.svglight fusion/message_information.svglight fusion/message_question.svglight fusion/message_warning.svglight fusion/minimize.svglight fusion/network_drive.svglight fusion/pause.svglight fusion/play.svglight fusion/radio_checked.svglight fusion/radio_checked_disabled.svglight fusion/radio_unchecked.svglight fusion/radio_unchecked_disabled.svglight fusion/restore.svglight fusion/restore_defaults.svglight fusion/right_arrow.svglight fusion/right_arrow_disabled.svglight fusion/right_arrow_hover.svglight fusion/seek_backward.svglight fusion/seek_forward.svglight fusion/shade.svglight fusion/sizegrip.svglight fusion/skip_backward.svglight fusion/skip_forward.svglight fusion/stop.svglight fusion/tab_close.svglight fusion/transparent.svglight fusion/trash.svglight fusion/undock.svglight fusion/undock_hover.svglight fusion/undock_hover_pressed.svglight fusion/unshade.svglight fusion/up_arrow.svglight fusion/up_arrow_disabled.svglight fusion/up_arrow_hover.svglight fusion/vertical_extension.svglight fusion/vista_shield.svglight fusion/vline.svglight fusion/vmovetoolbar.svglight fusion/volume.svglight fusion/volume_muted.svglight fusion/vseptoolbar.svglight fusion/window_close.svgdark_style.qssdark fusion/ads_close.svgdark fusion/ads_close_hover.svgdark fusion/ads_close_pressed.svgdark fusion/ads_detach.svgdark fusion/ads_detach_hover.svgdark fusion/ads_detach_hover_pressed.svgdark fusion/ads_maximize.svgdark fusion/ads_maximize_hover.svgdark fusion/ads_maximize_pressed.svgdark fusion/ads_menu_button.svgdark fusion/ads_menu_button_hover.svgdark fusion/ads_menu_button_pressed.svgdark fusion/branch_closed.svgdark fusion/branch_closed_hover.svgdark fusion/branch_end.svgdark fusion/branch_end_arrow.svgdark fusion/branch_more.svgdark fusion/branch_more_arrow.svgdark fusion/branch_open.svgdark fusion/branch_open_hover.svgdark fusion/browser_refresh.svgdark fusion/browser_refresh_stop.svgdark fusion/calendar_next.svgdark fusion/calendar_previous.svgdark fusion/checkbox_checked.svgdark fusion/checkbox_checked_disabled.svgdark fusion/checkbox_indeterminate.svgdark fusion/checkbox_indeterminate_disabled.svgdark fusion/checkbox_unchecked.svgdark fusion/checkbox_unchecked_disabled.svgdark fusion/clear_text.svgdark fusion/close.svgdark fusion/close_hover.svgdark fusion/close_pressed.svgdark fusion/computer.svgdark fusion/desktop.svgdark fusion/dialog_apply.svgdark fusion/dialog_cancel.svgdark fusion/dialog_close.svgdark fusion/dialog_discard.svgdark fusion/dialog_help.svgdark fusion/dialog_ignore.svgdark fusion/dialog_no.svgdark fusion/dialog_ok.svgdark fusion/dialog_open.svgdark fusion/dialog_reset.svgdark fusion/dialog_retry.svgdark fusion/dialog_save.svgdark fusion/dialog_save_all.svgdark fusion/dialog_yes_to_all.svgdark fusion/disc_drive.svgdark fusion/down_arrow.svgdark fusion/down_arrow_disabled.svgdark fusion/down_arrow_hover.svgdark fusion/file.svgdark fusion/file_dialog_contents.svgdark fusion/file_dialog_detailed.svgdark fusion/file_dialog_end.svgdark fusion/file_dialog_info.svgdark fusion/file_dialog_list.svgdark fusion/file_dialog_start.svgdark fusion/file_link.svgdark fusion/floppy_drive.svgdark fusion/folder.svgdark fusion/folder_link.svgdark fusion/folder_open.svgdark fusion/folder_open_link.svgdark fusion/hard_drive.svgdark fusion/help.svgdark fusion/hmovetoolbar.svgdark fusion/home_directory.svgdark fusion/horizontal_extension.svgdark fusion/hseptoolbar.svgdark fusion/left_arrow.svgdark fusion/left_arrow_disabled.svgdark fusion/left_arrow_hover.svgdark fusion/maximize.svgdark fusion/menu.svgdark fusion/message_critical.svgdark fusion/message_information.svgdark fusion/message_question.svgdark fusion/message_warning.svgdark fusion/minimize.svgdark fusion/network_drive.svgdark fusion/pause.svgdark fusion/play.svgdark fusion/radio_checked.svgdark fusion/radio_checked_disabled.svgdark fusion/radio_unchecked.svgdark fusion/radio_unchecked_disabled.svgdark fusion/restore.svgdark fusion/restore_defaults.svgdark fusion/right_arrow.svgdark fusion/right_arrow_disabled.svgdark fusion/right_arrow_hover.svgdark fusion/seek_backward.svgdark fusion/seek_forward.svgdark fusion/shade.svgdark fusion/sizegrip.svgdark fusion/skip_backward.svgdark fusion/skip_forward.svgdark fusion/stop.svgdark fusion/tab_close.svgdark fusion/transparent.svgdark fusion/trash.svgdark fusion/undock.svgdark fusion/undock_hover.svgdark fusion/undock_hover_pressed.svgdark fusion/unshade.svgdark fusion/up_arrow.svgdark fusion/up_arrow_disabled.svgdark fusion/up_arrow_hover.svgdark fusion/vertical_extension.svgdark fusion/vista_shield.svgdark fusion/vline.svgdark fusion/vmovetoolbar.svgdark fusion/volume.svgdark fusion/volume_muted.svgdark fusion/vseptoolbar.svgdark fusion/window_close.svglocales/ui_ar.qmlocales/ui_es_ES.qmlocales/ui_fr_FR.qmlocales/ui_ko.qmlocales/ui_pl_PL.qmlocales/ui_pt.qmlocales/ui_ru.qmlocales/ui_tr.qmlocales/ui_zh_CN.qmlocales/ui_de.qmlocales/ui_fa_IR.qmlocales/ui_hu.qmlocales/ui_nl_NL.qmlocales/ui_pt_BR.qmlocales/ui_sv.qmlocales/ui_tr_TR.qmlocales/ui_zh_TW.qm
================================================
FILE: resources/resources_generator.sh
================================================
#!/bin/bash
while getopts "trqh" arg;do
case $arg in
t)
translate="1";;
r)
resources="1";;
q)
create_qm_files="1";;
*)
echo "-t updates ui.ts file."
echo "-r updates resources.py file."
echo "-q create qm files from ts files."
esac
done
# finding parent directory
dir=`pwd`
parent_dir=`dirname $dir`
if [ "$translate" == "1" ];then
# generate ui.ts file
lupdate "$dir/translation_files.pro"
echo "$dir/locales/ui.ts is generated!"
fi
if [ "$resources" == "1" ];then
# generate resource.py file
# for pyqt5
# pyrcc5 resources.qrc -o "$parent_dir/persepolis/gui/resources.py"
# for pysside2
# rcc -g python -o "$parent_dir/persepolis/gui/resources.py" resources.qrc
pyside6-rcc resources.qrc -o "$parent_dir/persepolis/gui/resources.py"
#add some line to file
sed -i '/PySide6/d' "$parent_dir/persepolis/gui/resources.py"
sed -i '6i try:' "$parent_dir/persepolis/gui/resources.py"
sed -i '7i\ from PySide6 import QtCore' "$parent_dir/persepolis/gui/resources.py"
sed -i '8i except ImportError:' "$parent_dir/persepolis/gui/resources.py"
sed -i '9i\ from PyQt5 import QtCore' "$parent_dir/persepolis/gui/resources.py"
# sed -i '11i try:' "$parent_dir/persepolis/gui/resources.py"
# sed -i '12i\ import lzma' "$parent_dir/persepolis/gui/resources.py"
# sed -i '13i except:' "$parent_dir/persepolis/gui/resources.py"
# sed -i '14i\ print(\"lzma\ is\ not\ installed!\")' "$parent_dir/persepolis/gui/resources.py"
#
echo "$parent_dir/persepolis/gui/resource.py is generated!"
fi
if [ "$create_qm_files" == "1" ];then
for file in $dir/locales/* ;do
# generate qm files from ts files
pyside6-lrelease "$file"
done
fi
================================================
FILE: resources/translation_files.pro
================================================
TEMPLATE = app
TARGET = ts
INCLUDEPATH += persepolis
# Input
SOURCES += ../persepolis/gui/about_ui.py \
../persepolis/gui/addlink_ui.py \
../persepolis/gui/after_download_ui.py \
../persepolis/gui/log_window_ui.py \
../persepolis/gui/mainwindow_ui.py \
../persepolis/gui/progress_ui.py \
../persepolis/gui/setting_ui.py \
../persepolis/gui/text_queue_ui.py \
../persepolis/gui/video_finder_progress_ui.py\
../persepolis/scripts/after_download.py \
../persepolis/scripts/mainwindow.py \
../persepolis/scripts/progress.py \
../persepolis/scripts/setting.py \
../persepolis/scripts/video_finder_addlink.py
TRANSLATIONS += locales/ui.ts
================================================
FILE: resources/translators.txt
================================================
Arabic:
Majdi Sobain MajdiSobain
Saleh Alanazi Sal7_one
rayanm
Rex_sa rex07
Simo simonetworking
明 张 zhuangsha19740612
Chinese (China):
0day 0 0day
zyppe 210hcl
哥哥 好 485250464
江 王 664766245
x x 731515713
Charley Yang CharleyY
Alan Yee Choyee
Yonghui Chen ColoPaul
SY C DarciaLee_066
Hiyool
J L JULIANLEEEEE
Jack Su JackSu
快乐的老鼠宝宝 LaoShuBaby
haonan li Li_haonan
Liro Liu Liro
明阳 蒋 Lotumn
Allure R Riseup
Vento Fang Vento8866
安塞斯塔 薇薇安 VivianAncestor
Ye Wehchs WehchsYe
K X X_K
ing YIn YIning
yyy zzz ZZyyy
鹏 张 a5357663
yongkang yang acme969290422
AliReza AmirSamimi alireza_amirsamimi
altria morgan altria066
anakin micheal anakinlt
arthur 韩 arthurh
Leon Chen bioleon
Gabrielle Chou canonlayido
禅和 李 cc379
Sheng Cheng cs54ysm
Davin davinma
drhenk Dai drhenkdss
ZHAN Dr dts0080
dan chin egg233
chen chen false
yining fan fan211210
koishi a ghostxt0
ZARK LEE godiamguilty
jiangsheng hong hongjiangsheng
dp h huangdp
Jackson Hu huming2207
W Z icwaos
xiang wang ifoanle
海龙 江 jianghailong
haha ha jiangweiyang336
家俊 黄 jiayezhang22
liu li kulierhao
k y kyalt
leo xxx leoxxx
湖广 廖 liaohuguang
lei li lilei9587
旭麒 刘 liuxuqi
戈扬 赵 lucius.z
qihao mao maoqihao
monowolf Lucas monowolf
u muguo muguou
eo L nbnatcom
Ora Prinzing prinzingora
s k qqwwee
斌 熊 qw71111520
Jane Yang qwe866zxc
Minagawa Hifumi reeealin
354 6548 richard954
rose tiffany rose_tiffany
CUI SAN sanc163sa
佳 于 sheena.ringo
Sin Qiu sino1641
giga d togiga
思敏 鲍 tulipasky
刘 俊君 tusks
wang yuehu wangyuehu
xinpu wei weixinpu
mike williams williamsguo
T W wt1182657346
xiao wu wu12138
wwj402
cq n xiaolu
yin xiao xiaoyinge
一弘 郑 yihong2018
youngcrazyboy
卓琪 刘 you志者
an mu zhenlailu
long long zql00l
骏一 戚 zsns1994
徐 大侠 伏城
书罕 向 墨墨墨墨墨
chuantong xu 徐传统
00 000 敷衍伤情
花卷 春 春花卷
German (de):
Patrick Meier SonArc
Andreas Mueller anzbert
Dutch (Netherlands):
Heimen Stoffels Vistaus
French (France):
Cwpute BipBoup cwpute
Simon Porte sporte
Hungarian:
- Totya Totya
Korean:
JungHee Lee MarongHappy
Persian (Iran):
AliReza AmirSamimi alireza_amirsamimi
h_r 71 hayyan711
Mostafa Bodagh mosi2772
Mostafa Asadi mostafaasadi
Polish (Poland):
FadeMind
Maurycy Błaszczak LuxF3rre
Arkadiusz Rogóż arcio
Portuguese:
Antonio Barbosa AlvoErrado2
Portuguese (Brazil):
Cirnos Gersonzao
Carlos Henrique De Freitas Ferreira Neocazen
Thiago Schuch ThigSchuch
eltonfabricio10
Marli Masa marlimasa
Russian:
Victor ViktorOnlin
Михаил Сомов belliric300
Nikita Paseka deadman1987
Pavel irbinix
Sergey Suhih linuxmasterclub
Abdolreza Taslimijalilabadi rezaxx
Spanish (Spain):
Edgardo Castro Castro eddy9305
Jose G. Jimenez S. josegjimenez
Juan Manuel ptero.4
Rubén Fernández rubenterco
Toni Estévez toniem
Turkish (Turkey):
Oğuzhan Aksoy OnePage
abc Def abcmen
Yaşar Çiv yasarciv67
Turkish:
abc Def abcmen
Koray Biçer kbicer
Yaşar Çiv yasarciv67
Chinese (Taiwan):
Enfeng Tsao 5iderealArt
Sr R sr3296001
qge hao 鸟语花香
Portuguese:
Antonio Barbosa AlvoErrado2
Swedish:
Kristoffer Grundström Umeaboy
================================================
FILE: test/.pep8
================================================
[pycodestyle]
max_line_length = 120
================================================
FILE: test/test.py
================================================
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import sys
import os
import platform
# finding os platform
os_type = platform.system()
# Don't run persepolis as root!
if os_type == 'Linux' or os_type == 'FreeBSD' or os_type == 'OpenBSD' or os_type == 'Darwin':
uid = os.getuid()
if uid == 0:
print('Do not run persepolis as root.')
sys.exit(1)
cwd = os.path.abspath(__file__)
run_dir = os.path.dirname(cwd)
# if persepolis run in test folder
print('persepolis is running from test folder')
parent_dir = os.path.dirname(run_dir)
sys.path.insert(0, parent_dir)
from persepolis import __main__
================================================
FILE: uninstall.py
================================================
#!/usr/bin/env python3
# coding: utf-8
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
import platform
import glob
import os
import shutil
import sys
os_type = platform.system()
if os_type == 'Linux':
path_list = ['/usr/share/man/man1/persepolis.1.gz',
'/usr/share/pixmaps/com.github.persepolisdm.persepolis.svg',
'/usr/share/pixmaps/persepolis-tray.svg',
'/usr/share/applications/com.github.persepolisdm.persepolis.desktop',
'/usr/share/metainfo/com.github.persepolisdm.persepolis.appdata.xml',
'/usr/bin/persepolis',
'/usr/local/share/man/man1/persepolis.1.gz',
'/usr/local/share/pixmaps/com.github.persepolisdm.persepolis.svg',
'/usr/local/share/pixmaps/persepolis-tray.svg',
'/usr/local/share/applications/com.github.persepolisdm.persepolis.desktop',
'/usr/local/share/metainfo/com.github.persepolisdm.persepolis.appdata.xml',
'/usr/local/bin/persepolis']
elif os_type in ('FreeBSD', 'OpenBSD'):
path_list = ['/usr/local/share/man/man1/persepolis.1.gz',
'/usr/local/share/pixmaps/com.github.persepolisdm.persepolis.svg',
'/usr/local/share/pixmaps/persepolis-tray.svg',
'/usr/local/share/applications/com.github.persepolisdm.persepolis.desktop',
'/usr/local/share/metainfo/com.github.persepolisdm.persepolis.appdata.xml',
'/usr/local/bin/persepolis']
else:
print('This script is for Linux and BSD')
sys.exit(1)
# finding persepolis directories in /usr/lib/python3.6/site-packages/
python_version_list = ['python3.5', 'python3.6', 'python3.7', 'python3.8', 'python3.9', 'python3.10', 'python3.11', 'python3.12', 'python3.13']
for python_version in python_version_list:
# for BSD
if os_type == 'Linux':
pattern = '/usr/lib/' + python_version + '/site-packages/persepolis*'
for folder in glob.glob(pattern):
path_list.append(folder)
pattern2 = '/usr/local/lib/' + python_version + '/site-packages/persepolis*'
for folder in glob.glob(pattern2):
path_list.append(folder)
elif os_type == 'FreeBSD' or os_type == 'OpenBSD':
pattern = '/usr/local/lib/' + python_version + '/site-packages/persepolis*'
for folder in glob.glob(pattern):
path_list.append(folder)
print(path_list)
uid = os.getuid()
if uid != 0:
print('run this script as root.')
sys.exit(1)
for path in path_list:
if os.path.exists(path):
if os.path.isfile(path): # if path is for file
os.remove(path) # removing file
else:
shutil.rmtree(path) # removing folder
print(str(path) + ' is removed!')
print('uninstallation is complete!')
================================================
FILE: xdg/com.github.persepolisdm.persepolis.appdata.xml
================================================
com.github.persepolisdm.persepolisFSFAPGPL-3.0Persepolis Download ManagerDownload manager
Persepolis is easy to use graphical download manager that speed up your downloads!
Persepolis features:
Multi segment downloading.
Scheduling downloads.
Download queue.
Downloading video from Youtube and more.
Persepolis has browser extension for Firefox and Chromium.
com.github.persepolisdm.persepolis.desktop
Persepolis Download Manaer
https://persepolisdm.github.io/img/screen/persepolis.pnghttps://persepolisdm.github.io/https://github.com/persepolisdm/persepolis/issueshttps://github.com/persepolisdm/persepolis/wikihttps://www.transifex.com/persepolis/persepolis-translations/AliReza AmirSamimi
================================================
FILE: xdg/com.github.persepolisdm.persepolis.desktop.in
================================================
[Desktop Entry]
Actions=Open;Tray;
Categories=Qt;Network;
Comment[en_US]=A download manager written in Python
Comment=A download manager written in Python
Comment[fa]=نرم افزار مدیریت بارگیری نوشته شده با پایتون
Comment[ko]=Python으로 작성된 다운로드 관리자
Exec=@persepolisbin@
GenericName[en_US]=Download Manager
GenericName=Download Manager
GenericName[bn]=ডাউনলোড মেনেজার
GenericName[fa]=نرم افزار مدیریت بارگیری
GenericName[it]=Download Manager
GenericName[ko]=다운로드 관리자
Icon=com.github.persepolisdm.persepolis
Keywords=Internet;WWW;Web;
Name[en_US]=Persepolis Download Manager
Name=Persepolis Download Manager
Name[bn]=পার্সেপোলিস
Name[fa]=پرسپولیس
Name[it]=Download Manager Persepolis
StartupNotify=true
StartupWMClass=com.github.persepolisdm.persepolis
Terminal=false
Type=Application
X-KDE-SubstituteUID=false
[Desktop Action Open]
Exec=@persepolisbin@
Name=Start Persepolis
Name[bn]=পার্সেপোলিস শুরু করুন
Name[fa]=اجرای برنامه
Name[it]=Avvia Persepolis
Name[ko]=Persepolis 시작
[Desktop Action Tray]
Exec=@persepolisbin@ --tray
Name=Start in System Tray
Name[bn]=সিস্টেম-ট্রে-তে শুরু করুন
Name[fa]=اجرای برنامه در سینی نمایه
Name[it]=Avvia nella barra delle applicazioni
Name[ko]=시스템 트레이에서 시작
================================================
FILE: xdg/meson.build
================================================
desktop_file='com.github.persepolisdm.persepolis.desktop'
xml_file='com.github.persepolisdm.persepolis.appdata.xml'
conf = configuration_data()
conf.set('persepolisbin', join_paths(bindir, 'persepolis'))
configure_file(
input: 'com.github.persepolisdm.persepolis.desktop.in',
output: 'com.github.persepolisdm.persepolis.desktop',
configuration: conf,
install: true,
install_dir: desktopdir,
)
install_data(xml_file, install_dir: appdatadir)