Compare commits
No commits in common. "debian/latest" and "pristine-tar" have entirely different histories.
debian/lat
...
pristine-t
|
@ -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'))
|
|
@ -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
|
|
@ -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}"
|
|
@ -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
|
|
@ -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']
|
|
@ -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
|
|
@ -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/*
|
|
@ -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
674
COPYING
|
@ -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>.
|
|
@ -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
222
README.md
|
@ -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
|
184
bin/gpodder
184
bin/gpodder
|
@ -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()
|
|
@ -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!')
|
|
@ -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!
|
|
@ -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
|
|
@ -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.
|
|
@ -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 +0,0 @@
|
|||
README.md
|
|
@ -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
|
||||
},
|
|
@ -1,2 +0,0 @@
|
|||
disable_update_check_on_startup_default.patch
|
||||
switch-appindicator-extension-to-AyatanaAppIndicator-and-python3.patch
|
|
@ -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'
|
|
@ -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
|
|
@ -1 +0,0 @@
|
|||
3.0 (quilt)
|
|
@ -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.
|
@ -0,0 +1 @@
|
|||
06f5a6cb73f248a78489ac8300759240f2b360ff
|
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
06f5a6cb73f248a78489ac8300759240f2b360ff
|
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
06f5a6cb73f248a78489ac8300759240f2b360ff
|
175
makefile
175
makefile
|
@ -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
|
||||
|
||||
##########################################################################
|
2981
po/cs_CZ.po
2981
po/cs_CZ.po
File diff suppressed because it is too large
Load Diff
2950
po/es_ES.po
2950
po/es_ES.po
File diff suppressed because it is too large
Load Diff
2951
po/es_MX.po
2951
po/es_MX.po
File diff suppressed because it is too large
Load Diff
2789
po/fa_IR.po
2789
po/fa_IR.po
File diff suppressed because it is too large
Load Diff
2732
po/id_ID.po
2732
po/id_ID.po
File diff suppressed because it is too large
Load Diff
2894
po/ko_KR.po
2894
po/ko_KR.po
File diff suppressed because it is too large
Load Diff
2759
po/messages.pot
2759
po/messages.pot
File diff suppressed because it is too large
Load Diff
2965
po/pt_BR.po
2965
po/pt_BR.po
File diff suppressed because it is too large
Load Diff
2850
po/zh_CN.po
2850
po/zh_CN.po
File diff suppressed because it is too large
Load Diff
13
setup.cfg
13
setup.cfg
|
@ -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
214
setup.py
|
@ -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,
|
||||
)
|
|
@ -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;
|
|
@ -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;
|
|
@ -1,3 +0,0 @@
|
|||
[D-BUS Service]
|
||||
Name=org.gpodder
|
||||
Exec=__PREFIX__/bin/gpodder
|
|
@ -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)
|
|
@ -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.
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)]
|
|
@ -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])
|
|
@ -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)]
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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()
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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())
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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()
|
|
@ -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))
|
|
@ -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()
|
|
@ -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)
|
|
@ -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
Loading…
Reference in New Issue