Compare commits

...

No commits in common. "debian/latest" and "pristine-tar" have entirely different histories.

248 changed files with 3 additions and 145470 deletions

View File

@ -1,41 +0,0 @@
branches:
only:
- master
- /win.*/
# Start builds on tags only
# skip_non_tags: true
image:
- Visual Studio 2019
environment:
matrix:
- MSYS2_ARCH: i686
MSYSTEM: MINGW32
build_script:
- set
- set PATH=C:\msys64\%MSYSTEM%\bin;C:\msys64\usr\bin;%PATH%
- set CHERE_INVOKING=yes
# remove precisely conflicting packages
- bash -lc "pacman --noconfirm --ask 20 --remove mingw-w64-x86_64-gcc-ada mingw-w64-x86_64-gcc-objc mingw-w64-i686-gcc-ada mingw-w64-i686-gcc-objc"
# workaround updating msys2-runtime breaks all programs until last one exited
- bash -lc "pacman -Syuu --noconfirm"
- Powershell.exe "Stop-Process -name dirmngr -Erroraction silentlycontinue; echo killing_dirmng"
- Powershell.exe "Stop-Process -name gpg-agent -Erroraction silentlycontinue; echo killing_gpg-agent"
- bash -lc "pacman -Syuu --noconfirm"
- Powershell.exe "Stop-Process -name dirmngr -Erroraction silentlycontinue; echo killing_dirmng"
- Powershell.exe "Stop-Process -name gpg-agent -Erroraction silentlycontinue; echo killing_gpg-agent"
# finally run the install process
- bash -lc "bash .appveyor/msys2.sh"
artifacts:
- path: tools/win_installer/gpodder-*-contents.txt
- path: tools/win_installer/gpodder-*-installer.exe
- path: tools/win_installer/gpodder-*-portable.exe
deploy: off
on_failure:
- ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))

View File

@ -1,12 +0,0 @@
#!/bin/bash
set -e
src=notifu-src-1.6.1.zip
wget https://www.paralint.com/projects/notifu/dl/$src
checksum=$(sha256sum $src | cut -d " " -f1)
if [ "$checksum" = "0fdcd08d3e12d87af76cdaafbf1278c4fcf1baf5d6447cce1a676b8d78a4d8c3" ]; then
echo "$src checksum OK"
else
echo "$src checksum KO: got $checksum"
exit 1
fi

View File

@ -1,7 +0,0 @@
#!/bin/bash
set -e
cd tools/win_installer
# bash -xe to also see commands
bash -e ./build.sh "${APPVEYOR_REPO_COMMIT}"

View File

@ -1,31 +0,0 @@
# :noTabs=true:mode=yaml:tabSize=2:indentSize=2:
version: 2
jobs:
release-from-macos:
macos:
xcode: "13.4.1"
shell: /bin/bash --login -o pipefail
environment:
- BUNDLE_TAG: 22.8.27
steps:
- checkout
- run: >
curl -L -o "pythonbase-$BUNDLE_TAG.zip" "https://github.com/gpodder/gpodder-osx-bundle/releases/download/$BUNDLE_TAG/pythonbase-$BUNDLE_TAG.zip";
curl -L -o "pythonbase-$BUNDLE_TAG.zip.sha256" "https://github.com/gpodder/gpodder-osx-bundle/releases/download/$BUNDLE_TAG/pythonbase-$BUNDLE_TAG.zip.sha256";
saved_hash=$(awk '{print $1;}' < "pythonbase-$BUNDLE_TAG.zip.sha256");
comp_hash=$(openssl sha256 "pythonbase-$BUNDLE_TAG.zip" | awk '{print $2;}');
if [ "$saved_hash" != "$comp_hash" ]; then echo "E: $saved_hash != $comp_hash"; exit 1; else echo "valid hash"; fi;
LC_CTYPE=C.UTF-8 LANG=C.UTF-8 tools/mac-osx/release_on_mac.sh "$(pwd)/pythonbase-$BUNDLE_TAG.zip" || exit 1;
rm -Rf tools/mac-osx/_build/{gPodder.app,*.deps.zip*,gPodder.contents,run-*,gpo,gpodder-migrate2tres}
- store_artifacts:
path: tools/mac-osx/_build/
workflows:
version: 2
build-bundle:
jobs:
- release-from-macos:
filters:
branches:
ignore:
- adaptive

12
.github/FUNDING.yml vendored
View File

@ -1,12 +0,0 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: ['https://gpodder.net/contribute/'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@ -1,32 +0,0 @@
name: lint and test
on: [push, pull_request]
jobs:
linttest:
name: lint and unit tests
if: >-
github.event_name == 'push' ||
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
sudo apt-get update -q
sudo apt-get install intltool desktop-file-utils
pip3 install pytest-cov minimock pycodestyle isort codespell requests pytest pytest-httpserver
pip3 install podcastparser mygpoclient
- name: Lint
run: make lint
- name: Test
run: make releasetest

15
.gitignore vendored
View File

@ -1,15 +0,0 @@
*.pyc
__pycache__
*~
*.in.h
*.ui.h
.coverage
src/podcastparser.py
src/mygpoclient
src/dbus
build
share/applications/gpodder-url-handler.desktop
share/applications/gpodder.desktop
share/dbus-1/services/org.gpodder.service
share/locale/
venv/*

View File

@ -1,30 +0,0 @@
# Contributing to this repository <!-- omit in toc -->
## Getting started <!-- omit in toc -->
Before you begin:
- Ensure you are using Python 3.7+
- Check out the [existing issues](https://github.com/gpodder/gpodder/issues)
Contributions are made to this repo via Issues and Pull Requests (PRs). Make sure to search for existing Issues and PRs before creating your own.
## Getting the code and setting up the project
1. Fork this project
2. Clone the repository to your machine
3. Create a separate branch to get started, e.g. for feature `feat/branch-name-here` or fix `fix/fix-name-goes-here`
4. Make sure to create a new virtual environment and activate it:
```shell
python3 -m venv venv
source activate venv/bin/activate
```
5. Install dependencies: [Run from Git](https://gpodder.github.io/docs/run-from-git.html)
6. Start the program with debug mode: `./bin/gpodder -v`
7. Make the changes, commit in a branch and push the branch to your fork and then submit a Pull Request.
## Linting
To ensure code quality, we recommend you to run the linter before pushing the changes to your repo. In order to do so ensure the necessary packages are installed by executing:
```shell
pip3 install pytest-cov minimock pycodestyle isort requests pytest pytest-httpserver
```
Execute the linter in the root directory (Linux only): `make lint unittest`. On Windows execute: `pycodestyle share src/gpodder tools bin/* *.py`

674
COPYING
View File

@ -1,674 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
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 <http://www.gnu.org/licenses/>.
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:
<program> Copyright (C) <year> <name of author>
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
<http://www.gnu.org/licenses/>.
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
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

View File

@ -1,4 +0,0 @@
include README.md COPYING MANIFEST.in ChangeLog makefile setup.py
recursive-include share *
recursive-include po *
recursive-include tools *

222
README.md
View File

@ -1,222 +0,0 @@
___ _ _ ____
__ _| _ \___ __| |__| |___ _ _ |__ /
/ _` | _/ _ \/ _` / _` / -_) '_| |_ \
\__, |_| \___/\__,_\__,_\___|_| |___/
|___/
Media aggregator and podcast client
___
Copyright 2005-2022 The gPodder Team
## License
gPodder 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.
gPodder 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/>.
## Dependencies
- [Python 3.7](http://python.org/) or newer
- [Podcastparser](http://gpodder.org/podcastparser/) 0.6.0 or newer
- [mygpoclient](http://gpodder.org/mygpoclient/) 1.7 or newer
- [requests](https://requests.readthedocs.io) 2.24.0 or newer
- Python D-Bus bindings
As an alternative to python-dbus on Mac OS X and Windows, you can use
the dummy (no-op) D-Bus module provided in "tools/fake-dbus-module/".
For quick testing, see [Run from Git](https://gpodder.github.io/docs/run-from-git.html)
to install dependencies.
### GTK3 UI - Additional Dependencies
- [PyGObject](https://wiki.gnome.org/PyGObject) 3.22.0 or newer
- [GTK+3](https://www.gtk.org/) 3.16 or newer
### Optional Dependencies
- Bluetooth file sending: gnome-obex-send or bluetooth-sendto
- Size detection on Windows: PyWin32
- Native OS X support: ige-mac-integration
- MP3 Player Sync Support: python-eyed3 (0.7 or newer)
- iPod Sync Support: libgpod (tested with 0.8.3)
- Clickable links in GTK UI show notes: html5lib
- HTML show notes: WebKit2 gobject bindings
(webkit2gtk, webkitgtk4 or gir1.2-webkit2-4.0 packages).
- Better Youtube support (> 15 entries in feeds, download audio-only): youtube_dl or yt-dlp
### Build Dependencies
- help2man
- intltool
### Test Dependencies
- python-minimock
- pytest
- pytest-httpserver
- pytest-cov
- desktop-file-utils
## Testing
To run tests, use...
make unittest
To set a specific python binary set PYTHON:
PYTHON=python3 make unittest
Tests in gPodder are written in two different ways:
- [doctests](http://docs.python.org/3/library/doctest.html)
- [unittests](http://docs.python.org/3/library/unittest.html)
If you want to add doctests, simply write the doctest and make sure that
the module appears after `--doctest-modules` in `pytest.ini`. If you
add tests to any module in `src/gpodder` you have nothing to do.
If you want to add unit tests for a specific module (ex: gpodder.model),
you should add the tests as gpodder.test.model, or in other words:
The file: src/gpodder/model.py
is tested by: src/gpodder/test/model.py
After you've added the test, make sure that the module appears in
"test_modules" in src/gpodder/unittests.py - for the example above, the
unittests in src/gpodder/test/model.py are added as 'model'. For unit
tests, coverage reporting happens for the tested module (that's why the
test module name should mirror the module to be tested).
## Running and Installation
To run gPodder from source, use..
bin/gpodder # for the Gtk+ UI
bin/gpo # for the command-line interface
To install gPodder system-wide, use `make install`. By default, this
will install *all* UIs and all translations. The following environment
variables are processed by setup.py:
LINGUAS space-separated list of languages to install
GPODDER_INSTALL_UIS space-separated list of UIs to install
GPODDER_MANPATH_NO_SHARE if set, install manpages to $PREFIX/man/man1
See setup.py for a list of recognized UIs.
Example: Install the CLI and Gtk UI with German and Dutch translations:
export LINGUAS="de nl"
export GPODDER_INSTALL_UIS="cli gtk"
make install
The "make install" target also supports DESTDIR and PREFIX for installing
into an alternative root (default /) and prefix (default /usr):
make install DESTDIR=tmp/ PREFIX=/usr/local/
[*Debian*](https://wiki.debian.org/Python#Deviations_from_upstream) and *Ubuntu* use `dist-packages`
instead of `site-packages` for third party installs, so you'll want something like:
sudo python3 setup.py install --root / --prefix /usr/local --optimize=1 --install-lib=/usr/local/lib/python3.10/dist-packages
In fact, first try running `python -c "import sys; print(sys.path)"` to check what is the exact path.
It depends on your version of python.
## Portable Mode / Roaming Profiles
The run-time environment variable GPODDER_HOME is used to set
the location for storing the database and downloaded files.
This can be used for multiple configurations or to store the
download directory directly on a MP3 player or USB disk:
export GPODDER_HOME=/media/usbdisk/gpodder-data/
## OS X Specific Notes
- default GPODDER_HOME="$HOME/Library/Application Support/gPodder"
- default GPODDER_DOWNLOAD_DIR="$HOME/Library/Application Support/gPodder/download"
These settings may be modified by editing the following file of the .app :
/Applications/gPodder.app/Contents/MacOSX/_launcher
Add and edit the following lines to alter the launch environment on OS X :
export GPODDER_HOME="$HOME/Library/Application Support/gPodder"
export GPODDER_DOWNLOAD_DIR="$HOME/Library/Application Support/gPodder/download"
## Changing the Download Directory
The run-time environment variable GPODDER_DOWNLOAD_DIR is used to
set the location for storing the downloads only (independent of the
data directory GPODDER_HOME):
export GPODDER_DOWNLOAD_DIR=/media/BigDisk/Podcasts/
In this case, the database and settings will be stored in the default
location, with the downloads stored in /media/BigDisk/Podcasts/.
Another example would be to set both environment variables:
export GPODDER_HOME=~/.config/gpodder/
export GPODDER_DOWNLOAD_DIR=~/Podcasts/
This will store the database and settings files in ~/.config/gpodder/
and the downloads in ~/Podcasts/. If GPODDER_DOWNLOAD_DIR is not set,
$GPODDER_HOME/Downloads/ will be used if it is set.
## Logging
By default, gPodder writes log files to $GPODDER_HOME/Logs/ and removes
them after a certain amount of times. To avoid this behavior, you can set
the environment variable GPODDER_WRITE_LOGS to "no", e.g:
export GPODDER_WRITE_LOGS=no
## Extensions
Extensions are normally loaded from gPodder's "extensions/" folder (in
share/gpodder/extensions/) and from $GPODDER_HOME/Extensions/ - you can
override this by setting an environment variable:
export GPODDER_EXTENSIONS="/path/to/extension1.py extension2.py"
In addition to that, if you want to disable loading of all extensions,
you can do this by setting the following environment variable to a non-
empty value:
export GPODDER_DISABLE_EXTENSIONS=yes
If you want to report a bug, please try to disable all extensions and
check if the bug still appears to see if an extension causes the bug.
## More Information
- Homepage: http://gpodder.org/
- Bug tracker: https://github.com/gpodder/gpodder/issues
- Mailing list: http://freelists.org/list/gpodder
- IRC channel: #gpodder on irc.libera.chat

1303
bin/gpo

File diff suppressed because it is too large Load Diff

View File

@ -1,184 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client
# Copyright (c) 2005-2018 The gPodder Team
#
# gPodder 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.
#
# gPodder 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/>.
#
"""
gPodder enables you to subscribe to media feeds (RSS, Atom, YouTube,
Soundcloud and Vimeo) and automatically download new content.
This is the gPodder GUI. See gpo(1) for the command-line interface.
"""
import gettext
import logging
import os
import os.path
import platform
import subprocess
import sys
from optparse import OptionGroup, OptionParser
logger = logging.getLogger(__name__)
try:
import dbus
have_dbus = True
except ImportError:
print("""
Warning: python-dbus not found. Disabling D-Bus support.
""", file=sys.stderr)
have_dbus = False
def main():
# Paths to important files
gpodder_script = sys.argv[0]
gpodder_script = os.path.realpath(gpodder_script)
gpodder_dir = os.path.join(os.path.dirname(gpodder_script), '..')
prefix = os.path.abspath(os.path.normpath(gpodder_dir))
src_dir = os.path.join(prefix, 'src')
locale_dir = os.path.join(prefix, 'share', 'locale')
ui_folder = os.path.join(prefix, 'share', 'gpodder', 'ui')
images_folder = os.path.join(prefix, 'share', 'gpodder', 'images')
icons_folder = os.path.join(prefix, 'share', 'icons', 'hicolor', 'scalable')
icon_file = os.path.join(icons_folder, 'apps', 'gpodder-adaptive.svg')
if os.path.exists(os.path.join(src_dir, 'gpodder', '__init__.py')):
# Run gPodder from local source folder (not installed)
sys.path.insert(0, src_dir)
# on Mac OS X, read from the defaults database the locale of the user
if platform.system() == 'Darwin' and 'LANG' not in os.environ:
locale_cmd = ('defaults', 'read', 'NSGlobalDomain', 'AppleLocale')
process = subprocess.Popen(locale_cmd, stdout=subprocess.PIPE)
output, error_output = process.communicate()
# the output is a string like 'fr_FR', and we need 'fr_FR.utf-8'
user_locale = output.decode('utf-8').strip() + '.UTF-8'
os.environ['LANG'] = user_locale
print('Setting locale to', user_locale, file=sys.stderr)
# Set up the path to translation files
gettext.bindtextdomain('gpodder', locale_dir)
import gpodder # isort:skip
gpodder.prefix = prefix
# Package managers can install the empty file {prefix}/share/gpodder/no-update-check to disable update checks
gpodder.no_update_check_file = os.path.join(prefix, 'share', 'gpodder', 'no-update-check')
# Enable i18n for gPodder translations
_ = gpodder.gettext
# Set up paths to folder with GtkBuilder files and gpodder.svg
# gpodder.ui_folders.append(ui_folder)
gpodder.images_folder = images_folder
gpodder.icons_folder = icons_folder
gpodder.icon_file = icon_file
s_usage = 'usage: %%prog [options]\n\n%s' % (__doc__.strip())
s_version = '%%prog %s' % (gpodder.__version__)
parser = OptionParser(usage=s_usage, version=s_version)
grp_subscriptions = OptionGroup(parser, "Subscriptions")
parser.add_option_group(grp_subscriptions)
grp_subscriptions.add_option('-s', '--subscribe', dest='subscribe',
metavar='URL',
help=_('subscribe to the feed at URL'))
grp_logging = OptionGroup(parser, "Logging")
parser.add_option_group(grp_logging)
grp_logging.add_option("-v", "--verbose",
action="store_true", dest="verbose", default=False,
help=_("print logging output on the console"))
grp_logging.add_option("-q", "--quiet",
action="store_true", dest="quiet", default=False,
help=_("reduce warnings on the console"))
grp_advanced = OptionGroup(parser, "Advanced")
parser.add_option_group(grp_advanced)
grp_advanced.add_option("--close-after-startup", action="store_true",
help=_("exit once started up (for profiling)"))
# On Mac OS X, support the "psn" parameter for compatibility (bug 939)
if gpodder.ui.osx:
grp_advanced.add_option('-p', '--psn', dest='macpsn', metavar='PSN',
help=_('Mac OS X application process number'))
options, args = parser.parse_args(sys.argv)
gpodder.ui.gtk = True
gpodder.ui.adaptive = True
gpodder.ui.python3 = True
desktop_session = os.environ.get('DESKTOP_SESSION', 'unknown').lower()
xdg_current_desktop = os.environ.get('XDG_CURRENT_DESKTOP', 'unknown').lower()
gpodder.ui.unity = (desktop_session in ('ubuntu', 'ubuntu-2d', 'unity')
and xdg_current_desktop in ('unity', 'unity:unity7:ubuntu'))
from gpodder import log
log.setup(options.verbose, options.quiet)
if (not (gpodder.ui.win32 or gpodder.ui.osx)
and os.environ.get('DISPLAY', '') == ''
and os.environ.get('WAYLAND_DISPLAY', '') == ''):
logger.error('Cannot start gPodder: $DISPLAY or $WAYLAND_DISPLAY is not set.')
sys.exit(1)
if have_dbus:
# Try to find an already-running instance of gPodder
session_bus = dbus.SessionBus()
# Obtain a reference to an existing instance; don't call get_object if
# such an instance doesn't exist as it *will* create a new instance
if session_bus.name_has_owner(gpodder.dbus_bus_name):
try:
remote_object = session_bus.get_object(
gpodder.dbus_bus_name,
gpodder.dbus_gui_object_path)
# An instance of GUI is already running
logger.info('Activating existing instance via D-Bus.')
remote_object.show_gui_window(
dbus_interface=gpodder.dbus_interface)
if options.subscribe:
remote_object.subscribe_to_url(options.subscribe)
return
except dbus.exceptions.DBusException as dbus_exception:
logger.info('Cannot connect to remote object.', exc_info=True)
if gpodder.ui.gtk:
from gpodder.gtkui import app
gpodder.ui_folders.insert(0, os.path.join(ui_folder, 'gtk'))
app.main(options)
else:
logger.error('No GUI selected.')
if __name__ == '__main__':
main()

View File

@ -1,127 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client
# Copyright (c) 2005-2018 The gPodder Team
#
# gPodder 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.
#
# gPodder 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/>.
#
# gpodder-migrate2tres - Migrate data from gPodder 2.x to gPodder 3
# by Thomas Perl <thp@gpodder.org>; 2011-04-28
import configparser
import os
import shutil
import sys
gpodder_script = sys.argv[0]
gpodder_script = os.path.realpath(gpodder_script)
gpodder_dir = os.path.join(os.path.dirname(gpodder_script), '..')
prefix = os.path.abspath(os.path.normpath(gpodder_dir))
src_dir = os.path.join(prefix, 'src')
if os.path.exists(os.path.join(src_dir, 'gpodder', '__init__.py')):
# Run gPodder from local source folder (not installed)
sys.path.insert(0, src_dir)
import gpodder # isort:skip
gpodder.prefix = prefix
from gpodder import schema, util # isort:skip
old_database = os.path.expanduser('~/.config/gpodder/database.sqlite')
new_database = gpodder.database_file
old_config = os.path.expanduser('~/.config/gpodder/gpodder.conf')
new_config = gpodder.config_file
if not os.path.exists(old_database):
print("""
Turns out that you never ran gPodder 2.
Can't find this required file:
%(old_database)s
""" % locals(), file=sys.stderr)
sys.exit(1)
old_downloads = None
if os.path.exists(old_config):
parser = configparser.RawConfigParser()
parser.read(old_config)
try:
old_downloads = parser.get('gpodder-conf-1', 'download_dir')
except configparser.NoSectionError:
# The file is empty / section (gpodder-conf-1) not found
pass
except configparser.NoOptionError:
# The section is available, but the key (download_dir) is not
pass
if old_downloads is None:
# The user has no configuration. This usually happens when
# only the CLI version of gPodder is used. In this case, the
# download directory is most likely the default (bug 1434)
old_downloads = os.path.expanduser('~/gpodder-downloads')
new_downloads = gpodder.downloads
if not os.path.exists(old_downloads):
print("""
Old download directory does not exist. Creating empty one.
""", file=sys.stderr)
os.makedirs(old_downloads)
if any(os.path.exists(x) for x in (new_database, new_downloads)):
print("""
Existing gPodder 3 user data found.
To continue, please remove:
%(new_database)s
%(new_downloads)s
""" % locals(), file=sys.stderr)
sys.exit(1)
print("""
Would carry out the following actions:
Move downloads from %(old_downloads)s
to %(new_downloads)s
Convert database from %(old_database)s
to %(new_database)s
""" % locals(), file=sys.stderr)
result = input('Continue? (Y/n) ')
if result in 'Yy':
util.make_directory(gpodder.home)
schema.convert_gpodder2_db(old_database, new_database)
if not os.path.exists(new_database):
print('Could not convert database.', file=sys.stderr)
sys.exit(1)
shutil.move(old_downloads, new_downloads)
if not os.path.exists(new_downloads):
print('Could not move downloads.', file=sys.stderr)
sys.exit(1)
print('Done. Have fun with gPodder 3!')

11
debian/README.debian vendored
View File

@ -1,11 +0,0 @@
As of gPodder 3.6, alpha support for syncing directly to iPod devices
has been restored. (This was available in gPodder 2.x, but has not
been present in the 3.x version until this release.)
Users may be interested in the gpodder-migrate2tres command which will
migrate gPodder 2.x configurations to the format used by gPodder 3.x.
This binary is included in the gpodder_3.x packages. Similarly, users
can generate a backup of their existing 2.x configurations by using
the gpodder-backup script (currently shipped in the gpodder_2.x packages).
Thank you for using gPodder!

924
debian/changelog vendored
View File

@ -1,924 +0,0 @@
gpodder-adaptive (3.11.4+1-1.1) UNRELEASED; urgency=medium
[ Maryjane ]
* Updated manitainer to Maryjane
* Added libhandy 1 as a runtime dependency
* Import gpodder Debian directory from the salsa.debian.org repository
-- maryjane <maryjane@disroot.org> Fri, 23 Feb 2024 02:04:31 +0000
gpodder (3.10.17-1) unstable; urgency=medium
* New upstream version 3.10.17
* Refresh patches for new upstream version
-- tony mancill <tmancill@debian.org> Mon, 23 Nov 2020 10:26:51 -0800
gpodder (3.10.16-1) unstable; urgency=medium
* New upstream version 3.10.16
* Refresh patches against new upstream version
* Use debhelper-compat 13
-- tony mancill <tmancill@debian.org> Tue, 23 Jun 2020 21:46:05 -0700
gpodder (3.10.15-1) unstable; urgency=medium
* New upstream version 3.10.15
-- tony mancill <tmancill@debian.org> Wed, 15 Apr 2020 19:13:12 -0700
gpodder (3.10.14-1) unstable; urgency=medium
* New upstream version 3.10.14
* Refresh patches against new upstream version
* Remove address_syntax_warnings patch
-- tony mancill <tmancill@debian.org> Tue, 14 Apr 2020 21:06:01 -0700
gpodder (3.10.13-1) unstable; urgency=medium
* New upstream version 3.10.13
* Refresh patches against new upstream version
* Freshen years in debian/copyright
* Bump Standards-Version to 4.5.0
* Remove unneeded lintian overrides
-- tony mancill <tmancill@debian.org> Mon, 03 Feb 2020 01:05:08 -0800
gpodder (3.10.11-2) unstable; urgency=medium
* Remove Depends on dbus-x11; allow dbus-user-session to satisfy this
dependency. Thank you to John-Paul Durrieu for the bug report.
* Specify debhelper compat via debhelper-compat dependency
* Remove outdated debian NEWS file; it referred to upgrades prior
to old-old-stable.
* Add patch to address syntax warnings.
* Set "Rules-Requires-Root: no" in debian/control
-- tony mancill <tmancill@debian.org> Thu, 26 Dec 2019 20:18:05 -0800
gpodder (3.10.11-1) unstable; urgency=medium
* New upstream version 3.10.11
- Fixes the "Check for new episodes at startup" extension
* Bump Standards-Version to 4.4.1
-- tony mancill <tmancill@debian.org> Sun, 29 Sep 2019 19:05:48 -0700
gpodder (3.10.10-1) unstable; urgency=medium
* New upstream version 3.10.10
- Support for paginated feeds (RFC 5005)
- New extension to manage YouTube subscriptions and downloads
- Updated translations
- Numerous bugs fixes and improvements
(see: https://github.com/gpodder/gpodder/releases/tag/3.10.10)
* Refresh patches
* Bump Standards-Version to 4.4.0
* Use debhelper 12
* Add youtube-dl to Suggests
-- tony mancill <tmancill@debian.org> Sat, 28 Sep 2019 07:48:22 -0700
gpodder (3.10.9-1) unstable; urgency=medium
* New upstream version 3.10.9
- Uses HTTPS for YouTube.
Fixes https://github.com/gpodder/gpodder/issues/625
* Upload to unstable.
-- tony mancill <tmancill@debian.org> Thu, 13 Jun 2019 19:05:17 -0700
gpodder (3.10.8-1) experimental; urgency=medium
* New upstream version 3.10.8
- Updated Russian translation
-- tony mancill <tmancill@debian.org> Sat, 06 Apr 2019 08:12:32 -0700
gpodder (3.10.7-1) unstable; urgency=medium
* New upstream version 3.10.7
* Freshen patches for new upstream release
* debian/copyright: freshen copyright years and add comment regarding
AppStream metadata CC0-1.0 designation
-- tony mancill <tmancill@debian.org> Sat, 02 Feb 2019 15:17:35 -0800
gpodder (3.10.6-1) unstable; urgency=medium
[ Ondřej Nový ]
* d/copyright: Use https protocol in Format field
* d/changelog: Remove trailing whitespaces
[ tony mancill ]
* New upstream version 3.10.6
* Drop get-orig-source target from debian/rules
* Bump Standards-Version to 4.3.0
* Refresh debian/patches for new upstream version.
-- tony mancill <tmancill@debian.org> Sat, 19 Jan 2019 11:58:47 -0800
gpodder (3.10.3-2) unstable; urgency=medium
* Rename variable 'async' in services.py (Closes: #902794)
-- tony mancill <tmancill@debian.org> Sat, 30 Jun 2018 21:23:32 -0700
gpodder (3.10.3-1) unstable; urgency=medium
* New upstream version 3.10.3.
New features:
- #402 extension to run a command on download
- #431 update sonos extension to use soco >= 0.7 API
- #442 gpo command for downloading/deleting a single episode
- #384 YouTube feeds without API key
* Freshen patches for new upstream release
-- tony mancill <tmancill@debian.org> Sun, 17 Jun 2018 16:43:06 -0700
gpodder (3.10.1-2) unstable; urgency=medium
* Update Vcs fields for migration from Alioth -> Salsa
* Apply patch for Ayatana App Indicator (Closes: #898424) and replace
recommends on python3-appindicator with gir1.2-ayatanaappindicator3-0.1
- Thank you to Mike Gabriel <mike.gabriel@das-netzwerkteam.de>
* Bump Standards-Version to 4.1.4
* Use debhelper 11
-- tony mancill <tmancill@debian.org> Sun, 27 May 2018 14:25:36 -0700
gpodder (3.10.1-1) unstable; urgency=medium
* New upstream version 3.10.1
* Update debian/watch to repack with compression=xz
* Update year in debian/copyright
* Freshen patches for new upstream release
-- tony mancill <tmancill@debian.org> Wed, 21 Feb 2018 20:21:19 -0800
gpodder (3.10.0-5) unstable; urgency=medium
* Add python3-all and python3-setuptools to Build-Depends
* Add dbus-x11, python3-gi-cairo, and gir1.2-gtk-3.0 to Depends
Promote default-dbus-session-bus | dbus-session-bus to Depends
-- tony mancill <tmancill@debian.org> Sun, 11 Feb 2018 10:09:30 -0800
gpodder (3.10.0-4) unstable; urgency=medium
* Add missing dependency on python3-cairo (Closes: #889850)
-- tony mancill <tmancill@debian.org> Thu, 08 Feb 2018 20:03:06 -0800
gpodder (3.10.0-3) unstable; urgency=medium
* Replace patch for #888420 with upstream commits through e524572.
- Fixes OPML export.
- Fixes issues with YouTube extension downloads.
-- tony mancill <tmancill@debian.org> Sat, 27 Jan 2018 10:36:33 -0800
gpodder (3.10.0-2) unstable; urgency=medium
* Add patch for exception downloading YouTube videos (Closes: #888420)
-- tony mancill <tmancill@debian.org> Thu, 25 Jan 2018 21:12:06 -0800
gpodder (3.10.0-1) unstable; urgency=medium
* New upstream version 3.10.0
- no longer depends upon pygtk (Closes: #885295)
- now builds for Python3 (only)
* Update debian/watch to scan github
* Update Homepage, Vcs URLs, and freshen copyright years
* Add patches for UTF-8
* Use debhelper 10
* Update debian/rules and dependencies for Python3
* Bump Standards-Version to 4.1.3
-- tony mancill <tmancill@debian.org> Sun, 21 Jan 2018 18:33:01 -0800
gpodder (3.9.3-1) unstable; urgency=medium
* New upstream release.
-- tony mancill <tmancill@debian.org> Mon, 26 Dec 2016 20:47:50 -0800
gpodder (3.9.2-1) unstable; urgency=medium
* New upstream release.
* Replace build-dep on feedparser with podcastparser.
* Use HTTPS for Vcs-Git URL.
* Replace Recommends on dbus-x11 with
default-dbus-session-bus | dbus-session-bus. (Closes: #836099)
-- tony mancill <tmancill@debian.org> Wed, 14 Dec 2016 21:49:07 -0800
gpodder (3.9.1-1) unstable; urgency=medium
* New upstream release.
* Bump Standards-Version to 3.9.8.
-- tony mancill <tmancill@debian.org> Sun, 13 Nov 2016 16:47:26 -0800
gpodder (3.9.0-2) unstable; urgency=medium
* Remove depedency on python-webkit recommends on
libqtwebkit-qmlwebkitplugin. (Closes: #790218)
-- tony mancill <tmancill@debian.org> Mon, 15 Feb 2016 08:34:13 -0800
gpodder (3.9.0-1) unstable; urgency=medium
* New upstream release.
* Drop dependencies on libjs-jquery and libjs-jquery-mobile,
now that the web UI has been removed.
* Drop the use_local_jquery.patch.
-- tony mancill <tmancill@debian.org> Sat, 06 Feb 2016 09:30:56 -0800
gpodder (3.8.5-1) unstable; urgency=medium
* New upstream release.
* Remove .menu file. This package contains a desktop file.
* Clean up debian/copyright; remove files no longer part of the source.
* Drop build-dep on python-dev in favor of python for arch all package.
-- tony mancill <tmancill@debian.org> Thu, 03 Dec 2015 22:01:18 -0800
gpodder (3.8.4-1) unstable; urgency=medium
* New upstream release.
-- tony mancill <tmancill@debian.org> Wed, 27 May 2015 19:13:14 -0700
gpodder (3.8.3-2) unstable; urgency=medium
* Remove Recommends on python-gst0.10. (Closes: #785834)
* Add python-appindicator to Recommends. (Closes: #770717)
-- tony mancill <tmancill@debian.org> Wed, 20 May 2015 21:07:34 -0700
gpodder (3.8.3-1) unstable; urgency=medium
* New upstream release.
* Add patch to set the default for update checks to false.
* Bump Standards-Version to 3.9.6.
* Patch HTML to use local resources.
Adds libjs-jquery and libjs-jquery-mobile to dependencies.
-- tony mancill <tmancill@debian.org> Sun, 05 Apr 2015 21:21:39 -0700
gpodder (3.8.1-1) unstable; urgency=medium
* New upstream release.
- Fixes a bug with mixed-case password.
- Improves support for adding certain YouTube channels/feed URLs.
-- tony mancill <tmancill@debian.org> Tue, 09 Sep 2014 22:44:01 -0700
gpodder (3.8.0-1) unstable; urgency=medium
* New upstream release.
-- tony mancill <tmancill@debian.org> Sat, 26 Jul 2014 20:43:14 -0700
gpodder (3.7.0-2) unstable; urgency=medium
* Convert from python-support to dh-python.
* Bump debhelper dependency to DH9.
-- tony mancill <tmancill@debian.org> Tue, 27 May 2014 21:50:33 -0700
gpodder (3.7.0-1) unstable; urgency=medium
* New upstream release.
-- tony mancill <tmancill@debian.org> Sat, 17 May 2014 11:36:25 -0700
gpodder (3.6.1-1) unstable; urgency=medium
* New upstream release (Closes: #742191)
- Fixes YouTube integration bug.
- Use LC_ALL=C during manpage generation.
- Add prefix to path in desktop file.
-- tony mancill <tmancill@debian.org> Thu, 20 Mar 2014 21:41:43 -0700
gpodder (3.6.0-1) unstable; urgency=medium
* New upstream release.
- (Closes: #681638)
- Adds alpha support for iPod (see #688240)
* Remove gpodder-migrate2tres_manpage.patch; integrated upstream.
* Remove Suggests: python-eyed3 until 0.7 is in Debian (see #740457)
* Bump Standards-Version to 3.9.5.
* Update README.Debian.
-- tony mancill <tmancill@debian.org> Sat, 01 Mar 2014 15:26:20 -0800
gpodder (3.5.2-1) unstable; urgency=low
* New upstream.
* Update Vcs- URLs to canonical forms.
* Update d/copyright years.
-- tony mancill <tmancill@debian.org> Fri, 27 Sep 2013 21:20:17 -0700
gpodder (3.5.1-1) unstable; urgency=low
* New upstream release.
* d/control: declare versioned dependency on python-gtk2 (>= 2.16)
-- tony mancill <tmancill@debian.org> Fri, 12 Apr 2013 22:07:02 -0700
gpodder (3.5.0-1) unstable; urgency=low
* New upstream release.
* Mention gpodder-migrate2tres in NEWS.Debian file. (Closes: #695470)
-- tony mancill <tmancill@debian.org> Fri, 08 Mar 2013 21:21:00 -0800
gpodder (3.4.0-1) unstable; urgency=low
* New upstream release.
* Dependency on python-feedparser is now versioned (>= 5.1.2) to
address problems with some feeds.
-- tony mancill <tmancill@debian.org> Sun, 23 Dec 2012 19:24:26 -0800
gpodder (3.3.0-1) unstable; urgency=low
* New upstream release.
* Bump Standards-Version to 3.9.4.
* Update README.Debian to note that syncing to iPod is still not
supported in the 3.x versions.
-- tony mancill <tmancill@debian.org> Mon, 24 Sep 2012 10:34:12 -0700
gpodder (3.2.0-1) unstable; urgency=low
* New upstream release.
* Upload to unstable now that the 3.x series has feature parity with
2.x. (MP3 player device sync is now part of 3.x.)
* d/control:
- Add libqtwebkit-qmlwebkitplugin to Recommends.
- Move python-webkit from Recommends to Depends.
-- tony mancill <tmancill@debian.org> Wed, 25 Jul 2012 22:03:20 -0700
gpodder (3.1.2-1) experimental; urgency=low
* New upstream release.
- Closes: #582656 - intermittently redownloads all [...] episodes
- Closes: #669194 - Repeatedly shows deleted episode
- Closes: #672583 - when run with web interface errors get thrown
* Freshen gpodder-migrate2tres manpage for 3.1.x build.
-- tony mancill <tmancill@debian.org> Mon, 28 May 2012 18:30:33 -0700
gpodder (3.1.1-1) experimental; urgency=low
* New upstream release.
* Bump Standards-Version to 3.9.3 (no changes).
-- tony mancill <tmancill@debian.org> Tue, 15 May 2012 22:32:06 -0700
gpodder (3.1.0-1) experimental; urgency=low
* New upstream release.
-- tony mancill <tmancill@debian.org> Tue, 17 Apr 2012 23:24:49 -0700
gpodder (2.20.1-1) unstable; urgency=low
* New upstream release.
- Bugfix release containing fixes from 3.x release.
* Update d/copyright years and DEP5 fields.
-- tony mancill <tmancill@debian.org> Sun, 19 Feb 2012 10:37:24 -0800
gpodder (3.0.4-1) experimental; urgency=low
* New upstream release.
-- tony mancill <tmancill@debian.org> Tue, 24 Jan 2012 21:59:59 -0800
gpodder (3.0.3-1) experimental; urgency=low
* New upstream release (Closes: #654546)
-- tony mancill <tmancill@debian.org> Mon, 09 Jan 2012 18:26:11 -0800
gpodder (3.0.2-1) experimental; urgency=low
* New upstream release.
-- tony mancill <tmancill@debian.org> Wed, 14 Dec 2011 00:15:05 -0800
gpodder (3.0.1-1) experimental; urgency=low
* New upstream release.
* Uploading to experimental to allow for testing of 3.x.
-- tony mancill <tmancill@debian.org> Sat, 26 Nov 2011 11:19:43 -0800
gpodder (2.20-1) unstable; urgency=low
* New upstream release.
* Update debian/watch.
* Remove translations.patch.
* Tweak package description to address lintian warning.
-- tony mancill <tmancill@debian.org> Wed, 19 Oct 2011 19:36:49 -0700
gpodder (2.18-1) unstable; urgency=low
* New upstream release.
* Add translations.patch to preserve translations from upstream 2.16.
-- tony mancill <tmancill@debian.org> Tue, 16 Aug 2011 21:52:00 -0700
gpodder (2.16-1) unstable; urgency=low
* New upstream release.
-- tony mancill <tmancill@debian.org> Fri, 08 Jul 2011 21:54:34 -0700
gpodder (2.15-2) unstable; urgency=low
* This time without a patch that reverts the source to 2.14.
-- tony mancill <tmancill@debian.org> Tue, 31 May 2011 22:05:31 -0700
gpodder (2.15-1) unstable; urgency=low
* New upstream release
* Bump standards version to 3.9.2 (no changes necessary).
-- tony mancill <tmancill@debian.org> Tue, 03 May 2011 14:21:32 -0700
gpodder (2.14-1) unstable; urgency=low
* New upstream release
-- tony mancill <tmancill@debian.org> Tue, 05 Apr 2011 21:23:23 -0700
gpodder (2.13-3) unstable; urgency=low
* Incorporate upstream patch for .desktop file. (Closes: #620438)
-- tony mancill <tmancill@debian.org> Sat, 02 Apr 2011 21:02:05 -0700
gpodder (2.13-2) unstable; urgency=low
* Update Vcs-* in debian/control; now using git.debian.org
-- tony mancill <tmancill@debian.org> Sun, 27 Feb 2011 20:22:16 -0800
gpodder (2.13-1) unstable; urgency=low
* New upstream release
* Switch source format to "3.0 (quilt)"
* Simplify debian/rules.
* Build-Depend on debhelper (>= 7.3.7)
-- tony mancill <tmancill@debian.org> Fri, 25 Feb 2011 23:01:30 -0800
gpodder (2.12-1) unstable; urgency=low
* New upstream release (Closes: #607180)
* Update Thomas's email address in debian/control.
-- tony mancill <tmancill@debian.org> Wed, 12 Jan 2011 21:32:36 -0800
gpodder (2.11-1) unstable; urgency=low
* New upstream release
-- tony mancill <tmancill@debian.org> Sun, 19 Dec 2010 15:35:25 -0800
gpodder (2.10-1) unstable; urgency=low
* New upstream release
* Upload to unstable.
-- tony mancill <tmancill@debian.org> Sun, 05 Dec 2010 17:08:02 -0800
gpodder (2.9-1) experimental; urgency=low
* New upstream release
* Removes dependency on python-pymtp.
* Update debian/copyright to DEP5 format.
* Upload to experimental pending release of squeeze.
-- tony mancill <tmancill@debian.org> Tue, 12 Oct 2010 22:21:18 -0700
gpodder (2.8-1~pre0) experimental; urgency=low
* New upstream release
* Remove python-pymad from Suggests (no longer used by software)
* Add python-gst0.10 to Recommends (used for track length detection
and iPod sync)
* Update Standards-Version to 3.9.1 (no changes needed)
* Upload to experimental
-- tony mancill <tmancill@debian.org> Mon, 06 Sep 2010 12:14:32 -0700
gpodder (2.7-1) unstable; urgency=low
* New upstream release: "Proposition Infinity"
* debian/control:
- Update Standards Version to 3.9.0 (no changes).
- Add tmancill@debian.org to Uploaders:
- Add Vcs-Browser and Vcs-Svn fields for Debian packaging
-- tony mancill <tmancill@debian.org> Sun, 18 Jul 2010 16:35:57 -0700
gpodder (2.6-1) unstable; urgency=low
* "The Staircase Implementation" release (Closes: #582907)
* Upstream: Add option "Do nothing" for new episodes (Closes: #561632)
* Upstream: Fix for new GtkBuilder version (Closes: #581780)
* Upstream: Removed feed_update_skipping (Closes: #568853)
* Upstream: Better new episode detection code (Closes: #582656)
* debian/control: Add "Recommends:" on python-webkit
* debian/control: Better package description
* debian/watch: Added
-- Thomas Perl <thp@thpinfo.com> Thu, 27 May 2010 17:41:01 +0200
gpodder (2.3-1) unstable; urgency=low
* "The Adhesive Duck Deficiency" release
* Remove help2man and imagemagick build-dependencies
-- Thomas Perl <thp@thpinfo.com> Sat, 27 Feb 2010 23:02:54 +0100
gpodder (2.2-1) unstable; urgency=low
* The "LA X" release
* Add build dependency and dependency on python-mygpoclient
* Remove recommendation on python-gtkhtml2 (Closes: #561337)
* Remove build-depends on python-dev (not necessary for pure Python modules)
* Add "${misc:Depends}" to Depends in debian/control
* Upgrade to standards-version 3.8.4 (no changes necessary)
-- Thomas Perl <thp@thpinfo.com> Fri, 05 Feb 2010 16:16:28 +0100
gpodder (2.1-1) unstable; urgency=low
* "The Luminous Fish Effect" release
* Add a "Recommends:" on dbus-x11 (Closes: #548524)
* Recommend "python-simplejson" for Soundcloud support
-- Thomas Perl <thp@thpinfo.com> Sat, 12 Dec 2009 17:52:45 +0100
gpodder (2.0-1) unstable; urgency=low
* The "Day of the Tentacle" release
-- Thomas Perl <thp@thpinfo.com> Wed, 16 Sep 2009 17:39:06 +0200
gpodder (0.17.0-1) unstable; urgency=low
* The "Orientation" release
-- Thomas Perl <thp@thpinfo.com> Mon, 27 Jul 2009 14:31:01 +0200
gpodder (0.16.1-1) unstable; urgency=low
* The "Adrift" bugfix release
-- Thomas Perl <thp@thpinfo.com> Fri, 05 Jun 2009 13:23:47 +0200
gpodder (0.16.0-1) unstable; urgency=low
* The "Man of Science, Man of Faith" release
* Make SQLite handling more robust (from upstream) (Closes: #527387)
* Upstream applied dbus error running from cron patch (Closes: #520369)
* debian/control: Removed dependency on python-glade2 (now using GtkBuilder)
-- Thomas Perl <thp@thpinfo.com> Mon, 01 Jun 2009 15:08:14 +0200
gpodder (0.15.2-1) unstable; urgency=low
* "The Long Morrow" bugfix release
* Upstream applied dbus error running from cron patch (Closes: #520369)
* debian/compat: Upgrade to debhelper compatibility level 7 (no changes
needed after looking at the changes in the debhelper(7) manpage)
* debian/control: Update Standards-Version to 3.8.1 (no changes needed
after looking at upgrading-checklist.txt and referring to the policy)
-- Thomas Perl <thp@thpinfo.com> Sat, 11 Apr 2009 13:35:17 +0200
gpodder (0.15.1-1) unstable; urgency=low
* The "Passage on the Lady Anne" bugfix release
* debian/control: Add python-dbus as a dependency (missing from 0.15.0)
-- Thomas Perl <thp@thpinfo.com> Thu, 12 Mar 2009 20:23:39 +0100
gpodder (0.15.0-1) unstable; urgency=low
* "The Invaders" release
* debian/rules: Add ChangeLog to the release (included in tarball)
* debian/rules: Remove outdated exclusion of "gui.py.orig" for dh_clean
* debian/copyright: Update year to 2009; add "and the gPodder Team"
-- Thomas Perl <thp@thpinfo.com> Mon, 09 Mar 2009 13:11:11 +0100
gpodder (0.14.1-1) unstable; urgency=low
* "The Thirty-Fathom Grave" bugfix release
* Recommend python-gtkhtml2 for HTML episode shownotes
-- Thomas Perl <thp@thpinfo.com> Sun, 01 Feb 2009 21:58:50 +0100
gpodder (0.14.0-1) unstable; urgency=low
* The "A Short Drink From a Certain Fountain" release
-- Thomas Perl <thp@thpinfo.com> Thu, 11 Dec 2008 15:25:27 +0100
gpodder (0.13.1-1) unstable; urgency=low
* "The Brain Center at Whipple's" bugfix release
-- Thomas Perl <thp@perli.net> Thu, 30 Oct 2008 13:28:15 +0100
gpodder (0.13.0-1) unstable; urgency=low
* The "A Thing About Machines" release
* Push Standards-Version to 3.8.0
* Use "dh_icons" for updating the icon cache and remove custom postinst file
* Suggest python-pymtp for new Media Transfer Protocol MP3 player support
* Update application description to be more accurate for the recent version
* Update website URL, year and e-mail address in debian/copyright file
-- Thomas Perl <thp@perli.net> Mon, 06 Oct 2008 21:39:41 +0200
gpodder (0.12.2-1) unstable; urgency=low
* The "Of Late I Think of Cliffordville" bugfix release
-- Thomas Perl <thp@perli.net> Sun, 17 Aug 2008 15:51:54 +0200
gpodder (0.12.1-1) unstable; urgency=low
* "The Little People" bugfix release (Closes: #491696, #491610)
-- Thomas Perl <thp@perli.net> Thu, 24 Jul 2008 11:22:35 +0200
gpodder (0.12.0-1) unstable; urgency=low
* The "Metropolis" release (Closes: #478748, #489459)
-- Thomas Perl <thp@perli.net> Tue, 15 Jul 2008 11:01:45 +0200
gpodder (0.11.3-1) unstable; urgency=low
* The "To Serve Man" release (Closes: #481229, #482907)
-- Thomas Perl <thp@perli.net> Mon, 02 Jun 2008 11:30:42 +0200
gpodder (0.11.2-1) unstable; urgency=low
* The "Walk like a Panther" release
-- Thomas Perl <thp@perli.net> Sat, 26 Apr 2008 09:56:53 +0200
gpodder (0.11.1-1) unstable; urgency=low
* The "Attacked by Killer Tomatoes" release (Closes: #469736, #466496)
* Alternative dependency for gnome-bluetooth is bluez-gnome (contains
bluetooth-sendto, which is intended to replace gnome-obex-send)
-- Thomas Perl <thp@perli.net> Thu, 27 Mar 2008 14:32:18 +0100
gpodder (0.11.0-1) unstable; urgency=low
* The "Walking for a change makes me feel normal" release
* Suggest python-bluez or bluez-utils and gnome-bluetooth for Bluetooth file
transfer support (new in 0.11.0)
-- Thomas Perl <thp@perli.net> Mon, 25 Feb 2008 14:58:34 +0100
gpodder (0.10.4-1) unstable; urgency=low
* The "Faster Pussycats Kill" release
* Add a last-minute bugfix for the .desktop file (wrong language)
-- Thomas Perl <thp@perli.net> Tue, 22 Jan 2008 09:44:38 +0100
gpodder (0.10.3-1) unstable; urgency=low
* The "A Stop at Willoughby" release
* Patch from 0.10.2-2 is now included upstream
* Category in gpodder.menu is now "Applications/Network/Web News"
-- Thomas Perl <thp@perli.net> Thu, 13 Dec 2007 09:38:03 +0100
gpodder (0.10.2-2) unstable; urgency=low
* Fix bug that prevents the channel list from being saved when no
channels.opml file exists in gPodder's config directory (first run)
-- Thomas Perl <thp@perli.net> Sat, 01 Dec 2007 15:05:39 +0100
gpodder (0.10.2-1) unstable; urgency=low
* The "Ein schweineschnauzen Sandwich, bitte!" release
* Check for free disk space before saving channel list (Closes: #452490)
-- Thomas Perl <thp@perli.net> Mon, 26 Nov 2007 18:52:14 +0100
gpodder (0.10.1-1) unstable; urgency=low
* The "Nukular, das Wort heißt Nukular" release
-- Thomas Perl <thp@perli.net> Mon, 29 Oct 2007 13:18:08 +0100
gpodder (0.10.0-3) unstable; urgency=low
* Only support Python versions >= 2.4
-- Thomas Perl <thp@perli.net> Fri, 05 Oct 2007 09:37:04 +0200
gpodder (0.10.0-2) unstable; urgency=low
* Set XS-Python-Version to "all" (Closes: #445278)
* Update copyright file to reflect GPLv3 change
-- Thomas Perl <thp@perli.net> Fri, 21 Sep 2007 02:16:59 +0200
gpodder (0.10.0-1) unstable; urgency=low
* The "Hier spricht Frank Drebin" release
* New dependency: python-feedparser
* Removed dependencies: wget, python-xml
* Support for Atom feeds through feedparser (Closes: #430844)
* Deleting not-downloaded episodes enabled (Closes: 441285)
-- Thomas Perl <thp@perli.net> Fri, 21 Sep 2007 02:13:35 +0200
gpodder (0.9.5-2) unstable; urgency=low
* Fix problem with invalid file sizes in RSS feeds (Closes: #441284)
-- Thomas Perl <thp@perli.net> Sat, 08 Sep 2007 17:09:48 +0200
gpodder (0.9.5-1) unstable; urgency=low
* The "we can do funky release titles, too" release
* Change rules file to keep gui.py.orig
* Remove Recommends on python-id3 and mplayer
-- Thomas Perl <thp@perli.net> Sun, 26 Aug 2007 20:23:30 +0200
gpodder (0.9.4-1) unstable; urgency=low
* New upstream release (Closes: #432805, #433029)
* Added gpodder.menu file
* Remove locally-changed files (included upstream)
-- Thomas Perl <thp@perli.net> Sat, 21 Jul 2007 13:53:55 +0200
gpodder (0.9.3-2) unstable; urgency=low
* The "oh so many small bugs" release
* Workaround buggy RSS feeds with no titles, thanks to Holger Leskien
(Closes: #430843)
* Applied patch from Mykola Nikishov to fix a wget bug (Closes: #431446)
-- Thomas Perl <thp@perli.net> Mon, 25 Jun 2007 23:16:12 +0200
gpodder (0.9.3-1) unstable; urgency=low
* New upstream release
* Recommend mplayer, python-id3 and python-gpod and
suggest python-eyed3 and python-pymad as dependencies
-- Thomas Perl <thp@perli.net> Mon, 25 Jun 2007 23:14:00 +0200
gpodder (0.9.2-1) unstable; urgency=low
* New upstream release
-- Thomas Perl <thp@perli.net> Wed, 23 May 2007 15:05:21 +0200
gpodder (0.9.1-3) unstable; urgency=low
* Fixed FSF address from old GPL/LGPL license stanzas, metioned the
location of the full licenses on Debian systems for GPL/LGPL
-- Thomas Perl <thp@perli.net> Sun, 22 Apr 2007 23:33:12 +0200
gpodder (0.9.1-2) unstable; urgency=low
* Updated copyright file to state copyright of tepache and SimpleGladeApp
-- Thomas Perl <thp@perli.net> Sun, 22 Apr 2007 23:32:03 +0200
gpodder (0.9.1-1) unstable; urgency=low
* New upstream release
* Added postinst script (run gtk-update-icon-cache)
-- Thomas Perl <thp@perli.net> Thu, 05 Apr 2007 09:54:54 +0200
gpodder (0.9.0+svn200703221-1) unstable; urgency=low
* New upstream release (Closes: #415059)
-- Thomas Perl <thp@perli.net> Thu, 22 Mar 2007 13:16:46 +0100
gpodder (0.9.0+svn200703141-1) unstable; urgency=low
* New upstream release
-- Thomas Perl <thp@perli.net> Wed, 14 Mar 2007 20:59:20 +0100
gpodder (0.9.0+svn200703101-2) unstable; urgency=low
* Removed some unneeded dh_* commands from rules file
-- Thomas Perl <thp@perli.net> Mon, 12 Mar 2007 21:58:55 +0100
gpodder (0.9.0+svn200703101-1) unstable; urgency=low
* New upstream release
-- Thomas Perl <thp@perli.net> Sat, 10 Mar 2007 18:42:19 +0100
gpodder (0.9.0+svn200703081-1) unstable; urgency=low
* New upstream release
-- Thomas Perl <thp@perli.net> Fri, 9 Mar 2007 18:36:35 +0100
gpodder (0.9.0+svn200703072-1) unstable; urgency=low
* New upstream release
-- Thomas Perl <thp@perli.net> Wed, 7 Mar 2007 16:36:28 +0100
gpodder (0.9.0+svn20070307-1) unstable; urgency=low
* New upstream release
-- Thomas Perl <thp@perli.net> Wed, 7 Mar 2007 11:48:58 +0100
gpodder (0.9.0-3) unstable; urgency=low
* Add build-dependency on imagemagick (for convert call)
* Fix broken artwork installation in setup.py
-- Thomas Perl <thp@perli.net> Wed, 7 Mar 2007 11:09:54 +0100
gpodder (0.9.0-2) unstable; urgency=low
* Package for python-support and mentors.debian.net
* Cleanup of package structure, removed unneeded stuff from rules
-- Thomas Perl <thp@perli.net> Wed, 7 Mar 2007 00:13:40 +0100
gpodder (0.9.0-1etch0) unstable; urgency=low
* New upstream release
-- Thomas Perl <thp@perli.net> Tue, 6 Mar 2007 20:58:22 +0100
gpodder (0.8.9-1etch0) unstable; urgency=low
* New upstream release
-- Thomas Perl <thp@perli.net> Sat, 3 Feb 2007 12:04:19 +0100
gpodder (0.8.0-1sarge0) unstable; urgency=low
* New upstream release
-- Thomas Perl <thp@perli.net> Fri, 28 Jul 2006 14:58:26 +0200
gpodder (0.7.9-1sarge0) unstable; urgency=low
* New upstream release
-- Thomas Perl <thp@perli.net> Mon, 17 Jul 2006 17:36:33 +0200
gpodder (0.7-2) unstable; urgency=low
* Fixed problem with buggy RSS feeds (wrong "size")
-- Thomas Perl <thp@perli.net> Sat, 8 Apr 2006 19:40:20 +0200
gpodder (0.7-1) unstable; urgency=low
* Initial release
-- Thomas Perl <thp@perli.net> Sat, 8 Apr 2006 11:17:28 +0200

47
debian/control vendored
View File

@ -1,47 +0,0 @@
Source: gpodder-adaptive
Maintainer: Maryjane <maryjane@disroot.org>
XSBC-Original-Maintainer: Thomas Perl <m@thp.io>
Section: x11
Priority: optional
Standards-Version: 4.5.0
Build-Depends: debhelper-compat (= 13),
dh-python,
intltool,
python3,
python3-all,
python3-setuptools
Homepage: https://gpodder.org/
Vcs-Browser: https://salsa.debian.org/debian/gpodder
Vcs-Git: https://salsa.debian.org/debian/gpodder.git
Rules-Requires-Root: no
Package: gpodder-adaptive
Architecture: all
Depends: ${misc:Depends}, ${python3:Depends},
default-dbus-session-bus | dbus-session-bus,
gir1.2-gtk-3.0,
gir1.2-handy-1,
python3-gi,
python3-dbus,
python3-cairo,
python3-gi-cairo,
python3-mygpoclient,
python3-podcastparser,
python3-requests (>= 2.24)
Recommends:
gir1.2-ayatanaappindicator3-0.1,
libgpod4,
normalize-audio,
python3-eyed3,
python3-html5lib,
python3-simplejson
Suggests: mplayer, gnome-bluetooth, yt-dlp (>= 2023.02.17)
Conflicts: gpodder
Description: podcast client and feed aggregator
gPodder is a podcast receiver/catcher. You can subscribe to feeds
("podcasts") and automatically download new audio and video content.
Downloaded content can be played on your computer or synchronized to
iPods, MTP-based players, filesystem-based MP3 players and Bluetooth
enabled mobile phones. YouTube video feeds are also supported.
.
This package provides the "gpodder" GUI and the "gpo" CLI utility.

33
debian/copyright vendored
View File

@ -1,33 +0,0 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: gPodder
Upstream-Contact: Thomas Perl <thp[at]thpinfo.com>
Source: https://gpodder.org/
Comment: The share/metainfo/org.gpodder.gpodder.appdata.xml
indicates that the AppStream license is CC0-1.0, but that the
project license is GPL-3+.
Files: *
Copyright: 2005-2020, Thomas Perl and the gPodder Team
License: GPL-3+
Files: debian/*
Copyright: 2006-2014, Thomas Perl,
2010-2020, tony mancill <tmancill@debian.org>
License: GPL-3+
License: GPL-3+
gPodder 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.
.
gPodder 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/>
.
On Debian GNU/Linux systems, the complete text of the GNU General
Public License can be found in `/usr/share/common-licenses/GPL-3'.

1
debian/gpodder.docs vendored
View File

@ -1 +0,0 @@
README.md

View File

@ -1,18 +0,0 @@
Description: Modify the default value for check_on_startup to false.
This prevents an privacy/information disclosure unless the user
explicitly opts-in for the update check.
Forwarded: not-needed
Origin: vendor
Author: tony mancill <tmancill@debian.org>
--- a/src/gpodder/config.py
+++ b/src/gpodder/config.py
@@ -93,7 +93,7 @@
# Software updates from gpodder.org
'software_update': {
- 'check_on_startup': True, # check for updates on start
+ 'check_on_startup': False, # check for updates on start
'last_check': 0, # unix timestamp of last update check
'interval': 5, # interval (in days) to check for updates
},

View File

@ -1,2 +0,0 @@
disable_update_check_on_startup_default.patch
switch-appindicator-extension-to-AyatanaAppIndicator-and-python3.patch

View File

@ -1,11 +0,0 @@
--- a/share/gpodder/extensions/ubuntu_appindicator.py
+++ b/share/gpodder/extensions/ubuntu_appindicator.py
@@ -12,7 +12,7 @@
_ = gpodder.gettext
-__title__ = _('Ubuntu App Indicator')
+__title__ = _('Ayatana App Indicator')
__description__ = _('Show a status indicator in the top bar.')
__authors__ = 'Thomas Perl <thp@gpodder.org>'
__category__ = 'desktop-integration'

20
debian/rules vendored
View File

@ -1,20 +0,0 @@
#!/usr/bin/make -f
CHANGELOG = ChangeLog
DOCS = README.md
PYTHON = /usr/bin/python3
PREFIX = /usr
export DH_VERBOSE = 1
%:
dh $@ --with python3 --buildsystem=pybuild
override_dh_auto_test:
# skip tests
override_dh_auto_install:
PREFIX=$(PREFIX) make messages
PREFIX=$(PREFIX) make share/dbus-1/services/org.gpodder.service
DESTDIR=$(CURDIR)/debian/$(DEB_SOURCE) PREFIX=$(PREFIX) make install
dh_auto_install

View File

@ -1 +0,0 @@
3.0 (quilt)

3
debian/watch vendored
View File

@ -1,3 +0,0 @@
version=4
opts=repack,compression=xz,filenamemangle=s/.+\/v?(\d\S+)\.tar\.gz/gpodder-$1\.tar\.gz/ \
https://github.com/gpodder/gpodder/tags .*/v?(\d\S+)\.tar\.gz

Binary file not shown.

View File

@ -0,0 +1 @@
06f5a6cb73f248a78489ac8300759240f2b360ff

Binary file not shown.

View File

@ -0,0 +1 @@
06f5a6cb73f248a78489ac8300759240f2b360ff

Binary file not shown.

View File

@ -0,0 +1 @@
06f5a6cb73f248a78489ac8300759240f2b360ff

175
makefile
View File

@ -1,175 +0,0 @@
#
# gPodder - A media aggregator and podcast client
# Copyright (c) 2005-2018 The gPodder Team
#
# gPodder 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.
#
# gPodder 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/>.
#
#
##########################################################################
BINFILE = bin/gpodder
MANPAGES = share/man/man1/gpodder.1 share/man/man1/gpo.1
GPODDER_SERVICE_FILE=share/dbus-1/services/org.gpodder.service
GPODDER_SERVICE_FILE_IN=$(addsuffix .in,$(GPODDER_SERVICE_FILE))
DESKTOP_FILES_IN=$(wildcard share/applications/*.desktop.in)
DESKTOP_FILES_IN_H=$(patsubst %.desktop.in,%.desktop.in.h,$(DESKTOP_FILES_IN))
DESKTOP_FILES=$(patsubst %.desktop.in,%.desktop,$(DESKTOP_FILES_IN))
MESSAGES = po/messages.pot
POFILES = $(wildcard po/*.po)
LOCALEDIR = share/locale
MOFILES = $(patsubst po/%.po,$(LOCALEDIR)/%/LC_MESSAGES/gpodder.mo, $(POFILES))
UIFILES=$(wildcard share/gpodder/ui/gtk/*.ui \
share/gpodder/ui/adaptive/*.ui)
UIFILES_H=$(subst .ui,.ui.h,$(UIFILES))
GETTEXT_SOURCE=$(wildcard src/gpodder/*.py \
src/gpodder/gtkui/*.py \
src/gpodder/gtkui/interface/*.py \
src/gpodder/gtkui/desktop/*.py \
src/gpodder/plugins/*.py \
share/gpodder/extensions/*.py)
GETTEXT_SOURCE += $(UIFILES_H)
GETTEXT_SOURCE += $(wildcard bin/*[^~])
GETTEXT_SOURCE += $(DESKTOP_FILES_IN_H)
DESTDIR ?= /
PREFIX ?= /usr
PYTHON ?= python3
HELP2MAN ?= help2man
PYTEST ?= $(shell which pytest || which pytest-3)
##########################################################################
help:
@cat tools/make-help.txt
##########################################################################
unittest:
LC_ALL=C PYTHONPATH=src/ $(PYTEST) --ignore=tests --ignore=src/gpodder/utilwin32ctypes.py --doctest-modules src/gpodder/util.py src/gpodder/jsonconfig.py
LC_ALL=C PYTHONPATH=src/ $(PYTEST) tests --ignore=src/gpodder/utilwin32ctypes.py --ignore=src/mygpoclient --cov=gpodder
ISORTOPTS := -c share src/gpodder tools bin/* *.py
lint:
pycodestyle --version
pycodestyle share src/gpodder tools bin/* *.py
isort --version
isort -q $(ISORTOPTS) || isort --df $(ISORTOPTS)
codespell --quiet-level 3 --skip "./.git,*.po,./share/applications/gpodder.desktop"
release: distclean
$(PYTHON) setup.py sdist
releasetest: unittest $(DESKTOP_FILES) $(POFILES)
for f in $(DESKTOP_FILES); do desktop-file-validate $$f || exit 1; done
for f in $(POFILES); do msgfmt --check $$f || exit 1; done
$(GPODDER_SERVICE_FILE): $(GPODDER_SERVICE_FILE_IN)
sed -e 's#__PREFIX__#$(PREFIX)#' $< >$@
%.desktop: %.desktop.in $(POFILES)
sed -e 's#__PREFIX__#$(PREFIX)#' $< >$@.tmp
intltool-merge -d -u po $@.tmp $@
rm -f $@.tmp
%.desktop.in.h: %.desktop.in
intltool-extract --quiet --type=gettext/ini $<
install: messages $(GPODDER_SERVICE_FILE) $(DESKTOP_FILES)
$(PYTHON) setup.py install --root=$(DESTDIR) --prefix=$(PREFIX) --optimize=1
install-win: messages $(GPODDER_SERVICE_FILE) $(DESKTOP_FILES)
$(PYTHON) setup.py install
##########################################################################
ifdef VERSION
revbump:
LC_ALL=C sed -i "s/\(__version__\s*=\s*'\).*'/\1$(VERSION)'/" src/gpodder/__init__.py
LC_ALL=C sed -i "s/\(__date__\s*=\s*'\).*'/\1$(shell date "+%Y-%m-%d")'/" src/gpodder/__init__.py
LC_ALL=C sed -i "s/\(__copyright__\s*=.*2005-\)[0-9]*\(.*\)/\1$(shell date "+%Y")\2/" src/gpodder/__init__.py
$(MAKE) messages manpages
else
revbump:
@echo "Usage: make revbump VERSION=x.y.z"
endif
##########################################################################
manpages: $(MANPAGES)
share/man/man1/gpodder.1: src/gpodder/__init__.py $(BINFILE)
LC_ALL=C $(HELP2MAN) --name="$(shell $(PYTHON) setup.py --description)" -N $(BINFILE) >$@
share/man/man1/gpo.1: src/gpodder/__init__.py
sed -i 's/^\.TH.*/.TH GPO "1" "$(shell LANG=en date "+%B %Y")" "gpodder $(shell $(PYTHON) setup.py --version)" "User Commands"/' $@
##########################################################################
messages: $(MOFILES)
%.po: $(MESSAGES)
msgmerge --previous --silent $@ $< --output-file=$@
msgattrib --set-obsolete --ignore-file=$< -o $@ $@
msgattrib --no-obsolete -o $@ $@
$(LOCALEDIR)/%/LC_MESSAGES/gpodder.mo: po/%.po
@mkdir -p $(@D)
msgfmt $< -o $@
%.ui.h: %.ui
intltool-extract --quiet --type=gettext/glade $<
$(MESSAGES): $(GETTEXT_SOURCE)
xgettext --from-code=utf-8 -LPython -k_:1 -kN_:1 -kN_:1,2 -kn_:1,2 -o $(MESSAGES) $^
messages-force:
xgettext --from-code=utf-8 -LPython -k_:1 -kN_:1 -kN_:1,2 -kn_:1,2 -o $(MESSAGES) $(GETTEXT_SOURCE)
##########################################################################
# This only works in a Git working commit, and assumes that the local Git
# HEAD has already been pushed to the main repository. It's mainly useful
# for the gPodder maintainer to quickly generate a commit link that can be
# posted online in bug trackers and mailing lists.
headlink:
@echo http://gpodder.org/commit/`git show-ref HEAD | head -c8`
##########################################################################
clean:
$(PYTHON) setup.py clean
find src/ '(' -name '*.pyc' -o -name '*.pyo' ')' -exec rm '{}' +
find src/ -type d -name '__pycache__' -exec rm -r '{}' +
find share/gpodder/ui/ -name '*.ui.h' -exec rm '{}' +
rm -f MANIFEST .coverage messages.mo po/*.mo
rm -f $(GPODDER_SERVICE_FILE)
rm -f $(DESKTOP_FILES) $(DESKTOP_FILES_IN_H)
rm -rf build $(LOCALEDIR)
distclean: clean
rm -rf dist
##########################################################################
.PHONY: help unittest release releasetest install manpages clean distclean messages headlink lint revbump
##########################################################################

2759
po/ca.po

File diff suppressed because it is too large Load Diff

3085
po/cs.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

2972
po/da.po

File diff suppressed because it is too large Load Diff

2895
po/de.po

File diff suppressed because it is too large Load Diff

2956
po/el.po

File diff suppressed because it is too large Load Diff

2981
po/es.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

2947
po/eu.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

2979
po/fi.po

File diff suppressed because it is too large Load Diff

2869
po/fr.po

File diff suppressed because it is too large Load Diff

2949
po/gl.po

File diff suppressed because it is too large Load Diff

2943
po/he.po

File diff suppressed because it is too large Load Diff

2916
po/hu.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

2855
po/it.po

File diff suppressed because it is too large Load Diff

2920
po/kk.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

2888
po/nb.po

File diff suppressed because it is too large Load Diff

2843
po/nl.po

File diff suppressed because it is too large Load Diff

2852
po/nn.po

File diff suppressed because it is too large Load Diff

2952
po/pl.po

File diff suppressed because it is too large Load Diff

2955
po/pt.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

3006
po/ro.po

File diff suppressed because it is too large Load Diff

2917
po/ru.po

File diff suppressed because it is too large Load Diff

2865
po/sk.po

File diff suppressed because it is too large Load Diff

2981
po/sv.po

File diff suppressed because it is too large Load Diff

2861
po/tr.po

File diff suppressed because it is too large Load Diff

3001
po/uk.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +0,0 @@
[pycodestyle]
count=1
select = W1, W2, W3, E11, E121, E122, E123, E124, E125, E127, E129, E13, E2, E3, E401, E5, E703, E711, E712, E713, E721, E731, E74, E9
# https://pycodestyle.readthedocs.io/en/latest/intro.html#error-codes
max-line-length = 142
[isort]
known_third_party=dbus,gi,mutagen,cairo,requests,github3,jinja2,magic,youtube_dl,podcastparser,mygpoclient
known_first_party=gpodder,soco
[flake8]
max-line-length = 142
ignore = E126, E128, W503

214
setup.py
View File

@ -1,214 +0,0 @@
#!/usr/bin/env python3
#
# gPodder - A media aggregator and podcast client
# Copyright (c) 2005-2018 The gPodder Team
#
# gPodder 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.
#
# gPodder 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/>.
#
import os
import re
import sys
from distutils.core import setup
installing = ('install' in sys.argv and '--help' not in sys.argv)
# distutils depends on setup.py being executed from the same dir.
# Most of our custom commands work either way, but this makes
# it work in all cases.
os.chdir(os.path.dirname(os.path.realpath(__file__)))
# Read the metadata from gPodder's __init__ module (doesn't need importing)
main_module = open('src/gpodder/__init__.py', 'r', encoding='utf-8').read()
metadata = dict(re.findall("__([a-z_]+)__\\s*=\\s*'([^']+)'", main_module))
author, email = re.match(r'^(.*) <(.*)>$', metadata['author']).groups()
class MissingFile(BaseException):
pass
def info(message, item=None):
print('=>', message, item if item is not None else '')
def find_data_files(uis, scripts):
# Support for installing only a subset of translations
linguas = os.environ.get('LINGUAS', None)
if linguas is not None:
linguas = linguas.split()
info('Selected languages (from $LINGUAS):', linguas)
for dirpath, dirnames, filenames in os.walk('share'):
if not filenames:
continue
# Skip data folders if we don't want the corresponding UI
share_gpodder_ui = os.path.join('share', 'gpodder', 'ui')
if uis is not None and dirpath.startswith(share_gpodder_ui):
dirparts = dirpath.split(os.sep)
if not any(part in uis for part in dirparts):
info('Skipping folder:', dirpath)
continue
# Skip translations if $LINGUAS is set
share_locale = os.path.join('share', 'locale')
if linguas is not None and dirpath.startswith(share_locale):
_, _, language, _ = dirpath.split(os.sep, 3)
if language not in linguas:
info('Skipping translation:', language)
continue
# Skip desktop stuff if we don't have any UIs requiring it
skip_folder = False
uis_requiring_freedesktop = ('gtk',)
freedesktop_folders = ('applications', 'dbus-1', 'icons', 'metainfo')
for folder in freedesktop_folders:
share_folder = os.path.join('share', folder)
if dirpath.startswith(share_folder) and uis is not None:
if not any(ui in uis_requiring_freedesktop for ui in uis):
info('Skipping freedesktop.org folder:', dirpath)
skip_folder = True
break
if skip_folder:
continue
# Skip manpages if their scripts are not going to be installed
share_man = os.path.join('share', 'man')
if dirpath.startswith(share_man):
def have_script(filename):
if not filename.endswith('.1'):
return True
basename, _ = os.path.splitext(filename)
result = any(os.path.basename(s) == basename for s in scripts)
if not result:
info('Skipping manpage without script:', filename)
return result
filenames = list(filter(have_script, filenames))
def convert_filename(filename):
filename = os.path.join(dirpath, filename)
# Skip header files generated by "make messages"
if filename.endswith('.h'):
return None
# Skip .in files, but check if their target exist
if filename.endswith('.in'):
filename = filename[:-3]
if installing and not os.path.exists(filename):
raise MissingFile(filename)
return None
return filename
filenames = [_f for _f in map(convert_filename, filenames) if _f]
if filenames:
# Some distros/ports install manpages into $PREFIX/man instead
# of $PREFIX/share/man (e.g. FreeBSD). To allow this, we strip
# the "share/" part if the variable GPODDER_MANPATH_NO_SHARE is
# set to any value in the environment.
if dirpath.startswith(share_man):
if 'GPODDER_MANPATH_NO_SHARE' in os.environ:
dirpath = dirpath.replace(share_man, 'man')
yield (dirpath, filenames)
def find_packages(uis):
src_gpodder = os.path.join('src', 'gpodder')
for dirpath, dirnames, filenames in os.walk(src_gpodder):
if '__init__.py' not in filenames:
continue
skip = False
dirparts = dirpath.split(os.sep)
dirparts.pop(0)
package = '.'.join(dirparts)
# Extract all parts of the package name ending in "ui"
ui_parts = [p for p in dirparts if p.endswith('ui')]
if uis is not None and ui_parts:
# Strip the trailing "ui", e.g. "gtkui" -> "gtk"
folder_uis = [p[:-2] for p in ui_parts]
for folder_ui in folder_uis:
if folder_ui not in uis:
info('Skipping package:', package)
skip = True
break
if not skip:
yield package
def find_scripts(uis):
# Functions for scripts to check if they should be installed
file_checks = {
'gpo': lambda uis: 'cli' in uis,
'gpodder': lambda uis: any(ui in uis for ui in ('gtk',)),
}
for dirpath, dirnames, filenames in os.walk('bin'):
for filename in filenames:
# If we have a set of uis, check if we can skip this file
if uis is not None and filename in file_checks:
if not file_checks[filename](uis):
info('Skipping script:', filename)
continue
yield os.path.join(dirpath, filename)
# Recognized UIs: cli, gtk (default: install all UIs)
uis = os.environ.get('GPODDER_INSTALL_UIS', None)
if uis is not None:
uis = uis.split()
info('Selected UIs (from $GPODDER_INSTALL_UIS):', uis)
try:
packages = list(sorted(find_packages(uis)))
scripts = list(sorted(find_scripts(uis)))
data_files = list(sorted(find_data_files(uis, scripts)))
except MissingFile as mf:
print("""
Missing file: %s
If you want to install, use "make install" instead of using
setup.py directly. See the README file for more information.
""" % mf, file=sys.stderr)
sys.exit(1)
setup(
name='gpodder-adaptive',
version=metadata['version'],
description=metadata['tagline'],
license=metadata['license'],
url=metadata['url'],
author=author,
author_email=email,
package_dir={'': 'src'},
packages=packages,
scripts=scripts,
data_files=data_files,
)

View File

@ -1,8 +0,0 @@
[Desktop Entry]
_Name=gPodder-adaptive (subscribe to feed)
Exec=__PREFIX__/bin/gpodder -s %u
Icon=gpodder-adaptive
Terminal=false
NoDisplay=true
Type=Application
MimeType=x-scheme-handler/gpodder;x-scheme-handler/feed;x-scheme-handler/podcast;x-scheme-handler/pcast;

View File

@ -1,12 +0,0 @@
[Desktop Entry]
_Name=gPodder-adaptive
_X-GNOME-FullName=gPodder Podcast Client with adaptive interface
_GenericName=Podcast Client
_Comment=Subscribe to audio and video content from the web
Exec=__PREFIX__/bin/gpodder
Icon=gpodder-adaptive
Terminal=false
Type=Application
Categories=AudioVideo;Audio;Network;FileTransfer;News;GTK;
StartupWMClass=gpodder
X-Purism-FormFactor=Workstation;Mobile;

View File

@ -1,3 +0,0 @@
[D-BUS Service]
Name=org.gpodder
Exec=__PREFIX__/bin/gpodder

View File

@ -1,38 +0,0 @@
#!/usr/bin/env python3
# Example script that can be used as post-play extension in media players
#
# Set the configuration options "audio_played_dbus" and "video_played_dbus"
# to True to let gPodder leave the played status untouched when playing
# files in the media player. After playback has finished, call this script
# with the filename of the played episodes as single argument. The episode
# will be marked as played inside gPodder.
#
# Usage: gpodder_mark_played.py /path/to/episode.mp3
# (the gPodder GUI has to be running)
#
# Thomas Perl <thp@gpodder.org>; 2009-09-09
import os
import sys
import dbus
import gpodder
if len(sys.argv) != 2:
print("""
Usage: %s /path/to/episode.mp3
""" % (sys.argv[0],), file=sys.stderr)
sys.exit(1)
filename = os.path.abspath(sys.argv[1])
session_bus = dbus.SessionBus()
proxy = session_bus.get_object(gpodder.dbus_bus_name,
gpodder.dbus_gui_object_path)
interface = dbus.Interface(proxy, gpodder.dbus_interface)
if not interface.mark_episode_played(filename):
print('Warning: Could not mark episode as played.', file=sys.stderr)
sys.exit(2)

View File

@ -1,74 +0,0 @@
# Use a logger for debug output - this will be managed by gPodder.
import logging
logger = logging.getLogger(__name__)
# Provide some metadata that will be displayed in the gPodder GUI.
__title__ = 'Hello World Extension'
__description__ = 'Explain in one sentence what this extension does.'
__only_for__ = 'gtk, cli'
__authors__ = 'Thomas Perl <m@thp.io>'
class gPodderExtension:
# The extension will be instantiated the first time it's used.
# You can do some sanity checks here and raise an Exception if
# you want to prevent the extension from being loaded.
def __init__(self, container):
self.container = container
# This function will be called when the extension is enabled or
# loaded. This is when you want to create helper objects or hook
# into various parts of gPodder.
def on_load(self):
logger.info('Extension is being loaded.')
print('=' * 40)
print('container:', self.container)
print('container.manager:', self.container.manager)
print('container.config:', self.container.config)
print('container.manager.core:', self.container.manager.core)
print('container.manager.core.db:', self.container.manager.core.db)
print('container.manager.core.config:', self.container.manager.core.config)
print('container.manager.core.model:', self.container.manager.core.model)
print('=' * 40)
# This function will be called when the extension is disabled or
# when gPodder shuts down. You can use this to destroy/delete any
# objects that you created in on_load().
def on_unload(self):
logger.info('Extension is being unloaded.')
def on_ui_object_available(self, name, ui_object):
"""
Called by gPodder when ui is ready.
"""
if name == 'gpodder-gtk':
self.gpodder = ui_object
def on_create_menu(self):
return [("Say Hello", self.say_hello_cb)]
def say_hello_cb(self):
self.gpodder.notification("Hello Extension", "Message", widget=self.gpodder.main_window)
# Concurrency Warning: use gpodder.util.Popen() instead of subprocess.Popen()
#
# When using subprocess.Popen() to spawn a long-lived external command,
# such as ffmpeg, be sure to include the "close_fds=True" argument.
#
# https://docs.python.org/3/library/subprocess.html#subprocess.Popen
#
# This is especially important for extensions responding to
# on_episode_downloaded(), which runs whenever a download finishes.
#
# Otherwise that process will inherit ALL file descriptors gPodder
# happens to have open at the moment (like other active downloads).
# Those files will remain 'in-use' until that process exits, a race
# condition which prevents gPodder from renaming or deleting them on Windows.
#
# Caveat: On Windows, you cannot set close_fds to true and also
# redirect the standard handles (stdin, stdout or stderr). To collect
# output/errors from long-lived external commands, it may be necessary
# to create a (temp) log file and read it afterward.

View File

@ -1,143 +0,0 @@
# -*- coding: utf-8 -*-
# Convertes m4a audio files to mp3
# This requires ffmpeg to be installed. Also works as a context
# menu item for already-downloaded files.
#
# (c) 2011-11-23 Bernd Schlapsi <brot@gmx.info>
# Released under the same license terms as gPodder itself.
import logging
import os
import subprocess
import gpodder
from gpodder import util
logger = logging.getLogger(__name__)
_ = gpodder.gettext
__title__ = _('Convert audio files')
__description__ = _('Transcode audio files to mp3/ogg')
__authors__ = 'Bernd Schlapsi <brot@gmx.info>, Thomas Perl <thp@gpodder.org>'
__doc__ = 'https://gpodder.github.io/docs/extensions/audioconverter.html'
__payment__ = 'https://flattr.com/submit/auto?user_id=BerndSch&url=http://wiki.gpodder.org/wiki/Extensions/AudioConverter'
__category__ = 'post-download'
DefaultConfig = {
'use_opus': False, # Set to True to convert to .opus
'use_ogg': False, # Set to True to convert to .ogg
'context_menu': True, # Show the conversion option in the context menu
}
class gPodderExtension:
MIME_TYPES = ('audio/x-m4a', 'audio/mp4', 'audio/mp4a-latm', 'audio/mpeg', 'audio/ogg', 'audio/opus')
EXT = ('.m4a', '.ogg', '.opus', '.mp3')
CMD = {'avconv': {'.mp3': ['-n', '-i', '%(old_file)s', '-q:a', '2', '-id3v2_version', '3', '-write_id3v1', '1', '%(new_file)s'],
'.ogg': ['-n', '-i', '%(old_file)s', '-q:a', '2', '%(new_file)s'],
'.opus': ['-n', '-i', '%(old_file)s', '-b:a', '64k', '%(new_file)s']
},
'ffmpeg': {'.mp3': ['-n', '-i', '%(old_file)s', '-q:a', '2', '-id3v2_version', '3', '-write_id3v1', '1', '%(new_file)s'],
'.ogg': ['-n', '-i', '%(old_file)s', '-q:a', '2', '%(new_file)s'],
'.opus': ['-n', '-i', '%(old_file)s', '-b:a', '64k', '%(new_file)s']
}
}
def __init__(self, container):
self.container = container
self.config = self.container.config
# Dependency checks
self.command = self.container.require_any_command(['avconv', 'ffmpeg'])
# extract command without extension (.exe on Windows) from command-string
self.command_without_ext = os.path.basename(os.path.splitext(self.command)[0])
def on_episode_downloaded(self, episode):
self._convert_episode(episode)
def _get_new_extension(self):
if self.config.use_ogg:
extension = '.ogg'
elif self.config.use_opus:
extension = '.opus'
else:
extension = '.mp3'
return extension
def _check_source(self, episode):
if episode.extension() == self._get_new_extension():
return False
if episode.mime_type in self.MIME_TYPES:
return True
# Also check file extension (bug 1770)
if episode.extension() in self.EXT:
return True
return False
def on_episodes_context_menu(self, episodes):
if not self.config.context_menu:
return None
if not all(e.was_downloaded(and_exists=True) for e in episodes):
return None
if not any(self._check_source(episode) for episode in episodes):
return None
menu_item = _('Convert to %(format)s') % {'format': self._target_format()}
return [(menu_item, self._convert_episodes)]
def _target_format(self):
if self.config.use_ogg:
target_format = 'OGG'
elif self.config.use_opus:
target_format = 'OPUS'
else:
target_format = 'MP3'
return target_format
def _convert_episode(self, episode):
if not self._check_source(episode):
return
new_extension = self._get_new_extension()
old_filename = episode.local_filename(create=False)
filename, old_extension = os.path.splitext(old_filename)
new_filename = filename + new_extension
cmd_param = self.CMD[self.command_without_ext][new_extension]
cmd = [self.command] + \
[param % {'old_file': old_filename, 'new_file': new_filename}
for param in cmd_param]
if gpodder.ui.win32:
ffmpeg = util.Popen(cmd)
ffmpeg.wait()
stdout, stderr = ("<unavailable>",) * 2
else:
ffmpeg = util.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = ffmpeg.communicate()
if ffmpeg.returncode == 0:
util.rename_episode_file(episode, new_filename)
os.remove(old_filename)
logger.info('Converted audio file to %(format)s.' % {'format': new_extension})
gpodder.user_extensions.on_notification_show(_('File converted'), episode.title)
else:
logger.warning('Error converting audio file: %s / %s', stdout, stderr)
gpodder.user_extensions.on_notification_show(_('Conversion failed'), episode.title)
def _convert_episodes(self, episodes):
# not running in background because there is no feedback to the user
# which one is being converted and nothing prevents from clicking convert twice.
for episode in episodes:
self._convert_episode(episode)

View File

@ -1,76 +0,0 @@
# -*- coding: utf-8 -*-
#
# gPodder extension for running a command on successful episode download
#
import datetime
import logging
import os
import gpodder
from gpodder import util
logger = logging.getLogger(__name__)
_ = gpodder.gettext
__title__ = _('Run a Command on Download')
__description__ = _('Run a predefined external command upon download completion.')
__authors__ = 'Eric Le Lay <elelay@macports.org>'
__doc__ = 'https://gpodder.github.io/docs/extensions/commandondownload.html'
__category__ = 'post-download'
__only_for__ = 'gtk, cli'
DefaultConfig = {
'command': "zenity --info --width=600 --text=\"file=$filename "
"podcast=$podcast title=$title published=$published "
"section=$section playlist_title=$playlist_title\""
}
class gPodderExtension:
def __init__(self, container):
self.container = container
def on_episode_downloaded(self, episode):
cmd_template = self.container.config.command
info = self.read_episode_info(episode)
if info is None:
return
self.run_command(cmd_template, info)
def read_episode_info(self, episode):
filename = episode.local_filename(create=False, check_only=True)
if filename is None:
logger.warning("%s: missing episode filename", __title__)
return None
info = {
'filename': filename,
'playlist_title': None,
'podcast': None,
'published': None,
'section': None,
'title': None,
}
info['podcast'] = episode.channel.title
info['title'] = episode.title
info['section'] = episode.channel.section
published = datetime.datetime.fromtimestamp(episode.published)
info['published'] = published.strftime('%Y-%m-%d %H:%M')
info['playlist_title'] = episode.playlist_title()
return info
def run_command(self, command, info):
env = os.environ.copy()
env.update(info)
proc = util.Popen(command, shell=True, env=env, close_fds=True)
proc.wait()
if proc.returncode == 0:
logger.info("%s succeeded", command)
else:
logger.warning("%s run with exit code %i", command, proc.returncode)

View File

@ -1,103 +0,0 @@
# -*- coding: utf-8 -*-
# Concatenate multiple videos to a single file using ffmpeg
# 2014-05-03 Thomas Perl <thp.io/about>
# Released under the same license terms as gPodder itself.
import logging
import os
from gi.repository import Gtk
import gpodder
from gpodder import util
from gpodder.gtkui.interface.progress import ProgressIndicator
logger = logging.getLogger(__name__)
_ = gpodder.gettext
__title__ = _('Concatenate videos')
__description__ = _('Add a context menu item for concatenating multiple videos')
__authors__ = 'Thomas Perl <thp@gpodder.org>'
__category__ = 'interface'
__only_for__ = 'gtk'
class gPodderExtension:
def __init__(self, container):
self.container = container
self.gpodder = None
self.have_ffmpeg = (util.find_command('ffmpeg') is not None)
def on_ui_object_available(self, name, ui_object):
if name == 'gpodder-gtk':
self.gpodder = ui_object
def _get_save_filename(self):
dlg = Gtk.FileChooserDialog(title=_('Save video'),
parent=self.gpodder.get_dialog_parent(),
action=Gtk.FileChooserAction.SAVE)
dlg.add_button(_('_Cancel'), Gtk.ResponseType.CANCEL)
dlg.add_button(_('_Save'), Gtk.ResponseType.OK)
if dlg.run() == Gtk.ResponseType.OK:
filename = dlg.get_filename()
dlg.destroy()
return filename
dlg.destroy()
def _concatenate_videos(self, episodes):
episodes = self._get_sorted_episode_list(episodes)
# TODO: Show file list dialog for reordering
out_filename = self._get_save_filename()
if out_filename is None:
return
list_filename = os.path.join(os.path.dirname(out_filename),
'.' + os.path.splitext(os.path.basename(out_filename))[0] + '.txt')
with open(list_filename, 'w') as fp:
fp.write('\n'.join("file '%s'\n" % episode.local_filename(create=False)
for episode in episodes))
indicator = ProgressIndicator(_('Concatenating video files'),
_('Writing %(filename)s') % {
'filename': os.path.basename(out_filename)},
False, self.gpodder.get_dialog_parent())
def convert():
ffmpeg = util.Popen(['ffmpeg', '-f', 'concat', '-nostdin', '-y',
'-i', list_filename, '-c', 'copy', out_filename],
close_fds=True)
result = ffmpeg.wait()
util.delete_file(list_filename)
indicator.on_finished()
util.idle_add(lambda: self.gpodder.show_message(
_('Videos successfully converted') if result == 0 else
_('Error converting videos'),
_('Concatenation result'), important=True))
util.run_in_background(convert, True)
def _is_downloaded_video(self, episode):
return episode.file_exists() and episode.file_type() == 'video'
def _get_sorted_episode_list(self, episodes):
return sorted([e for e in episodes if self._is_downloaded_video(e)],
key=lambda e: e.published)
def on_episodes_context_menu(self, episodes):
if self.gpodder is None or not self.have_ffmpeg:
return None
episodes = self._get_sorted_episode_list(episodes)
if len(episodes) < 2:
return None
return [(_('Concatenate videos'), self._concatenate_videos)]

View File

@ -1,304 +0,0 @@
# -*- coding: utf-8 -*-
# Extension script to add a context menu item for enqueueing episodes in a player
# Requirements: gPodder 3.x (or "tres" branch newer than 2011-06-08)
# (c) 2011-06-08 Thomas Perl <thp.io/about>
# Released under the same license terms as gPodder itself.
import functools
import logging
import gpodder
from gpodder import util
logger = logging.getLogger(__name__)
_ = gpodder.gettext
__title__ = _('Enqueue/Resume in media players')
__description__ = _('Add a context menu item for enqueueing/resuming playback of episodes in installed media players')
__authors__ = 'Thomas Perl <thp@gpodder.org>, Bernd Schlapsi <brot@gmx.info>'
__doc__ = 'https://gpodder.github.io/docs/extensions/enqueueinmediaplayer.html'
__payment__ = 'https://flattr.com/submit/auto?user_id=BerndSch&url=http://wiki.gpodder.org/wiki/Extensions/EnqueueInMediaplayer'
__category__ = 'interface'
__only_for__ = 'gtk'
DefaultConfig = {
'enqueue_after_download': False, # Set to True to enqueue an episode right after downloading
'default_player': '', # Set to the player to be used for auto-enqueueing (otherwise pick first installed)
}
class Player(object):
def __init__(self, slug, application, command):
self.slug = slug
self.application = application
self.title = '/'.join((_('Enqueue in'), application))
self.command = command
self.gpodder = None
def is_installed(self):
raise NotImplemented('Must be implemented by subclass')
def open_files(self, filenames):
raise NotImplemented('Must be implemented by subclass')
def enqueue_episodes(self, episodes, config=None):
filenames = [episode.get_playback_url(config=config) for episode in episodes]
self.open_files(filenames)
for episode in episodes:
episode.playback_mark()
if self.gpodder is not None:
self.gpodder.update_episode_list_icons(selected=True)
class FreeDesktopPlayer(Player):
def is_installed(self):
return util.find_command(self.command[0]) is not None
def open_files(self, filenames):
util.Popen(self.command + filenames)
class Win32Player(Player):
def is_installed(self):
if not gpodder.ui.win32:
return False
from gpodder.gtkui.desktopfile import win32_read_registry_key
try:
self.command = win32_read_registry_key(self.command)
return True
except Exception as e:
logger.warning('Win32 player not found: %s (%s)', self.command, e)
return False
def open_files(self, filenames):
for cmd in util.format_desktop_command(self.command, filenames):
util.Popen(cmd, close_fds=True)
class MPRISResumer(FreeDesktopPlayer):
"""
resume episod playback at saved time
"""
OBJECT_PLAYER = '/org/mpris/MediaPlayer2'
OBJECT_DBUS = '/org/freedesktop/DBus'
INTERFACE_PLAYER = 'org.mpris.MediaPlayer2.Player'
INTERFACE_PROPS = 'org.freedesktop.DBus.Properties'
SIGNAL_PROP_CHANGE = 'PropertiesChanged'
NAME_DBUS = 'org.freedesktop.DBus'
def __init__(self, slug, application, command, bus_name):
super(MPRISResumer, self).__init__(slug, application, command)
self.title = '/'.join((_('Resume in'), application))
self.bus_name = bus_name
self.player = None
self.position_us = None
self.url = None
def is_installed(self):
if gpodder.ui.win32:
return False
return util.find_command(self.command[0]) is not None
def enqueue_episodes(self, episodes, config=None):
self.do_enqueue(episodes[0].get_playback_url(config=config),
episodes[0].current_position)
for episode in episodes:
episode.playback_mark()
if self.gpodder is not None:
self.gpodder.update_episode_list_icons(selected=True)
def init_dbus(self):
bus = gpodder.dbus_session_bus
if not bus.name_has_owner(self.bus_name):
logger.debug('MPRISResumer %s is not there...', self.bus_name)
return False
self.player = bus.get_object(self.bus_name, self.OBJECT_PLAYER)
self.signal_match = self.player.connect_to_signal(self.SIGNAL_PROP_CHANGE,
self.on_prop_change,
dbus_interface=self.INTERFACE_PROPS)
return True
def enqueue_when_ready(self, filename, pos):
def name_owner_changed(name, old_owner, new_owner):
logger.debug('name_owner_changed "%s" "%s" "%s"',
name, old_owner, new_owner)
if name == self.bus_name:
logger.debug('MPRISResumer player %s is there', name)
cancel.remove()
util.idle_add(lambda: self.do_enqueue(filename, pos))
bus = gpodder.dbus_session_bus
obj = bus.get_object(self.NAME_DBUS, self.OBJECT_DBUS)
cancel = obj.connect_to_signal('NameOwnerChanged', name_owner_changed, dbus_interface=self.NAME_DBUS)
def do_enqueue(self, filename, pos):
def on_reply():
logger.debug('MPRISResumer opened %s', self.url)
def on_error(exception):
logger.error('MPRISResumer error %s', repr(exception))
self.signal_match.remove()
if filename.startswith('/'):
try:
import pathlib
self.url = pathlib.Path(filename).as_uri()
except ImportError:
self.url = 'file://' + filename
self.position_us = pos * 1000 * 1000 # pos in microseconds
if self.init_dbus():
# async to not freeze the ui waiting for the application to answer
self.player.OpenUri(self.url,
dbus_interface=self.INTERFACE_PLAYER,
reply_handler=on_reply,
error_handler=on_error)
else:
self.enqueue_when_ready(filename, pos)
logger.debug('MPRISResumer launching player %s', self.application)
super(MPRISResumer, self).open_files([])
def on_prop_change(self, interface, props, invalidated_props):
def on_reply():
pass
def on_error(exception):
logger.error('MPRISResumer SetPosition error %s', repr(exception))
self.signal_match.remove()
metadata = props.get('Metadata', {})
url = metadata.get('xesam:url')
track_id = metadata.get('mpris:trackid')
if url is not None and track_id is not None:
if url == self.url:
logger.info('Enqueue %s setting track %s position=%d',
url, track_id, self.position_us)
self.player.SetPosition(str(track_id), self.position_us,
dbus_interface=self.INTERFACE_PLAYER,
reply_handler=on_reply,
error_handler=on_error)
else:
logger.debug('Changed but wrong url: %s, giving up', url)
self.signal_match.remove()
PLAYERS = [
# Amarok, http://amarok.kde.org/
FreeDesktopPlayer('amarok', 'Amarok', ['amarok', '--play', '--append']),
# VLC, http://videolan.org/
FreeDesktopPlayer('vlc', 'VLC', ['vlc', '--started-from-file', '--playlist-enqueue']),
# Totem, https://live.gnome.org/Totem
FreeDesktopPlayer('totem', 'Totem', ['totem', '--enqueue']),
# DeaDBeeF, http://deadbeef.sourceforge.net/
FreeDesktopPlayer('deadbeef', 'DeaDBeeF', ['deadbeef', '--queue']),
# gmusicbrowser, http://gmusicbrowser.org/
FreeDesktopPlayer('gmusicbrowser', 'gmusicbrowser', ['gmusicbrowser', '-enqueue']),
# Audacious, http://audacious-media-player.org/
FreeDesktopPlayer('audacious', 'Audacious', ['audacious', '--enqueue']),
# Clementine, http://www.clementine-player.org/
FreeDesktopPlayer('clementine', 'Clementine', ['clementine', '--append']),
# Strawberry, https://www.strawberrymusicplayer.org/
FreeDesktopPlayer('strawberry', 'Strawberry', ['strawberry', '--append']),
# Parole, http://docs.xfce.org/apps/parole/start
FreeDesktopPlayer('parole', 'Parole', ['parole', '-a']),
# Winamp 2.x, http://www.oldversion.com/windows/winamp/
Win32Player('winamp', 'Winamp', r'HKEY_CLASSES_ROOT\Winamp.File\shell\Enqueue\command'),
# VLC media player, http://videolan.org/vlc/
Win32Player('vlc', 'VLC', r'HKEY_CLASSES_ROOT\VLC.mp3\shell\AddToPlaylistVLC\command'),
# foobar2000, http://www.foobar2000.org/
Win32Player('foobar2000', 'foobar2000', r'HKEY_CLASSES_ROOT\foobar2000.MP3\shell\enqueue\command'),
]
RESUMERS = [
# doesn't play on my system, but the track is appended.
MPRISResumer('amarok', 'Amarok', ['amarok', '--play'], 'org.mpris.MediaPlayer2.amarok'),
MPRISResumer('vlc', 'VLC', ['vlc', '--started-from-file'], 'org.mpris.MediaPlayer2.vlc'),
# totem mpris2 plugin is broken for me: it raises AttributeError:
# File "/usr/lib/totem/plugins/dbus/dbusservice.py", line 329, in OpenUri
# self.totem.add_to_playlist_and_play (uri)
# MPRISResumer('totem', 'Totem', ['totem'], 'org.mpris.MediaPlayer2.totem'),
# with https://github.com/Serranya/deadbeef-mpris2-plugin
MPRISResumer('resume in deadbeef', 'DeaDBeeF', ['deadbeef'], 'org.mpris.MediaPlayer2.DeaDBeeF'),
# the gPodder Downloads directory must be in gmusicbrowser's library
MPRISResumer('resume in gmusicbrowser', 'gmusicbrowser', ['gmusicbrowser'], 'org.mpris.MediaPlayer2.gmusicbrowser'),
# Audacious doesn't implement MPRIS2.OpenUri
# MPRISResumer('audacious', 'resume in Audacious', ['audacious', '--enqueue'], 'org.mpris.MediaPlayer2.audacious'),
# beware: clementine never exits on my system (even when launched from cmdline)
# so the zombie clementine process will get all the bus messages and never answer
# resulting in freezes and timeouts!
MPRISResumer('clementine', 'Clementine', ['clementine'], 'org.mpris.MediaPlayer2.clementine'),
# just enable the plugin
MPRISResumer('parole', 'Parole', ['parole'], 'org.mpris.MediaPlayer2.parole'),
]
class gPodderExtension:
def __init__(self, container):
self.container = container
self.config = container.config
self.gpodder_config = self.container.manager.core.config
# Only display media players that can be found at extension load time
self.players = [player for player in PLAYERS if player.is_installed()]
self.resumers = [r for r in RESUMERS if r.is_installed()]
def on_ui_object_available(self, name, ui_object):
if name == 'gpodder-gtk':
for p in self.players + self.resumers:
p.gpodder = ui_object
def on_episodes_context_menu(self, episodes):
if not any(e.file_exists() for e in episodes):
return None
ret = [(p.title, functools.partial(p.enqueue_episodes, config=self.gpodder_config))
for p in self.players]
# needs dbus, doesn't handle more than 1 episode
# and no point in using DBus when episode is not played.
if not hasattr(gpodder.dbus_session_bus, 'fake') and \
len(episodes) == 1 and episodes[0].current_position > 0:
ret.extend([(p.title, functools.partial(p.enqueue_episodes, config=self.gpodder_config))
for p in self.resumers])
return ret
def on_episode_downloaded(self, episode):
if self.config.enqueue_after_download:
if not self.config.default_player and len(self.players):
player = self.players[0]
logger.info('Picking first installed player: %s (%s)', player.slug, player.application)
else:
player = next((player for player in self.players if self.config.default_player == player.slug), None)
if player is None:
logger.info('No player set, use one of: %r', [player.slug for player in self.players])
return
logger.info('Enqueueing downloaded file in %s', player.application)
player.enqueue_episodes([episode])

View File

@ -1,43 +0,0 @@
# -*- coding: utf-8 -*-
# Add a context menu to show the episode/podcast website (bug 1958)
# (c) 2014-10-20 Thomas Perl <thp.io/about>
# Released under the same license terms as gPodder itself.
import logging
import gpodder
from gpodder import util
logger = logging.getLogger(__name__)
_ = gpodder.gettext
__title__ = _('"Open website" episode and podcast context menu')
__description__ = _('Add a context menu item for opening the website of an episode or podcast')
__authors__ = 'Thomas Perl <thp@gpodder.org>'
__category__ = 'interface'
__only_for__ = 'gtk'
class gPodderExtension:
def __init__(self, container):
self.container = container
def has_website(self, episodes):
for episode in episodes:
if episode.link:
return True
def open_website(self, episodes):
for episode in episodes:
if episode.link:
util.open_website(episode.link)
def open_channel_website(self, channel):
util.open_website(channel.link)
def on_episodes_context_menu(self, episodes):
return [(_('Open website'), self.open_website if self.has_website(episodes) else None)]
def on_channel_context_menu(self, channel):
return [(_('Open website'), self.open_channel_website if channel.link else None)]

View File

@ -1,293 +0,0 @@
# -*- coding: utf-8 -*-
# Disable automatic downloads based on episode title.
# Released under the same license terms as gPodder itself.
import re
import gpodder
import gi # isort:skip
gi.require_version('Gtk', '3.0') # isort:skip
from gi.repository import Gtk # isort:skip
_ = gpodder.gettext
__title__ = _('Filter Episodes')
__description__ = _('Disable automatic downloads based on episode title.')
__only_for__ = 'gtk, cli'
__authors__ = 'Brand Huntsman <http://qzx.com/mail/>'
__doc__ = 'https://gpodder.github.io/docs/extensions/filter.html'
DefaultConfig = {
'filters': []
}
class BlockExceptFrame:
"""
Utility class to manage a Block or Except frame, with sub-widgets:
- Creation as well as internal UI change is handled;
- Changes to the other widget and to the model have to be handled outside.
It's less optimized than mapping each widget to a different signal handler,
but makes shorter code.
"""
def __init__(self, value, enable_re, enable_ic, on_change_cb):
self.on_change_cb = on_change_cb
self.frame = Gtk.Frame()
frame_vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.frame.add(frame_vbox)
# checkbox and text entry
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
hbox.set_border_width(5)
frame_vbox.add(hbox)
self.checkbox = Gtk.CheckButton()
self.checkbox.set_active(value is not False)
hbox.pack_start(self.checkbox, False, False, 3)
self.entry = Gtk.Entry()
hbox.pack_start(self.entry, True, True, 5)
# lower hbox
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
hbox.set_border_width(5)
frame_vbox.add(hbox)
# regular expression checkbox
self.checkbox_re = Gtk.CheckButton(_('Regular Expression'))
hbox.pack_end(self.checkbox_re, False, False, 10)
# ignore case checkbox
self.checkbox_ic = Gtk.CheckButton(_('Ignore Case'))
hbox.pack_end(self.checkbox_ic, False, False, 10)
if value is False:
self.entry.set_sensitive(False)
self.entry.set_editable(False)
self.checkbox_re.set_sensitive(False)
self.checkbox_ic.set_sensitive(False)
else:
self.entry.set_text(value)
self.checkbox_re.set_active(enable_re)
self.checkbox_ic.set_active(enable_ic)
self.checkbox.connect('toggled', self.toggle_active)
self.entry.connect('changed', self.emit_change)
self.checkbox_re.connect('toggled', self.emit_change)
self.checkbox_ic.connect('toggled', self.emit_change)
def toggle_active(self, widget):
enabled = widget.get_active()
if enabled:
# enable text and RE/IC checkboxes
self.entry.set_sensitive(True)
self.entry.set_editable(True)
self.checkbox_re.set_sensitive(True)
self.checkbox_ic.set_sensitive(True)
else:
# clear and disable text and RE/IC checkboxes
self.entry.set_sensitive(False)
self.entry.set_text('')
self.entry.set_editable(False)
self.checkbox_re.set_active(False)
self.checkbox_re.set_sensitive(False)
self.checkbox_ic.set_active(False)
self.checkbox_ic.set_sensitive(False)
self.emit_change(widget)
def emit_change(self, widget):
del widget
if self.on_change_cb:
self.on_change_cb(active=self.checkbox.get_active(),
text=self.entry.get_text(),
regexp=self.checkbox_re.get_active(),
ignore_case=self.checkbox_ic.get_active())
class gPodderExtension:
def __init__(self, container):
self.core = container.manager.core # gpodder core
self.filters = container.config.filters # all filters
# the following are only valid when podcast channel settings dialog is open
# self.gpodder = gPodder
# self.ui_object = gPodderChannel
# self.channel = PodcastChannel
# self.url = current filter url
# self.f = current filter
# self.block_widget = block BlockExceptFrame
# self.allow_widget = allow BlockExceptFrame
def on_ui_object_available(self, name, ui_object):
if name == 'channel-gtk':
# to close channel settings dialog after re-filtering
self.ui_object = ui_object
elif name == 'gpodder-gtk':
# to update episode list after re-filtering
self.gpodder = ui_object
# add filter tab to podcast channel settings dialog
def on_channel_settings(self, channel):
return [(_('Filter'), self.show_channel_settings_tab)]
def show_channel_settings_tab(self, channel):
self.channel = channel
self.url = channel.url
self.f = self.find_filter(self.url)
block = self.key('block')
allow = self.key('allow')
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
box.set_border_width(10)
# note about Cancel
note = Gtk.Label(use_markup=True, wrap=True, label=_(
'<b>Note:</b> The Cancel button does <b>not</b> return the '
'filter settings to the values they had before. '
'The changes are saved immediately after they are made.'))
box.add(note)
# block widgets
self.block_widget = BlockExceptFrame(value=block,
enable_re=self.key('block_re') is not False,
enable_ic=self.key('block_ic') is not False,
on_change_cb=self.on_block_changed)
self.block_widget.frame.set_label(_('Block'))
box.add(self.block_widget.frame)
self.block_widget.checkbox.set_sensitive(allow is False)
# allow widgets
self.allow_widget = BlockExceptFrame(value=allow,
enable_re=self.key('allow_re') is not False,
enable_ic=self.key('allow_ic') is not False,
on_change_cb=self.on_allow_changed)
self.allow_widget.frame.set_label(_('Except'))
box.add(self.allow_widget.frame)
if self.f is None:
self.allow_widget.frame.set_sensitive(False)
# help
label = Gtk.Label(_(
'Clicking the block checkbox and leaving it empty will disable auto-download for all episodes in this channel.'
' The patterns match partial text in episode title, and an empty pattern matches any title.'
' The except pattern unblocks blocked episodes (to block all then unblock some).'))
label.set_line_wrap(True)
box.add(label)
# re-filter
separator = Gtk.HSeparator()
box.add(separator)
button = Gtk.Button(_('Filter episodes now'))
button.connect('clicked', self.refilter_podcast)
box.add(button)
label2 = Gtk.Label(_('Undoes any episodes you marked as old.'))
box.add(label2)
box.show_all()
return box
# return filter for a given podcast channel url
def find_filter(self, url):
for f in self.filters:
if f['url'] == url:
return f
return None
# return value for a given key in current filter
def key(self, key):
if self.f is None:
return False
return self.f.get(key, False)
def on_block_changed(self, active, text, regexp, ignore_case):
self.on_changed('block', active, text, regexp, ignore_case)
self.allow_widget.frame.set_sensitive(self.f is not None)
def on_allow_changed(self, active, text, regexp, ignore_case):
self.on_changed('allow', active, text, regexp, ignore_case)
self.block_widget.checkbox.set_sensitive(self.f is None or self.key('allow') is False)
# update filter when toggling block/allow checkbox
def on_changed(self, field, enabled, text, regexp, ignore_case):
if enabled:
if self.f is None:
self.f = {'url': self.url}
self.filters.append(self.f)
self.filters.sort(key=lambda e: e['url'])
self.f[field] = text
if regexp:
self.f[field + '_re'] = True
else:
self.f.pop(field + '_re', None)
if ignore_case:
self.f[field + '_ic'] = True
else:
self.f.pop(field + '_ic', None)
else:
if self.f is not None:
self.f.pop(field + '_ic', None)
self.f.pop(field + '_re', None)
self.f.pop(field, None)
if len(self.f.keys()) == 1:
self.filters.remove(self.f)
self.f = None
# save config
self.core.config.schedule_save()
# remove filter when podcast channel is removed
def on_podcast_delete(self, podcast):
f = self.find_filter(podcast.url)
if f is not None:
self.filters.remove(f)
# save config
self.core.config.schedule_save()
# mark new episodes as old to disable automatic download when they match a block filter
def on_podcast_updated(self, podcast):
self.filter_podcast(podcast, False)
# re-filter episodes after changing filters
def refilter_podcast(self, widget):
if self.filter_podcast(self.channel, True):
self.channel.db.commit()
self.gpodder.update_episode_list_model()
self.ui_object.main_window.destroy()
# compare filter pattern to episode title
def compare(self, title, pattern, regexp, ignore_case):
if regexp is not False:
return regexp.search(title)
elif ignore_case:
return (pattern.casefold() in title.casefold())
else:
return (pattern in title)
# filter episodes that aren't downloaded or deleted
def filter_podcast(self, podcast, mark_new):
f = self.find_filter(podcast.url)
if f is not None:
block = f.get('block', False)
allow = f.get('allow', False)
block_ic = True if block is not False and f.get('block_ic', False) else False
allow_ic = True if allow is not False and f.get('allow_ic', False) else False
block_re = re.compile(block, re.IGNORECASE if block_ic else False) if block is not False and f.get('block_re', False) else False
allow_re = re.compile(allow, re.IGNORECASE if allow_ic else False) if allow is not False and f.get('allow_re', False) else False
else:
block = False
allow = False
changes = False
for e in podcast.get_episodes(gpodder.STATE_NORMAL):
if allow is not False and self.compare(e.title, allow, allow_re, allow_ic):
# allow episode
if mark_new and not e.is_new:
e.mark_new()
changes = True
continue
if block is not False and self.compare(e.title, block, block_re, block_ic):
# block episode - mark as old to disable automatic download
if e.is_new:
e.mark_old()
changes = True
continue
if mark_new and not e.is_new:
e.mark_new()
changes = True
return changes

View File

@ -1,124 +0,0 @@
# -*- coding: utf-8 -*-
#
# Gtk Status Icon (gPodder bug 1495)
# Thomas Perl <thp@gpodder.org>; 2012-07-31
#
import logging
import os.path
from gi.repository import GdkPixbuf, Gtk
import gpodder
from gpodder.gtkui import draw
logger = logging.getLogger(__name__)
_ = gpodder.gettext
__title__ = _('Gtk Status Icon')
__description__ = _('Show a status icon for Gtk-based Desktops.')
__category__ = 'desktop-integration'
__only_for__ = 'gtk'
__disable_in__ = 'unity,win32'
DefaultConfig = {
'download_progress_bar': False, # draw progress bar on icon while downloading?
}
class gPodderExtension:
def __init__(self, container):
self.container = container
self.config = self.container.config
self.status_icon = None
self.icon_name = None
self.gpodder = None
self.last_progress = 1
def set_icon(self, use_pixbuf=False):
path = os.path.join(os.path.dirname(__file__), '..', '..', 'icons')
icon_path = os.path.abspath(path)
theme = Gtk.IconTheme.get_default()
theme.append_search_path(icon_path)
if self.icon_name is None:
if theme.has_icon('gpodder'):
self.icon_name = 'gpodder'
else:
self.icon_name = 'stock_mic'
if self.status_icon is None:
self.status_icon = Gtk.StatusIcon.new_from_icon_name(self.icon_name)
return
# If current mode matches desired mode, nothing to do.
is_pixbuf = (self.status_icon.get_storage_type() == Gtk.ImageType.PIXBUF)
if is_pixbuf == use_pixbuf:
return
if not use_pixbuf:
self.status_icon.set_from_icon_name(self.icon_name)
else:
# Currently icon is not a pixbuf => was loaded by name, at which
# point size was automatically determined.
icon_size = self.status_icon.get_size()
icon_pixbuf = theme.load_icon(self.icon_name, icon_size, Gtk.IconLookupFlags.USE_BUILTIN)
self.status_icon.set_from_pixbuf(icon_pixbuf)
def on_load(self):
self.set_icon()
self.status_icon.connect('activate', self.on_toggle_visible)
self.status_icon.set_has_tooltip(True)
self.status_icon.set_tooltip_text("gPodder")
def on_toggle_visible(self, status_icon):
if self.gpodder is None:
return
visibility = self.gpodder.main_window.get_visible()
self.gpodder.main_window.set_visible(not visibility)
def on_unload(self):
if self.status_icon is not None:
self.status_icon.set_visible(False)
self.status_icon = None
self.icon_name = None
def on_ui_object_available(self, name, ui_object):
if name == 'gpodder-gtk':
self.gpodder = ui_object
def get_icon_pixbuf(self):
assert self.status_icon is not None
if self.status_icon.get_storage_type() != Gtk.ImageType.PIXBUF:
self.set_icon(use_pixbuf=True)
return self.status_icon.get_pixbuf()
def on_download_progress(self, progress):
logger.debug("download progress: %f", progress)
if not self.config.download_progress_bar:
# reset the icon in case option was turned off during download
if self.last_progress < 1:
self.last_progress = 1
self.set_icon()
# in any case, we're now done
return
if progress == 1:
self.set_icon() # no progress bar
self.last_progress = progress
return
# Only update in 3-percent-steps to save some resources
if abs(progress - self.last_progress) < 0.03 and progress > self.last_progress:
return
icon = self.get_icon_pixbuf().copy()
progressbar = draw.progressbar_pixbuf(icon.get_width(), icon.get_height(), progress)
progressbar.composite(icon, 0, 0, icon.get_width(), icon.get_height(), 0, 0, 1, 1, GdkPixbuf.InterpType.NEAREST, 255)
self.status_icon.set_from_pixbuf(icon)
self.last_progress = progress

View File

@ -1,28 +0,0 @@
# -*- coding: utf-8 -*-
# Minimize gPodder's main window on startup
# Thomas Perl <thp@gpodder.org>; 2012-07-31
import gpodder
from gpodder import util
_ = gpodder.gettext
__title__ = _('Minimize on start')
__description__ = _('Minimizes the gPodder window on startup.')
__category__ = 'interface'
__only_for__ = 'gtk'
class gPodderExtension:
def __init__(self, container):
self.container = container
def on_ui_object_available(self, name, ui_object):
if name == 'gpodder-gtk':
self.ui_object = ui_object
def on_application_started(self):
if self.ui_object:
self.ui_object.main_window.iconify()
util.idle_add(self.ui_object.main_window.iconify)

View File

@ -1,338 +0,0 @@
# -*- coding: utf-8 -*-
#
# gPodder extension for listening to notifications from MPRIS-capable
# players and translating them to gPodder's Media Player D-Bus API
#
# Copyright (c) 2013-2014 Dov Feldstern <dovdevel@gmail.com>
#
# 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/>.
import collections
import logging
import time
import urllib.error
import urllib.parse
import urllib.request
import dbus
import dbus.service
import gpodder
logger = logging.getLogger(__name__)
_ = gpodder.gettext
__title__ = _('MPRIS Listener')
__description__ = _('Convert MPRIS notifications to gPodder Media Player D-Bus API')
__authors__ = 'Dov Feldstern <dovdevel@gmail.com>'
__doc__ = 'https://gpodder.github.io/docs/extensions/mprislistener.html'
__category__ = 'desktop-integration'
__only_for__ = 'freedesktop'
USECS_IN_SEC = 1000000
TrackInfo = collections.namedtuple('TrackInfo',
['uri', 'length', 'status', 'pos', 'rate'])
def subsecond_difference(usec1, usec2):
return usec1 is not None and usec2 is not None and abs(usec1 - usec2) < USECS_IN_SEC
class CurrentTrackTracker(object):
'''An instance of this class is responsible for tracking the state of the
currently playing track -- it's playback status, playing position, etc.
'''
def __init__(self, notifier):
self.uri = None
self.length = None
self.pos = None
self.rate = None
self.status = None
self._notifier = notifier
self._prev_notif = ()
def _calc_update(self):
now = time.time()
logger.debug('CurrentTrackTracker: calculating at %d (status: %r)',
now, self.status)
try:
if self.status != 'Playing':
logger.debug('CurrentTrackTracker: not currently playing, no change')
return
if self.pos is None or self.rate is None:
logger.debug('CurrentTrackTracker: unknown pos/rate, no change')
return
logger.debug('CurrentTrackTracker: %f @%f (diff: %f)',
self.pos, self.rate, now - self._last_time)
self.pos = self.pos + self.rate * (now - self._last_time) * USECS_IN_SEC
finally:
self._last_time = now
def update_needed(self, current, updated):
for field in updated:
if field == 'pos':
if not subsecond_difference(updated['pos'], current['pos']):
return True
elif updated[field] != current[field]:
return True
# no unequal field was found, no new info here!
return False
def update(self, **kwargs):
# check if there is any new info here -- if not, no need to update!
cur = self.getinfo()._asdict()
if not self.update_needed(cur, kwargs):
return
# there *is* new info, go ahead and update...
uri = kwargs.pop('uri', None)
if uri is not None:
length = kwargs.pop('length') # don't know how to handle uri with no length
if uri != cur['uri']:
# if this is a new uri, and the previous state was 'Playing',
# notify that the previous track has stopped before updating to
# the new track.
if cur['status'] == 'Playing':
logger.debug('notify Stopped: new uri: old %s new %s',
cur['uri'], uri)
self.notify_stop()
self.uri = uri
self.length = float(length)
if 'pos' in kwargs:
# If the position is being updated, and the current status was Playing
# If the status *is* playing, and *was* playing, but the position
# has changed discontinuously, notify a stop for the old position
if (cur['status'] == 'Playing'
and ('status' not in kwargs or kwargs['status'] == 'Playing') and not
subsecond_difference(cur['pos'], kwargs['pos'])):
logger.debug('notify Stopped: playback discontinuity:'
+ 'calc: %r observed: %r', cur['pos'], kwargs['pos'])
self.notify_stop()
if ((kwargs['pos']) <= 0
and self.pos is not None
and self.length is not None
and (self.length - USECS_IN_SEC) < self.pos
and self.pos < (self.length + 2 * USECS_IN_SEC)):
logger.debug('pos=0 end of stream (calculated pos: %f/%f [%f])',
self.pos / USECS_IN_SEC, self.length / USECS_IN_SEC,
(self.pos / USECS_IN_SEC) - (self.length / USECS_IN_SEC))
self.pos = self.length
kwargs.pop('pos') # remove 'pos' even though we're not using it
else:
if self.pos is not None and self.length is not None:
logger.debug("%r %r", self.pos, self.length)
logger.debug('pos=0 not end of stream (calculated pos: %f/%f [%f])',
self.pos / USECS_IN_SEC, self.length / USECS_IN_SEC,
(self.pos / USECS_IN_SEC) - (self.length / USECS_IN_SEC))
newpos = kwargs.pop('pos')
self.pos = newpos if newpos >= 0 else 0
if 'status' in kwargs:
self.status = kwargs.pop('status')
if 'rate' in kwargs:
self.rate = kwargs.pop('rate')
if kwargs:
logger.error('unexpected update fields %r', kwargs)
# notify about the current state
if self.status == 'Playing':
self.notify_playing()
else:
logger.debug('notify Stopped: status %r', self.status)
self.notify_stop()
def getinfo(self):
self._calc_update()
return TrackInfo(self.uri, self.length, self.status, self.pos, self.rate)
def notify_stop(self):
self.notify('Stopped')
def notify_playing(self):
self.notify('Playing')
def notify(self, status):
if (self.uri is None
or self.pos is None
or self.status is None
or self.length is None
or self.length <= 0):
return
pos = self.pos // USECS_IN_SEC
parsed_url = urllib.parse.urlparse(self.uri)
if (not parsed_url.scheme) or parsed_url.scheme == 'file':
file_uri = urllib.request.url2pathname(urllib.parse.urlparse(self.uri).path).encode('utf-8')
else:
file_uri = self.uri
total_time = self.length // USECS_IN_SEC
if status == 'Stopped':
end_position = pos
start_position = self._notifier.start_position
if self._prev_notif != (start_position, end_position, total_time, file_uri):
self._notifier.PlaybackStopped(start_position, end_position,
total_time, file_uri)
self._prev_notif = (start_position, end_position, total_time, file_uri)
elif status == 'Playing':
start_position = pos
if self._prev_notif != (start_position, file_uri):
self._notifier.PlaybackStarted(start_position, file_uri)
self._prev_notif = (start_position, file_uri)
self._notifier.start_position = start_position
logger.info('CurrentTrackTracker: %s: %r %s', status, self, file_uri)
def __repr__(self):
return '%s: %s at %d/%d (@%f)' % (
self.uri or 'None',
self.status or 'None',
(self.pos or 0) // USECS_IN_SEC,
(self.length or 0) // USECS_IN_SEC,
self.rate or 0)
class MPRISDBusReceiver(object):
INTERFACE_PROPS = 'org.freedesktop.DBus.Properties'
SIGNAL_PROP_CHANGE = 'PropertiesChanged'
PATH_MPRIS = '/org/mpris/MediaPlayer2'
INTERFACE_MPRIS = 'org.mpris.MediaPlayer2.Player'
SIGNAL_SEEKED = 'Seeked'
OTHER_MPRIS_INTERFACES = ['org.mpris.MediaPlayer2',
'org.mpris.MediaPlayer2.TrackList',
'org.mpris.MediaPlayer2.Playlists']
def __init__(self, bus, notifier):
self.bus = bus
self.cur = CurrentTrackTracker(notifier)
self.bus.add_signal_receiver(self.on_prop_change,
self.SIGNAL_PROP_CHANGE,
self.INTERFACE_PROPS,
None,
self.PATH_MPRIS,
sender_keyword='sender')
self.bus.add_signal_receiver(self.on_seeked,
self.SIGNAL_SEEKED,
self.INTERFACE_MPRIS,
None,
None)
def stop_receiving(self):
self.bus.remove_signal_receiver(self.on_prop_change,
self.SIGNAL_PROP_CHANGE,
self.INTERFACE_PROPS,
None,
self.PATH_MPRIS)
self.bus.remove_signal_receiver(self.on_seeked,
self.SIGNAL_SEEKED,
self.INTERFACE_MPRIS,
None,
None)
def on_prop_change(self, interface_name, changed_properties,
invalidated_properties, path=None, sender=None):
if interface_name != self.INTERFACE_MPRIS:
if interface_name not in self.OTHER_MPRIS_INTERFACES:
logger.warning('unexpected interface: %s, props=%r', interface_name, list(changed_properties.keys()))
return
if sender is None:
logger.warning('No sender associated to D-Bus signal, please report a bug')
return
collected_info = {}
logger.debug("on_prop_change %r", changed_properties.keys())
if 'PlaybackStatus' in changed_properties:
collected_info['status'] = str(changed_properties['PlaybackStatus'])
if 'Metadata' in changed_properties:
logger.debug("Metadata %r", changed_properties['Metadata'].keys())
# on stop there is no xesam:url
if 'xesam:url' in changed_properties['Metadata']:
collected_info['uri'] = changed_properties['Metadata']['xesam:url']
collected_info['length'] = changed_properties['Metadata'].get('mpris:length', 0.0)
if 'Rate' in changed_properties:
collected_info['rate'] = changed_properties['Rate']
# Fix #788 pos=0 when Stopped resulting in not saving position on VLC quit
if changed_properties.get('PlaybackStatus') != 'Stopped':
try:
collected_info['pos'] = self.query_property(sender, 'Position')
except dbus.exceptions.DBusException:
pass
if 'status' not in collected_info:
try:
collected_info['status'] = str(self.query_property(
sender, 'PlaybackStatus'))
except dbus.exceptions.DBusException:
pass
logger.debug('collected info: %r', collected_info)
self.cur.update(**collected_info)
def on_seeked(self, position):
logger.debug('seeked to pos: %f', position)
self.cur.update(pos=position)
def query_property(self, sender, prop):
proxy = self.bus.get_object(sender, self.PATH_MPRIS)
props = dbus.Interface(proxy, self.INTERFACE_PROPS)
return props.Get(self.INTERFACE_MPRIS, prop)
class gPodderNotifier(dbus.service.Object):
def __init__(self, bus, path):
dbus.service.Object.__init__(self, bus, path)
self.start_position = 0
@dbus.service.signal(dbus_interface='org.gpodder.player', signature='us')
def PlaybackStarted(self, start_position, file_uri):
logger.info('PlaybackStarted: %s: %d', file_uri, start_position)
@dbus.service.signal(dbus_interface='org.gpodder.player', signature='uuus')
def PlaybackStopped(self, start_position, end_position, total_time, file_uri):
logger.info('PlaybackStopped: %s: %d--%d/%d',
file_uri, start_position, end_position, total_time)
# Finally, this is the extension, which just pulls this all together
class gPodderExtension:
def __init__(self, container):
self.container = container
self.path = '/org/gpodder/player/notifier'
self.notifier = None
self.rcvr = None
def on_load(self):
if gpodder.dbus_session_bus is None:
logger.debug("dbus session bus not available, not loading")
else:
self.session_bus = gpodder.dbus_session_bus
self.notifier = gPodderNotifier(self.session_bus, self.path)
self.rcvr = MPRISDBusReceiver(self.session_bus, self.notifier)
def on_unload(self):
if self.notifier is not None:
self.notifier.remove_from_connection(self.session_bus, self.path)
if self.rcvr is not None:
self.rcvr.stop_receiving()

View File

@ -1,113 +0,0 @@
# -*- coding: utf-8 -*-
# This extension adjusts the volume of audio files to a standard level
# Supported file formats are mp3 and ogg
#
# Requires: normalize-audio, mpg123
#
# (c) 2011-11-06 Bernd Schlapsi <brot@gmx.info>
# Released under the same license terms as gPodder itself.
import logging
import os
import subprocess
import gpodder
from gpodder import util
logger = logging.getLogger(__name__)
_ = gpodder.gettext
__title__ = _('Normalize audio with re-encoding')
__description__ = _('Normalize the volume of audio files with normalize-audio')
__authors__ = 'Bernd Schlapsi <brot@gmx.info>'
__doc__ = 'https://gpodder.github.io/docs/extensions/normalizeaudio.html'
__payment__ = 'https://flattr.com/submit/auto?user_id=BerndSch&url=http://wiki.gpodder.org/wiki/Extensions/NormalizeAudio'
__category__ = 'post-download'
DefaultConfig = {
'context_menu': True, # Show action in the episode list context menu
}
# a tuple of (extension, command)
CONVERT_COMMANDS = {
'.ogg': 'normalize-ogg',
'.mp3': 'normalize-mp3',
}
class gPodderExtension:
MIME_TYPES = ('audio/mpeg', 'audio/ogg', )
EXT = ('.mp3', '.ogg', )
def __init__(self, container):
self.container = container
# Dependency check
self.container.require_command('normalize-ogg')
self.container.require_command('normalize-mp3')
self.container.require_command('normalize-audio')
def on_load(self):
logger.info('Extension "%s" is being loaded.' % __title__)
def on_unload(self):
logger.info('Extension "%s" is being unloaded.' % __title__)
def on_episode_downloaded(self, episode):
self._convert_episode(episode)
def on_episodes_context_menu(self, episodes):
if not self.container.config.context_menu:
return None
if not any(self._check_source(episode) for episode in episodes):
return None
return [(self.container.metadata.title, self.convert_episodes)]
def _check_source(self, episode):
if not episode.file_exists():
return False
if episode.mime_type in self.MIME_TYPES:
return True
if episode.extension() in self.EXT:
return True
return False
def _convert_episode(self, episode):
if episode.file_type() != 'audio':
return
filename = episode.local_filename(create=False)
if filename is None:
return
basename, extension = os.path.splitext(filename)
cmd = [CONVERT_COMMANDS.get(extension, 'normalize-audio'), filename]
# Set cwd to prevent normalize from placing files in the directory gpodder was started from.
if gpodder.ui.win32:
p = util.Popen(cmd, cwd=episode.channel.save_dir)
p.wait()
stdout, stderr = ("<unavailable>",) * 2
else:
p = util.Popen(cmd, cwd=episode.channel.save_dir,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = p.communicate()
if p.returncode == 0:
logger.info('normalize-audio processing successful.')
gpodder.user_extensions.on_notification_show(_('File normalized'),
episode.title)
else:
logger.warning('normalize-audio failed: %s / %s', stdout, stderr)
def convert_episodes(self, episodes):
for episode in episodes:
self._convert_episode(episode)

View File

@ -1,156 +0,0 @@
# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client
# Copyright (c) 2005-2018 The gPodder Team
#
# gPodder 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.
#
# gPodder 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/>.
#
# Notification implementation for Windows
# Sean Munkel; 2012-12-29
"""
Current state (2018/07/29 ELL):
- I can't get pywin32 to work in msys2 (the platform used for this python3/gtk3 installer)
so existing code using COM doesn't work.
- Gio.Notification is not implemented on windows yet.
see https://bugzilla.gnome.org/show_bug.cgi?id=776583
- Gtk.StatusIcon with a context works but is deprecated. Showing a balloon using set_tooltip_markup
doesn't work.
See https://github.com/afiskon/py-gtk-example
- hexchat have implemented a solid c++ solution.
See https://github.com/hexchat/hexchat/tree/master/src/fe-gtk/notifications
I've chosen to implement notifications by calling a PowerShell script invoking
Windows Toast Notification API or Balloon Notification as fallback.
It's tested on Win7 32bit and Win10 64bit VMs from modern.ie
So we have a working solution until Gio.Notification is implemented on Windows.
"""
import logging
import os
import os.path
import subprocess
import sys
import tempfile
import gpodder
logger = logging.getLogger(__name__)
_ = gpodder.gettext
__title__ = _('Notification Bubbles for Windows')
__description__ = _('Display notification bubbles for different events.')
__authors__ = 'Sean Munkel <SeanMunkel@gmail.com>'
__category__ = 'desktop-integration'
__mandatory_in__ = 'win32'
__only_for__ = 'win32'
class gPodderExtension(object):
def __init__(self, *args):
gpodder_script = sys.argv[0]
gpodder_script = os.path.realpath(gpodder_script)
self._icon = os.path.join(os.path.dirname(gpodder_script), "gpodder.ico")
def on_notification_show(self, title, message):
script = """
try {{
if ([Environment]::OSVersion.Version -ge (new-object 'Version' 10,0,10240)) {{
# use Windows 10 Toast notification
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
[Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
# Need a real AppID (see https://stackoverflow.com/q/46814858)
# use gPodder app id if it's the installed, otherwise use PowerShell's AppID
try {{
$gpo_appid = Get-StartApps -Name "gpodder"
}} catch {{
write-host "Get-StartApps not available"
$gpo_appid = $null
}}
if ($gpo_appid -ne $null) {{
$APP_ID = $gpo_appid[0].AppID
}} else {{
$APP_ID = '{{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}}\\WindowsPowerShell\\v1.0\\powershell.exe'
}}
$template = @"
<toast activationType="protocol" launch="" duration="long">
<visual>
<binding template="ToastGeneric">
<image placement="appLogoOverride" src="{icon}" />
<text><![CDATA[{title}]]></text>
<text><![CDATA[{message}]]></text>
</binding>
</visual>
<audio silent="true" />
</toast>
"@
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
$xml.LoadXml($template)
$toast = New-Object Windows.UI.Notifications.ToastNotification $xml
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($APP_ID).Show($toast)
Remove-Item -LiteralPath $MyInvocation.MyCommand.Path -Force # Delete this script temp file.
}} else {{
# use older Balloon notification when not on Windows 10
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$o = New-Object System.Windows.Forms.NotifyIcon
$o.Icon = "{icon}"
$o.BalloonTipIcon = "None"
$o.BalloonTipText = @"
{message}
"@
$o.BalloonTipTitle = @"
{title}
"@
$o.Visible = $True
$Delay = 10 # Delay value in seconds.
$o.ShowBalloonTip($Delay*1000)
Start-Sleep -s $Delay
$o.Dispose()
Remove-Item -LiteralPath $MyInvocation.MyCommand.Path -Force # Delete this script temp file.
}}
}} catch {{
write-host "Caught an exception:"
write-host "Exception Type: $($_.Exception.GetType().FullName)"
write-host "Exception Message: $($_.Exception.Message)"
exit 1
}}
""".format(icon=self._icon, message=message, title=title)
fh, path = tempfile.mkstemp(suffix=".ps1")
with open(fh, "w", encoding="utf_8_sig") as f:
f.write(script)
try:
# hide powershell command window using startupinfo
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags = subprocess.CREATE_NEW_CONSOLE | subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
# to run 64bit powershell on Win10 64bit when running from 32bit gPodder
# (we need 64bit powershell on Win10 otherwise Get-StartApps is not available)
powershell = r"{}\sysnative\WindowsPowerShell\v1.0\powershell.exe".format(os.environ["SystemRoot"])
if not os.path.exists(powershell):
powershell = "powershell.exe"
subprocess.Popen([powershell,
"-ExecutionPolicy", "Bypass", "-File", path],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
startupinfo=startupinfo)
except subprocess.CalledProcessError as e:
logger.error("Error in on_notification_show(title=%r, message=%r):\n"
"\t%r exit code %i\n\tstdout=%s\n\tstderr=%s",
title, message, e.cmd, e.returncode, e.stdout, e.stderr)
except FileNotFoundError:
logger.error("Error in on_notification_show(title=%r, message=%r): %s not found",
title, message, powershell)
def on_unload(self):
pass

View File

@ -1,72 +0,0 @@
# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client
# Copyright (c) 2005-2011 Thomas Perl and the gPodder Team
#
# gPodder 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.
#
# gPodder 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/>.
#
# Bernd Schlapsi <brot@gmx.info>; 2011-11-20
__title__ = 'Gtk+ Desktop Notifications'
__description__ = 'Display notification bubbles for different events.'
__category__ = 'desktop-integration'
__only_for__ = 'gtk'
__mandatory_in__ = 'gtk'
__disable_in__ = 'win32'
import logging
import gpodder
logger = logging.getLogger(__name__)
try:
import gi
gi.require_version('Notify', '0.7')
from gi.repository import Notify
pynotify = True
except ImportError:
pynotify = None
except ValueError:
pynotify = None
if pynotify is None:
class gPodderExtension(object):
def __init__(self, container):
logger.info('Could not find PyNotify.')
else:
class gPodderExtension(object):
def __init__(self, container):
self.container = container
def on_load(self):
Notify.init('gPodder')
def on_unload(self):
Notify.uninit()
def on_notification_show(self, title, message):
if not message and not title:
return
notify = Notify.Notification.new(title or '', message or '',
gpodder.icon_file)
try:
notify.show()
except:
# See http://gpodder.org/bug/966
pass

View File

@ -1,116 +0,0 @@
# -*- coding: utf-8 -*-
# Rename files after download based on the episode title
# Copyright (c) 2011-04-04 Thomas Perl <thp.io>
# Licensed under the same terms as gPodder itself
import logging
import os
import time
import gpodder
from gpodder import util
from gpodder.model import PodcastEpisode
logger = logging.getLogger(__name__)
_ = gpodder.gettext
__title__ = _('Rename episodes after download')
__description__ = _('Rename episodes to "<Episode Title>.<ext>" on download')
__authors__ = 'Bernd Schlapsi <brot@gmx.info>, Thomas Perl <thp@gpodder.org>'
__doc__ = 'https://gpodder.github.io/docs/extensions/renameafterdownload.html'
__payment__ = 'https://flattr.com/submit/auto?user_id=BerndSch&url=http://wiki.gpodder.org/wiki/Extensions/RenameAfterDownload'
__category__ = 'post-download'
DefaultConfig = {
'add_sortdate': False, # Add the sortdate as prefix
'add_podcast_title': False, # Add the podcast title as prefix
'sortdate_after_podcast_title': False, # put the sortdate after podcast title
}
class gPodderExtension:
def __init__(self, container):
self.container = container
self.gpodder = None
self.config = self.container.config
def on_episode_downloaded(self, episode):
current_filename = episode.local_filename(create=False)
new_filename = self.make_filename(current_filename, episode.title,
episode.sortdate, episode.channel.title)
if new_filename != current_filename:
logger.info('Renaming: %s -> %s', current_filename, new_filename)
os.rename(current_filename, new_filename)
util.rename_episode_file(episode, new_filename)
def on_ui_object_available(self, name, ui_object):
if name == 'gpodder-gtk':
self.gpodder = ui_object
def on_create_menu(self):
return [(_("Rename all downloaded episodes"), self.rename_all_downloaded_episodes)]
def rename_all_downloaded_episodes(self):
episodes = [e for c in self.gpodder.channels for e in [e for e in c.children if e.state == gpodder.STATE_DOWNLOADED]]
number_of_episodes = len(episodes)
if number_of_episodes == 0:
self.gpodder.show_message(_('No downloaded episodes to rename'),
_('Rename all downloaded episodes'), important=True)
from gpodder.gtkui.interface.progress import ProgressIndicator
progress_indicator = ProgressIndicator(
_('Renaming all downloaded episodes'),
'', True, self.gpodder.get_dialog_parent(), number_of_episodes)
for episode in episodes:
self.on_episode_downloaded(episode)
if not progress_indicator.on_tick():
break
renamed_count = progress_indicator.tick_counter
progress_indicator.on_finished()
if renamed_count > 0:
self.gpodder.show_message(_('Renamed %(count)d downloaded episodes') % {'count': renamed_count},
_('Rename all downloaded episodes'), important=True)
def make_filename(self, current_filename, title, sortdate, podcast_title):
dirname = os.path.dirname(current_filename)
filename = os.path.basename(current_filename)
basename, ext = os.path.splitext(filename)
new_basename = []
new_basename.append(title)
if self.config.sortdate_after_podcast_title:
if self.config.add_sortdate:
new_basename.insert(0, sortdate)
if self.config.add_podcast_title:
new_basename.insert(0, podcast_title)
else:
if self.config.add_podcast_title:
new_basename.insert(0, podcast_title)
if self.config.add_sortdate:
new_basename.insert(0, sortdate)
new_basename = ' - '.join(new_basename)
# Remove unwanted characters and shorten filename (#494)
# Also sanitize ext (see #591 where ext=.mp3?dest-id=754182)
new_basename, ext = util.sanitize_filename_ext(
new_basename,
ext,
PodcastEpisode.MAX_FILENAME_LENGTH,
PodcastEpisode.MAX_FILENAME_WITH_EXT_LENGTH)
new_filename = os.path.join(dirname, new_basename + ext)
if new_filename == current_filename:
return current_filename
for filename in util.generate_names(new_filename):
# Avoid filename collisions
if not os.path.exists(filename):
return filename

View File

@ -1,99 +0,0 @@
# -*- coding: utf-8 -*-
####
# 01/2011 Bernd Schlapsi <brot@gmx.info>
#
# This script is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# gPodder 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/>.
#
# Dependencies:
# * python-mutagen (Mutagen is a Python module to handle audio metadata)
#
# This extension scripts removes coverart from all downloaded ogg files.
# The reason for this script is that my media player (MEIZU SL6)
# couldn't handle ogg files with included coverart
import logging
import os
from mutagen.oggvorbis import OggVorbis
import gpodder
logger = logging.getLogger(__name__)
_ = gpodder.gettext
__title__ = _('Remove cover art from OGG files')
__description__ = _('removes coverart from all downloaded ogg files')
__authors__ = 'Bernd Schlapsi <brot@gmx.info>'
__doc__ = 'https://gpodder.github.io/docs/extensions/removeoggcover.html'
__payment__ = 'https://flattr.com/submit/auto?user_id=BerndSch&url=http://wiki.gpodder.org/wiki/Extensions/RemoveOGGCover'
__category__ = 'post-download'
DefaultConfig = {
'context_menu': True, # Show item in context menu
}
class gPodderExtension:
def __init__(self, container):
self.container = container
self.config = self.container.config
def on_episode_downloaded(self, episode):
self.rm_ogg_cover(episode)
def on_episodes_context_menu(self, episodes):
if not self.config.context_menu:
return None
episode_types = [e.mime_type for e in episodes
if e.mime_type is not None and e.file_exists()]
if 'audio/ogg' not in episode_types:
return None
return [(_('Remove cover art'), self._rm_ogg_covers)]
def _rm_ogg_covers(self, episodes):
for episode in episodes:
self.rm_ogg_cover(episode)
def rm_ogg_cover(self, episode):
filename = episode.local_filename(create=False)
if filename is None:
return
basename, extension = os.path.splitext(filename)
if episode.file_type() != 'audio':
return
if extension.lower() != '.ogg':
return
try:
ogg = OggVorbis(filename)
found = False
for key in ogg.keys():
if key.startswith('cover'):
found = True
ogg.pop(key)
if found:
logger.info('Removed cover art from OGG file: %s', filename)
ogg.save()
except Exception as e:
logger.warning('Failed to remove OGG cover: %s', e, exc_info=True)

View File

@ -1,149 +0,0 @@
# -*- coding: utf-8 -*-
# Requirements: apt-get install python-kaa-metadata ffmpeg python-dbus
# To use, copy it as a Python script into ~/.config/gpodder/extensions/rockbox_mp4_convert.py
# See the module "gpodder.extensions" for a description of when each extension
# gets called and what the parameters of each extension are.
# Based on Rename files after download based on the episode title
# And patch in Bug https://bugs.gpodder.org/show_bug.cgi?id=1263
# Copyright (c) 2011-04-06 Guy Sheffer <guysoft at gmail.com>
# Copyright (c) 2011-04-04 Thomas Perl <thp.io>
# Licensed under the same terms as gPodder itself
import logging
import os
import shlex
import subprocess
import kaa.metadata
import gpodder
from gpodder import util
logger = logging.getLogger(__name__)
_ = gpodder.gettext
__title__ = _('Convert video files to MP4 for Rockbox')
__description__ = _('Converts all videos to a Rockbox-compatible format')
__authors__ = 'Guy Sheffer <guysoft@gmail.com>, Thomas Perl <thp@gpodder.org>, Bernd Schlapsi <brot@gmx.info>'
__category__ = 'post-download'
DefaultConfig = {
'device_height': 176.0,
'device_width': 224.0,
'ffmpeg_options': '-vcodec mpeg2video -b 500k -ab 192k -ac 2 -ar 44100 -acodec libmp3lame',
}
ROCKBOX_EXTENSION = "mpg"
EXTENTIONS_TO_CONVERT = ['.mp4', "." + ROCKBOX_EXTENSION]
FFMPEG_CMD = 'ffmpeg -y -i "%(from)s" -s %(width)sx%(height)s %(options)s "%(to)s"'
class gPodderExtension:
def __init__(self, container):
self.container = container
program = shlex.split(FFMPEG_CMD)[0]
if not util.find_command(program):
raise ImportError("Couldn't find program '%s'" % program)
def on_load(self):
logger.info('Extension "%s" is being loaded.' % __title__)
def on_unload(self):
logger.info('Extension "%s" is being unloaded.' % __title__)
def on_episode_downloaded(self, episode):
current_filename = episode.local_filename(False)
converted_filename = self._convert_mp4(episode, current_filename)
if converted_filename is not None:
util.rename_episode_file(episode, converted_filename)
os.remove(current_filename)
logger.info('Conversion for %s was successfully' % current_filename)
gpodder.user_extensions.on_notification_show(_('File converted'), episode.title)
def _get_rockbox_filename(self, origin_filename):
if not os.path.exists(origin_filename):
logger.info("File '%s' don't exists." % origin_filename)
return None
dirname = os.path.dirname(origin_filename)
filename = os.path.basename(origin_filename)
basename, ext = os.path.splitext(filename)
if ext not in EXTENTIONS_TO_CONVERT:
logger.info("Ignore file with file-extension %s." % ext)
return None
if filename.endswith(ROCKBOX_EXTENSION):
new_filename = "%s-convert.%s" % (basename, ROCKBOX_EXTENSION)
else:
new_filename = "%s.%s" % (basename, ROCKBOX_EXTENSION)
return os.path.join(dirname, new_filename)
def _calc_resolution(self, video_width, video_height, device_width, device_height):
if video_height is None:
return None
width_ratio = device_width // video_width
height_ratio = device_height // video_height
dest_width = device_width
dest_height = width_ratio * video_height
if dest_height > device_height:
dest_width = height_ratio * video_width
dest_height = device_height
return (int(round(dest_width)), round(int(dest_height)))
def _convert_mp4(self, episode, from_file):
"""Convert MP4 file to rockbox mpg file"""
# generate new filename and check if the file already exists
to_file = self._get_rockbox_filename(from_file)
if to_file is None:
return None
if os.path.isfile(to_file):
return to_file
logger.info("Converting: %s", from_file)
gpodder.user_extensions.on_notification_show("Converting", episode.title)
# calculationg the new screen resolution
info = kaa.metadata.parse(from_file)
resolution = self._calc_resolution(
info.video[0].width,
info.video[0].height,
self.container.config.device_width,
self.container.config.device_height
)
if resolution is None:
logger.error("Error calculating the new screen resolution")
return None
convert_command = FFMPEG_CMD % {
'from': from_file,
'to': to_file,
'width': str(resolution[0]),
'height': str(resolution[1]),
'options': self.container.config.ffmpeg_options
}
if gpodder.ui.win32:
p = util.Popen(shlex.split(convert_command))
p.wait()
stdout, stderr = ("<unavailable>",) * 2
else:
process = util.Popen(shlex.split(convert_command),
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = process.communicate()
if process.returncode != 0:
logger.error(stderr)
return None
gpodder.user_extensions.on_notification_show("Converting finished", episode.title)
return to_file

View File

@ -1,46 +0,0 @@
# Copies cover art to a file based device
#
# (c) 2014-04-10 Alex Mayer <magictrick4906@aim.com>
# Released under the same license terms as gPodder itself.
# Use a logger for debug output - this will be managed by gPodder
import logging
import os
import shutil
import gpodder
logger = logging.getLogger(__name__)
_ = gpodder.gettext
# Provide some metadata that will be displayed in the gPodder GUI
__title__ = _('Rockbox Cover Art Sync')
__description__ = _('Copy Cover Art To Rockboxed Media Player')
__only_for__ = 'gtk, cli'
__authors__ = 'Alex Mayer <magictrick4906@aim.com>'
DefaultConfig = {
"art_name_on_device": "cover.jpg" # The file name that will be used on the device for cover art
}
class gPodderExtension:
def __init__(self, container):
self.container = container
self.config = self.container.config
def on_episode_synced(self, device, episode):
# check that we have the functions we need
if hasattr(device, 'get_episode_folder_on_device'):
# get the file and folder names we need
episode_folder = os.path.dirname(episode.local_filename(False))
device_folder = device.get_episode_folder_on_device(episode)
episode_art = os.path.join(episode_folder, "folder.jpg")
device_art = os.path.join(device_folder, self.config.art_name_on_device)
# make sure we have art to copy and it doesn't already exist
if os.path.isfile(episode_art) and not os.path.isfile(device_art):
logger.info('Syncing cover art for %s', episode.channel.title)
# copy and rename art
shutil.copy(episode_art, device_art)

View File

@ -1,86 +0,0 @@
# -*- coding: utf-8 -*-
# Extension script to stream podcasts to Sonos speakers
# Requirements: gPodder 3.x and the soco module >= 0.7 (https://pypi.python.org/pypi/soco)
# (c) 2013-01-19 Stefan Kögl <stefan@skoegl.net>
# Released under the same license terms as gPodder itself.
import logging
from functools import partial
import requests
import gpodder
import soco
_ = gpodder.gettext
logger = logging.getLogger(__name__)
__title__ = _('Stream to Sonos')
__description__ = _('Stream podcasts to Sonos speakers')
__authors__ = 'Stefan Kögl <stefan@skoegl.net>'
__category__ = 'interface'
__only_for__ = 'gtk'
def SONOS_CAN_PLAY(e):
return 'audio' in e.file_type()
class gPodderExtension:
def __init__(self, container):
speakers = soco.discover()
logger.info('Found Sonos speakers: %s' % ', '.join(name.player_name for name in speakers))
self.speakers = {}
for speaker in speakers:
try:
info = speaker.get_speaker_info()
except requests.ConnectionError as ce:
# ignore speakers we can't connect to
continue
name = info.get('zone_name', None)
uid = speaker.uid
# devices that do not have a name are probably bridges
if name:
self.speakers[uid] = speaker
def _stream_to_speaker(self, speaker_uid, episodes):
""" Play or enqueue selected episodes """
urls = [episode.url for episode in episodes if SONOS_CAN_PLAY(episode)]
logger.info('Streaming to Sonos %s: %s' % (self.speakers[speaker_uid].ip_address, ', '.join(urls)))
controller = self.speakers[speaker_uid].group.coordinator
# enqueue and play
for episode in episodes:
controller.play_uri(episode.url)
episode.playback_mark()
controller.play()
def on_episodes_context_menu(self, episodes):
""" Adds a context menu for each Sonos speaker group """
# Only show context menu if we can play at least one file
if not any(SONOS_CAN_PLAY(e) for e in episodes):
return []
menu_entries = []
for uid in list(self.speakers.keys()):
callback = partial(self._stream_to_speaker, uid)
controller = self.speakers[uid]
is_grouped = ' (Grouped)' if len(controller.group.members) > 1 else ''
name = controller.group.label + is_grouped
item = ('/'.join((_('Stream to Sonos'), name)), callback)
menu_entries.append(item)
# Remove any duplicate group names. I doubt Sonos allows duplicate speaker names,
# but we do initially get duplicated group names with the loop above
return list(dict(menu_entries).items())

View File

@ -1,304 +0,0 @@
# -*- coding: utf-8 -*-
####
# 01/2011 Bernd Schlapsi <brot@gmx.info>
#
# This script is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# gPodder 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/>.
#
# Dependencies:
# * python-mutagen (Mutagen is a Python module to handle audio metadata)
#
# This extension script adds episode title and podcast title to the audio file
# The episode title is written into the title tag
# The podcast title is written into the album tag
import base64
import datetime
import logging
import mimetypes
from mutagen import File
from mutagen.easyid3 import EasyID3
from mutagen.easymp4 import EasyMP4Tags
from mutagen.flac import Picture
from mutagen.id3 import APIC, ID3
from mutagen.mp3 import MP3, EasyMP3
from mutagen.mp4 import MP4Cover, MP4Tags
import gpodder
from gpodder import coverart
logger = logging.getLogger(__name__)
# workaround for https://github.com/quodlibet/mutagen/issues/334
# can't add_tags to MP4 when file has no tag
MP4Tags._padding = 0
_ = gpodder.gettext
__title__ = _('Tag downloaded files using Mutagen')
__description__ = _('Add episode and podcast titles to MP3/OGG tags')
__authors__ = 'Bernd Schlapsi <brot@gmx.info>'
__doc__ = 'https://gpodder.github.io/docs/extensions/tagging.html'
__payment__ = 'https://flattr.com/submit/auto?user_id=BerndSch&url=http://wiki.gpodder.org/wiki/Extensions/Tagging'
__category__ = 'post-download'
DefaultConfig = {
'strip_album_from_title': True,
'genre_tag': 'Podcast',
'always_remove_tags': False,
'auto_embed_coverart': False,
'set_artist_to_album': False,
'set_version': 4,
'modify_tags': True,
'remove_before_modify': False
}
class AudioFile(object):
def __init__(self, filename, album, title, subtitle, genre, pubDate, cover):
self.filename = filename
self.album = album
self.title = title
self.subtitle = subtitle
self.genre = genre
self.pubDate = pubDate
self.cover = cover
def remove_tags(self):
audio = File(self.filename, easy=True)
if audio.tags is not None:
audio.delete()
audio.save()
def write_basic_tags(self, remove_before_modify, modify_tags, set_artist_to_album, set_version):
audio = File(self.filename, easy=True)
if audio is None:
logger.warning("Unable to add tags to file '%s'", self.filename)
return
if audio.tags is None:
audio.add_tags()
if modify_tags:
if remove_before_modify:
audio.delete()
if self.album is not None:
audio.tags['album'] = self.album
if self.title is not None:
audio.tags['title'] = self.title
if self.subtitle is not None:
audio.tags['subtitle'] = self.subtitle
if self.subtitle is not None:
audio.tags['comments'] = self.subtitle
if self.genre is not None:
audio.tags['genre'] = self.genre
if self.pubDate is not None:
audio.tags['date'] = self.pubDate
if set_artist_to_album:
audio.tags['artist'] = self.album
if type(audio) is EasyMP3:
audio.save(v2_version=set_version)
else:
# Not actually audio
audio.save()
def insert_coverart(self):
""" implement the cover art logic in the subclass
"""
None
def get_cover_picture(self, cover):
""" Returns mutagen Picture class for the cover image
Useful for OGG and FLAC format
Picture type = cover image
see http://flac.sourceforge.net/documentation_tools_flac.html#encoding_options
"""
f = file(cover)
p = Picture()
p.type = 3
p.data = f.read()
p.mime = mimetypes.guess_type(cover)[0]
f.close()
return p
class OggFile(AudioFile):
def __init__(self, filename, album, title, subtitle, genre, pubDate, cover):
super(OggFile, self).__init__(filename, album, title, subtitle, genre, pubDate, cover)
def insert_coverart(self):
audio = File(self.filename, easy=True)
p = self.get_cover_picture(self.cover)
audio['METADATA_BLOCK_PICTURE'] = base64.b64encode(p.write())
audio.save()
class Mp4File(AudioFile):
def __init__(self, filename, album, title, subtitle, genre, pubDate, cover):
super(Mp4File, self).__init__(filename, album, title, subtitle, genre, pubDate, cover)
def insert_coverart(self):
audio = File(self.filename)
if self.cover.endswith('png'):
cover_format = MP4Cover.FORMAT_PNG
else:
cover_format = MP4Cover.FORMAT_JPEG
data = open(self.cover, 'rb').read()
audio.tags['covr'] = [MP4Cover(data, cover_format)]
audio.save()
class Mp3File(AudioFile):
def __init__(self, filename, album, title, subtitle, genre, pubDate, cover):
super(Mp3File, self).__init__(filename, album, title, subtitle, genre, pubDate, cover)
def insert_coverart(self):
audio = MP3(self.filename, ID3=ID3)
if audio.tags is None:
audio.add_tags()
audio.tags.add(
APIC(
encoding=3, # 3 is for utf-8
mime=mimetypes.guess_type(self.cover)[0],
type=3,
desc='Cover',
data=open(self.cover, 'rb').read()
)
)
audio.save()
class gPodderExtension:
def __init__(self, container):
self.container = container
# fix #737 EasyID3 doesn't recognize subtitle and comment tags
EasyID3.RegisterTextKey("comments", "COMM")
EasyID3.RegisterTextKey("subtitle", "TIT3")
EasyMP4Tags.RegisterTextKey("comments", "desc")
EasyMP4Tags.RegisterFreeformKey("subtitle", "SUBTITLE")
def on_episode_downloaded(self, episode):
info = self.read_episode_info(episode)
if info['filename'] is None:
return
self.write_info2file(info, episode)
def get_audio(self, info, episode):
audio = None
cover = None
audioClass = None
if self.container.config.auto_embed_coverart:
cover = self.get_cover(episode.channel)
if info['filename'].endswith('.mp3'):
audioClass = Mp3File
elif info['filename'].endswith('.ogg'):
audioClass = OggFile
elif info['filename'].endswith('.m4a') or info['filename'].endswith('.mp4'):
audioClass = Mp4File
elif File(info['filename'], easy=True):
# mutagen can work with it: at least add basic tags
audioClass = AudioFile
if audioClass:
audio = audioClass(info['filename'],
info['album'],
info['title'],
info['subtitle'],
info['genre'],
info['pubDate'],
cover)
return audio
def read_episode_info(self, episode):
info = {
'filename': None,
'album': None,
'title': None,
'subtitle': None,
'genre': None,
'pubDate': None
}
# read filename (incl. file path) from gPodder database
info['filename'] = episode.local_filename(create=False, check_only=True)
if info['filename'] is None:
return
# read title+album from gPodder database
info['album'] = episode.channel.title
title = episode.title
if (self.container.config.strip_album_from_title and title and info['album'] and title.startswith(info['album'])):
info['title'] = title[len(info['album']):].lstrip()
else:
info['title'] = title
info['subtitle'] = episode._text_description
if self.container.config.genre_tag is not None:
info['genre'] = self.container.config.genre_tag
# convert pubDate to string
try:
pubDate = datetime.datetime.fromtimestamp(episode.pubDate)
info['pubDate'] = pubDate.strftime('%Y-%m-%d %H:%M')
except:
try:
# since version 3 the published date has a new/other name
pubDate = datetime.datetime.fromtimestamp(episode.published)
info['pubDate'] = pubDate.strftime('%Y-%m-%d %H:%M')
except:
info['pubDate'] = None
return info
def write_info2file(self, info, episode):
audio = self.get_audio(info, episode)
if self.container.config.always_remove_tags:
audio.remove_tags()
else:
audio.write_basic_tags(self.container.config.remove_before_modify,
self.container.config.modify_tags,
self.container.config.set_artist_to_album,
self.container.config.set_version)
if self.container.config.auto_embed_coverart:
audio.insert_coverart()
logger.info('tagging.on_episode_downloaded(%s/%s)', episode.channel.title, episode.title)
def get_cover(self, podcast):
downloader = coverart.CoverDownloader()
return downloader.get_cover(podcast.cover_file, podcast.cover_url,
podcast.url, podcast.title, None, None, True)

View File

@ -1,215 +0,0 @@
# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client
# Copyright (c) 2005-2018 The gPodder Team
#
# gPodder 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.
#
# gPodder 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/>.
#
# Windows 7 taskbar progress
# Sean Munkel; 2013-01-05
import ctypes
import functools
import logging
from ctypes import (HRESULT, POINTER, Structure, alignment, c_int, c_uint,
c_ulong, c_ulonglong, c_ushort, c_wchar_p, sizeof)
from ctypes.wintypes import tagRECT
from comtypes import COMMETHOD, GUID, IUnknown, client, wireHWND
import gpodder
import gi # isort:skip
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk # isort:skip
_ = gpodder.gettext
__title__ = _('Show download progress on the taskbar')
__description__ = _('Displays the progress on the Windows taskbar.')
__authors__ = 'Sean Munkel <seanmunkel@gmail.com>'
__category__ = 'desktop-integration'
__only_for__ = 'win32'
logger = logging.getLogger(__name__)
WSTRING = c_wchar_p
# values for enumeration 'TBPFLAG'
TBPF_NOPROGRESS = 0
TBPF_INDETERMINATE = 1
TBPF_NORMAL = 2
TBPF_ERROR = 4
TBPF_PAUSED = 8
TBPFLAG = c_int # enum
# values for enumeration 'TBATFLAG'
TBATF_USEMDITHUMBNAIL = 1
TBATF_USEMDILIVEPREVIEW = 2
TBATFLAG = c_int # enum
# return code
S_OK = HRESULT(0).value
class tagTHUMBBUTTON(Structure):
_fields_ = [
('dwMask', c_ulong),
('iId', c_uint),
('iBitmap', c_uint),
('hIcon', POINTER(IUnknown)),
('szTip', c_ushort * 260),
('dwFlags', c_ulong)]
class ITaskbarList(IUnknown):
_case_insensitive_ = True
_iid_ = GUID('{56FDF342-FD6D-11D0-958A-006097C9A090}')
_idlflags_ = []
_methods_ = [
COMMETHOD([], HRESULT, 'HrInit'),
COMMETHOD([], HRESULT, 'AddTab',
(['in'], c_int, 'hwnd')),
COMMETHOD([], HRESULT, 'DeleteTab',
(['in'], c_int, 'hwnd')),
COMMETHOD([], HRESULT, 'ActivateTab',
(['in'], c_int, 'hwnd')),
COMMETHOD([], HRESULT, 'SetActivateAlt',
(['in'], c_int, 'hwnd'))]
class ITaskbarList2(ITaskbarList):
_case_insensitive_ = True
_iid_ = GUID('{602D4995-B13A-429B-A66E-1935E44F4317}')
_idlflags_ = []
_methods_ = [
COMMETHOD([], HRESULT, 'MarkFullscreenWindow',
(['in'], c_int, 'hwnd'),
(['in'], c_int, 'fFullscreen'))]
class ITaskbarList3(ITaskbarList2):
_case_insensitive_ = True
_iid_ = GUID('{EA1AFB91-9E28-4B86-90E9-9E9F8A5EEFAF}')
_idlflags_ = []
_methods_ = [
COMMETHOD([], HRESULT, 'SetProgressValue',
(['in'], c_int, 'hwnd'),
(['in'], c_ulonglong, 'ullCompleted'),
(['in'], c_ulonglong, 'ullTotal')),
COMMETHOD([], HRESULT, 'SetProgressState',
(['in'], c_int, 'hwnd'),
(['in'], TBPFLAG, 'tbpFlags')),
COMMETHOD([], HRESULT, 'RegisterTab',
(['in'], c_int, 'hwndTab'),
(['in'], wireHWND, 'hwndMDI')),
COMMETHOD([], HRESULT, 'UnregisterTab',
(['in'], c_int, 'hwndTab')),
COMMETHOD([], HRESULT, 'SetTabOrder',
(['in'], c_int, 'hwndTab'),
(['in'], c_int, 'hwndInsertBefore')),
COMMETHOD([], HRESULT, 'SetTabActive',
(['in'], c_int, 'hwndTab'),
(['in'], c_int, 'hwndMDI'),
(['in'], TBATFLAG, 'tbatFlags')),
COMMETHOD([], HRESULT, 'ThumbBarAddButtons',
(['in'], c_int, 'hwnd'),
(['in'], c_uint, 'cButtons'),
(['in'], POINTER(tagTHUMBBUTTON), 'pButton')),
COMMETHOD([], HRESULT, 'ThumbBarUpdateButtons',
(['in'], c_int, 'hwnd'),
(['in'], c_uint, 'cButtons'),
(['in'], POINTER(tagTHUMBBUTTON), 'pButton')),
COMMETHOD([], HRESULT, 'ThumbBarSetImageList',
(['in'], c_int, 'hwnd'),
(['in'], POINTER(IUnknown), 'himl')),
COMMETHOD([], HRESULT, 'SetOverlayIcon',
(['in'], c_int, 'hwnd'),
(['in'], POINTER(IUnknown), 'hIcon'),
(['in'], WSTRING, 'pszDescription')),
COMMETHOD([], HRESULT, 'SetThumbnailTooltip',
(['in'], c_int, 'hwnd'),
(['in'], WSTRING, 'pszTip')),
COMMETHOD([], HRESULT, 'SetThumbnailClip',
(['in'], c_int, 'hwnd'),
(['in'], POINTER(tagRECT), 'prcClip'))]
assert sizeof(tagTHUMBBUTTON) in [540, 552], sizeof(tagTHUMBBUTTON)
assert alignment(tagTHUMBBUTTON) in [4, 8], alignment(tagTHUMBBUTTON)
def consume_events():
""" consume pending events """
while Gtk.events_pending():
Gtk.main_iteration()
# based on http://stackoverflow.com/a/1744503/905256
class gPodderExtension:
def __init__(self, container):
self.container = container
self.window_handle = None
self.restart_warning = True
def on_load(self):
self.taskbar = client.CreateObject(
'{56FDF344-FD6D-11d0-958A-006097C9A090}',
interface=ITaskbarList3)
ret = self.taskbar.HrInit()
if ret != S_OK:
logger.warning("taskbar.HrInit failed: %r", ret)
del self.taskbar
def on_unload(self):
# let the window change state? otherwise gpodder is stuck on exit
# (tested on windows 7 pro)
consume_events()
if self.taskbar is not None:
self.taskbar.SetProgressState(self.window_handle, TBPF_NOPROGRESS)
# let the taskbar change state otherwise gpodder is stuck on exit
# (tested on windows 7 pro)
consume_events()
def on_ui_object_available(self, name, ui_object):
def callback(self, window, *args):
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object]
win_gpointer = ctypes.pythonapi.PyCapsule_GetPointer(window.get_window().__gpointer__, None)
gdkdll = ctypes.CDLL("libgdk-3-0.dll")
self.window_handle = gdkdll.gdk_win32_window_get_handle(win_gpointer)
ret = self.taskbar.ActivateTab(self.window_handle)
if ret != S_OK:
logger.warning("taskbar.ActivateTab failed: %r", ret)
del self.taskbar
if name == 'gpodder-gtk':
ui_object.main_window.connect('realize',
functools.partial(callback, self))
def on_download_progress(self, progress):
if not self.taskbar:
return
if self.window_handle is None:
if not self.restart_warning:
return
logger.warning("No window handle available, a restart max fix this")
self.restart_warning = False
return
if 0 < progress < 1:
self.taskbar.SetProgressState(self.window_handle, TBPF_NORMAL)
self.taskbar.SetProgressValue(self.window_handle,
int(progress * 100), 100)
else:
self.taskbar.SetProgressState(self.window_handle, TBPF_NOPROGRESS)

View File

@ -1,113 +0,0 @@
# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4
import json
import logging
import os
import re
from datetime import timedelta
import gpodder
from gpodder import util
logger = logging.getLogger(__name__)
_ = gpodder.gettext
__title__ = _('Subtitle Downloader for TED Talks')
__description__ = _('Downloads .srt subtitles for TED Talks Videos')
__authors__ = 'Danilo Shiga <daniloshiga@gmail.com>'
__category__ = 'post-download'
__only_for__ = 'gtk, cli'
class gPodderExtension(object):
"""
TED Subtitle Download Extension
Downloads ted subtitles
"""
def __init__(self, container):
self.container = container
def milli_to_srt(self, time):
"""Converts milliseconds to srt time format"""
srt_time = timedelta(milliseconds=time)
srt_time = str(srt_time)
if '.' in srt_time:
srt_time = srt_time.replace('.', ',')[:11]
else:
# ',000' required to be a valid srt line
srt_time += ',000'
return srt_time
def ted_to_srt(self, jsonstring, introduration):
"""Converts the json object to srt format"""
jsonobject = json.loads(jsonstring)
srtContent = ''
for captionIndex, caption in enumerate(jsonobject['captions'], 1):
startTime = self.milli_to_srt(introduration + caption['startTime'])
endTime = self.milli_to_srt(introduration + caption['startTime']
+ caption['duration'])
srtContent += ''.join([str(captionIndex), os.linesep, startTime,
' --> ', endTime, os.linesep,
caption['content'], os.linesep * 2])
return srtContent
def get_data_from_url(self, url):
try:
response = util.urlopen(url).read()
except Exception as e:
logger.warning("subtitle url returned error %s", e)
return ''
return response
def get_srt_filename(self, audio_filename):
basename, _ = os.path.splitext(audio_filename)
return basename + '.srt'
def on_episode_downloaded(self, episode):
guid_result = re.search(r'talk.ted.com:(\d+)', episode.guid)
if guid_result is not None:
talkId = int(guid_result.group(1))
else:
logger.debug('Not a TED Talk. Ignoring.')
return
sub_url = 'http://www.ted.com/talks/subtitles/id/%s/lang/eng' % talkId
logger.info('subtitle url: %s', sub_url)
sub_data = self.get_data_from_url(sub_url)
if not sub_data:
return
logger.info('episode url: %s', episode.link)
episode_data = self.get_data_from_url(episode.link)
if not episode_data:
return
INTRO_DEFAULT = 15
try:
# intro in the data could be 15 or 15.33
intro = episode_data
intro = episode_data.split('introDuration":')[1] \
.split(',')[0] or INTRO_DEFAULT
intro = int(float(intro) * 1000)
except (ValueError, IndexError) as e:
logger.info("Couldn't parse introDuration string: %s", intro)
intro = INTRO_DEFAULT * 1000
current_filename = episode.local_filename(create=False)
srt_filename = self.get_srt_filename(current_filename)
sub = self.ted_to_srt(sub_data, int(intro))
try:
with open(srt_filename, 'w+') as srtFile:
srtFile.write(sub.encode("utf-8"))
except Exception as e:
logger.warning("Can't write srt file: %s", e)
def on_episode_delete(self, episode, filename):
srt_filename = self.get_srt_filename(filename)
if os.path.exists(srt_filename):
os.remove(srt_filename)

View File

@ -1,73 +0,0 @@
# -*- coding: utf-8 -*-
# Ubuntu AppIndicator Icon
# Thomas Perl <thp@gpodder.org>; 2012-02-24
import logging
from gi.repository import AppIndicator3 as appindicator
from gi.repository import Gtk
import gpodder
_ = gpodder.gettext
__title__ = _('Ubuntu App Indicator')
__description__ = _('Show a status indicator in the top bar.')
__authors__ = 'Thomas Perl <thp@gpodder.org>'
__category__ = 'desktop-integration'
__only_for__ = 'gtk'
__mandatory_in__ = 'unity'
__disable_in__ = 'win32'
logger = logging.getLogger(__name__)
DefaultConfig = {
'visible': True, # Set to False if you don't want to show the appindicator
}
class gPodderExtension:
def __init__(self, container):
self.container = container
self.config = container.config
self.indicator = None
self.gpodder = None
def on_load(self):
if self.config.visible:
self.indicator = appindicator.Indicator.new('gpodder', 'gpodder',
appindicator.IndicatorCategory.APPLICATION_STATUS)
self.indicator.set_status(appindicator.IndicatorStatus.ACTIVE)
def _rebuild_menu(self):
menu = Gtk.Menu()
toggle_visible = Gtk.CheckMenuItem(_('Show main window'))
toggle_visible.set_active(True)
def on_toggle_visible(menu_item):
if menu_item.get_active():
self.gpodder.main_window.show()
else:
self.gpodder.main_window.hide()
toggle_visible.connect('activate', on_toggle_visible)
menu.append(toggle_visible)
menu.append(Gtk.SeparatorMenuItem())
quit_gpodder = Gtk.MenuItem(_('Quit'))
def on_quit(menu_item):
self.gpodder.on_gPodder_delete_event(self.gpodder.main_window)
quit_gpodder.connect('activate', on_quit)
menu.append(quit_gpodder)
menu.show_all()
self.indicator.set_menu(menu)
def on_unload(self):
self.indicator = None
def on_ui_object_available(self, name, ui_object):
if name == 'gpodder-gtk':
self.gpodder = ui_object
self._rebuild_menu()

View File

@ -1,58 +0,0 @@
# -*- coding: utf-8 -*-
# Ubuntu Unity Launcher Integration
# Thomas Perl <thp@gpodder.org>; 2012-02-06
import logging
import gpodder
import gi # isort:skip
gi.require_version('Unity', '7.0') # isort:skip
from gi.repository import GLib, Unity # isort:skip
_ = gpodder.gettext
logger = logging.getLogger(__name__)
__title__ = _('Ubuntu Unity Integration')
__description__ = _('Show download progress in the Unity Launcher icon.')
__authors__ = 'Thomas Perl <thp@gpodder.org>'
__category__ = 'desktop-integration'
__only_for__ = 'unity'
__mandatory_in__ = 'unity'
__disable_in__ = 'win32'
class LauncherEntry:
FILENAME = 'gpodder.desktop'
def __init__(self):
self.launcher = Unity.LauncherEntry.get_for_desktop_id(
self.FILENAME)
def set_count(self, count):
self.launcher.set_property('count', count)
self.launcher.set_property('count_visible', count > 0)
def set_progress(self, progress):
self.launcher.set_property('progress', progress)
self.launcher.set_property('progress_visible', 0. <= progress < 1.)
class gPodderExtension:
FILENAME = 'gpodder.desktop'
def __init__(self, container):
self.container = container
self.launcher_entry = None
def on_load(self):
logger.info('Starting Ubuntu Unity Integration.')
self.launcher_entry = LauncherEntry()
def on_unload(self):
self.launcher_entry = None
def on_download_progress(self, progress):
GLib.idle_add(self.launcher_entry.set_progress, float(progress))

View File

@ -1,36 +0,0 @@
# -*- coding: utf-8 -*-
# Starts episode update search on startup
#
# (c) 2012-10-13 Bernd Schlapsi <brot@gmx.info>
# Released under the same license terms as gPodder itself.
import logging
import gpodder
logger = logging.getLogger(__name__)
_ = gpodder.gettext
__title__ = _('Search for new episodes on startup')
__description__ = _('Starts the search for new episodes on startup')
__authors__ = 'Bernd Schlapsi <brot@gmx.info>'
__doc__ = 'https://gpodder.github.io/docs/extensions/searchepisodeonstartup.html'
__payment__ = 'https://flattr.com/submit/auto?user_id=BerndSch&url=http://wiki.gpodder.org/wiki/Extensions/SearchEpisodeOnStartup'
__category__ = 'interface'
__only_for__ = 'gtk'
class gPodderExtension:
def __init__(self, container):
self.container = container
self.config = self.container.config
self.gpodder = None
def on_ui_object_available(self, name, ui_object):
if name == 'gpodder-gtk':
self.gpodder = ui_object
def on_find_partial_downloads_done(self):
if self.gpodder:
self.gpodder.update_feed_cache()

View File

@ -1,122 +0,0 @@
# -*- coding: utf-8 -*-
# Convertes video files to avi or mp4
# This requires ffmpeg to be installed. Also works as a context
# menu item for already-downloaded files.
#
# (c) 2011-08-05 Thomas Perl <thp.io/about>
# Released under the same license terms as gPodder itself.
import logging
import os
import subprocess
import gpodder
from gpodder import util
logger = logging.getLogger(__name__)
_ = gpodder.gettext
__title__ = _('Convert video files')
__description__ = _('Transcode video files to avi/mp4/m4v')
__authors__ = 'Thomas Perl <thp@gpodder.org>, Bernd Schlapsi <brot@gmx.info>'
__doc__ = 'https://gpodder.github.io/docs/extensions/videoconverter.html'
__payment__ = 'https://flattr.com/submit/auto?user_id=BerndSch&url=http://wiki.gpodder.org/wiki/Extensions/VideoConverter'
__category__ = 'post-download'
DefaultConfig = {
'output_format': 'mp4', # At the moment we support/test only mp4, m4v and avi
'context_menu': True, # Show the conversion option in the context menu
}
class gPodderExtension:
MIME_TYPES = ('video/mp4', 'video/m4v', 'video/x-flv', )
EXT = ('.mp4', '.m4v', '.flv', )
CMD = {'avconv': ['-i', '%(old_file)s', '-codec', 'copy', '%(new_file)s'],
'ffmpeg': ['-i', '%(old_file)s', '-codec', 'copy', '%(new_file)s']
}
def __init__(self, container):
self.container = container
self.config = self.container.config
# Dependency checks
self.command = self.container.require_any_command(['avconv', 'ffmpeg'])
# extract command without extension (.exe on Windows) from command-string
command_without_ext = os.path.basename(os.path.splitext(self.command)[0])
self.command_param = self.CMD[command_without_ext]
def on_episode_downloaded(self, episode):
self._convert_episode(episode)
def _get_new_extension(self):
ext = self.config.output_format
if not ext.startswith('.'):
ext = '.' + ext
return ext
def _check_source(self, episode):
if episode.extension() == self._get_new_extension():
return False
if episode.mime_type in self.MIME_TYPES:
return True
# Also check file extension (bug 1770)
if episode.extension() in self.EXT:
return True
return False
def on_episodes_context_menu(self, episodes):
if not self.config.context_menu:
return None
if not all(e.was_downloaded(and_exists=True) for e in episodes):
return None
if not any(self._check_source(episode) for episode in episodes):
return None
menu_item = _('Convert to %(format)s') % {'format': self.config.output_format}
return [(menu_item, self._convert_episodes)]
def _convert_episode(self, episode):
if not self._check_source(episode):
return
new_extension = self._get_new_extension()
old_filename = episode.local_filename(create=False)
filename, old_extension = os.path.splitext(old_filename)
new_filename = filename + new_extension
cmd = [self.command] + \
[param % {'old_file': old_filename, 'new_file': new_filename}
for param in self.command_param]
if gpodder.ui.win32:
ffmpeg = util.Popen(cmd)
ffmpeg.wait()
stdout, stderr = ("<unavailable>",) * 2
else:
ffmpeg = util.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = ffmpeg.communicate()
if ffmpeg.returncode == 0:
util.rename_episode_file(episode, new_filename)
os.remove(old_filename)
logger.info('Converted video file to %(format)s.' % {'format': self.config.output_format})
gpodder.user_extensions.on_notification_show(_('File converted'), episode.title)
else:
logger.warning('Error converting video file: %s / %s', stdout, stderr)
gpodder.user_extensions.on_notification_show(_('Conversion failed'), episode.title)
def _convert_episodes(self, episodes):
for episode in episodes:
self._convert_episode(episode)

View File

@ -1,609 +0,0 @@
# -*- coding: utf-8 -*-
# Manage YouTube subscriptions using youtube-dl (https://github.com/ytdl-org/youtube-dl)
# Requirements: youtube-dl module (pip install youtube_dl)
# (c) 2019-08-17 Eric Le Lay <elelay.fr:contact>
# Released under the same license terms as gPodder itself.
import logging
import os
import re
import sys
import time
from collections.abc import Iterable
try:
import yt_dlp as youtube_dl
program_name = 'yt-dlp'
want_ytdl_version = '2023.06.22'
except:
import youtube_dl
program_name = 'youtube-dl'
want_ytdl_version = '2023.02.17' # youtube-dl has been patched, but not yet released
import gpodder
from gpodder import download, feedcore, model, registry, util, youtube
import gi # isort:skip
gi.require_version('Gtk', '3.0') # isort:skip
from gi.repository import Gtk # isort:skip
_ = gpodder.gettext
logger = logging.getLogger(__name__)
__title__ = 'youtube-dl'
__description__ = _('Manage YouTube subscriptions using youtube-dl (pip install youtube_dl) or yt-dlp (pip install yt-dlp)')
__only_for__ = 'gtk, cli'
__authors__ = 'Eric Le Lay <elelay.fr:contact>'
__doc__ = 'https://gpodder.github.io/docs/extensions/youtubedl.html'
want_ytdl_version_msg = _('Your version of youtube-dl/yt-dlp %(have_version)s has known issues, please upgrade to %(want_version)s or newer.')
DefaultConfig = {
# youtube-dl downloads and parses each video page to get information about it, which is very slow.
# Set to False to fall back to the fast but limited (only 15 episodes) gpodder code
'manage_channel': True,
# If for some reason youtube-dl download doesn't work for you, you can fallback to gpodder code.
# Set to False to fall back to default gpodder code (less available formats).
'manage_downloads': True,
# Embed all available subtitles to downloaded videos. Needs ffmpeg.
'embed_subtitles': False,
}
# youtube feed still preprocessed by youtube.py (compat)
CHANNEL_RE = re.compile(r'''https://www.youtube.com/feeds/videos.xml\?channel_id=(.+)''')
USER_RE = re.compile(r'''https://www.youtube.com/feeds/videos.xml\?user=(.+)''')
PLAYLIST_RE = re.compile(r'''https://www.youtube.com/feeds/videos.xml\?playlist_id=(.+)''')
def youtube_parsedate(s):
"""Parse a string into a unix timestamp
Only strings provided by youtube-dl API are
parsed with this function (20170920).
"""
if s:
return time.mktime(time.strptime(s, "%Y%m%d"))
return 0
def video_guid(video_id):
"""
generate same guid as youtube
"""
return 'yt:video:{}'.format(video_id)
class YoutubeCustomDownload(download.CustomDownload):
"""
Represents the download of a single episode using youtube-dl.
Actual youtube-dl interaction via gPodderYoutubeDL.
"""
def __init__(self, ytdl, url, episode):
self._ytdl = ytdl
self._url = url
self._reporthook = None
self._prev_dl_bytes = 0
self._episode = episode
self._partial_filename = None
@property
def partial_filename(self):
return self._partial_filename
@partial_filename.setter
def partial_filename(self, val):
self._partial_filename = val
def retrieve_resume(self, tempname, reporthook=None):
"""
called by download.DownloadTask to perform the download.
"""
self._reporthook = reporthook
# outtmpl: use given tempname by DownloadTask
# (escape % because outtmpl used as a string template by youtube-dl)
outtmpl = tempname.replace('%', '%%')
info, opts = self._ytdl.fetch_info(self._url, outtmpl, self._my_hook)
if program_name == 'yt-dlp':
default = opts['outtmpl']['default'] if isinstance(opts['outtmpl'], dict) else opts['outtmpl']
self.partial_filename = os.path.join(opts['paths']['home'], default) % info
elif program_name == 'youtube-dl':
self.partial_filename = opts['outtmpl'] % info
res = self._ytdl.fetch_video(info, opts)
if program_name == 'yt-dlp':
# yt-dlp downloads to whatever file name it wants, so rename
filepath = res.get('requested_downloads', [{}])[0].get('filepath')
if filepath is None:
raise Exception("Could not determine youtube-dl output file")
if filepath != tempname:
logger.debug('yt-dlp downloaded to "%s" instead of "%s", moving',
os.path.basename(filepath),
os.path.basename(tempname))
os.remove(tempname)
os.rename(filepath, tempname)
if 'duration' in res and res['duration']:
self._episode.total_time = res['duration']
headers = {}
# youtube-dl doesn't return a content-type but an extension
if 'ext' in res:
dot_ext = '.{}'.format(res['ext'])
if program_name == 'youtube-dl':
# See #673 when merging multiple formats, the extension is appended to the tempname
# by youtube-dl resulting in empty .partial file + .partial.mp4 exists
# and #796 .mkv is chosen by ytdl sometimes
for try_ext in (dot_ext, ".mp4", ".m4a", ".webm", ".mkv"):
tempname_with_ext = tempname + try_ext
if os.path.isfile(tempname_with_ext):
logger.debug('youtube-dl downloaded to "%s" instead of "%s", moving',
os.path.basename(tempname_with_ext),
os.path.basename(tempname))
os.remove(tempname)
os.rename(tempname_with_ext, tempname)
dot_ext = try_ext
break
ext_filetype = util.mimetype_from_extension(dot_ext)
if ext_filetype:
# YouTube weba formats have a webm extension and get a video/webm mime-type
# but audio content has no width or height, so change it to audio/webm for correct icon and player
if ext_filetype.startswith('video/') and ('height' not in res or res['height'] is None):
ext_filetype = ext_filetype.replace('video/', 'audio/')
headers['content-type'] = ext_filetype
return headers, res.get('url', self._url)
def _my_hook(self, d):
if d['status'] == 'downloading':
if self._reporthook:
dl_bytes = d['downloaded_bytes']
total_bytes = d.get('total_bytes') or d.get('total_bytes_estimate') or 0
self._reporthook(self._prev_dl_bytes + dl_bytes,
1,
self._prev_dl_bytes + total_bytes)
elif d['status'] == 'finished':
dl_bytes = d['downloaded_bytes']
self._prev_dl_bytes += dl_bytes
if self._reporthook:
self._reporthook(self._prev_dl_bytes, 1, self._prev_dl_bytes)
elif d['status'] == 'error':
logger.error('download hook error: %r', d)
else:
logger.debug('unknown download hook status: %r', d)
class YoutubeFeed(model.Feed):
"""
Represents the youtube feed for model.PodcastChannel
"""
def __init__(self, url, cover_url, description, max_episodes, ie_result, downloader):
self._url = url
self._cover_url = cover_url
self._description = description
self._max_episodes = max_episodes
ie_result['entries'] = self._process_entries(ie_result.get('entries', []))
self._ie_result = ie_result
self._downloader = downloader
def _process_entries(self, entries):
filtered_entries = []
seen_guids = set()
for i, e in enumerate(entries): # consumes the generator!
if e.get('_type', 'video') in ('url', 'url_transparent') and e.get('ie_key') == 'Youtube':
guid = video_guid(e['id'])
e['guid'] = guid
if guid in seen_guids:
logger.debug('dropping already seen entry %s title="%s"', guid, e.get('title'))
else:
filtered_entries.append(e)
seen_guids.add(guid)
else:
logger.debug('dropping entry not youtube video %r', e)
if len(filtered_entries) == self._max_episodes:
# entries is a generator: stopping now prevents it to download more pages
logger.debug('stopping entry enumeration')
break
return filtered_entries
def get_title(self):
return '{} (YouTube)'.format(self._ie_result.get('title') or self._ie_result.get('id') or self._url)
def get_link(self):
return self._ie_result.get('webpage_url')
def get_description(self):
return self._description
def get_cover_url(self):
return self._cover_url
def get_http_etag(self):
""" :return str: optional -- last HTTP etag header, for conditional request next time """
# youtube-dl doesn't provide it!
return None
def get_http_last_modified(self):
""" :return str: optional -- last HTTP Last-Modified header, for conditional request next time """
# youtube-dl doesn't provide it!
return None
def get_new_episodes(self, channel, existing_guids):
# entries are already sorted by decreasing date
# trim guids to max episodes
entries = [e for i, e in enumerate(self._ie_result['entries'])
if not self._max_episodes or i < self._max_episodes]
all_seen_guids = set(e['guid'] for e in entries)
# only fetch new ones from youtube since they are so slow to get
new_entries = [e for e in entries if e['guid'] not in existing_guids]
logger.debug('%i/%i new entries', len(new_entries), len(all_seen_guids))
self._ie_result['entries'] = new_entries
self._downloader.refresh_entries(self._ie_result)
# episodes from entries
episodes = []
for en in self._ie_result['entries']:
guid = video_guid(en['id'])
if en.get('ext'):
mime_type = util.mimetype_from_extension('.{}'.format(en['ext']))
else:
mime_type = 'application/octet-stream'
if en.get('filesize'):
filesize = int(en['filesize'] or 0)
else:
filesize = sum(int(f.get('filesize') or 0)
for f in en.get('requested_formats', []))
ep = {
'title': en.get('title', guid),
'link': en.get('webpage_url'),
'episode_art_url': en.get('thumbnail'),
'description': util.remove_html_tags(en.get('description') or ''),
'description_html': '',
'url': en.get('webpage_url'),
'file_size': filesize,
'mime_type': mime_type,
'guid': guid,
'published': youtube_parsedate(en.get('upload_date', None)),
'total_time': int(en.get('duration') or 0),
}
episode = channel.episode_factory(ep)
episode.save()
episodes.append(episode)
return episodes, all_seen_guids
def get_next_page(self, channel, max_episodes):
"""
Paginated feed support (RFC 5005).
If the feed is paged, return the next feed page.
Returned page will in turn be asked for the next page, until None is returned.
:return feedcore.Result: the next feed's page,
as a fully parsed Feed or None
"""
return None
class gPodderYoutubeDL(download.CustomDownloader):
def __init__(self, gpodder_config, my_config, force=False):
"""
:param force: force using this downloader even if config says don't manage downloads
"""
self.gpodder_config = gpodder_config
self.my_config = my_config
self.force = force
# cachedir is not much used in youtube-dl, but set it anyway
cachedir = os.path.join(gpodder.home, 'youtube-dl')
os.makedirs(cachedir, exist_ok=True)
self._ydl_opts = {
'cachedir': cachedir,
'noprogress': True, # prevent progress bar from appearing in console
}
# prevent escape codes in desktop notifications on errors
if program_name == 'yt-dlp':
self._ydl_opts['color'] = 'no_color'
else:
self._ydl_opts['no_color'] = True
if gpodder.verbose:
self._ydl_opts['verbose'] = True
else:
self._ydl_opts['quiet'] = True
# Don't create downloaders for URLs supported by these youtube-dl extractors
self.ie_blacklist = ["Generic"]
# Cache URL regexes from youtube-dl matches here, seed with youtube regex
self.regex_cache = [(re.compile(r'https://www.youtube.com/watch\?v=.+'),)]
# #686 on windows without a console, sys.stdout is None, causing exceptions
# when adding podcasts.
# See https://docs.python.org/3/library/sys.html#sys.__stderr__ Note
if not sys.stdout:
logger.debug('no stdout, setting youtube-dl logger')
self._ydl_opts['logger'] = logger
def add_format(self, gpodder_config, opts, fallback=None):
""" construct youtube-dl -f argument from configured format. """
# You can set a custom format or custom formats by editing the config for key
# `youtube.preferred_fmt_ids`
#
# It takes a list of format strings separated by comma: bestaudio, 18
# they are translated to youtube dl format bestaudio/18, meaning preferably
# the best audio quality (audio-only) and MP4 360p if it's not available.
#
# See https://github.com/ytdl-org/youtube-dl#format-selection for details
# about youtube-dl format specification.
fmt_ids = youtube.get_fmt_ids(gpodder_config.youtube, False)
opts['format'] = '/'.join(str(fmt) for fmt in fmt_ids)
if fallback:
opts['format'] += '/' + fallback
logger.debug('format=%s', opts['format'])
def fetch_info(self, url, tempname, reporthook):
subs = self.my_config.embed_subtitles
opts = {
'paths': {'home': os.path.dirname(tempname)},
# Postprocessing in yt-dlp breaks without ext
'outtmpl': (os.path.basename(tempname) if program_name == 'yt-dlp'
else tempname) + '.%(ext)s',
'nopart': True, # don't append .part (already .partial)
'retries': 3, # retry a few times
'progress_hooks': [reporthook], # to notify UI
'writesubtitles': subs,
'subtitleslangs': ['all'] if subs else [],
'postprocessors': [{'key': 'FFmpegEmbedSubtitle'}] if subs else [],
}
opts.update(self._ydl_opts)
self.add_format(self.gpodder_config, opts)
with youtube_dl.YoutubeDL(opts) as ydl:
info = ydl.extract_info(url, download=False)
return info, opts
def fetch_video(self, info, opts):
with youtube_dl.YoutubeDL(opts) as ydl:
return ydl.process_video_result(info, download=True)
def refresh_entries(self, ie_result):
# only interested in video metadata
opts = {
'skip_download': True, # don't download the video
'youtube_include_dash_manifest': False, # don't download the DASH manifest
}
self.add_format(self.gpodder_config, opts, fallback='18')
opts.update(self._ydl_opts)
new_entries = []
# refresh videos one by one to catch single videos blocked by youtube
for e in ie_result.get('entries', []):
tmp = {k: v for k, v in ie_result.items() if k != 'entries'}
tmp['entries'] = [e]
try:
with youtube_dl.YoutubeDL(opts) as ydl:
ydl.process_ie_result(tmp, download=False)
new_entries.extend(tmp.get('entries'))
except youtube_dl.utils.DownloadError as ex:
if ex.exc_info[0] == youtube_dl.utils.ExtractorError:
# for instance "This video contains content from xyz, who has blocked it on copyright grounds"
logger.warning('Skipping %s: %s', e.get('title', ''), ex.exc_info[1])
continue
logger.exception('Skipping %r: %s', tmp, ex.exc_info)
ie_result['entries'] = new_entries
def refresh(self, url, channel_url, max_episodes):
"""
Fetch a channel or playlist contents.
Doesn't yet fetch video entry information, so we only get the video id and title.
"""
# Duplicate a bit of the YoutubeDL machinery here because we only
# want to parse the channel/playlist first, not to fetch video entries.
# We call YoutubeDL.extract_info(process=False), so we
# have to call extract_info again ourselves when we get a result of type 'url'.
def extract_type(ie_result):
result_type = ie_result.get('_type', 'video')
if result_type not in ('url', 'playlist', 'multi_video'):
raise Exception('Unsuported result_type: {}'.format(result_type))
has_playlist = result_type in ('playlist', 'multi_video')
return result_type, has_playlist
opts = {
'youtube_include_dash_manifest': False, # only interested in video title and id
}
opts.update(self._ydl_opts)
with youtube_dl.YoutubeDL(opts) as ydl:
ie_result = ydl.extract_info(url, download=False, process=False)
result_type, has_playlist = extract_type(ie_result)
while not has_playlist:
if result_type in ('url', 'url_transparent'):
ie_result['url'] = youtube_dl.utils.sanitize_url(ie_result['url'])
if result_type == 'url':
logger.debug("extract_info(%s) to get the video list", ie_result['url'])
# We have to add extra_info to the results because it may be
# contained in a playlist
ie_result = ydl.extract_info(ie_result['url'],
download=False,
process=False,
ie_key=ie_result.get('ie_key'))
result_type, has_playlist = extract_type(ie_result)
cover_url = youtube.get_cover(channel_url) # youtube-dl doesn't provide the cover url!
description = youtube.get_channel_desc(channel_url) # youtube-dl doesn't provide the description!
return feedcore.Result(feedcore.UPDATED_FEED,
YoutubeFeed(url, cover_url, description, max_episodes, ie_result, self))
def fetch_channel(self, channel, max_episodes=0):
"""
called by model.gPodderFetcher to get a custom feed.
:returns feedcore.Result: a YoutubeFeed or None if channel is not a youtube channel or playlist
"""
if not self.my_config.manage_channel:
return None
url = None
m = CHANNEL_RE.match(channel.url)
if m:
url = 'https://www.youtube.com/channel/{}/videos'.format(m.group(1))
else:
m = USER_RE.match(channel.url)
if m:
url = 'https://www.youtube.com/user/{}/videos'.format(m.group(1))
else:
m = PLAYLIST_RE.match(channel.url)
if m:
url = 'https://www.youtube.com/playlist?list={}'.format(m.group(1))
if url:
logger.info('youtube-dl handling %s => %s', channel.url, url)
return self.refresh(url, channel.url, max_episodes)
return None
def is_supported_url(self, url):
if url is None:
return False
for i, res in enumerate(self.regex_cache):
if next(filter(None, (r.match(url) for r in res)), None) is not None:
if i > 0:
self.regex_cache.remove(res)
self.regex_cache.insert(0, res)
return True
with youtube_dl.YoutubeDL(self._ydl_opts) as ydl:
# youtube-dl returns a list, yt-dlp returns a dict
ies = ydl._ies
if isinstance(ydl._ies, dict):
ies = ydl._ies.values()
for ie in ies:
if ie.suitable(url) and ie.ie_key() not in self.ie_blacklist:
self.regex_cache.insert(
0, (ie._VALID_URL_RE if isinstance(ie._VALID_URL_RE, Iterable)
else (ie._VALID_URL_RE,)))
return True
return False
def custom_downloader(self, unused_config, episode):
"""
called from registry.custom_downloader.resolve
"""
if not self.force and not self.my_config.manage_downloads:
return None
try: # Reject URLs linking to known media files
(_, ext) = util.filename_from_url(episode.url)
if util.file_type_by_extension(ext) is not None:
return None
except Exception:
pass
if self.is_supported_url(episode.url):
return YoutubeCustomDownload(self, episode.url, episode)
return None
class gPodderExtension:
def __init__(self, container):
self.container = container
self.ytdl = None
self.infobar = None
def on_load(self):
self.ytdl = gPodderYoutubeDL(self.container.manager.core.config, self.container.config)
logger.info('Registering youtube-dl. (using %s %s)' % (program_name, youtube_dl.version.__version__))
registry.feed_handler.register(self.ytdl.fetch_channel)
registry.custom_downloader.register(self.ytdl.custom_downloader)
if youtube_dl.utils.version_tuple(youtube_dl.version.__version__) < youtube_dl.utils.version_tuple(want_ytdl_version):
logger.error(want_ytdl_version_msg
% {'have_version': youtube_dl.version.__version__, 'want_version': want_ytdl_version})
def on_unload(self):
logger.info('Unregistering youtube-dl.')
try:
registry.feed_handler.unregister(self.ytdl.fetch_channel)
except ValueError:
pass
try:
registry.custom_downloader.unregister(self.ytdl.custom_downloader)
except ValueError:
pass
self.ytdl = None
def on_ui_object_available(self, name, ui_object):
if name == 'gpodder-gtk':
self.gpodder = ui_object
if youtube_dl.utils.version_tuple(youtube_dl.version.__version__) < youtube_dl.utils.version_tuple(want_ytdl_version):
ui_object.notification(want_ytdl_version_msg %
{'have_version': youtube_dl.version.__version__, 'want_version': want_ytdl_version},
_('Old youtube-dl'), important=True, widget=ui_object.main_window)
def on_episodes_context_menu(self, episodes):
if not self.container.config.manage_downloads and any(e.can_download() for e in episodes):
return [(_("Download with youtube-dl"), self.download_episodes)]
def download_episodes(self, episodes):
episodes = [e for e in episodes if e.can_download()]
# create a new gPodderYoutubeDL to force using it even if manage_downloads is False
downloader = gPodderYoutubeDL(self.container.manager.core.config, self.container.config, force=True)
self.gpodder.download_episode_list(episodes, downloader=downloader)
def toggle_manage_channel(self, widget):
self.container.config.manage_channel = widget.get_active()
def toggle_manage_downloads(self, widget):
self.container.config.manage_downloads = widget.get_active()
def toggle_embed_subtitles(self, widget):
if widget.get_active():
if not util.find_command('ffmpeg'):
self.infobar.show()
widget.set_active(False)
self.container.config.embed_subtitles = False
else:
self.container.config.embed_subtitles = True
else:
self.container.config.embed_subtitles = False
def show_preferences(self):
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
box.set_border_width(10)
label = Gtk.Label('%s %s' % (program_name, youtube_dl.version.__version__))
box.pack_start(label, False, False, 0)
box.pack_start(Gtk.HSeparator(), False, False, 0)
checkbox = Gtk.CheckButton(_('Parse YouTube channel feeds with youtube-dl to access more than 15 episodes'))
checkbox.set_active(self.container.config.manage_channel)
checkbox.connect('toggled', self.toggle_manage_channel)
box.pack_start(checkbox, False, False, 0)
box.pack_start(Gtk.HSeparator(), False, False, 0)
checkbox = Gtk.CheckButton(_('Download all supported episodes with youtube-dl'))
checkbox.set_active(self.container.config.manage_downloads)
checkbox.connect('toggled', self.toggle_manage_downloads)
box.pack_start(checkbox, False, False, 0)
note = Gtk.Label(use_markup=True, wrap=True, label=_(
'youtube-dl provides access to additional YouTube formats and DRM content.'
' Episodes from non-YouTube channels, that have youtube-dl support, will <b>fail</b> to download unless you manually'
' <a href="https://gpodder.github.io/docs/youtube.html#formats">add custom formats</a> for each site.'
' <b>Download with youtube-dl</b> appears in the episode menu when this option is disabled,'
' and can be used to manually download from supported sites.'))
note.connect('activate-link', lambda label, url: util.open_website(url))
note.set_property('xalign', 0.0)
box.add(note)
box.pack_start(Gtk.HSeparator(), False, False, 0)
checkbox = Gtk.CheckButton(_('Embed all available subtitles in downloaded video'))
checkbox.set_active(self.container.config.embed_subtitles)
checkbox.connect('toggled', self.toggle_embed_subtitles)
box.pack_start(checkbox, False, False, 0)
infobar = Gtk.InfoBar()
infobar.get_content_area().add(Gtk.Label(wrap=True, label=_(
'The "ffmpeg" command was not found. FFmpeg is required for embedding subtitles.')))
self.infobar = infobar
box.pack_end(infobar, False, False, 0)
box.show_all()
infobar.hide()
return box
def on_preferences(self):
return [(_('youtube-dl'), self.show_preferences)]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 916 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 780 B

Some files were not shown because too many files have changed in this diff Show More