Merge branch 'gtk3'
This commit is contained in:
commit
66a354d9d1
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
*.pyc
|
||||||
|
__pycache__
|
||||||
|
src/podcastparser.py
|
||||||
|
src/mygpoclient
|
||||||
|
src/dbus
|
21
.travis.yml
21
.travis.yml
|
@ -2,24 +2,11 @@ language: python
|
||||||
dist: trusty
|
dist: trusty
|
||||||
sudo: required
|
sudo: required
|
||||||
python:
|
python:
|
||||||
- "2.7"
|
- "3.5"
|
||||||
install:
|
install:
|
||||||
- sudo apt-get update -q
|
- sudo apt-get update -q
|
||||||
- sudo apt-get install intltool desktop-file-utils wine mingw-w64-i686-dev binutils-mingw-w64-i686 gcc-mingw-w64 xvfb
|
- sudo apt-get install intltool desktop-file-utils
|
||||||
- pip install coverage minimock
|
- pip3 install coverage minimock
|
||||||
- python tools/localdepends.py
|
- python3 tools/localdepends.py
|
||||||
script:
|
script:
|
||||||
- make releasetest
|
- make releasetest
|
||||||
- make -C tools/win32-setup
|
|
||||||
- make -C tools/win32-portable
|
|
||||||
deploy:
|
|
||||||
provider: releases
|
|
||||||
api_key:
|
|
||||||
secure: "huPoTQRwhXZVD45JSBnCgtrzofpcotXShBWk9FYH2MOFwXQHRbp2ueaD0rxQxHNBTBXQDnOX+OQMnh99peYlxB1bPAx6LUMBgtesxvsUc3T5m7yZvqXyDBhjIBycYwxG0fBrnxEokaJKQDnZ4S/cKmk766iwhyGr66s+l9UBD/Y="
|
|
||||||
file:
|
|
||||||
- tools/win32-setup/gpodder-*-setup.exe
|
|
||||||
- tools/win32-portable/gpodder-*-win32.zip
|
|
||||||
file_glob: true
|
|
||||||
on:
|
|
||||||
tags: true
|
|
||||||
skip_cleanup: true
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
include README COPYING MANIFEST.in ChangeLog makefile setup.py
|
include README.md COPYING MANIFEST.in ChangeLog makefile setup.py
|
||||||
recursive-include share *
|
recursive-include share *
|
||||||
recursive-include po *
|
recursive-include po *
|
||||||
recursive-include tools *
|
recursive-include tools *
|
||||||
|
|
219
README
219
README
|
@ -1,219 +0,0 @@
|
||||||
|
|
||||||
___ _ _ ____
|
|
||||||
__ _| _ \___ __| |__| |___ _ _ |__ /
|
|
||||||
/ _` | _/ _ \/ _` / _` / -_) '_| |_ \
|
|
||||||
\__, |_| \___/\__,_\__,_\___|_| |___/
|
|
||||||
|___/
|
|
||||||
Media aggregator and podcast client
|
|
||||||
|
|
||||||
............................................................................
|
|
||||||
|
|
||||||
Copyright 2005-2017 Thomas Perl and 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 2.7.9 or newer http://python.org/
|
|
||||||
- Podcastparser 0.6.0 or newer http://gpodder.org/podcastparser/
|
|
||||||
- mygpoclient 1.7 or newer http://gpodder.org/mygpoclient/
|
|
||||||
- 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, you can use the script tools/localdepends.py to
|
|
||||||
install local copies of podcastparser and mygpoclient into "src/" from
|
|
||||||
PyPI. With this, you get a self-contained gPodder CLI codebase.
|
|
||||||
|
|
||||||
|
|
||||||
[ GTK UI - ADDITIONAL DEPENDENCIES ]
|
|
||||||
|
|
||||||
- PyGTK 2.16 or newer http://pygtk.org/
|
|
||||||
|
|
||||||
|
|
||||||
[ 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: python-gpod
|
|
||||||
- Clickable links in GTK UI show notes: html5lib
|
|
||||||
|
|
||||||
|
|
||||||
[ BUILD DEPENDENCIES ]
|
|
||||||
|
|
||||||
- help2man
|
|
||||||
- intltool
|
|
||||||
|
|
||||||
|
|
||||||
[ TEST DEPENDENCIES ]
|
|
||||||
|
|
||||||
- python-minimock
|
|
||||||
- python-coverage
|
|
||||||
- desktop-file-utils
|
|
||||||
|
|
||||||
[ TESTING ]
|
|
||||||
|
|
||||||
To run tests, use...
|
|
||||||
make unittest
|
|
||||||
|
|
||||||
To set a specific python binary set PYTHON:
|
|
||||||
PYTHON=python2 make unittest
|
|
||||||
|
|
||||||
Tests in gPodder are written in two different ways:
|
|
||||||
|
|
||||||
- doctests (see http://docs.python.org/2/library/doctest.html)
|
|
||||||
- unittests (see http://docs.python.org/2/library/unittest.html)
|
|
||||||
|
|
||||||
If you want to add doctests, simply write the doctest and make sure that
|
|
||||||
the module appears in "doctest_modules" in src/gpodder/unittests.py. For
|
|
||||||
example, the doctests in src/gpodder/util.py are added as 'util' (the
|
|
||||||
"gpodder" prefix must not be specified there).
|
|
||||||
|
|
||||||
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/
|
|
||||||
|
|
||||||
|
|
||||||
[ PYTHON 3 SUPPORT ]
|
|
||||||
|
|
||||||
For Python 3 support, we recommend you use the "gtk3" branch [from a git clone](https://github.com/gpodder/gpodder/wiki/Run-from-Git). There, gPodder has been updated to use to gtk3 and Python 3.
|
|
||||||
|
|
||||||
|
|
||||||
[ 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 enviroment 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.freenode.net
|
|
||||||
|
|
||||||
............................................................................
|
|
||||||
Last updated: 2016-11-30 by Thomas Perl <thp.io/about>
|
|
||||||
|
|
212
README.md
Normal file
212
README.md
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
___ _ _ ____
|
||||||
|
__ _| _ \___ __| |__| |___ _ _ |__ /
|
||||||
|
/ _` | _/ _ \/ _` / _` / -_) '_| |_ \
|
||||||
|
\__, |_| \___/\__,_\__,_\___|_| |___/
|
||||||
|
|___/
|
||||||
|
Media aggregator and podcast client
|
||||||
|
___
|
||||||
|
|
||||||
|
Copyright 2005-2017 Thomas Perl and 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.5](http://python.org/) or newer
|
||||||
|
- [Podcastparser](http://gpodder.org/podcastparser/) 0.6.0 or newer
|
||||||
|
- [mygpoclient](http://gpodder.org/mygpoclient/) 1.7 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, you can use the script tools/localdepends.py to
|
||||||
|
install local copies of podcastparser and mygpoclient into "src/" from
|
||||||
|
PyPI. With this, you get a self-contained gPodder CLI codebase.
|
||||||
|
|
||||||
|
|
||||||
|
### GTK3 UI - Additional Dependencies
|
||||||
|
|
||||||
|
- [PyGObject](https://wiki.gnome.org/PyGObject) 3.22.0 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: python-gpod
|
||||||
|
- Clickable links in GTK UI show notes: html5lib
|
||||||
|
- HTML show notes: WebKit2 gobject bindings
|
||||||
|
(webkit2gtk, webkitgtk4 or gir1.2-webkit2-4.0 packages).
|
||||||
|
|
||||||
|
|
||||||
|
### Build Dependencies
|
||||||
|
|
||||||
|
- help2man
|
||||||
|
- intltool
|
||||||
|
|
||||||
|
|
||||||
|
### Test Dependencies
|
||||||
|
|
||||||
|
- python-minimock
|
||||||
|
- python-coverage
|
||||||
|
- 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 in "doctest_modules" in src/gpodder/unittests.py. For
|
||||||
|
example, the doctests in src/gpodder/util.py are added as 'util' (the
|
||||||
|
"gpodder" prefix must not be specified there).
|
||||||
|
|
||||||
|
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/
|
||||||
|
|
||||||
|
|
||||||
|
## 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.freenode.net
|
126
bin/gpo
126
bin/gpo
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -63,7 +63,7 @@
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import print_function
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import collections
|
import collections
|
||||||
|
@ -142,38 +142,6 @@ def incolor(color_id, s):
|
||||||
return '\033[9%dm%s\033[0m' % (color_id, s)
|
return '\033[9%dm%s\033[0m' % (color_id, s)
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def safe_print(*args, **kwargs):
|
|
||||||
def convert(arg):
|
|
||||||
return unicode(util.convert_bytes(arg))
|
|
||||||
|
|
||||||
ofile = kwargs.get('file', sys.stdout)
|
|
||||||
output = u' '.join(map(convert, args))
|
|
||||||
if ofile.encoding is None:
|
|
||||||
output = util.sanitize_encoding(output)
|
|
||||||
else:
|
|
||||||
output = output.encode(ofile.encoding, 'replace')
|
|
||||||
|
|
||||||
try:
|
|
||||||
ofile.write(output)
|
|
||||||
except Exception, e:
|
|
||||||
print("""
|
|
||||||
*** ENCODING FAIL ***
|
|
||||||
|
|
||||||
Please report this to https://github.com/gpodder/gpodder/issues:
|
|
||||||
|
|
||||||
args = %s
|
|
||||||
map(convert, args) = %s
|
|
||||||
|
|
||||||
Exception = %s
|
|
||||||
""" % (repr(args), repr(map(convert, args)), e))
|
|
||||||
|
|
||||||
ofile.write(kwargs.get('end', os.linesep))
|
|
||||||
ofile.flush()
|
|
||||||
|
|
||||||
# On Python 3 the encoding insanity is gone, so our safe_print()
|
|
||||||
# function simply becomes the normal print() function. Good stuff!
|
|
||||||
if sys.version_info >= (3,):
|
|
||||||
safe_print = print
|
|
||||||
|
|
||||||
# ANSI Colors: red = 1, green = 2, yellow = 3, blue = 4
|
# ANSI Colors: red = 1, green = 2, yellow = 3, blue = 4
|
||||||
inred, ingreen, inyellow, inblue = (functools.partial(incolor, x)
|
inred, ingreen, inyellow, inblue = (functools.partial(incolor, x)
|
||||||
|
@ -241,7 +209,7 @@ class gPodderCli(object):
|
||||||
|
|
||||||
# Generator for all prefixes of a given string (longest first)
|
# Generator for all prefixes of a given string (longest first)
|
||||||
# e.g. ['gpodder', 'gpodde', 'gpodd', 'gpod', 'gpo', 'gp', 'g']
|
# e.g. ['gpodder', 'gpodde', 'gpodd', 'gpod', 'gpo', 'gp', 'g']
|
||||||
mkprefixes = lambda n: (n[:x] for x in xrange(len(n), 0, -1))
|
mkprefixes = lambda n: (n[:x] for x in range(len(n), 0, -1))
|
||||||
|
|
||||||
# Return True if the given prefix is unique in "names"
|
# Return True if the given prefix is unique in "names"
|
||||||
is_unique = lambda p: len([n for n in names if n.startswith(p)]) == 1
|
is_unique = lambda p: len([n for n in names if n.startswith(p)]) == 1
|
||||||
|
@ -276,13 +244,13 @@ class gPodderCli(object):
|
||||||
else:
|
else:
|
||||||
line = line + (' '*(self.COLUMNS-7-len(line)))
|
line = line + (' '*(self.COLUMNS-7-len(line)))
|
||||||
self._current_action = line
|
self._current_action = line
|
||||||
safe_print(self._current_action, end='')
|
print(self._current_action, end='')
|
||||||
|
|
||||||
def _update_action(self, progress):
|
def _update_action(self, progress):
|
||||||
if have_ansi:
|
if have_ansi:
|
||||||
progress = '%3.0f%%' % (progress*100.,)
|
progress = '%3.0f%%' % (progress*100.,)
|
||||||
result = '['+inblue(progress)+']'
|
result = '['+inblue(progress)+']'
|
||||||
safe_print('\r' + self._current_action + result, end='')
|
print('\r' + self._current_action + result, end='')
|
||||||
|
|
||||||
def _finish_action(self, success=True, skip=False):
|
def _finish_action(self, success=True, skip=False):
|
||||||
if skip:
|
if skip:
|
||||||
|
@ -293,9 +261,9 @@ class gPodderCli(object):
|
||||||
result = '['+inred('FAIL')+']'
|
result = '['+inred('FAIL')+']'
|
||||||
|
|
||||||
if have_ansi:
|
if have_ansi:
|
||||||
safe_print('\r' + self._current_action + result)
|
print('\r' + self._current_action + result)
|
||||||
else:
|
else:
|
||||||
safe_print(result)
|
print(result)
|
||||||
self._current_action = ''
|
self._current_action = ''
|
||||||
|
|
||||||
def _atexit(self):
|
def _atexit(self):
|
||||||
|
@ -354,7 +322,7 @@ class gPodderCli(object):
|
||||||
if title is not None:
|
if title is not None:
|
||||||
podcast.rename(title)
|
podcast.rename(title)
|
||||||
podcast.save()
|
podcast.save()
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
logger.warn('Cannot subscribe: %s', e, exc_info=True)
|
logger.warn('Cannot subscribe: %s', e, exc_info=True)
|
||||||
if hasattr(e, 'strerror'):
|
if hasattr(e, 'strerror'):
|
||||||
self._error(e.strerror)
|
self._error(e.strerror)
|
||||||
|
@ -371,7 +339,7 @@ class gPodderCli(object):
|
||||||
for key in self._config.all_keys():
|
for key in self._config.all_keys():
|
||||||
if search_for is None or search_for.lower() in key.lower():
|
if search_for is None or search_for.lower() in key.lower():
|
||||||
value = config_value_to_string(self._config._lookup(key))
|
value = config_value_to_string(self._config._lookup(key))
|
||||||
safe_print(key, '=', value)
|
print(key, '=', value)
|
||||||
|
|
||||||
def set(self, key=None, value=None):
|
def set(self, key=None, value=None):
|
||||||
if value is None:
|
if value is None:
|
||||||
|
@ -427,17 +395,17 @@ class gPodderCli(object):
|
||||||
def status_str(episode):
|
def status_str(episode):
|
||||||
# is new
|
# is new
|
||||||
if self.is_episode_new(episode):
|
if self.is_episode_new(episode):
|
||||||
return u' * '
|
return ' * '
|
||||||
# is downloaded
|
# is downloaded
|
||||||
if (episode.state == gpodder.STATE_DOWNLOADED):
|
if (episode.state == gpodder.STATE_DOWNLOADED):
|
||||||
return u' ▉ '
|
return ' ▉ '
|
||||||
# is deleted
|
# is deleted
|
||||||
if (episode.state == gpodder.STATE_DELETED):
|
if (episode.state == gpodder.STATE_DELETED):
|
||||||
return u' ░ '
|
return ' ░ '
|
||||||
|
|
||||||
return u' '
|
return ' '
|
||||||
|
|
||||||
episodes = (u'%3d. %s %s' % (i+1, status_str(e), e.title)
|
episodes = ('%3d. %s %s' % (i+1, status_str(e), e.title)
|
||||||
for i, e in enumerate(podcast.get_all_episodes()))
|
for i, e in enumerate(podcast.get_all_episodes()))
|
||||||
return episodes
|
return episodes
|
||||||
|
|
||||||
|
@ -456,8 +424,8 @@ class gPodderCli(object):
|
||||||
title, url, status = podcast.title, podcast.url, \
|
title, url, status = podcast.title, podcast.url, \
|
||||||
feed_update_status_msg(podcast)
|
feed_update_status_msg(podcast)
|
||||||
episodes = self._episodesList(podcast)
|
episodes = self._episodesList(podcast)
|
||||||
episodes = u'\n '.join(episodes)
|
episodes = '\n '.join(episodes)
|
||||||
self._pager(u"""
|
self._pager("""
|
||||||
Title: %(title)s
|
Title: %(title)s
|
||||||
URL: %(url)s
|
URL: %(url)s
|
||||||
Feed update is %(status)s
|
Feed update is %(status)s
|
||||||
|
@ -475,24 +443,24 @@ class gPodderCli(object):
|
||||||
podcast_printed = False
|
podcast_printed = False
|
||||||
if url is None or podcast.url == url:
|
if url is None or podcast.url == url:
|
||||||
episodes = self._episodesList(podcast)
|
episodes = self._episodesList(podcast)
|
||||||
episodes = u'\n '.join(episodes)
|
episodes = '\n '.join(episodes)
|
||||||
output.append(u"""
|
output.append("""
|
||||||
Episodes from %s:
|
Episodes from %s:
|
||||||
%s
|
%s
|
||||||
""" % (podcast.url, episodes))
|
""" % (podcast.url, episodes))
|
||||||
|
|
||||||
self._pager(u'\n'.join(output))
|
self._pager('\n'.join(output))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def list(self):
|
def list(self):
|
||||||
for podcast in self._model.get_podcasts():
|
for podcast in self._model.get_podcasts():
|
||||||
if not podcast.pause_subscription:
|
if not podcast.pause_subscription:
|
||||||
safe_print('#', ingreen(podcast.title))
|
print('#', ingreen(podcast.title))
|
||||||
else:
|
else:
|
||||||
safe_print('#', inred(podcast.title),
|
print('#', inred(podcast.title),
|
||||||
'-', _('Updates disabled'))
|
'-', _('Updates disabled'))
|
||||||
|
|
||||||
safe_print(podcast.url)
|
print(podcast.url)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -507,7 +475,7 @@ class gPodderCli(object):
|
||||||
@FirstArgumentIsPodcastURL
|
@FirstArgumentIsPodcastURL
|
||||||
def update(self, url=None):
|
def update(self, url=None):
|
||||||
count = 0
|
count = 0
|
||||||
safe_print(_('Checking for new episodes'))
|
print(_('Checking for new episodes'))
|
||||||
for podcast in self._model.get_podcasts():
|
for podcast in self._model.get_podcasts():
|
||||||
if url is not None and podcast.url != url:
|
if url is not None and podcast.url != url:
|
||||||
continue
|
continue
|
||||||
|
@ -521,7 +489,7 @@ class gPodderCli(object):
|
||||||
self._finish_action(skip=True)
|
self._finish_action(skip=True)
|
||||||
|
|
||||||
util.delete_empty_folders(gpodder.downloads)
|
util.delete_empty_folders(gpodder.downloads)
|
||||||
safe_print(inblue(self._pending_message(count)))
|
print(inblue(self._pending_message(count)))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@FirstArgumentIsPodcastURL
|
@FirstArgumentIsPodcastURL
|
||||||
|
@ -533,13 +501,13 @@ class gPodderCli(object):
|
||||||
for episode in podcast.get_all_episodes():
|
for episode in podcast.get_all_episodes():
|
||||||
if self.is_episode_new(episode):
|
if self.is_episode_new(episode):
|
||||||
if not podcast_printed:
|
if not podcast_printed:
|
||||||
safe_print('#', ingreen(podcast.title))
|
print('#', ingreen(podcast.title))
|
||||||
podcast_printed = True
|
podcast_printed = True
|
||||||
safe_print(' ', episode.title)
|
print(' ', episode.title)
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
util.delete_empty_folders(gpodder.downloads)
|
util.delete_empty_folders(gpodder.downloads)
|
||||||
safe_print(inblue(self._pending_message(count)))
|
print(inblue(self._pending_message(count)))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _download_episode(self, episode):
|
def _download_episode(self, episode):
|
||||||
|
@ -565,12 +533,12 @@ class gPodderCli(object):
|
||||||
last_podcast = None
|
last_podcast = None
|
||||||
for episode in episodes:
|
for episode in episodes:
|
||||||
if episode.channel != last_podcast:
|
if episode.channel != last_podcast:
|
||||||
safe_print(inblue(episode.channel.title))
|
print(inblue(episode.channel.title))
|
||||||
last_podcast = episode.channel
|
last_podcast = episode.channel
|
||||||
self._download_episode(episode)
|
self._download_episode(episode)
|
||||||
|
|
||||||
util.delete_empty_folders(gpodder.downloads)
|
util.delete_empty_folders(gpodder.downloads)
|
||||||
safe_print(len(episodes), 'episodes downloaded.')
|
print(len(episodes), 'episodes downloaded.')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@FirstArgumentIsPodcastURL
|
@FirstArgumentIsPodcastURL
|
||||||
|
@ -606,7 +574,7 @@ class gPodderCli(object):
|
||||||
def youtube(self, url):
|
def youtube(self, url):
|
||||||
fmt_ids = youtube.get_fmt_ids(self._config.youtube)
|
fmt_ids = youtube.get_fmt_ids(self._config.youtube)
|
||||||
yurl = youtube.get_real_download_url(url, fmt_ids)
|
yurl = youtube.get_real_download_url(url, fmt_ids)
|
||||||
safe_print(yurl)
|
print(yurl)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -672,11 +640,11 @@ class gPodderCli(object):
|
||||||
return
|
return
|
||||||
|
|
||||||
if not interactive_console or is_single_command:
|
if not interactive_console or is_single_command:
|
||||||
safe_print('\n'.join(url for title, url in results))
|
print('\n'.join(url for title, url in results))
|
||||||
return
|
return
|
||||||
|
|
||||||
def show_list():
|
def show_list():
|
||||||
self._pager('\n'.join(u'%3d: %s\n %s' %
|
self._pager('\n'.join('%3d: %s\n %s' %
|
||||||
(index+1, title, url if title != url else '')
|
(index+1, title, url if title != url else '')
|
||||||
for index, (title, url) in enumerate(results)))
|
for index, (title, url) in enumerate(results)))
|
||||||
|
|
||||||
|
@ -684,7 +652,7 @@ class gPodderCli(object):
|
||||||
|
|
||||||
msg = _('Enter index to subscribe, ? for list')
|
msg = _('Enter index to subscribe, ? for list')
|
||||||
while True:
|
while True:
|
||||||
index = raw_input(msg + ': ')
|
index = input(msg + ': ')
|
||||||
|
|
||||||
if not index:
|
if not index:
|
||||||
return
|
return
|
||||||
|
@ -728,7 +696,7 @@ class gPodderCli(object):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def help(self):
|
def help(self):
|
||||||
safe_print(stylize(__doc__), file=sys.stderr, end='')
|
print(stylize(__doc__), file=sys.stderr, end='')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# -------------------------------------------------------------------
|
# -------------------------------------------------------------------
|
||||||
|
@ -739,14 +707,14 @@ class gPodderCli(object):
|
||||||
rows_needed = len(output.splitlines()) + 2
|
rows_needed = len(output.splitlines()) + 2
|
||||||
rows, cols = get_terminal_size()
|
rows, cols = get_terminal_size()
|
||||||
if rows_needed < rows:
|
if rows_needed < rows:
|
||||||
safe_print(output)
|
print(output)
|
||||||
else:
|
else:
|
||||||
pydoc.pager(util.sanitize_encoding(output))
|
pydoc.pager(output)
|
||||||
else:
|
else:
|
||||||
safe_print(output)
|
print(output)
|
||||||
|
|
||||||
def _shell(self):
|
def _shell(self):
|
||||||
safe_print(os.linesep.join(x.strip() for x in ("""
|
print(os.linesep.join(x.strip() for x in ("""
|
||||||
gPodder %(__version__)s (%(__date__)s) - %(__url__)s
|
gPodder %(__version__)s (%(__date__)s) - %(__url__)s
|
||||||
%(__copyright__)s
|
%(__copyright__)s
|
||||||
License: %(__license__)s
|
License: %(__license__)s
|
||||||
|
@ -764,12 +732,12 @@ class gPodderCli(object):
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
line = raw_input('gpo> ')
|
line = input('gpo> ')
|
||||||
except EOFError:
|
except EOFError:
|
||||||
safe_print('')
|
print('')
|
||||||
break
|
break
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
safe_print('')
|
print('')
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if self._prefixes.get(line, line) in self.EXIT_COMMANDS:
|
if self._prefixes.get(line, line) in self.EXIT_COMMANDS:
|
||||||
|
@ -777,7 +745,7 @@ class gPodderCli(object):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
args = shlex.split(line)
|
args = shlex.split(line)
|
||||||
except ValueError, value_error:
|
except ValueError as value_error:
|
||||||
self._error(_('Syntax error: %(error)s') %
|
self._error(_('Syntax error: %(error)s') %
|
||||||
{'error': value_error})
|
{'error': value_error})
|
||||||
continue
|
continue
|
||||||
|
@ -792,13 +760,13 @@ class gPodderCli(object):
|
||||||
self._atexit()
|
self._atexit()
|
||||||
|
|
||||||
def _error(self, *args):
|
def _error(self, *args):
|
||||||
safe_print(inred(' '.join(args)), file=sys.stderr)
|
print(inred(' '.join(args)), file=sys.stderr)
|
||||||
|
|
||||||
# Warnings look like error messages for now
|
# Warnings look like error messages for now
|
||||||
_warn = _error
|
_warn = _error
|
||||||
|
|
||||||
def _info(self, *args):
|
def _info(self, *args):
|
||||||
safe_print(*args)
|
print(*args)
|
||||||
|
|
||||||
def _checkargs(self, func, command_line):
|
def _checkargs(self, func, command_line):
|
||||||
args, varargs, keywords, defaults = inspect.getargspec(func)
|
args, varargs, keywords, defaults = inspect.getargspec(func)
|
||||||
|
@ -872,9 +840,9 @@ class gPodderCli(object):
|
||||||
return self._checkargs(func, command_line)
|
return self._checkargs(func, command_line)
|
||||||
|
|
||||||
if command in self._expansions:
|
if command in self._expansions:
|
||||||
safe_print(_('Ambiguous command. Did you mean..'))
|
print(_('Ambiguous command. Did you mean..'))
|
||||||
for cmd in self._expansions[command]:
|
for cmd in self._expansions[command]:
|
||||||
safe_print(' ', inblue(cmd))
|
print(' ', inblue(cmd))
|
||||||
else:
|
else:
|
||||||
self._error(_('The requested function is not available.'))
|
self._error(_('The requested function is not available.'))
|
||||||
|
|
||||||
|
@ -897,5 +865,5 @@ if __name__ == '__main__':
|
||||||
elif interactive_console:
|
elif interactive_console:
|
||||||
cli._shell()
|
cli._shell()
|
||||||
else:
|
else:
|
||||||
safe_print(__doc__, end='')
|
print(__doc__, end='')
|
||||||
|
|
||||||
|
|
14
bin/gpodder
14
bin/gpodder
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -43,9 +43,9 @@ try:
|
||||||
import dbus.glib
|
import dbus.glib
|
||||||
have_dbus = True
|
have_dbus = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print >>sys.stderr, """
|
print("""
|
||||||
Warning: python-dbus not found. Disabling D-Bus support.
|
Warning: python-dbus not found. Disabling D-Bus support.
|
||||||
"""
|
""", file=sys.stderr)
|
||||||
have_dbus = False
|
have_dbus = False
|
||||||
|
|
||||||
from optparse import OptionParser
|
from optparse import OptionParser
|
||||||
|
@ -73,9 +73,9 @@ def main():
|
||||||
process = subprocess.Popen(locale_cmd, stdout=subprocess.PIPE)
|
process = subprocess.Popen(locale_cmd, stdout=subprocess.PIPE)
|
||||||
output, error_output = process.communicate()
|
output, error_output = process.communicate()
|
||||||
# the output is a string like 'fr_FR', and we need 'fr_FR.utf-8'
|
# the output is a string like 'fr_FR', and we need 'fr_FR.utf-8'
|
||||||
user_locale = output.strip() + '.UTF-8'
|
user_locale = output.decode('utf-8').strip() + '.UTF-8'
|
||||||
os.environ['LANG'] = user_locale
|
os.environ['LANG'] = user_locale
|
||||||
print >>sys.stderr, 'Setting locale to', user_locale
|
print('Setting locale to', user_locale, file=sys.stderr)
|
||||||
|
|
||||||
# Set up the path to translation files
|
# Set up the path to translation files
|
||||||
gettext.bindtextdomain('gpodder', locale_dir)
|
gettext.bindtextdomain('gpodder', locale_dir)
|
||||||
|
@ -112,7 +112,7 @@ def main():
|
||||||
options, args = parser.parse_args(sys.argv)
|
options, args = parser.parse_args(sys.argv)
|
||||||
|
|
||||||
gpodder.ui.gtk = True
|
gpodder.ui.gtk = True
|
||||||
gpodder.ui.python2 = True
|
gpodder.ui.python3 = True
|
||||||
|
|
||||||
gpodder.ui.unity = (os.environ.get('DESKTOP_SESSION', 'unknown').lower() in
|
gpodder.ui.unity = (os.environ.get('DESKTOP_SESSION', 'unknown').lower() in
|
||||||
('ubuntu', 'ubuntu-2d'))
|
('ubuntu', 'ubuntu-2d'))
|
||||||
|
@ -140,7 +140,7 @@ def main():
|
||||||
remote_object.subscribe_to_url(options.subscribe)
|
remote_object.subscribe_to_url(options.subscribe)
|
||||||
|
|
||||||
return
|
return
|
||||||
except dbus.exceptions.DBusException, dbus_exception:
|
except dbus.exceptions.DBusException as dbus_exception:
|
||||||
logger.info('Cannot connect to remote object.', exc_info=True)
|
logger.info('Cannot connect to remote object.', exc_info=True)
|
||||||
|
|
||||||
if not gpodder.ui.win32 and os.environ.get('DISPLAY', '') == '':
|
if not gpodder.ui.win32 and os.environ.get('DISPLAY', '') == '':
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import ConfigParser
|
import configparser
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
gpodder_script = sys.argv[0]
|
gpodder_script = sys.argv[0]
|
||||||
|
@ -56,25 +56,25 @@ old_config = os.path.expanduser('~/.config/gpodder/gpodder.conf')
|
||||||
new_config = gpodder.config_file
|
new_config = gpodder.config_file
|
||||||
|
|
||||||
if not os.path.exists(old_database):
|
if not os.path.exists(old_database):
|
||||||
print >>sys.stderr, """
|
print("""
|
||||||
Turns out that you never ran gPodder 2.
|
Turns out that you never ran gPodder 2.
|
||||||
Can't find this required file:
|
Can't find this required file:
|
||||||
|
|
||||||
%(old_database)s
|
%(old_database)s
|
||||||
""" % locals()
|
""" % locals(), file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
old_downloads = None
|
old_downloads = None
|
||||||
|
|
||||||
if os.path.exists(old_config):
|
if os.path.exists(old_config):
|
||||||
parser = ConfigParser.RawConfigParser()
|
parser = configparser.RawConfigParser()
|
||||||
parser.read(old_config)
|
parser.read(old_config)
|
||||||
try:
|
try:
|
||||||
old_downloads = parser.get('gpodder-conf-1', 'download_dir')
|
old_downloads = parser.get('gpodder-conf-1', 'download_dir')
|
||||||
except ConfigParser.NoSectionError:
|
except configparser.NoSectionError:
|
||||||
# The file is empty / section (gpodder-conf-1) not found
|
# The file is empty / section (gpodder-conf-1) not found
|
||||||
pass
|
pass
|
||||||
except ConfigParser.NoOptionError:
|
except configparser.NoOptionError:
|
||||||
# The section is available, but the key (download_dir) is not
|
# The section is available, but the key (download_dir) is not
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -87,22 +87,22 @@ if old_downloads is None:
|
||||||
new_downloads = gpodder.downloads
|
new_downloads = gpodder.downloads
|
||||||
|
|
||||||
if not os.path.exists(old_downloads):
|
if not os.path.exists(old_downloads):
|
||||||
print >>sys.stderr, """
|
print("""
|
||||||
Old download directory does not exist. Creating empty one.
|
Old download directory does not exist. Creating empty one.
|
||||||
"""
|
""", file=sys.stderr)
|
||||||
os.makedirs(old_downloads)
|
os.makedirs(old_downloads)
|
||||||
|
|
||||||
if any(os.path.exists(x) for x in (new_database, new_downloads)):
|
if any(os.path.exists(x) for x in (new_database, new_downloads)):
|
||||||
print >>sys.stderr, """
|
print("""
|
||||||
Existing gPodder 3 user data found.
|
Existing gPodder 3 user data found.
|
||||||
To continue, please remove:
|
To continue, please remove:
|
||||||
|
|
||||||
%(new_database)s
|
%(new_database)s
|
||||||
%(new_downloads)s
|
%(new_downloads)s
|
||||||
""" % locals()
|
""" % locals(), file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
print >>sys.stderr, """
|
print("""
|
||||||
Would carry out the following actions:
|
Would carry out the following actions:
|
||||||
|
|
||||||
Move downloads from %(old_downloads)s
|
Move downloads from %(old_downloads)s
|
||||||
|
@ -111,21 +111,21 @@ print >>sys.stderr, """
|
||||||
Convert database from %(old_database)s
|
Convert database from %(old_database)s
|
||||||
to %(new_database)s
|
to %(new_database)s
|
||||||
|
|
||||||
""" % locals()
|
""" % locals(), file=sys.stderr)
|
||||||
|
|
||||||
result = raw_input('Continue? (Y/n) ')
|
result = input('Continue? (Y/n) ')
|
||||||
|
|
||||||
if result in 'Yy':
|
if result in 'Yy':
|
||||||
util.make_directory(gpodder.home)
|
util.make_directory(gpodder.home)
|
||||||
schema.convert_gpodder2_db(old_database, new_database)
|
schema.convert_gpodder2_db(old_database, new_database)
|
||||||
if not os.path.exists(new_database):
|
if not os.path.exists(new_database):
|
||||||
print >>sys.stderr, 'Could not convert database.'
|
print('Could not convert database.', file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
shutil.move(old_downloads, new_downloads)
|
shutil.move(old_downloads, new_downloads)
|
||||||
if not os.path.exists(new_downloads):
|
if not os.path.exists(new_downloads):
|
||||||
print >>sys.stderr, 'Could not move downloads.'
|
print('Could not move downloads.', file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
print 'Done. Have fun with gPodder 3!'
|
print('Done. Have fun with gPodder 3!')
|
||||||
|
|
||||||
|
|
13
makefile
13
makefile
|
@ -50,7 +50,7 @@ GETTEXT_SOURCE += $(DESKTOP_FILES_IN_H)
|
||||||
DESTDIR ?= /
|
DESTDIR ?= /
|
||||||
PREFIX ?= /usr
|
PREFIX ?= /usr
|
||||||
|
|
||||||
PYTHON ?= python
|
PYTHON ?= python3
|
||||||
HELP2MAN ?= help2man
|
HELP2MAN ?= help2man
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
@ -84,12 +84,6 @@ $(GPODDER_SERVICE_FILE): $(GPODDER_SERVICE_FILE_IN)
|
||||||
install: messages $(GPODDER_SERVICE_FILE) $(DESKTOP_FILES)
|
install: messages $(GPODDER_SERVICE_FILE) $(DESKTOP_FILES)
|
||||||
$(PYTHON) setup.py install --root=$(DESTDIR) --prefix=$(PREFIX) --optimize=1
|
$(PYTHON) setup.py install --root=$(DESTDIR) --prefix=$(PREFIX) --optimize=1
|
||||||
|
|
||||||
release-win32:
|
|
||||||
$(MAKE) -C tools/win32-setup
|
|
||||||
cp tools/win32-setup/gpodder-*-setup.exe .
|
|
||||||
$(MAKE) -C tools/win32-portable
|
|
||||||
cp tools/win32-portable/gpodder-*-win32.zip .
|
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
manpage: $(MANPAGE)
|
manpage: $(MANPAGE)
|
||||||
|
@ -137,16 +131,13 @@ clean:
|
||||||
rm -f $(GPODDER_SERVICE_FILE)
|
rm -f $(GPODDER_SERVICE_FILE)
|
||||||
rm -f $(DESKTOP_FILES) $(DESKTOP_FILES_IN_H)
|
rm -f $(DESKTOP_FILES) $(DESKTOP_FILES_IN_H)
|
||||||
rm -rf build $(LOCALEDIR)
|
rm -rf build $(LOCALEDIR)
|
||||||
rm -f gpodder-*-win32.zip gpodder-*-setup.exe
|
|
||||||
|
|
||||||
distclean: clean
|
distclean: clean
|
||||||
rm -rf dist
|
rm -rf dist
|
||||||
-$(MAKE) -C tools/win32-portable distclean
|
|
||||||
-$(MAKE) -C tools/win32-setup distclean
|
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
.PHONY: help unittest release releasetest install manpage clean distclean messages headlink release-win32
|
.PHONY: help unittest release releasetest install manpage clean distclean messages headlink
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
|
18
setup.py
18
setup.py
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
#
|
#
|
||||||
# gPodder - A media aggregator and podcast client
|
# gPodder - A media aggregator and podcast client
|
||||||
|
@ -37,7 +37,7 @@ author, email = re.match(r'^(.*) <(.*)>$', metadata['author']).groups()
|
||||||
class MissingFile(BaseException): pass
|
class MissingFile(BaseException): pass
|
||||||
|
|
||||||
def info(message, item=None):
|
def info(message, item=None):
|
||||||
print '=>', message, item if item is not None else ''
|
print('=>', message, item if item is not None else '')
|
||||||
|
|
||||||
|
|
||||||
def find_data_files(uis, scripts):
|
def find_data_files(uis, scripts):
|
||||||
|
@ -94,7 +94,7 @@ def find_data_files(uis, scripts):
|
||||||
if not result:
|
if not result:
|
||||||
info('Skipping manpage without script:', filename)
|
info('Skipping manpage without script:', filename)
|
||||||
return result
|
return result
|
||||||
filenames = filter(have_script, filenames)
|
filenames = list(filter(have_script, filenames))
|
||||||
|
|
||||||
def convert_filename(filename):
|
def convert_filename(filename):
|
||||||
filename = os.path.join(dirpath, filename)
|
filename = os.path.join(dirpath, filename)
|
||||||
|
@ -112,7 +112,7 @@ def find_data_files(uis, scripts):
|
||||||
|
|
||||||
return filename
|
return filename
|
||||||
|
|
||||||
filenames = filter(None, map(convert_filename, filenames))
|
filenames = [_f for _f in map(convert_filename, filenames) if _f]
|
||||||
if filenames:
|
if filenames:
|
||||||
# Some distros/ports install manpages into $PREFIX/man instead
|
# Some distros/ports install manpages into $PREFIX/man instead
|
||||||
# of $PREFIX/share/man (e.g. FreeBSD). To allow this, we strip
|
# of $PREFIX/share/man (e.g. FreeBSD). To allow this, we strip
|
||||||
|
@ -137,10 +137,10 @@ def find_packages(uis):
|
||||||
package = '.'.join(dirparts)
|
package = '.'.join(dirparts)
|
||||||
|
|
||||||
# Extract all parts of the package name ending in "ui"
|
# Extract all parts of the package name ending in "ui"
|
||||||
ui_parts = filter(lambda p: p.endswith('ui'), dirparts)
|
ui_parts = [p for p in dirparts if p.endswith('ui')]
|
||||||
if uis is not None and ui_parts:
|
if uis is not None and ui_parts:
|
||||||
# Strip the trailing "ui", e.g. "gtkui" -> "gtk"
|
# Strip the trailing "ui", e.g. "gtkui" -> "gtk"
|
||||||
folder_uis = map(lambda p: p[:-2], ui_parts)
|
folder_uis = [p[:-2] for p in ui_parts]
|
||||||
for folder_ui in folder_uis:
|
for folder_ui in folder_uis:
|
||||||
if folder_ui not in uis:
|
if folder_ui not in uis:
|
||||||
info('Skipping package:', package)
|
info('Skipping package:', package)
|
||||||
|
@ -181,13 +181,13 @@ try:
|
||||||
packages = list(sorted(find_packages(uis)))
|
packages = list(sorted(find_packages(uis)))
|
||||||
scripts = list(sorted(find_scripts(uis)))
|
scripts = list(sorted(find_scripts(uis)))
|
||||||
data_files = list(sorted(find_data_files(uis, scripts)))
|
data_files = list(sorted(find_data_files(uis, scripts)))
|
||||||
except MissingFile, mf:
|
except MissingFile as mf:
|
||||||
print >>sys.stderr, """
|
print("""
|
||||||
Missing file: %s
|
Missing file: %s
|
||||||
|
|
||||||
If you want to install, use "make install" instead of using
|
If you want to install, use "make install" instead of using
|
||||||
setup.py directly. See the README file for more information.
|
setup.py directly. See the README file for more information.
|
||||||
""" % mf.message
|
""" % mf.message, file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/python
|
#!/usr/bin/python3
|
||||||
# Example script that can be used as post-play extension in media players
|
# Example script that can be used as post-play extension in media players
|
||||||
#
|
#
|
||||||
# Set the configuration options "audio_played_dbus" and "video_played_dbus"
|
# Set the configuration options "audio_played_dbus" and "video_played_dbus"
|
||||||
|
@ -16,9 +16,9 @@ import sys
|
||||||
import os
|
import os
|
||||||
|
|
||||||
if len(sys.argv) != 2:
|
if len(sys.argv) != 2:
|
||||||
print >>sys.stderr, """
|
print("""
|
||||||
Usage: %s /path/to/episode.mp3
|
Usage: %s /path/to/episode.mp3
|
||||||
""" % (sys.argv[0],)
|
""" % (sys.argv[0],), file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
filename = os.path.abspath(sys.argv[1])
|
filename = os.path.abspath(sys.argv[1])
|
||||||
|
@ -32,6 +32,6 @@ proxy = session_bus.get_object(gpodder.dbus_bus_name, \
|
||||||
interface = dbus.Interface(proxy, gpodder.dbus_interface)
|
interface = dbus.Interface(proxy, gpodder.dbus_interface)
|
||||||
|
|
||||||
if not interface.mark_episode_played(filename):
|
if not interface.mark_episode_played(filename):
|
||||||
print >>sys.stderr, 'Warning: Could not mark episode as played.'
|
print('Warning: Could not mark episode as played.', file=sys.stderr)
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
|
|
|
@ -21,15 +21,15 @@ class gPodderExtension:
|
||||||
# into various parts of gPodder.
|
# into various parts of gPodder.
|
||||||
def on_load(self):
|
def on_load(self):
|
||||||
logger.info('Extension is being loaded.')
|
logger.info('Extension is being loaded.')
|
||||||
print '='*40
|
print('='*40)
|
||||||
print 'container:', self.container
|
print('container:', self.container)
|
||||||
print 'container.manager:', self.container.manager
|
print('container.manager:', self.container.manager)
|
||||||
print 'container.config:', self.container.config
|
print('container.config:', self.container.config)
|
||||||
print 'container.manager.core:', self.container.manager.core
|
print('container.manager.core:', self.container.manager.core)
|
||||||
print 'container.manager.core.db:', self.container.manager.core.db
|
print('container.manager.core.db:', self.container.manager.core.db)
|
||||||
print 'container.manager.core.config:', self.container.manager.core.config
|
print('container.manager.core.config:', self.container.manager.core.config)
|
||||||
print 'container.manager.core.model:', self.container.manager.core.model
|
print('container.manager.core.model:', self.container.manager.core.model)
|
||||||
print '='*40
|
print('='*40)
|
||||||
|
|
||||||
# This function will be called when the extension is disabled or
|
# This function will be called when the extension is disabled or
|
||||||
# when gPodder shuts down. You can use this to destroy/delete any
|
# when gPodder shuts down. You can use this to destroy/delete any
|
||||||
|
@ -49,5 +49,4 @@ class gPodderExtension:
|
||||||
return [("Say Hello", self.say_hello_cb)]
|
return [("Say Hello", self.say_hello_cb)]
|
||||||
|
|
||||||
def say_hello_cb(self):
|
def say_hello_cb(self):
|
||||||
print("HELLO")
|
|
||||||
self.gpodder.notification("Hello Extension", "Message", widget=self.gpodder.main_window)
|
self.gpodder.notification("Hello Extension", "Message", widget=self.gpodder.main_window)
|
||||||
|
|
|
@ -8,7 +8,7 @@ import subprocess
|
||||||
import gpodder
|
import gpodder
|
||||||
from gpodder import util
|
from gpodder import util
|
||||||
|
|
||||||
import gtk
|
from gi.repository import Gtk
|
||||||
from gpodder.gtkui.interface.progress import ProgressIndicator
|
from gpodder.gtkui.interface.progress import ProgressIndicator
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
@ -34,13 +34,13 @@ class gPodderExtension:
|
||||||
self.gpodder = ui_object
|
self.gpodder = ui_object
|
||||||
|
|
||||||
def _get_save_filename(self):
|
def _get_save_filename(self):
|
||||||
dlg = gtk.FileChooserDialog(title=_('Save video'),
|
dlg = Gtk.FileChooserDialog(title=_('Save video'),
|
||||||
parent=self.gpodder.get_dialog_parent(),
|
parent=self.gpodder.get_dialog_parent(),
|
||||||
action=gtk.FILE_CHOOSER_ACTION_SAVE)
|
action=Gtk.FileChooserAction.SAVE)
|
||||||
dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
|
dlg.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
|
||||||
dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
|
dlg.add_button(Gtk.STOCK_SAVE, Gtk.ResponseType.OK)
|
||||||
|
|
||||||
if dlg.run() == gtk.RESPONSE_OK:
|
if dlg.run() == Gtk.ResponseType.OK:
|
||||||
filename = dlg.get_filename()
|
filename = dlg.get_filename()
|
||||||
dlg.destroy()
|
dlg.destroy()
|
||||||
return filename
|
return filename
|
||||||
|
|
|
@ -262,7 +262,7 @@ class gPodderExtension:
|
||||||
self.config = container.config
|
self.config = container.config
|
||||||
|
|
||||||
# Only display media players that can be found at extension load time
|
# Only display media players that can be found at extension load time
|
||||||
self.players = [p for p in PLAYERS if p.is_installed()]
|
self.players = [player for player in PLAYERS if player.is_installed()]
|
||||||
self.resumers = [r for r in RESUMERS if r.is_installed()]
|
self.resumers = [r for r in RESUMERS if r.is_installed()]
|
||||||
|
|
||||||
def on_ui_object_available(self, name, ui_object):
|
def on_ui_object_available(self, name, ui_object):
|
||||||
|
|
|
@ -14,10 +14,10 @@ _ = gpodder.gettext
|
||||||
__title__ = _('Gtk Status Icon')
|
__title__ = _('Gtk Status Icon')
|
||||||
__description__ = _('Show a status icon for Gtk-based Desktops.')
|
__description__ = _('Show a status icon for Gtk-based Desktops.')
|
||||||
__category__ = 'desktop-integration'
|
__category__ = 'desktop-integration'
|
||||||
__only_for__ = 'gtk,python2'
|
__only_for__ = 'gtk'
|
||||||
__disable_in__ = 'unity,win32'
|
__disable_in__ = 'unity,win32,python3'
|
||||||
|
|
||||||
import gtk
|
from gi.repository import Gtk
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
from gpodder.gtkui import draw
|
from gpodder.gtkui import draw
|
||||||
|
@ -39,7 +39,7 @@ class gPodderExtension:
|
||||||
path = os.path.join(os.path.dirname(__file__), '..', '..', 'icons')
|
path = os.path.join(os.path.dirname(__file__), '..', '..', 'icons')
|
||||||
icon_path = os.path.abspath(path)
|
icon_path = os.path.abspath(path)
|
||||||
|
|
||||||
theme = gtk.icon_theme_get_default()
|
theme = Gtk.IconTheme.get_default()
|
||||||
theme.append_search_path(icon_path)
|
theme.append_search_path(icon_path)
|
||||||
|
|
||||||
if self.icon_name is None:
|
if self.icon_name is None:
|
||||||
|
@ -49,11 +49,11 @@ class gPodderExtension:
|
||||||
self.icon_name = 'stock_mic'
|
self.icon_name = 'stock_mic'
|
||||||
|
|
||||||
if self.status_icon is None:
|
if self.status_icon is None:
|
||||||
self.status_icon = gtk.status_icon_new_from_icon_name(self.icon_name)
|
self.status_icon = Gtk.status_icon_new_from_icon_name(self.icon_name)
|
||||||
return
|
return
|
||||||
|
|
||||||
# If current mode matches desired mode, nothing to do.
|
# If current mode matches desired mode, nothing to do.
|
||||||
is_pixbuf = (self.status_icon.get_storage_type() == gtk.IMAGE_PIXBUF)
|
is_pixbuf = (self.status_icon.get_storage_type() == Gtk.ImageType.PIXBUF)
|
||||||
if is_pixbuf == use_pixbuf:
|
if is_pixbuf == use_pixbuf:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ class gPodderExtension:
|
||||||
# Currently icon is not a pixbuf => was loaded by name, at which
|
# Currently icon is not a pixbuf => was loaded by name, at which
|
||||||
# point size was automatically determined.
|
# point size was automatically determined.
|
||||||
icon_size = self.status_icon.get_size()
|
icon_size = self.status_icon.get_size()
|
||||||
icon_pixbuf = theme.load_icon(self.icon_name, icon_size, gtk.ICON_LOOKUP_USE_BUILTIN)
|
icon_pixbuf = theme.load_icon(self.icon_name, icon_size, Gtk.IconLookupFlags.USE_BUILTIN)
|
||||||
self.status_icon.set_from_pixbuf(icon_pixbuf)
|
self.status_icon.set_from_pixbuf(icon_pixbuf)
|
||||||
|
|
||||||
def on_load(self):
|
def on_load(self):
|
||||||
|
@ -91,7 +91,7 @@ class gPodderExtension:
|
||||||
|
|
||||||
def get_icon_pixbuf(self):
|
def get_icon_pixbuf(self):
|
||||||
assert self.status_icon is not None
|
assert self.status_icon is not None
|
||||||
if self.status_icon.get_storage_type() != gtk.IMAGE_PIXBUF:
|
if self.status_icon.get_storage_type() != Gtk.ImageType.PIXBUF:
|
||||||
self.set_icon(use_pixbuf=True)
|
self.set_icon(use_pixbuf=True)
|
||||||
return self.status_icon.get_pixbuf()
|
return self.status_icon.get_pixbuf()
|
||||||
|
|
||||||
|
@ -117,7 +117,7 @@ class gPodderExtension:
|
||||||
|
|
||||||
icon = self.get_icon_pixbuf().copy()
|
icon = self.get_icon_pixbuf().copy()
|
||||||
progressbar = draw.progressbar_pixbuf(icon.get_width(), icon.get_height(), progress)
|
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, gtk.gdk.INTERP_NEAREST, 255)
|
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.status_icon.set_from_pixbuf(icon)
|
||||||
self.last_progress = progress
|
self.last_progress = progress
|
||||||
|
|
|
@ -24,8 +24,8 @@ import dbus.service
|
||||||
import gpodder
|
import gpodder
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
import urllib
|
import urllib.request, urllib.parse, urllib.error
|
||||||
import urlparse
|
import urllib.parse
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
_ = gpodder.gettext
|
_ = gpodder.gettext
|
||||||
|
@ -43,7 +43,7 @@ TrackInfo = collections.namedtuple('TrackInfo',
|
||||||
['uri', 'length', 'status', 'pos', 'rate'])
|
['uri', 'length', 'status', 'pos', 'rate'])
|
||||||
|
|
||||||
def subsecond_difference(usec1, usec2):
|
def subsecond_difference(usec1, usec2):
|
||||||
return abs(usec1 - usec2) < USECS_IN_SEC
|
return usec1 is not None and usec2 is not None and abs(usec1 - usec2) < USECS_IN_SEC
|
||||||
|
|
||||||
class CurrentTrackTracker(object):
|
class CurrentTrackTracker(object):
|
||||||
'''An instance of this class is responsible for tracking the state of the
|
'''An instance of this class is responsible for tracking the state of the
|
||||||
|
@ -117,7 +117,7 @@ class CurrentTrackTracker(object):
|
||||||
# If the status *is* playing, and *was* playing, but the position
|
# If the status *is* playing, and *was* playing, but the position
|
||||||
# has changed discontinuously, notify a stop for the old position
|
# has changed discontinuously, notify a stop for the old position
|
||||||
if ( cur['status'] == 'Playing'
|
if ( cur['status'] == 'Playing'
|
||||||
and (not kwargs.has_key('status') or kwargs['status'] == 'Playing')
|
and ('status' not in kwargs or kwargs['status'] == 'Playing')
|
||||||
and not subsecond_difference(cur['pos'], kwargs['pos'])
|
and not subsecond_difference(cur['pos'], kwargs['pos'])
|
||||||
):
|
):
|
||||||
logger.debug('notify Stopped: playback discontinuity:' +
|
logger.debug('notify Stopped: playback discontinuity:' +
|
||||||
|
@ -125,6 +125,7 @@ class CurrentTrackTracker(object):
|
||||||
self.notify_stop()
|
self.notify_stop()
|
||||||
|
|
||||||
if ( (kwargs['pos']) == 0
|
if ( (kwargs['pos']) == 0
|
||||||
|
and self.pos is not None
|
||||||
and self.pos > (self.length - USECS_IN_SEC)
|
and self.pos > (self.length - USECS_IN_SEC)
|
||||||
and self.pos < (self.length + 2 * USECS_IN_SEC)
|
and self.pos < (self.length + 2 * USECS_IN_SEC)
|
||||||
):
|
):
|
||||||
|
@ -176,7 +177,7 @@ class CurrentTrackTracker(object):
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
pos = self.pos // USECS_IN_SEC
|
pos = self.pos // USECS_IN_SEC
|
||||||
file_uri = urllib.url2pathname(urlparse.urlparse(self.uri).path).encode('utf-8')
|
file_uri = urllib.request.url2pathname(urllib.parse.urlparse(self.uri).path).encode('utf-8')
|
||||||
total_time = self.length // USECS_IN_SEC
|
total_time = self.length // USECS_IN_SEC
|
||||||
|
|
||||||
if status == 'Stopped':
|
if status == 'Stopped':
|
||||||
|
@ -200,8 +201,8 @@ class CurrentTrackTracker(object):
|
||||||
return '%s: %s at %d/%d (@%f)' % (
|
return '%s: %s at %d/%d (@%f)' % (
|
||||||
self.uri or 'None',
|
self.uri or 'None',
|
||||||
self.status or 'None',
|
self.status or 'None',
|
||||||
(self.pos or 0) / USECS_IN_SEC,
|
(self.pos or 0) // USECS_IN_SEC,
|
||||||
(self.length or 0) / USECS_IN_SEC,
|
(self.length or 0) // USECS_IN_SEC,
|
||||||
self.rate or 0)
|
self.rate or 0)
|
||||||
|
|
||||||
class MPRISDBusReceiver(object):
|
class MPRISDBusReceiver(object):
|
||||||
|
@ -246,23 +247,23 @@ class MPRISDBusReceiver(object):
|
||||||
invalidated_properties, path=None):
|
invalidated_properties, path=None):
|
||||||
if interface_name != self.INTERFACE_MPRIS:
|
if interface_name != self.INTERFACE_MPRIS:
|
||||||
if interface_name not in self.OTHER_MPRIS_INTERFACES:
|
if interface_name not in self.OTHER_MPRIS_INTERFACES:
|
||||||
logger.warn('unexpected interface: %s, props=%r', interface_name, changed_properties.keys())
|
logger.warn('unexpected interface: %s, props=%r', interface_name, list(changed_properties.keys()))
|
||||||
return
|
return
|
||||||
|
|
||||||
collected_info = {}
|
collected_info = {}
|
||||||
|
|
||||||
if changed_properties.has_key('PlaybackStatus'):
|
if 'PlaybackStatus' in changed_properties:
|
||||||
collected_info['status'] = str(changed_properties['PlaybackStatus'])
|
collected_info['status'] = str(changed_properties['PlaybackStatus'])
|
||||||
if changed_properties.has_key('Metadata'):
|
if 'Metadata' in changed_properties:
|
||||||
# on stop there is no xesam:url
|
# on stop there is no xesam:url
|
||||||
if changed_properties['Metadata'].has_key('xesam:url'):
|
if 'xesam:url' in changed_properties['Metadata']:
|
||||||
collected_info['uri'] = changed_properties['Metadata']['xesam:url']
|
collected_info['uri'] = changed_properties['Metadata']['xesam:url']
|
||||||
collected_info['length'] = changed_properties['Metadata']['mpris:length']
|
collected_info['length'] = changed_properties['Metadata']['mpris:length']
|
||||||
if changed_properties.has_key('Rate'):
|
if 'Rate' in changed_properties:
|
||||||
collected_info['rate'] = changed_properties['Rate']
|
collected_info['rate'] = changed_properties['Rate']
|
||||||
collected_info['pos'] = self.query_position()
|
collected_info['pos'] = self.query_position()
|
||||||
|
|
||||||
if not collected_info.has_key('status'):
|
if 'status' not in collected_info:
|
||||||
collected_info['status'] = str(self.query_status())
|
collected_info['status'] = str(self.query_status())
|
||||||
logger.debug('collected info: %r', collected_info)
|
logger.debug('collected info: %r', collected_info)
|
||||||
|
|
||||||
|
|
|
@ -32,9 +32,14 @@ import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pynotify
|
import gi
|
||||||
|
gi.require_version('Notify', '0.7')
|
||||||
|
from gi.repository import Notify
|
||||||
|
pynotify = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pynotify = None
|
pynotify = None
|
||||||
|
except ValueError:
|
||||||
|
pynotify = None
|
||||||
|
|
||||||
|
|
||||||
if pynotify is None:
|
if pynotify is None:
|
||||||
|
@ -47,16 +52,16 @@ else:
|
||||||
self.container = container
|
self.container = container
|
||||||
|
|
||||||
def on_load(self):
|
def on_load(self):
|
||||||
pynotify.init('gPodder')
|
Notify.init('gPodder')
|
||||||
|
|
||||||
def on_unload(self):
|
def on_unload(self):
|
||||||
pynotify.uninit()
|
Notify.uninit()
|
||||||
|
|
||||||
def on_notification_show(self, title, message):
|
def on_notification_show(self, title, message):
|
||||||
if not message and not title:
|
if not message and not title:
|
||||||
return
|
return
|
||||||
|
|
||||||
notify = pynotify.Notification(title or '', message or '',
|
notify = Notify.Notification.new(title or '', message or '',
|
||||||
gpodder.icon_file)
|
gpodder.icon_file)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -48,7 +48,7 @@ class gPodderExtension:
|
||||||
basename, ext = os.path.splitext(filename)
|
basename, ext = os.path.splitext(filename)
|
||||||
|
|
||||||
new_basename = []
|
new_basename = []
|
||||||
new_basename.append(util.sanitize_encoding(title) + ext)
|
new_basename.append(title + ext)
|
||||||
if self.config.add_podcast_title:
|
if self.config.add_podcast_title:
|
||||||
new_basename.insert(0, podcast_title)
|
new_basename.insert(0, podcast_title)
|
||||||
if self.config.add_sortdate:
|
if self.config.add_sortdate:
|
||||||
|
@ -56,8 +56,7 @@ class gPodderExtension:
|
||||||
new_basename = ' - '.join(new_basename)
|
new_basename = ' - '.join(new_basename)
|
||||||
|
|
||||||
# On Windows, force ASCII encoding for filenames (bug 1724)
|
# On Windows, force ASCII encoding for filenames (bug 1724)
|
||||||
new_basename = util.sanitize_filename(new_basename,
|
new_basename = util.sanitize_filename(new_basename)
|
||||||
use_ascii=gpodder.ui.win32)
|
|
||||||
new_filename = os.path.join(dirname, new_basename)
|
new_filename = os.path.join(dirname, new_basename)
|
||||||
|
|
||||||
if new_filename == current_filename:
|
if new_filename == current_filename:
|
||||||
|
|
|
@ -94,6 +94,6 @@ class gPodderExtension:
|
||||||
if found:
|
if found:
|
||||||
logger.info('Removed cover art from OGG file: %s', filename)
|
logger.info('Removed cover art from OGG file: %s', filename)
|
||||||
ogg.save()
|
ogg.save()
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
logger.warn('Failed to remove OGG cover: %s', e, exc_info=True)
|
logger.warn('Failed to remove OGG cover: %s', e, exc_info=True)
|
||||||
|
|
||||||
|
|
|
@ -88,8 +88,8 @@ class gPodderExtension:
|
||||||
if video_height is None:
|
if video_height is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
width_ratio = device_width / video_width
|
width_ratio = device_width // video_width
|
||||||
height_ratio = device_height / video_height
|
height_ratio = device_height // video_height
|
||||||
|
|
||||||
dest_width = device_width
|
dest_width = device_width
|
||||||
dest_height = width_ratio * video_height
|
dest_height = width_ratio * video_height
|
||||||
|
@ -134,9 +134,6 @@ class gPodderExtension:
|
||||||
'options': self.container.config.ffmpeg_options
|
'options': self.container.config.ffmpeg_options
|
||||||
}
|
}
|
||||||
|
|
||||||
# Prior to Python 2.7.3, this module (shlex) did not support Unicode input.
|
|
||||||
convert_command = util.sanitize_encoding(convert_command)
|
|
||||||
|
|
||||||
process = subprocess.Popen(shlex.split(convert_command),
|
process = subprocess.Popen(shlex.split(convert_command),
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
stdout, stderr = process.communicate()
|
stdout, stderr = process.communicate()
|
||||||
|
|
|
@ -162,7 +162,7 @@ class Mp3File(AudioFile):
|
||||||
encoding = 3, # 3 is for utf-8
|
encoding = 3, # 3 is for utf-8
|
||||||
mime = mimetypes.guess_type(self.cover)[0],
|
mime = mimetypes.guess_type(self.cover)[0],
|
||||||
type = 3,
|
type = 3,
|
||||||
desc = u'Cover',
|
desc = 'Cover',
|
||||||
data = open(self.cover).read()
|
data = open(self.cover).read()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -257,7 +257,7 @@ class gPodderExtension:
|
||||||
if self.container.config.auto_embed_coverart:
|
if self.container.config.auto_embed_coverart:
|
||||||
audio.insert_coverart()
|
audio.insert_coverart()
|
||||||
|
|
||||||
logger.info(u'tagging.on_episode_downloaded(%s/%s)', episode.channel.title, episode.title)
|
logger.info('tagging.on_episode_downloaded(%s/%s)', episode.channel.title, episode.title)
|
||||||
|
|
||||||
def get_cover(self, podcast):
|
def get_cover(self, podcast):
|
||||||
downloader = coverart.CoverDownloader()
|
downloader = coverart.CoverDownloader()
|
||||||
|
|
|
@ -58,7 +58,7 @@ class gPodderExtension(object):
|
||||||
def get_data_from_url(self, url):
|
def get_data_from_url(self, url):
|
||||||
try:
|
try:
|
||||||
response = util.urlopen(url).read()
|
response = util.urlopen(url).read()
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
logger.warn("subtitle url returned error %s", e)
|
logger.warn("subtitle url returned error %s", e)
|
||||||
return ''
|
return ''
|
||||||
return response
|
return response
|
||||||
|
@ -93,7 +93,7 @@ class gPodderExtension(object):
|
||||||
intro = episode_data.split('introDuration":')[1] \
|
intro = episode_data.split('introDuration":')[1] \
|
||||||
.split(',')[0] or INTRO_DEFAULT
|
.split(',')[0] or INTRO_DEFAULT
|
||||||
intro = int(float(intro)*1000)
|
intro = int(float(intro)*1000)
|
||||||
except (ValueError, IndexError), e:
|
except (ValueError, IndexError) as e:
|
||||||
logger.info("Couldn't parse introDuration string: %s", intro)
|
logger.info("Couldn't parse introDuration string: %s", intro)
|
||||||
intro = INTRO_DEFAULT * 1000
|
intro = INTRO_DEFAULT * 1000
|
||||||
current_filename = episode.local_filename(create=False)
|
current_filename = episode.local_filename(create=False)
|
||||||
|
@ -103,7 +103,7 @@ class gPodderExtension(object):
|
||||||
try:
|
try:
|
||||||
with open(srt_filename, 'w+') as srtFile:
|
with open(srt_filename, 'w+') as srtFile:
|
||||||
srtFile.write(sub.encode("utf-8"))
|
srtFile.write(sub.encode("utf-8"))
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
logger.warn("Can't write srt file: %s",e)
|
logger.warn("Can't write srt file: %s",e)
|
||||||
|
|
||||||
def on_episode_delete(self, episode, filename):
|
def on_episode_delete(self, episode, filename):
|
||||||
|
|
|
@ -17,7 +17,7 @@ __disable_in__ = 'win32'
|
||||||
|
|
||||||
|
|
||||||
import appindicator
|
import appindicator
|
||||||
import gtk
|
from gi.repository import Gtk
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
@ -43,8 +43,8 @@ class gPodderExtension:
|
||||||
self.indicator.set_status(appindicator.STATUS_ACTIVE)
|
self.indicator.set_status(appindicator.STATUS_ACTIVE)
|
||||||
|
|
||||||
def _rebuild_menu(self):
|
def _rebuild_menu(self):
|
||||||
menu = gtk.Menu()
|
menu = Gtk.Menu()
|
||||||
toggle_visible = gtk.CheckMenuItem(_('Show main window'))
|
toggle_visible = Gtk.CheckMenuItem(_('Show main window'))
|
||||||
toggle_visible.set_active(True)
|
toggle_visible.set_active(True)
|
||||||
def on_toggle_visible(menu_item):
|
def on_toggle_visible(menu_item):
|
||||||
if menu_item.get_active():
|
if menu_item.get_active():
|
||||||
|
@ -53,8 +53,8 @@ class gPodderExtension:
|
||||||
self.gpodder.main_window.hide()
|
self.gpodder.main_window.hide()
|
||||||
toggle_visible.connect('activate', on_toggle_visible)
|
toggle_visible.connect('activate', on_toggle_visible)
|
||||||
menu.append(toggle_visible)
|
menu.append(toggle_visible)
|
||||||
menu.append(gtk.SeparatorMenuItem())
|
menu.append(Gtk.SeparatorMenuItem())
|
||||||
quit_gpodder = gtk.MenuItem(_('Quit'))
|
quit_gpodder = Gtk.MenuItem(_('Quit'))
|
||||||
def on_quit(menu_item):
|
def on_quit(menu_item):
|
||||||
self.gpodder.on_gPodder_delete_event(self.gpodder.main_window)
|
self.gpodder.on_gPodder_delete_event(self.gpodder.main_window)
|
||||||
quit_gpodder.connect('activate', on_quit)
|
quit_gpodder.connect('activate', on_quit)
|
||||||
|
|
|
@ -53,7 +53,7 @@ if __name__ != '__main__':
|
||||||
try:
|
try:
|
||||||
self.process.stdin.write('progress %f\n' % progress)
|
self.process.stdin.write('progress %f\n' % progress)
|
||||||
self.process.stdin.flush()
|
self.process.stdin.flush()
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
logger.debug('Ubuntu progress update failed.', exc_info=True)
|
logger.debug('Ubuntu progress update failed.', exc_info=True)
|
||||||
else:
|
else:
|
||||||
from gi.repository import Unity, GObject
|
from gi.repository import Unity, GObject
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--*- mode: xml -*-->
|
<!--*- mode: xml -*-->
|
||||||
<interface>
|
<interface>
|
||||||
|
<!-- interface-requires gtk+ 3.10 -->
|
||||||
<object class="GtkAdjustment" id="adjustment1">
|
<object class="GtkAdjustment" id="adjustment1">
|
||||||
<property name="upper">10240</property>
|
<property name="upper">10240</property>
|
||||||
<property name="lower">0.5</property>
|
<property name="lower">0.5</property>
|
||||||
|
@ -15,389 +16,8 @@
|
||||||
<property name="step_increment">1</property>
|
<property name="step_increment">1</property>
|
||||||
<property name="page_size">0</property>
|
<property name="page_size">0</property>
|
||||||
</object>
|
</object>
|
||||||
<object class="GtkUIManager" id="uimanager1">
|
<object class="GtkApplicationWindow" id="gPodder">
|
||||||
<child>
|
<property name="application">app</property>
|
||||||
<object class="GtkActionGroup" id="actiongroup1">
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="menuPodcasts">
|
|
||||||
<property name="name">menuPodcasts</property>
|
|
||||||
<property name="label" translatable="yes">_Podcasts</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="itemUpdate">
|
|
||||||
<property name="stock_id">gtk-refresh</property>
|
|
||||||
<property name="name">itemUpdate</property>
|
|
||||||
<property name="label" translatable="yes">Check for new episodes</property>
|
|
||||||
<signal handler="on_itemUpdate_activate" name="activate"/>
|
|
||||||
</object>
|
|
||||||
<accelerator key="R" modifiers="GDK_CONTROL_MASK"/>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="itemDownloadAllNew">
|
|
||||||
<property name="stock_id">gtk-goto-bottom</property>
|
|
||||||
<property name="name">itemDownloadAllNew</property>
|
|
||||||
<property name="label" translatable="yes">Download new episodes</property>
|
|
||||||
<signal handler="on_itemDownloadAllNew_activate" name="activate"/>
|
|
||||||
</object>
|
|
||||||
<accelerator key="N" modifiers="GDK_CONTROL_MASK"/>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="itemRemoveOldEpisodes">
|
|
||||||
<property name="stock_id">gtk-delete</property>
|
|
||||||
<property name="name">itemRemoveOldEpisodes</property>
|
|
||||||
<property name="label" translatable="yes">Delete episodes</property>
|
|
||||||
<signal handler="on_itemRemoveOldEpisodes_activate" name="activate"/>
|
|
||||||
</object>
|
|
||||||
<accelerator key="K" modifiers="GDK_CONTROL_MASK"/>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="itemPreferences">
|
|
||||||
<property name="stock_id">gtk-preferences</property>
|
|
||||||
<property name="name">itemPreferences</property>
|
|
||||||
<property name="label" translatable="yes">Preferences</property>
|
|
||||||
<signal handler="on_itemPreferences_activate" name="activate"/>
|
|
||||||
</object>
|
|
||||||
<accelerator key="P" modifiers="GDK_CONTROL_MASK"/>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="itemQuit">
|
|
||||||
<property name="stock_id">gtk-quit</property>
|
|
||||||
<property name="name">itemQuit</property>
|
|
||||||
<property name="label" translatable="yes">Quit</property>
|
|
||||||
<signal handler="on_gPodder_delete_event" name="activate"/>
|
|
||||||
</object>
|
|
||||||
<accelerator key="Q" modifiers="GDK_CONTROL_MASK"/>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="menuSubscriptions">
|
|
||||||
<property name="name">menuSubscriptions</property>
|
|
||||||
<property name="label" translatable="yes">_Subscriptions</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="itemFind">
|
|
||||||
<property name="stock_id">gtk-find</property>
|
|
||||||
<property name="name">itemFind</property>
|
|
||||||
<property name="label" translatable="yes">Discover new podcasts</property>
|
|
||||||
<signal handler="on_itemImportChannels_activate" name="activate"/>
|
|
||||||
</object>
|
|
||||||
<accelerator key="F" modifiers="GDK_CONTROL_MASK"/>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="itemAddChannel">
|
|
||||||
<property name="stock_id">gtk-add</property>
|
|
||||||
<property name="name">itemAddChannel</property>
|
|
||||||
<property name="label" translatable="yes">Add podcast via URL</property>
|
|
||||||
<signal handler="on_itemAddChannel_activate" name="activate"/>
|
|
||||||
</object>
|
|
||||||
<accelerator key="L" modifiers="GDK_CONTROL_MASK"/>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="itemEditChannel">
|
|
||||||
<property name="stock_id">gtk-edit</property>
|
|
||||||
<property name="name">itemEditChannel</property>
|
|
||||||
<property name="label" translatable="yes">Podcast settings</property>
|
|
||||||
<signal handler="on_itemEditChannel_activate" name="activate"/>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="itemRemoveChannel">
|
|
||||||
<property name="stock_id">gtk-remove</property>
|
|
||||||
<property name="name">itemRemoveChannel</property>
|
|
||||||
<property name="label" translatable="yes">Unsubscribe</property>
|
|
||||||
<signal handler="on_itemRemoveChannel_activate" name="activate"/>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="itemMassUnsubscribe">
|
|
||||||
<property name="stock_id">gtk-remove</property>
|
|
||||||
<property name="label" translatable="yes">Remove podcasts</property>
|
|
||||||
<signal handler="on_itemMassUnsubscribe_activate" name="activate"/>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="itemUpdateChannel">
|
|
||||||
<property name="stock_id">gtk-refresh</property>
|
|
||||||
<property name="name">itemUpdateChannel</property>
|
|
||||||
<property name="label" translatable="yes">Update podcast</property>
|
|
||||||
<signal handler="on_itemUpdateChannel_activate" name="activate"/>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="item_import_from_file">
|
|
||||||
<property name="stock_id">gtk-open</property>
|
|
||||||
<property name="name">item_import_from_file</property>
|
|
||||||
<property name="label" translatable="yes">Import from OPML file</property>
|
|
||||||
<signal handler="on_item_import_from_file_activate" name="activate"/>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="itemExportChannels">
|
|
||||||
<property name="stock_id">gtk-save-as</property>
|
|
||||||
<property name="name">itemExportChannels</property>
|
|
||||||
<property name="label" translatable="yes">Export to OPML file</property>
|
|
||||||
<signal handler="on_itemExportChannels_activate" name="activate"/>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="item_goto_mygpo">
|
|
||||||
<property name="label" translatable="yes">Go to gpodder.net</property>
|
|
||||||
<signal handler="on_goto_mygpo" name="activate"/>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="menuChannels">
|
|
||||||
<property name="name">menuChannels</property>
|
|
||||||
<property name="label" translatable="yes">_Episodes</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="itemPlaySelected">
|
|
||||||
<property name="stock_id">gtk-media-play</property>
|
|
||||||
<property name="name">itemPlaySelected</property>
|
|
||||||
<property name="label" translatable="yes">Play</property>
|
|
||||||
<signal handler="on_playback_selected_episodes" name="activate"/>
|
|
||||||
</object>
|
|
||||||
<accelerator key="Return" modifiers="GDK_SHIFT_MASK"/>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="itemOpenSelected">
|
|
||||||
<property name="stock_id">gtk-open</property>
|
|
||||||
<property name="name">itemOpenSelected</property>
|
|
||||||
<property name="label" translatable="yes">Open</property>
|
|
||||||
<signal handler="on_playback_selected_episodes" name="activate"/>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="itemDownloadSelected">
|
|
||||||
<property name="stock_id">gtk-goto-bottom</property>
|
|
||||||
<property name="name">itemDownloadSelected</property>
|
|
||||||
<property name="label" translatable="yes">Download</property>
|
|
||||||
<signal handler="on_download_selected_episodes" name="activate"/>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="item_cancel_download">
|
|
||||||
<property name="stock_id">gtk-stop</property>
|
|
||||||
<property name="name">item_cancel_download</property>
|
|
||||||
<property name="label" translatable="yes">Cancel</property>
|
|
||||||
<signal handler="on_item_cancel_download_activate" name="activate"/>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="itemDeleteSelected">
|
|
||||||
<property name="stock_id">gtk-delete</property>
|
|
||||||
<property name="name">itemDeleteSelected</property>
|
|
||||||
<property name="label" translatable="yes">Delete</property>
|
|
||||||
<signal handler="on_btnDownloadedDelete_clicked" name="activate"/>
|
|
||||||
</object>
|
|
||||||
<accelerator key="Delete" modifiers="0"/>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="item_toggle_played">
|
|
||||||
<property name="stock_id">gtk-apply</property>
|
|
||||||
<property name="name">item_toggle_played</property>
|
|
||||||
<property name="label" translatable="yes">Toggle new status</property>
|
|
||||||
<signal handler="on_item_toggle_played_activate" name="activate"/>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="item_toggle_lock">
|
|
||||||
<property name="stock_id">gtk-dialog-authentication</property>
|
|
||||||
<property name="name">item_toggle_lock</property>
|
|
||||||
<property name="label" translatable="yes">Change delete lock</property>
|
|
||||||
<signal handler="on_item_toggle_lock_activate" name="activate"/>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="item_episode_details">
|
|
||||||
<property name="stock_id">gtk-info</property>
|
|
||||||
<property name="name">item_episode_details</property>
|
|
||||||
<property name="label" translatable="yes">Episode details</property>
|
|
||||||
<signal handler="on_shownotes_selected_episodes" name="activate"/>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="menuExtras">
|
|
||||||
<property name="name">menuExtras</property>
|
|
||||||
<property name="label" translatable="yes">E_xtras</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="item_sync">
|
|
||||||
<property name="stock_id">gtk-refresh</property>
|
|
||||||
<property name="name">item_sync</property>
|
|
||||||
<property name="label" translatable="yes">Sync to device</property>
|
|
||||||
<signal handler="on_sync_to_device_activate" name="activate"/>
|
|
||||||
</object>
|
|
||||||
<accelerator key="S" modifiers="GDK_CONTROL_MASK"/>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="item_update_youtube_subscriptions">
|
|
||||||
<property name="name">item_update_youtube_subscriptions</property>
|
|
||||||
<property name="label" translatable="yes">Update YouTube subscriptions</property>
|
|
||||||
<signal handler="on_update_youtube_subscriptions_activate" name="activate"/>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="menuView">
|
|
||||||
<property name="name">menuView</property>
|
|
||||||
<property name="label" translatable="yes">_View</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkToggleAction" id="itemShowToolbar">
|
|
||||||
<property name="active">True</property>
|
|
||||||
<property name="name">itemShowToolbar</property>
|
|
||||||
<property name="label" translatable="yes">Toolbar</property>
|
|
||||||
<signal handler="on_itemShowToolbar_activate" name="activate"/>
|
|
||||||
</object>
|
|
||||||
<accelerator key="T" modifiers="GDK_CONTROL_MASK"/>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkToggleAction" id="itemShowDescription">
|
|
||||||
<property name="active">True</property>
|
|
||||||
<property name="name">itemShowDescription</property>
|
|
||||||
<property name="label" translatable="yes">Episode descriptions</property>
|
|
||||||
<signal handler="on_itemShowDescription_activate" name="activate"/>
|
|
||||||
</object>
|
|
||||||
<accelerator key="D" modifiers="GDK_CONTROL_MASK"/>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkRadioAction" id="item_view_episodes_all">
|
|
||||||
<property name="name">item_view_episodes_all</property>
|
|
||||||
<property name="label" translatable="yes">All episodes</property>
|
|
||||||
<signal handler="on_item_view_episodes_changed" name="changed"/>
|
|
||||||
</object>
|
|
||||||
<accelerator key="0" modifiers="GDK_CONTROL_MASK"/>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkRadioAction" id="item_view_episodes_undeleted">
|
|
||||||
<property name="active">True</property>
|
|
||||||
<property name="group">item_view_episodes_all</property>
|
|
||||||
<property name="name">item_view_episodes_undeleted</property>
|
|
||||||
<property name="label" translatable="yes">Hide deleted episodes</property>
|
|
||||||
<signal handler="on_item_view_episodes_changed" name="changed"/>
|
|
||||||
</object>
|
|
||||||
<accelerator key="1" modifiers="GDK_CONTROL_MASK"/>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkRadioAction" id="item_view_episodes_downloaded">
|
|
||||||
<property name="group">item_view_episodes_all</property>
|
|
||||||
<property name="name">item_view_episodes_downloaded</property>
|
|
||||||
<property name="label" translatable="yes">Downloaded episodes</property>
|
|
||||||
<signal handler="on_item_view_episodes_changed" name="changed"/>
|
|
||||||
</object>
|
|
||||||
<accelerator key="2" modifiers="GDK_CONTROL_MASK"/>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkRadioAction" id="item_view_episodes_unplayed">
|
|
||||||
<property name="group">item_view_episodes_all</property>
|
|
||||||
<property name="name">item_view_episodes_unplayed</property>
|
|
||||||
<property name="label" translatable="yes">Unplayed episodes</property>
|
|
||||||
<signal handler="on_item_view_episodes_changed" name="changed"/>
|
|
||||||
</object>
|
|
||||||
<accelerator key="3" modifiers="GDK_CONTROL_MASK"/>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkToggleAction" id="item_view_hide_boring_podcasts">
|
|
||||||
<property name="active">False</property>
|
|
||||||
<property name="name">item_view_hide_boring_podcasts</property>
|
|
||||||
<property name="label" translatable="yes">Hide podcasts without episodes</property>
|
|
||||||
<signal handler="on_item_view_hide_boring_podcasts_toggled" name="toggled"/>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="menuHelp">
|
|
||||||
<property name="name">menuHelp</property>
|
|
||||||
<property name="label" translatable="yes">_Help</property>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="help">
|
|
||||||
<property name="stock_id">gtk-help</property>
|
|
||||||
<property name="name">help</property>
|
|
||||||
<property name="label" translatable="yes">User manual</property>
|
|
||||||
<signal handler="on_help_activate" name="activate"/>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="item_check_for_updates">
|
|
||||||
<property name="name">item_check_for_updates</property>
|
|
||||||
<property name="label" translatable="yes">Software updates</property>
|
|
||||||
<signal handler="on_check_for_updates_activate" name="activate"/>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkAction" id="itemAbout">
|
|
||||||
<property name="stock_id">gtk-about</property>
|
|
||||||
<property name="name">itemAbout</property>
|
|
||||||
<signal handler="on_itemAbout_activate" name="activate"/>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<ui>
|
|
||||||
<menubar name="mainMenu">
|
|
||||||
<menu action="menuPodcasts">
|
|
||||||
<menuitem action="itemUpdate"/>
|
|
||||||
<menuitem action="itemDownloadAllNew"/>
|
|
||||||
<menuitem action="itemRemoveOldEpisodes"/>
|
|
||||||
<separator/>
|
|
||||||
<menuitem action="itemPreferences"/>
|
|
||||||
<separator/>
|
|
||||||
<menuitem action="itemQuit"/>
|
|
||||||
</menu>
|
|
||||||
<menu action="menuSubscriptions">
|
|
||||||
<menuitem action="itemFind"/>
|
|
||||||
<menuitem action="itemAddChannel"/>
|
|
||||||
<menuitem action="itemMassUnsubscribe"/>
|
|
||||||
<separator/>
|
|
||||||
<menuitem action="itemUpdateChannel"/>
|
|
||||||
<menuitem action="itemEditChannel"/>
|
|
||||||
<separator/>
|
|
||||||
<menuitem action="item_import_from_file"/>
|
|
||||||
<menuitem action="itemExportChannels"/>
|
|
||||||
</menu>
|
|
||||||
<menu action="menuChannels">
|
|
||||||
<menuitem action="itemPlaySelected"/>
|
|
||||||
<menuitem action="itemOpenSelected"/>
|
|
||||||
<menuitem action="itemDownloadSelected"/>
|
|
||||||
<menuitem action="item_cancel_download"/>
|
|
||||||
<menuitem action="itemDeleteSelected"/>
|
|
||||||
<separator/>
|
|
||||||
<menuitem action="item_toggle_played"/>
|
|
||||||
<menuitem action="item_toggle_lock"/>
|
|
||||||
<separator/>
|
|
||||||
<menuitem action="item_episode_details"/>
|
|
||||||
</menu>
|
|
||||||
<menu action="menuExtras">
|
|
||||||
<menuitem action="item_sync"/>
|
|
||||||
<menuitem action="item_update_youtube_subscriptions"/>
|
|
||||||
</menu>
|
|
||||||
<menu action="menuView">
|
|
||||||
<menuitem action="itemShowToolbar"/>
|
|
||||||
<menuitem action="itemShowDescription"/>
|
|
||||||
<separator/>
|
|
||||||
<menuitem action="item_view_episodes_all"/>
|
|
||||||
<menuitem action="item_view_episodes_undeleted"/>
|
|
||||||
<menuitem action="item_view_episodes_downloaded"/>
|
|
||||||
<menuitem action="item_view_episodes_unplayed"/>
|
|
||||||
<separator/>
|
|
||||||
<menuitem action="item_view_hide_boring_podcasts"/>
|
|
||||||
</menu>
|
|
||||||
<menu action="menuHelp">
|
|
||||||
<menuitem action="help"/>
|
|
||||||
<menuitem action="item_goto_mygpo"/>
|
|
||||||
<menuitem action="item_check_for_updates"/>
|
|
||||||
<separator/>
|
|
||||||
<menuitem action="itemAbout"/>
|
|
||||||
</menu>
|
|
||||||
</menubar>
|
|
||||||
</ui>
|
|
||||||
</object>
|
|
||||||
<object class="GtkWindow" id="gPodder">
|
|
||||||
<property name="visible">False</property>
|
<property name="visible">False</property>
|
||||||
<property name="title">gPodder</property>
|
<property name="title">gPodder</property>
|
||||||
<property name="window_position">GTK_WIN_POS_CENTER</property>
|
<property name="window_position">GTK_WIN_POS_CENTER</property>
|
||||||
|
@ -408,23 +28,11 @@
|
||||||
<property name="type_hint">GDK_WINDOW_TYPE_HINT_NORMAL</property>
|
<property name="type_hint">GDK_WINDOW_TYPE_HINT_NORMAL</property>
|
||||||
<property name="focus_on_map">True</property>
|
<property name="focus_on_map">True</property>
|
||||||
<property name="urgency_hint">False</property>
|
<property name="urgency_hint">False</property>
|
||||||
<signal handler="on_gPodder_delete_event" name="delete_event"/>
|
<signal handler="on_gPodder_delete_event" name="delete-event"/>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkVBox" id="vMain">
|
<object class="GtkGrid" id="vMain">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="homogeneous">False</property>
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
|
||||||
<object class="GtkMenuBar" constructor="uimanager1" id="mainMenu">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="pack_direction">GTK_PACK_DIRECTION_LTR</property>
|
|
||||||
<property name="child_pack_direction">GTK_PACK_DIRECTION_LTR</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="padding">0</property>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkToolbar" id="toolbar">
|
<object class="GtkToolbar" id="toolbar">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
@ -496,7 +104,7 @@
|
||||||
<property name="visible_horizontal">True</property>
|
<property name="visible_horizontal">True</property>
|
||||||
<property name="visible_vertical">True</property>
|
<property name="visible_vertical">True</property>
|
||||||
<property name="is_important">False</property>
|
<property name="is_important">False</property>
|
||||||
<signal handler="on_itemPreferences_activate" name="clicked"/>
|
<property name="action-name">app.preferences</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">False</property>
|
<property name="expand">False</property>
|
||||||
|
@ -529,17 +137,14 @@
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
|
||||||
<property name="padding">0</property>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkHBox" id="hboxContainer">
|
<object class="GtkGrid" id="hboxContainer">
|
||||||
<property name="border_width">5</property>
|
<property name="border_width">5</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="homogeneous">False</property>
|
<property name="orientation">horizontal</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="vexpand">True</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkNotebook" id="wNotebook">
|
<object class="GtkNotebook" id="wNotebook">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
@ -551,21 +156,23 @@
|
||||||
<property name="enable_popup">False</property>
|
<property name="enable_popup">False</property>
|
||||||
<signal handler="on_wNotebook_switch_page" name="switch_page"/>
|
<signal handler="on_wNotebook_switch_page" name="switch_page"/>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkHPaned" id="channelPaned">
|
<object class="GtkPaned" id="channelPaned">
|
||||||
<property name="border_width">5</property>
|
<property name="border_width">5</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkVBox" id="vboxChannelNavigator">
|
<object class="GtkGrid" id="vboxChannelNavigator">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="homogeneous">False</property>
|
<property name="row_spacing">5</property>
|
||||||
<property name="spacing">5</property>
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkScrolledWindow" id="scrolledwindow6">
|
<object class="GtkScrolledWindow" id="scrolledwindow6">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
|
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
|
||||||
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
|
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
|
||||||
|
<property name="vexpand">True</property>
|
||||||
<property name="shadow_type">GTK_SHADOW_IN</property>
|
<property name="shadow_type">GTK_SHADOW_IN</property>
|
||||||
<property name="window_placement">GTK_CORNER_TOP_LEFT</property>
|
<property name="window_placement">GTK_CORNER_TOP_LEFT</property>
|
||||||
<child>
|
<child>
|
||||||
|
@ -583,21 +190,17 @@
|
||||||
<signal handler="on_treeChannels_row_activated" name="row_activated"/>
|
<signal handler="on_treeChannels_row_activated" name="row_activated"/>
|
||||||
<signal handler="on_treeChannels_cursor_changed" name="cursor_changed"/>
|
<signal handler="on_treeChannels_cursor_changed" name="cursor_changed"/>
|
||||||
<signal handler="on_treeview_query_tooltip" name="query-tooltip"/>
|
<signal handler="on_treeview_query_tooltip" name="query-tooltip"/>
|
||||||
<signal handler="on_treeview_expose_event" name="expose-event"/>
|
<signal handler="on_treeview_expose_event" name="draw"/>
|
||||||
<signal handler="on_treeview_button_pressed" name="button-press-event"/>
|
<signal handler="on_treeview_button_pressed" name="button-press-event"/>
|
||||||
<signal handler="on_treeview_podcasts_button_released" name="button-release-event"/>
|
<signal handler="on_treeview_podcasts_button_released" name="button-release-event"/>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
|
||||||
<property name="padding">0</property>
|
|
||||||
<property name="expand">True</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkHBox" id="hbox_search_podcasts">
|
<object class="GtkGrid" id="hbox_search_podcasts">
|
||||||
<property name="spacing">6</property>
|
<property name="column_spacing">6</property>
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkEntry" id="entry_search_podcasts">
|
<object class="GtkEntry" id="entry_search_podcasts">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
@ -608,41 +211,34 @@
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkVBox" id="vbox42">
|
<object class="GtkGrid" id="vbox42">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="homogeneous">False</property>
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkButton" id="btnUpdateFeeds">
|
<object class="GtkButton" id="btnUpdateFeeds">
|
||||||
<property name="label" translatable="yes">Check for new episodes</property>
|
<property name="label" translatable="yes">Check for new episodes</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
<property name="focus_on_click">True</property>
|
<property name="focus_on_click">True</property>
|
||||||
<signal handler="on_itemUpdate_activate" name="clicked"/>
|
<property name="action-name">win.update</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="padding">0</property>
|
|
||||||
<property name="expand">True</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkHBox" id="hboxUpdateFeeds">
|
<object class="GtkGrid" id="hboxUpdateFeeds">
|
||||||
<property name="homogeneous">False</property>
|
<property name="column_spacing">6</property>
|
||||||
<property name="spacing">6</property>
|
<property name="orientation">horizontal</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkProgressBar" id="pbFeedUpdate">
|
<object class="GtkProgressBar" id="pbFeedUpdate">
|
||||||
|
<property name="hexpand">True</property>
|
||||||
<property name="pulse_step">0.10000000149</property>
|
<property name="pulse_step">0.10000000149</property>
|
||||||
|
<property name="show-text">True</property>
|
||||||
<property name="ellipsize">PANGO_ELLIPSIZE_MIDDLE</property>
|
<property name="ellipsize">PANGO_ELLIPSIZE_MIDDLE</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="padding">0</property>
|
|
||||||
<property name="expand">True</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
|
@ -658,25 +254,12 @@
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
|
||||||
<property name="padding">0</property>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="padding">0</property>
|
|
||||||
<property name="expand">True</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
|
||||||
<property name="padding">0</property>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
|
@ -685,9 +268,10 @@
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkVBox" id="vbox_episode_list">
|
<object class="GtkGrid" id="vbox_episode_list">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="spacing">6</property>
|
<property name="row_spacing">6</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkScrolledWindow" id="scrollAvailable">
|
<object class="GtkScrolledWindow" id="scrollAvailable">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
@ -695,6 +279,8 @@
|
||||||
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
|
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
|
||||||
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
|
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
|
||||||
<property name="shadow_type">GTK_SHADOW_IN</property>
|
<property name="shadow_type">GTK_SHADOW_IN</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="vexpand">True</property>
|
||||||
<property name="window_placement">GTK_CORNER_TOP_LEFT</property>
|
<property name="window_placement">GTK_CORNER_TOP_LEFT</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkTreeView" id="treeAvailable">
|
<object class="GtkTreeView" id="treeAvailable">
|
||||||
|
@ -711,29 +297,22 @@
|
||||||
<property name="hover_expand">False</property>
|
<property name="hover_expand">False</property>
|
||||||
<signal handler="on_treeAvailable_row_activated" name="row_activated"/>
|
<signal handler="on_treeAvailable_row_activated" name="row_activated"/>
|
||||||
<signal handler="on_treeview_query_tooltip" name="query-tooltip"/>
|
<signal handler="on_treeview_query_tooltip" name="query-tooltip"/>
|
||||||
<signal handler="on_treeview_expose_event" name="expose-event"/>
|
<signal handler="on_treeview_expose_event" name="draw"/>
|
||||||
<signal handler="on_treeview_button_pressed" name="button-press-event"/>
|
<signal handler="on_treeview_button_pressed" name="button-press-event"/>
|
||||||
<signal handler="on_treeview_episodes_button_released" name="button-release-event"/>
|
<signal handler="on_treeview_episodes_button_released" name="button-release-event"/>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
|
||||||
<property name="expand">True</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkHBox" id="hbox_search_episodes">
|
<object class="GtkGrid" id="hbox_search_episodes">
|
||||||
<property name="spacing">6</property>
|
<property name="column_spacing">6</property>
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="label_search_episodes">
|
<object class="GtkLabel" id="label_search_episodes">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="label" translatable="yes">Filter:</property>
|
<property name="label" translatable="yes">Filter:</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkEntry" id="entry_search_episodes">
|
<object class="GtkEntry" id="entry_search_episodes">
|
||||||
|
@ -745,10 +324,6 @@
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
|
@ -775,15 +350,16 @@
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkVBox" id="vboxDownloadStatusWidgets">
|
<object class="GtkGrid" id="vboxDownloadStatusWidgets">
|
||||||
<property name="border_width">5</property>
|
<property name="border_width">5</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="homogeneous">False</property>
|
<property name="row_spacing">5</property>
|
||||||
<property name="spacing">5</property>
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkScrolledWindow" id="scrolledwindow1">
|
<object class="GtkScrolledWindow" id="scrolledwindow1">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
|
<property name="vexpand">True</property>
|
||||||
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
|
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
|
||||||
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
|
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
|
||||||
<property name="shadow_type">GTK_SHADOW_IN</property>
|
<property name="shadow_type">GTK_SHADOW_IN</property>
|
||||||
|
@ -795,32 +371,30 @@
|
||||||
<property name="headers_visible">False</property>
|
<property name="headers_visible">False</property>
|
||||||
<property name="rules_hint">False</property>
|
<property name="rules_hint">False</property>
|
||||||
<property name="rubber-banding">True</property>
|
<property name="rubber-banding">True</property>
|
||||||
<property name="reorderable">False</property>
|
<property name="reorderable">True</property>
|
||||||
<property name="enable_search">True</property>
|
<property name="enable_search">True</property>
|
||||||
<property name="fixed_height_mode">False</property>
|
<property name="fixed_height_mode">False</property>
|
||||||
<property name="hover_selection">False</property>
|
<property name="hover_selection">False</property>
|
||||||
<property name="hover_expand">False</property>
|
<property name="hover_expand">False</property>
|
||||||
<signal handler="on_treeDownloads_row_activated" name="row_activated"/>
|
<signal handler="on_treeDownloads_row_activated" name="row_activated"/>
|
||||||
<signal handler="on_treeview_expose_event" name="expose-event"/>
|
<signal handler="on_treeview_expose_event" name="draw"/>
|
||||||
<signal handler="on_treeview_button_pressed" name="button-press-event"/>
|
<signal handler="on_treeview_button_pressed" name="button-press-event"/>
|
||||||
<signal handler="on_treeview_downloads_button_released" name="button-release-event"/>
|
<signal handler="on_treeview_downloads_button_released" name="button-release-event"/>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
|
||||||
<property name="padding">0</property>
|
|
||||||
<property name="expand">True</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkHBox" id="hboxDownloadSettings">
|
<object class="GtkGrid" id="hboxDownloadSettings">
|
||||||
|
<property name="border_width">5</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="spacing">10</property>
|
<property name="column_spacing">10</property>
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkHBox" id="hboxDownloadLimit">
|
<object class="GtkGrid" id="hboxDownloadLimit">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="spacing">5</property>
|
<property name="column_spacing">5</property>
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkCheckButton" id="cbLimitDownloads">
|
<object class="GtkCheckButton" id="cbLimitDownloads">
|
||||||
<property name="label" translatable="yes">Limit rate to</property>
|
<property name="label" translatable="yes">Limit rate to</property>
|
||||||
|
@ -830,9 +404,6 @@
|
||||||
<property name="draw_indicator">True</property>
|
<property name="draw_indicator">True</property>
|
||||||
<signal name="toggled" handler="on_cbLimitDownloads_toggled"/>
|
<signal name="toggled" handler="on_cbLimitDownloads_toggled"/>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkSpinButton" id="spinLimitDownloads">
|
<object class="GtkSpinButton" id="spinLimitDownloads">
|
||||||
|
@ -843,9 +414,6 @@
|
||||||
<property name="digits">1</property>
|
<property name="digits">1</property>
|
||||||
<property name="adjustment">adjustment1</property>
|
<property name="adjustment">adjustment1</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="labelLimitRate">
|
<object class="GtkLabel" id="labelLimitRate">
|
||||||
|
@ -853,27 +421,20 @@
|
||||||
<property name="xalign">0</property>
|
<property name="xalign">0</property>
|
||||||
<property name="label" translatable="yes">KiB/s</property>
|
<property name="label" translatable="yes">KiB/s</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="DownloadSettingsSpacer">
|
<object class="GtkLabel" id="DownloadSettingsSpacer">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
|
||||||
<property name="expand">True</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkHBox" id="hboxDownloadRate">
|
<object class="GtkGrid" id="hboxDownloadRate">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="spacing">5</property>
|
<property name="column_spacing">5</property>
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkCheckButton" id="cbMaxDownloads">
|
<object class="GtkCheckButton" id="cbMaxDownloads">
|
||||||
<property name="label" translatable="yes">Limit downloads to</property>
|
<property name="label" translatable="yes">Limit downloads to</property>
|
||||||
|
@ -883,9 +444,6 @@
|
||||||
<property name="draw_indicator">True</property>
|
<property name="draw_indicator">True</property>
|
||||||
<signal name="toggled" handler="on_cbMaxDownloads_toggled"/>
|
<signal name="toggled" handler="on_cbMaxDownloads_toggled"/>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkSpinButton" id="spinMaxDownloads">
|
<object class="GtkSpinButton" id="spinMaxDownloads">
|
||||||
|
@ -895,21 +453,10 @@
|
||||||
<property name="climb_rate">1</property>
|
<property name="climb_rate">1</property>
|
||||||
<property name="adjustment">adjustment2</property>
|
<property name="adjustment">adjustment2</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
|
||||||
<property name="padding">0</property>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
|
@ -930,17 +477,9 @@
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
|
||||||
<property name="padding">0</property>
|
|
||||||
<property name="expand">True</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="padding">0</property>
|
|
||||||
<property name="expand">True</property>
|
|
||||||
<property name="fill">True</property>
|
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
|
|
|
@ -5,9 +5,11 @@
|
||||||
<property name="title" translatable="yes">Add a new podcast</property>
|
<property name="title" translatable="yes">Add a new podcast</property>
|
||||||
<property name="type_hint">dialog</property>
|
<property name="type_hint">dialog</property>
|
||||||
<property name="modal">True</property>
|
<property name="modal">True</property>
|
||||||
|
<property name="transient-for">parent_widget</property>
|
||||||
<property name="default_width">400</property>
|
<property name="default_width">400</property>
|
||||||
<child internal-child="vbox">
|
<child internal-child="vbox">
|
||||||
<object class="GtkVBox" id="vboxmain">
|
<object class="GtkBox" id="vboxmain">
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
<child internal-child="action_area">
|
<child internal-child="action_area">
|
||||||
<object class="GtkHButtonBox" id="hbuttonbox">
|
<object class="GtkHButtonBox" id="hbuttonbox">
|
||||||
<property name="layout_style">GTK_BUTTONBOX_END</property>
|
<property name="layout_style">GTK_BUTTONBOX_END</property>
|
||||||
|
@ -37,11 +39,12 @@
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkHBox" id="hboxurlentry">
|
<object class="GtkBox" id="hboxurlentry">
|
||||||
<property name="border_width">10</property>
|
<property name="border_width">10</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="homogeneous">False</property>
|
<property name="homogeneous">False</property>
|
||||||
<property name="spacing">5</property>
|
<property name="spacing">5</property>
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="label_add">
|
<object class="GtkLabel" id="label_add">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
|
|
@ -6,16 +6,18 @@
|
||||||
<property name="border_width">10</property>
|
<property name="border_width">10</property>
|
||||||
<property name="title" translatable="yes">gPodder Podcast Editor</property>
|
<property name="title" translatable="yes">gPodder Podcast Editor</property>
|
||||||
<property name="modal">True</property>
|
<property name="modal">True</property>
|
||||||
|
<property name="transient-for">parent_widget</property>
|
||||||
<property name="window_position">center-on-parent</property>
|
<property name="window_position">center-on-parent</property>
|
||||||
<property name="default_width">500</property>
|
<property name="default_width">500</property>
|
||||||
<property name="default_height">400</property>
|
<property name="default_height">400</property>
|
||||||
<property name="type_hint">dialog</property>
|
<property name="type_hint">dialog</property>
|
||||||
<signal name="destroy" handler="on_gPodderChannel_destroy" swapped="no"/>
|
<signal name="destroy" handler="on_gPodderChannel_destroy" swapped="no"/>
|
||||||
<child internal-child="vbox">
|
<child internal-child="vbox">
|
||||||
<object class="GtkVBox" id="vboxChannelEditorMain">
|
<object class="GtkBox" id="vboxChannelEditorMain">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="spacing">5</property>
|
<property name="spacing">5</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
<child internal-child="action_area">
|
<child internal-child="action_area">
|
||||||
<object class="GtkHButtonBox" id="hboxButtons">
|
<object class="GtkHButtonBox" id="hboxButtons">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
@ -263,44 +265,44 @@
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkVBox" id="vboxiPodProperties">
|
<object class="GtkBox" id="vboxiPodProperties">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="border_width">10</property>
|
<property name="border_width">10</property>
|
||||||
<property name="spacing">5</property>
|
<property name="spacing">5</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="label96">
|
<object class="GtkGrid" id="table10">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="xalign">0</property>
|
|
||||||
<property name="label" translatable="yes"><b>HTTP/FTP Authentication</b></property>
|
|
||||||
<property name="use_markup">True</property>
|
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
<property name="position">0</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkTable" id="table10">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">False</property>
|
|
||||||
<property name="n_rows">2</property>
|
|
||||||
<property name="n_columns">2</property>
|
|
||||||
<property name="column_spacing">6</property>
|
<property name="column_spacing">6</property>
|
||||||
<property name="row_spacing">6</property>
|
<property name="row_spacing">6</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="label96">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">False</property>
|
||||||
|
<property name="xalign">0</property>
|
||||||
|
<property name="label" translatable="yes"><b>HTTP/FTP Authentication</b></property>
|
||||||
|
<property name="use_markup">True</property>
|
||||||
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="left_attach">0</property>
|
||||||
|
<property name="top_attach">0</property>
|
||||||
|
<property name="width">3</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="label93">
|
<object class="GtkLabel" id="label93">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="xalign">0</property>
|
<property name="xalign">0</property>
|
||||||
<property name="label" translatable="yes">Username:</property>
|
<property name="label" translatable="yes">Username:</property>
|
||||||
|
<property name="expand">False</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="x_options">GTK_FILL</property>
|
<property name="left_attach">0</property>
|
||||||
<property name="y_options"/>
|
<property name="top_attach">1</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="label94">
|
<object class="GtkLabel" id="label94">
|
||||||
|
@ -308,12 +310,11 @@
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="xalign">0</property>
|
<property name="xalign">0</property>
|
||||||
<property name="label" translatable="yes">Password:</property>
|
<property name="label" translatable="yes">Password:</property>
|
||||||
|
<property name="expand">False</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="top_attach">1</property>
|
<property name="left_attach">0</property>
|
||||||
<property name="bottom_attach">2</property>
|
<property name="top_attach">2</property>
|
||||||
<property name="x_options">GTK_FILL</property>
|
|
||||||
<property name="y_options"/>
|
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
|
@ -325,11 +326,12 @@
|
||||||
<property name="secondary_icon_activatable">False</property>
|
<property name="secondary_icon_activatable">False</property>
|
||||||
<property name="primary_icon_sensitive">True</property>
|
<property name="primary_icon_sensitive">True</property>
|
||||||
<property name="secondary_icon_sensitive">True</property>
|
<property name="secondary_icon_sensitive">True</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="vexpand">False</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="left_attach">1</property>
|
<property name="left_attach">1</property>
|
||||||
<property name="right_attach">2</property>
|
<property name="top_attach">1</property>
|
||||||
<property name="y_options"/>
|
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
|
@ -341,65 +343,50 @@
|
||||||
<property name="secondary_icon_activatable">False</property>
|
<property name="secondary_icon_activatable">False</property>
|
||||||
<property name="primary_icon_sensitive">True</property>
|
<property name="primary_icon_sensitive">True</property>
|
||||||
<property name="secondary_icon_sensitive">True</property>
|
<property name="secondary_icon_sensitive">True</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="vexpand">False</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="left_attach">1</property>
|
<property name="left_attach">1</property>
|
||||||
<property name="right_attach">2</property>
|
<property name="top_attach">2</property>
|
||||||
<property name="top_attach">1</property>
|
|
||||||
<property name="bottom_attach">2</property>
|
|
||||||
<property name="y_options"/>
|
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
<child>
|
||||||
<packing>
|
<object class="GtkHSeparator" id="hseparator13">
|
||||||
<property name="expand">False</property>
|
<property name="visible">True</property>
|
||||||
<property name="fill">True</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="position">1</property>
|
</object>
|
||||||
</packing>
|
<packing>
|
||||||
</child>
|
<property name="left_attach">0</property>
|
||||||
<child>
|
<property name="top_attach">3</property>
|
||||||
<object class="GtkHSeparator" id="hseparator13">
|
<property name="width">3</property>
|
||||||
<property name="visible">True</property>
|
</packing>
|
||||||
<property name="can_focus">False</property>
|
</child>
|
||||||
</object>
|
<child>
|
||||||
<packing>
|
<object class="GtkLabel" id="label97">
|
||||||
<property name="expand">False</property>
|
<property name="visible">True</property>
|
||||||
<property name="fill">True</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="position">2</property>
|
<property name="xalign">0</property>
|
||||||
</packing>
|
<property name="label" translatable="yes"><b>Locations</b></property>
|
||||||
</child>
|
<property name="use_markup">True</property>
|
||||||
<child>
|
</object>
|
||||||
<object class="GtkLabel" id="label97">
|
<packing>
|
||||||
<property name="visible">True</property>
|
<property name="left_attach">0</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="top_attach">4</property>
|
||||||
<property name="xalign">0</property>
|
<property name="width">3</property>
|
||||||
<property name="label" translatable="yes"><b>Locations</b></property>
|
</packing>
|
||||||
<property name="use_markup">True</property>
|
</child>
|
||||||
</object>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
<property name="position">3</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<object class="GtkTable" id="table3">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">False</property>
|
|
||||||
<property name="n_rows">2</property>
|
|
||||||
<property name="n_columns">3</property>
|
|
||||||
<property name="column_spacing">6</property>
|
|
||||||
<property name="row_spacing">6</property>
|
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="label29">
|
<object class="GtkLabel" id="label29">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="xalign">0</property>
|
<property name="xalign">0</property>
|
||||||
<property name="label" translatable="yes">Download to:</property>
|
<property name="label" translatable="yes">Download to:</property>
|
||||||
|
<property name="expand">False</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="x_options">GTK_FILL</property>
|
<property name="left_attach">0</property>
|
||||||
<property name="y_options"/>
|
<property name="top_attach">5</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
|
@ -410,11 +397,13 @@
|
||||||
<property name="label">download to label</property>
|
<property name="label">download to label</property>
|
||||||
<property name="selectable">True</property>
|
<property name="selectable">True</property>
|
||||||
<property name="ellipsize">start</property>
|
<property name="ellipsize">start</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="vexpand">False</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="left_attach">1</property>
|
<property name="left_attach">1</property>
|
||||||
<property name="right_attach">3</property>
|
<property name="top_attach">5</property>
|
||||||
<property name="y_options"/>
|
<property name="width">2</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
|
@ -423,12 +412,11 @@
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="xalign">0</property>
|
<property name="xalign">0</property>
|
||||||
<property name="label" translatable="yes">Website:</property>
|
<property name="label" translatable="yes">Website:</property>
|
||||||
|
<property name="expand">False</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="top_attach">1</property>
|
<property name="top_attach">6</property>
|
||||||
<property name="bottom_attach">2</property>
|
<property name="left_attach">0</property>
|
||||||
<property name="x_options">GTK_FILL</property>
|
|
||||||
<property name="y_options"/>
|
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
|
@ -439,13 +427,12 @@
|
||||||
<property name="label" translatable="yes">website label</property>
|
<property name="label" translatable="yes">website label</property>
|
||||||
<property name="selectable">True</property>
|
<property name="selectable">True</property>
|
||||||
<property name="ellipsize">end</property>
|
<property name="ellipsize">end</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="vexpand">False</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="left_attach">1</property>
|
<property name="left_attach">1</property>
|
||||||
<property name="right_attach">2</property>
|
<property name="top_attach">6</property>
|
||||||
<property name="top_attach">1</property>
|
|
||||||
<property name="bottom_attach">2</property>
|
|
||||||
<property name="y_options"/>
|
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
|
@ -465,17 +452,14 @@
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="left_attach">2</property>
|
<property name="left_attach">2</property>
|
||||||
<property name="right_attach">3</property>
|
<property name="top_attach">6</property>
|
||||||
<property name="top_attach">1</property>
|
|
||||||
<property name="bottom_attach">2</property>
|
|
||||||
<property name="x_options">GTK_FILL</property>
|
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">False</property>
|
<property name="expand">False</property>
|
||||||
<property name="fill">True</property>
|
<property name="fill">True</property>
|
||||||
<property name="position">4</property>
|
<property name="position">1</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
<interface>
|
<interface>
|
||||||
<object class="GtkDialog" id="gPodderConfigEditor">
|
<object class="GtkDialog" id="gPodderConfigEditor">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="has_separator">False</property>
|
|
||||||
<property name="title" translatable="yes">gPodder Configuration Editor</property>
|
<property name="title" translatable="yes">gPodder Configuration Editor</property>
|
||||||
<property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
|
<property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
|
||||||
<property name="modal">True</property>
|
<property name="modal">True</property>
|
||||||
|
<property name="transient-for">parent_widget</property>
|
||||||
<property name="default_width">750</property>
|
<property name="default_width">750</property>
|
||||||
<property name="default_height">450</property>
|
<property name="default_height">450</property>
|
||||||
<property name="destroy_with_parent">False</property>
|
<property name="destroy_with_parent">False</property>
|
||||||
|
@ -17,19 +17,22 @@
|
||||||
<property name="urgency_hint">False</property>
|
<property name="urgency_hint">False</property>
|
||||||
<signal handler="on_gPodderConfigEditor_destroy" name="destroy"/>
|
<signal handler="on_gPodderConfigEditor_destroy" name="destroy"/>
|
||||||
<child internal-child="vbox">
|
<child internal-child="vbox">
|
||||||
<object class="GtkVBox" id="vbox13">
|
<object class="GtkBox" id="vbox13">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="homogeneous">False</property>
|
<property name="homogeneous">False</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkVBox" id="vbox_for_episode_selector">
|
<object class="GtkBox" id="vbox_for_episode_selector">
|
||||||
<property name="border_width">5</property>
|
<property name="border_width">5</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="spacing">5</property>
|
<property name="spacing">5</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkHBox" id="hbox38">
|
<object class="GtkBox" id="hbox38">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="homogeneous">False</property>
|
<property name="homogeneous">False</property>
|
||||||
<property name="spacing">6</property>
|
<property name="spacing">6</property>
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="label121">
|
<object class="GtkLabel" id="label121">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
@ -115,6 +118,11 @@
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="padding">0</property>
|
||||||
|
<property name="expand">True</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkHButtonBox" id="hbuttonbox2">
|
<object class="GtkHButtonBox" id="hbuttonbox2">
|
||||||
|
|
|
@ -3,12 +3,10 @@
|
||||||
<interface>
|
<interface>
|
||||||
<object class="GtkDialog" id="gPodderEpisodeSelector">
|
<object class="GtkDialog" id="gPodderEpisodeSelector">
|
||||||
<property name="visible">False</property>
|
<property name="visible">False</property>
|
||||||
<property name="has_separator">True</property>
|
|
||||||
<property name="title" translatable="yes">Select episodes</property>
|
<property name="title" translatable="yes">Select episodes</property>
|
||||||
<property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
|
<property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
|
||||||
<property name="modal">True</property>
|
<property name="modal">True</property>
|
||||||
<property name="default_width">600</property>
|
<property name="transient-for">parent_widget</property>
|
||||||
<property name="default_height">400</property>
|
|
||||||
<property name="destroy_with_parent">False</property>
|
<property name="destroy_with_parent">False</property>
|
||||||
<property name="skip_taskbar_hint">False</property>
|
<property name="skip_taskbar_hint">False</property>
|
||||||
<property name="skip_pager_hint">False</property>
|
<property name="skip_pager_hint">False</property>
|
||||||
|
@ -16,14 +14,16 @@
|
||||||
<property name="focus_on_map">True</property>
|
<property name="focus_on_map">True</property>
|
||||||
<property name="urgency_hint">False</property>
|
<property name="urgency_hint">False</property>
|
||||||
<child internal-child="vbox">
|
<child internal-child="vbox">
|
||||||
<object class="GtkVBox" id="vbox10">
|
<object class="GtkBox" id="vbox10">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="homogeneous">False</property>
|
<property name="homogeneous">False</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkVBox" id="vbox_for_episode_selector">
|
<object class="GtkBox" id="vbox_for_episode_selector">
|
||||||
<property name="border_width">5</property>
|
<property name="border_width">5</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="spacing">5</property>
|
<property name="spacing">5</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="labelInstructions">
|
<object class="GtkLabel" id="labelInstructions">
|
||||||
<property name="label">additional text</property>
|
<property name="label">additional text</property>
|
||||||
|
@ -71,10 +71,11 @@
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkHBox" id="hboxButtons">
|
<object class="GtkBox" id="hboxButtons">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="homogeneous">False</property>
|
<property name="homogeneous">False</property>
|
||||||
<property name="spacing">5</property>
|
<property name="spacing">5</property>
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkButton" id="btnCheckAll">
|
<object class="GtkButton" id="btnCheckAll">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
@ -91,10 +92,11 @@
|
||||||
<property name="left_padding">0</property>
|
<property name="left_padding">0</property>
|
||||||
<property name="right_padding">0</property>
|
<property name="right_padding">0</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkHBox" id="hbox34">
|
<object class="GtkBox" id="hbox34">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="homogeneous">False</property>
|
<property name="homogeneous">False</property>
|
||||||
<property name="spacing">2</property>
|
<property name="spacing">2</property>
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkImage" id="image2636">
|
<object class="GtkImage" id="image2636">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
@ -151,10 +153,11 @@
|
||||||
<property name="left_padding">0</property>
|
<property name="left_padding">0</property>
|
||||||
<property name="right_padding">0</property>
|
<property name="right_padding">0</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkHBox" id="hbox33">
|
<object class="GtkBox" id="hbox33">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="homogeneous">False</property>
|
<property name="homogeneous">False</property>
|
||||||
<property name="spacing">2</property>
|
<property name="spacing">2</property>
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkImage" id="image2635">
|
<object class="GtkImage" id="image2635">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
@ -221,12 +224,18 @@
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
|
<packing>
|
||||||
|
<property name="padding">0</property>
|
||||||
|
<property name="expand">True</property>
|
||||||
|
<property name="fill">True</property>
|
||||||
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child internal-child="action_area">
|
<child internal-child="action_area">
|
||||||
<object class="GtkHBox" id="hbox35">
|
<object class="GtkBox" id="hbox35">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="homogeneous">False</property>
|
<property name="homogeneous">False</property>
|
||||||
<property name="spacing">5</property>
|
<property name="spacing">5</property>
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkButton" id="btnRemoveAction">
|
<object class="GtkButton" id="btnRemoveAction">
|
||||||
<property name="visible">False</property>
|
<property name="visible">False</property>
|
||||||
|
|
|
@ -8,15 +8,17 @@
|
||||||
<property name="border_width">6</property>
|
<property name="border_width">6</property>
|
||||||
<property name="title" translatable="yes">Find new podcasts</property>
|
<property name="title" translatable="yes">Find new podcasts</property>
|
||||||
<property name="modal">True</property>
|
<property name="modal">True</property>
|
||||||
|
<property name="transient-for">parent_widget</property>
|
||||||
<property name="window_position">center-on-parent</property>
|
<property name="window_position">center-on-parent</property>
|
||||||
<property name="default_width">600</property>
|
<property name="default_width">600</property>
|
||||||
<property name="default_height">400</property>
|
<property name="default_height">400</property>
|
||||||
<property name="type_hint">dialog</property>
|
<property name="type_hint">dialog</property>
|
||||||
<child internal-child="vbox">
|
<child internal-child="vbox">
|
||||||
<object class="GtkVBox" id="vb_directory">
|
<object class="GtkBox" id="vb_directory">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="spacing">6</property>
|
<property name="spacing">6</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
<child internal-child="action_area">
|
<child internal-child="action_area">
|
||||||
<object class="GtkHButtonBox" id="hboxBottomButtons">
|
<object class="GtkHButtonBox" id="hboxBottomButtons">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
@ -94,9 +96,10 @@
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkHPaned" id="hpaned">
|
<object class="GtkPaned" id="hpaned">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">True</property>
|
<property name="can_focus">True</property>
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkScrolledWindow" id="sw_providers">
|
<object class="GtkScrolledWindow" id="sw_providers">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
@ -121,15 +124,17 @@
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkVBox" id="vb_podcasts">
|
<object class="GtkBox" id="vb_podcasts">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="spacing">5</property>
|
<property name="spacing">5</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkHBox" id="hb_text_entry">
|
<object class="GtkBox" id="hb_text_entry">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="spacing">5</property>
|
<property name="spacing">5</property>
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="lb_search">
|
<object class="GtkLabel" id="lb_search">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
|
|
@ -26,9 +26,9 @@
|
||||||
<property name="value">7</property>
|
<property name="value">7</property>
|
||||||
</object>
|
</object>
|
||||||
<object class="GtkDialog" id="gPodderPreferences">
|
<object class="GtkDialog" id="gPodderPreferences">
|
||||||
<property name="visible">True</property>
|
<property name="visible">False</property>
|
||||||
<property name="modal">True</property>
|
<property name="modal">True</property>
|
||||||
<property name="has-separator">False</property>
|
<property name="transient-for">parent_widget</property>
|
||||||
<property name="window-position">GTK_WIN_POS_CENTER_ON_PARENT</property>
|
<property name="window-position">GTK_WIN_POS_CENTER_ON_PARENT</property>
|
||||||
<property name="default_height">260</property>
|
<property name="default_height">260</property>
|
||||||
<property name="default_width">320</property>
|
<property name="default_width">320</property>
|
||||||
|
@ -36,23 +36,23 @@
|
||||||
<property name="type_hint">dialog</property>
|
<property name="type_hint">dialog</property>
|
||||||
<signal name="destroy" handler="on_dialog_destroy"/>
|
<signal name="destroy" handler="on_dialog_destroy"/>
|
||||||
<child internal-child="vbox">
|
<child internal-child="vbox">
|
||||||
<object class="GtkVBox" id="vbox">
|
<object class="GtkBox" id="vbox">
|
||||||
<property name="border_width">2</property>
|
<property name="border_width">2</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkNotebook" id="notebook">
|
<object class="GtkNotebook" id="notebook">
|
||||||
<property name="border_width">6</property>
|
<property name="border_width">6</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkVBox" id="vbox_general">
|
<object class="GtkBox" id="vbox_general">
|
||||||
<property name="border_width">12</property>
|
<property name="border_width">12</property>
|
||||||
<property name="spacing">6</property>
|
<property name="spacing">6</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkTable" id="table_players">
|
<object class="GtkGrid" id="table_players">
|
||||||
<property name="column_spacing">6</property>
|
<property name="column_spacing">6</property>
|
||||||
<property name="n_columns">3</property>
|
|
||||||
<property name="n_rows">2</property>
|
|
||||||
<property name="row_spacing">6</property>
|
<property name="row_spacing">6</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<child>
|
<child>
|
||||||
|
@ -61,9 +61,6 @@
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="xalign">0.0</property>
|
<property name="xalign">0.0</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
|
||||||
<property name="x_options">fill</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="label_video_player">
|
<object class="GtkLabel" id="label_video_player">
|
||||||
|
@ -72,30 +69,28 @@
|
||||||
<property name="xalign">0.0</property>
|
<property name="xalign">0.0</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="bottom_attach">2</property>
|
<property name="left_attach">0</property>
|
||||||
<property name="top_attach">1</property>
|
<property name="top_attach">1</property>
|
||||||
<property name="x_options">fill</property>
|
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkComboBox" id="combo_audio_player_app">
|
<object class="GtkComboBox" id="combo_audio_player_app">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
<signal name="changed" handler="on_combo_audio_player_app_changed"/>
|
<signal name="changed" handler="on_combo_audio_player_app_changed"/>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="left_attach">1</property>
|
<property name="left_attach">1</property>
|
||||||
<property name="right_attach">2</property>
|
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkComboBox" id="combo_video_player_app">
|
<object class="GtkComboBox" id="combo_video_player_app">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
<signal name="changed" handler="on_combo_video_player_app_changed"/>
|
<signal name="changed" handler="on_combo_video_player_app_changed"/>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="bottom_attach">2</property>
|
|
||||||
<property name="left_attach">1</property>
|
<property name="left_attach">1</property>
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="top_attach">1</property>
|
<property name="top_attach">1</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
|
@ -112,8 +107,7 @@
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="left_attach">2</property>
|
<property name="left_attach">2</property>
|
||||||
<property name="right_attach">3</property>
|
<property name="top_attach">0</property>
|
||||||
<property name="x_options">fill</property>
|
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
|
@ -128,11 +122,8 @@
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="bottom_attach">2</property>
|
|
||||||
<property name="left_attach">2</property>
|
<property name="left_attach">2</property>
|
||||||
<property name="right_attach">3</property>
|
|
||||||
<property name="top_attach">1</property>
|
<property name="top_attach">1</property>
|
||||||
<property name="x_options">fill</property>
|
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
|
@ -175,15 +166,14 @@
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkVBox" id="vbox_video">
|
<object class="GtkBox" id="vbox_video">
|
||||||
<property name="border_width">12</property>
|
<property name="border_width">12</property>
|
||||||
<property name="spacing">6</property>
|
<property name="spacing">6</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkTable" id="table_video">
|
<object class="GtkGrid" id="table_video">
|
||||||
<property name="column_spacing">6</property>
|
<property name="column_spacing">6</property>
|
||||||
<property name="n_columns">3</property>
|
|
||||||
<property name="n_rows">3</property>
|
|
||||||
<property name="row_spacing">6</property>
|
<property name="row_spacing">6</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<child>
|
<child>
|
||||||
|
@ -195,21 +185,18 @@
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="top_attach">0</property>
|
<property name="top_attach">0</property>
|
||||||
<property name="bottom_attach">1</property>
|
|
||||||
<property name="x_options">fill</property>
|
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkComboBox" id="combobox_preferred_youtube_format">
|
<object class="GtkComboBox" id="combobox_preferred_youtube_format">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
<signal name="changed" handler="on_combobox_preferred_youtube_format_changed" swapped="no"/>
|
<signal name="changed" handler="on_combobox_preferred_youtube_format_changed" swapped="no"/>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="left_attach">1</property>
|
<property name="left_attach">1</property>
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="top_attach">0</property>
|
<property name="top_attach">0</property>
|
||||||
<property name="bottom_attach">1</property>
|
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
|
@ -221,23 +208,18 @@
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="left_attach">0</property>
|
<property name="left_attach">0</property>
|
||||||
<property name="right_attach">1</property>
|
|
||||||
<property name="top_attach">1</property>
|
<property name="top_attach">1</property>
|
||||||
<property name="bottom_attach">2</property>
|
|
||||||
<property name="x_options">fill</property>
|
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkEntry" id="entry_youtube_api_key">
|
<object class="GtkEntry" id="entry_youtube_api_key">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
<signal handler="on_youtube_api_key_changed" name="changed"/>
|
<signal handler="on_youtube_api_key_changed" name="changed"/>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="left_attach">1</property>
|
<property name="left_attach">1</property>
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="top_attach">1</property>
|
<property name="top_attach">1</property>
|
||||||
<property name="bottom_attach">2</property>
|
|
||||||
<property name="x_options">fill</property>
|
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
|
@ -253,10 +235,7 @@
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="top_attach">1</property>
|
<property name="top_attach">1</property>
|
||||||
<property name="bottom_attach">2</property>
|
|
||||||
<property name="left_attach">2</property>
|
<property name="left_attach">2</property>
|
||||||
<property name="right_attach">3</property>
|
|
||||||
<property name="x_options">fill</property>
|
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
|
@ -268,8 +247,7 @@
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="top_attach">2</property>
|
<property name="top_attach">2</property>
|
||||||
<property name="bottom_attach">3</property>
|
<property name="left_attach">0</property>
|
||||||
<property name="x_options">fill</property>
|
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
|
@ -280,9 +258,7 @@
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="left_attach">1</property>
|
<property name="left_attach">1</property>
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="top_attach">2</property>
|
<property name="top_attach">2</property>
|
||||||
<property name="bottom_attach">3</property>
|
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
|
@ -296,10 +272,11 @@
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkVBox" id="vbox_extensions">
|
<object class="GtkBox" id="vbox_extensions">
|
||||||
<property name="border_width">12</property>
|
<property name="border_width">12</property>
|
||||||
<property name="spacing">6</property>
|
<property name="spacing">6</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkScrolledWindow" id="scrolledwindow2">
|
<object class="GtkScrolledWindow" id="scrolledwindow2">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
@ -477,14 +454,16 @@
|
||||||
</child>
|
</child>
|
||||||
|
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkVBox" id="vbox_updating">
|
<object class="GtkBox" id="vbox_updating">
|
||||||
<property name="border_width">12</property>
|
<property name="border_width">12</property>
|
||||||
<property name="spacing">6</property>
|
<property name="spacing">6</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkHBox" id="hbox_updating_interval">
|
<object class="GtkBox" id="hbox_updating_interval">
|
||||||
<property name="spacing">6</property>
|
<property name="spacing">6</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="label_update_interval">
|
<object class="GtkLabel" id="label_update_interval">
|
||||||
<property name="label" translatable="yes">Update interval:</property>
|
<property name="label" translatable="yes">Update interval:</property>
|
||||||
|
@ -497,13 +476,15 @@
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkHScale" id="hscale_update_interval">
|
<object class="GtkScale" id="hscale_update_interval">
|
||||||
<property name="digits">0</property>
|
<property name="digits">0</property>
|
||||||
<property name="is_focus">True</property>
|
<property name="is_focus">True</property>
|
||||||
<property name="restrict_to_fill_level">False</property>
|
<property name="restrict_to_fill_level">False</property>
|
||||||
<property name="value_pos">bottom</property>
|
<property name="value_pos">bottom</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="adjustment">adjustment_update_interval</property>
|
<property name="adjustment">adjustment_update_interval</property>
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
<signal name="format-value" handler="format_update_interval_value"/>
|
<signal name="format-value" handler="format_update_interval_value"/>
|
||||||
<signal name="value-changed" handler="on_update_interval_value_changed"/>
|
<signal name="value-changed" handler="on_update_interval_value_changed"/>
|
||||||
</object>
|
</object>
|
||||||
|
@ -526,9 +507,10 @@
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkHBox" id="hbox_episode_limit">
|
<object class="GtkBox" id="hbox_episode_limit">
|
||||||
<property name="spacing">6</property>
|
<property name="spacing">6</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="label_episode_limit">
|
<object class="GtkLabel" id="label_episode_limit">
|
||||||
<property name="label" translatable="yes">Maximum number of episodes per podcast:</property>
|
<property name="label" translatable="yes">Maximum number of episodes per podcast:</property>
|
||||||
|
@ -565,9 +547,10 @@
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkHBox" id="hbox_auto_download">
|
<object class="GtkBox" id="hbox_auto_download">
|
||||||
<property name="spacing">6</property>
|
<property name="spacing">6</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="label_auto_download">
|
<object class="GtkLabel" id="label_auto_download">
|
||||||
<property name="label" translatable="yes">When new episodes are found:</property>
|
<property name="label" translatable="yes">When new episodes are found:</property>
|
||||||
|
@ -600,14 +583,16 @@
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkVBox" id="vbox_downloads">
|
<object class="GtkBox" id="vbox_downloads">
|
||||||
<property name="border_width">12</property>
|
<property name="border_width">12</property>
|
||||||
<property name="spacing">6</property>
|
<property name="spacing">6</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkHBox" id="hbox_expiration">
|
<object class="GtkBox" id="hbox_expiration">
|
||||||
<property name="spacing">6</property>
|
<property name="spacing">6</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="label_expiration">
|
<object class="GtkLabel" id="label_expiration">
|
||||||
<property name="label" translatable="yes">Delete played episodes:</property>
|
<property name="label" translatable="yes">Delete played episodes:</property>
|
||||||
|
@ -620,12 +605,14 @@
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkHScale" id="hscale_expiration">
|
<object class="GtkScale" id="hscale_expiration">
|
||||||
<property name="digits">0</property>
|
<property name="digits">0</property>
|
||||||
<property name="is_focus">True</property>
|
<property name="is_focus">True</property>
|
||||||
<property name="value_pos">bottom</property>
|
<property name="value_pos">bottom</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="adjustment">adjustment_expiration</property>
|
<property name="adjustment">adjustment_expiration</property>
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
<signal name="format-value" handler="format_expiration_value"/>
|
<signal name="format-value" handler="format_expiration_value"/>
|
||||||
<signal name="value-changed" handler="on_expiration_value_changed"/>
|
<signal name="value-changed" handler="on_expiration_value_changed"/>
|
||||||
</object>
|
</object>
|
||||||
|
@ -665,10 +652,11 @@
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkVBox" id="vbox_devices">
|
<object class="GtkBox" id="vbox_devices">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="border_width">12</property>
|
<property name="border_width">12</property>
|
||||||
<property name="spacing">6</property>
|
<property name="spacing">6</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkTable" id="table_devices">
|
<object class="GtkTable" id="table_devices">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
|
|
@ -5,17 +5,19 @@
|
||||||
<property name="default_height">230</property>
|
<property name="default_height">230</property>
|
||||||
<property name="default_width">340</property>
|
<property name="default_width">340</property>
|
||||||
<property name="modal">True</property>
|
<property name="modal">True</property>
|
||||||
|
<property name="transient-for">parent_widget</property>
|
||||||
<property name="title" translatable="yes">Getting started</property>
|
<property name="title" translatable="yes">Getting started</property>
|
||||||
<property name="has_separator">False</property>
|
|
||||||
<child internal-child="vbox">
|
<child internal-child="vbox">
|
||||||
<object class="GtkVBox" id="dialog1-vbox">
|
<object class="GtkBox" id="dialog1-vbox">
|
||||||
<property name="border_width">2</property>
|
<property name="border_width">2</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkVBox" id="vbox1">
|
<object class="GtkBox" id="vbox1">
|
||||||
<property name="border_width">12</property>
|
<property name="border_width">12</property>
|
||||||
<property name="spacing">12</property>
|
<property name="spacing">12</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkTable" id="table1">
|
<object class="GtkTable" id="table1">
|
||||||
<property name="column_spacing">6</property>
|
<property name="column_spacing">6</property>
|
||||||
|
@ -56,9 +58,10 @@
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkVBox" id="vbox_buttons">
|
<object class="GtkBox" id="vbox_buttons">
|
||||||
<property name="spacing">6</property>
|
<property name="spacing">6</property>
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkButton" id="btnOPML">
|
<object class="GtkButton" id="btnOPML">
|
||||||
<property name="is_focus">True</property>
|
<property name="is_focus">True</property>
|
||||||
|
|
203
share/gpodder/ui/gtk/menus.ui
Normal file
203
share/gpodder/ui/gtk/menus.ui
Normal file
|
@ -0,0 +1,203 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<interface>
|
||||||
|
<menu id="app-menu">
|
||||||
|
<section>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">app.preferences</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Preferences</attribute>
|
||||||
|
<attribute name="accel"><Primary>p</attribute>
|
||||||
|
</item>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">app.gotoMygpo</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Go to gpodder.net</attribute>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">app.checkForUpdates</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Software updates</attribute>
|
||||||
|
</item>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<item>
|
||||||
|
<attribute name="label" translatable="yes">Help</attribute>
|
||||||
|
<attribute name="action">app.help</attribute>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">app.about</attribute>
|
||||||
|
<attribute name="label" translatable="yes">About</attribute>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">app.quit</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Quit</attribute>
|
||||||
|
<attribute name="accel"><Primary>q</attribute>
|
||||||
|
</item>
|
||||||
|
</section>
|
||||||
|
</menu>
|
||||||
|
<menu id="menubar">
|
||||||
|
<submenu id="menuPodcasts">
|
||||||
|
<attribute name="label" translatable="yes">Podcasts</attribute>
|
||||||
|
<section>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.update</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Check for new episodes</attribute>
|
||||||
|
<attribute name="accel"><Primary>r</attribute>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.downloadAllNew</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Download new episodes</attribute>
|
||||||
|
<attribute name="accel"><Primary>n</attribute>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.removeOldEpisodes</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Delete episodes</attribute>
|
||||||
|
<attribute name="accel"><Primary>k</attribute>
|
||||||
|
</item>
|
||||||
|
</section>
|
||||||
|
</submenu>
|
||||||
|
<submenu id="menuSubscriptions">
|
||||||
|
<attribute name="label">Subscriptions</attribute>
|
||||||
|
<section>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.discover</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Discover new podcasts</attribute>
|
||||||
|
<attribute name="accel"><Primary>f</attribute>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.addChannel</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Add podcast via URL</attribute>
|
||||||
|
<attribute name="accel"><Primary>l</attribute>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.massUnsubscribe</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Delete podcasts</attribute>
|
||||||
|
</item>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.updateChannel</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Update podcast</attribute>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.editChannel</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Podcast settings</attribute>
|
||||||
|
</item>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.importFromFile</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Import from OPML file</attribute>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.exportChannels</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Export to OPML file</attribute>
|
||||||
|
</item>
|
||||||
|
</section>
|
||||||
|
</submenu>
|
||||||
|
<submenu id="menuChannels">
|
||||||
|
<attribute name="label">Episodes</attribute>
|
||||||
|
<section>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.play</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Play</attribute>
|
||||||
|
<attribute name="accel"><Shift>Return</attribute>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.open</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Open</attribute>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.download</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Download</attribute>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.cancel</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Cancel</attribute>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.delete</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Delete</attribute>
|
||||||
|
</item>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.toggleEpisodeNew</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Toggle new status</attribute>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.toggleEpisodeLock</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Change delete lock</attribute>
|
||||||
|
</item>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.toggleShownotes</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Episode details</attribute>
|
||||||
|
</item>
|
||||||
|
</section>
|
||||||
|
</submenu>
|
||||||
|
<submenu id="menuExtras">
|
||||||
|
<attribute name="label" translatable="yes">E_xtras</attribute>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.sync</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Sync to device</attribute>
|
||||||
|
<attribute name="accel"><Primary>s</attribute>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.updateYoutubeSubscriptions</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Update YouTube subscriptions</attribute>
|
||||||
|
</item>
|
||||||
|
</submenu>
|
||||||
|
<submenu id="menuView">
|
||||||
|
<attribute name="label">View</attribute>
|
||||||
|
<section>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.showToolbar</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Toolbar</attribute>
|
||||||
|
<attribute name="accel"><Primary>t</attribute>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.showEpisodeDescription</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Episode descriptions</attribute>
|
||||||
|
<attribute name="accel"><Primary>d</attribute>
|
||||||
|
</item>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.viewEpisodes</attribute>
|
||||||
|
<attribute name="label" translatable="yes">All episodes</attribute>
|
||||||
|
<attribute name="target">VIEW_ALL</attribute>
|
||||||
|
<attribute name="accel"><Primary>0</attribute>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.viewEpisodes</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Hide deleted episodes</attribute>
|
||||||
|
<attribute name="target">VIEW_UNDELETED</attribute>
|
||||||
|
<attribute name="accel"><Primary>1</attribute>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.viewEpisodes</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Downloaded episodes</attribute>
|
||||||
|
<attribute name="target">VIEW_DOWNLOADED</attribute>
|
||||||
|
<attribute name="accel"><Primary>2</attribute>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.viewEpisodes</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Unplayed episodes</attribute>
|
||||||
|
<attribute name="target">VIEW_UNPLAYED</attribute>
|
||||||
|
<attribute name="accel"><Primary>3</attribute>
|
||||||
|
</item>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<item>
|
||||||
|
<attribute name="action">win.viewHideBoringPodcasts</attribute>
|
||||||
|
<attribute name="label" translatable="yes">Hide podcasts without episodes</attribute>
|
||||||
|
</item>
|
||||||
|
</section>
|
||||||
|
<submenu id="menuViewColumns">
|
||||||
|
<attribute name="label" translatable="yes">Visible columns</attribute>
|
||||||
|
</submenu>
|
||||||
|
</submenu>
|
||||||
|
</menu>
|
||||||
|
</interface>
|
||||||
|
<!-- :noTabs=true:tabSize=2:indentSize=2: -->
|
|
@ -20,8 +20,8 @@
|
||||||
# This metadata block gets parsed by setup.py - use single quotes only
|
# This metadata block gets parsed by setup.py - use single quotes only
|
||||||
__tagline__ = 'Media aggregator and podcast client'
|
__tagline__ = 'Media aggregator and podcast client'
|
||||||
__author__ = 'Thomas Perl <thp@gpodder.org>'
|
__author__ = 'Thomas Perl <thp@gpodder.org>'
|
||||||
__version__ = '3.9.5'
|
__version__ = '3.9.3'
|
||||||
__date__ = '2017-12-16'
|
__date__ = '2016-12-22'
|
||||||
__copyright__ = '© 2005-2017 Thomas Perl and the gPodder Team'
|
__copyright__ = '© 2005-2017 Thomas Perl and the gPodder Team'
|
||||||
__license__ = 'GNU General Public License, version 3 or later'
|
__license__ = 'GNU General Public License, version 3 or later'
|
||||||
__url__ = 'http://gpodder.org/'
|
__url__ = 'http://gpodder.org/'
|
||||||
|
@ -38,7 +38,7 @@ import locale
|
||||||
try:
|
try:
|
||||||
import podcastparser
|
import podcastparser
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print """
|
print("""
|
||||||
Error: Module "podcastparser" (python-podcastparser) not found.
|
Error: Module "podcastparser" (python-podcastparser) not found.
|
||||||
The podcastparser module can be downloaded from
|
The podcastparser module can be downloaded from
|
||||||
http://gpodder.org/podcastparser/
|
http://gpodder.org/podcastparser/
|
||||||
|
@ -46,15 +46,15 @@ except ImportError:
|
||||||
From a source checkout, you can download local copies of all
|
From a source checkout, you can download local copies of all
|
||||||
CLI dependencies for debugging (will be placed into "src/"):
|
CLI dependencies for debugging (will be placed into "src/"):
|
||||||
|
|
||||||
python tools/localdepends.py
|
python3 tools/localdepends.py
|
||||||
"""
|
""")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
del podcastparser
|
del podcastparser
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import mygpoclient
|
import mygpoclient
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print """
|
print("""
|
||||||
Error: Module "mygpoclient" (python-mygpoclient) not found.
|
Error: Module "mygpoclient" (python-mygpoclient) not found.
|
||||||
The mygpoclient module can be downloaded from
|
The mygpoclient module can be downloaded from
|
||||||
http://gpodder.org/mygpoclient/
|
http://gpodder.org/mygpoclient/
|
||||||
|
@ -62,19 +62,19 @@ except ImportError:
|
||||||
From a source checkout, you can download local copies of all
|
From a source checkout, you can download local copies of all
|
||||||
CLI dependencies for debugging (will be placed into "src/"):
|
CLI dependencies for debugging (will be placed into "src/"):
|
||||||
|
|
||||||
python tools/localdepends.py
|
python3 tools/localdepends.py
|
||||||
"""
|
""")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
del mygpoclient
|
del mygpoclient
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import sqlite3
|
import sqlite3
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print """
|
print("""
|
||||||
Error: Module "sqlite3" not found.
|
Error: Module "sqlite3" not found.
|
||||||
Build Python with SQLite 3 support or get it from
|
Build Python with SQLite 3 support or get it from
|
||||||
http://code.google.com/p/pysqlite/
|
http://code.google.com/p/pysqlite/
|
||||||
"""
|
""")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
del sqlite3
|
del sqlite3
|
||||||
|
|
||||||
|
@ -121,18 +121,9 @@ except AttributeError:
|
||||||
gettext = t.gettext
|
gettext = t.gettext
|
||||||
ngettext = t.ngettext
|
ngettext = t.ngettext
|
||||||
|
|
||||||
if ui.win32:
|
|
||||||
try:
|
|
||||||
# Workaround for bug 650
|
|
||||||
from gtk.glade import bindtextdomain
|
|
||||||
bindtextdomain(textdomain, locale_dir)
|
|
||||||
del bindtextdomain
|
|
||||||
except:
|
|
||||||
# Ignore for missing glade module
|
|
||||||
pass
|
|
||||||
del t
|
del t
|
||||||
|
|
||||||
# Set up textdomain for gtk.Builder (this accesses the C library functions)
|
# Set up textdomain for Gtk.Builder (this accesses the C library functions)
|
||||||
if hasattr(locale, 'bindtextdomain'):
|
if hasattr(locale, 'bindtextdomain'):
|
||||||
locale.bindtextdomain(textdomain, locale_dir)
|
locale.bindtextdomain(textdomain, locale_dir)
|
||||||
|
|
||||||
|
@ -152,7 +143,7 @@ images_folder = None
|
||||||
user_extensions = None
|
user_extensions = None
|
||||||
|
|
||||||
# Episode states used in the database
|
# Episode states used in the database
|
||||||
STATE_NORMAL, STATE_DOWNLOADED, STATE_DELETED = range(3)
|
STATE_NORMAL, STATE_DOWNLOADED, STATE_DELETED = list(range(3))
|
||||||
|
|
||||||
# Paths (gPodder's home folder, config, db, download and data prefix)
|
# Paths (gPodder's home folder, config, db, download and data prefix)
|
||||||
home = None
|
home = None
|
||||||
|
@ -193,13 +184,13 @@ default_home = fixup_home(default_home)
|
||||||
set_home(os.environ.get(ENV_HOME, default_home))
|
set_home(os.environ.get(ENV_HOME, default_home))
|
||||||
|
|
||||||
if home != default_home:
|
if home != default_home:
|
||||||
print >>sys.stderr, 'Storing data in', home, '(GPODDER_HOME is set)'
|
print('Storing data in', home, '(GPODDER_HOME is set)', file=sys.stderr)
|
||||||
|
|
||||||
if ENV_DOWNLOADS in os.environ:
|
if ENV_DOWNLOADS in os.environ:
|
||||||
# Allow to relocate the downloads folder (pull request 4, bug 466)
|
# Allow to relocate the downloads folder (pull request 4, bug 466)
|
||||||
downloads = os.environ[ENV_DOWNLOADS]
|
downloads = os.environ[ENV_DOWNLOADS]
|
||||||
print >>sys.stderr, 'Storing downloads in %s (%s is set)' % (downloads,
|
print('Storing downloads in %s (%s is set)' % (downloads,
|
||||||
ENV_DOWNLOADS)
|
ENV_DOWNLOADS), file=sys.stderr)
|
||||||
|
|
||||||
# Plugins to load by default
|
# Plugins to load by default
|
||||||
DEFAULT_PLUGINS = [
|
DEFAULT_PLUGINS = [
|
||||||
|
@ -220,5 +211,5 @@ def load_plugins():
|
||||||
for plugin in PLUGINS:
|
for plugin in PLUGINS:
|
||||||
try:
|
try:
|
||||||
__import__(plugin)
|
__import__(plugin)
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
print >>sys.stderr, 'Cannot load plugin: %s (%s)' % (plugin, e)
|
print('Cannot load plugin: %s (%s)' % (plugin, e), file=sys.stderr)
|
||||||
|
|
|
@ -68,7 +68,7 @@ def find_partial_downloads(channels, start_progress_callback, progress_callback,
|
||||||
filename = episode.local_filename(create=False, check_only=True)
|
filename = episode.local_filename(create=False, check_only=True)
|
||||||
if filename in candidates:
|
if filename in candidates:
|
||||||
found += 1
|
found += 1
|
||||||
progress_callback(episode.title, float(found)/count)
|
progress_callback(episode.title, found/count)
|
||||||
candidates.remove(filename)
|
candidates.remove(filename)
|
||||||
partial_files.remove(filename+'.partial')
|
partial_files.remove(filename+'.partial')
|
||||||
|
|
||||||
|
|
|
@ -147,6 +147,8 @@ defaults = {
|
||||||
'download_list': {
|
'download_list': {
|
||||||
'remove_finished': True,
|
'remove_finished': True,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'html_shownotes': True, # enable webkit renderer
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -228,7 +230,7 @@ def config_value_to_string(config_value):
|
||||||
|
|
||||||
if config_type == list:
|
if config_type == list:
|
||||||
return ','.join(map(config_value_to_string, config_value))
|
return ','.join(map(config_value_to_string, config_value))
|
||||||
elif config_type in (str, unicode):
|
elif config_type in (str, str):
|
||||||
return config_value
|
return config_value
|
||||||
else:
|
else:
|
||||||
return str(config_value)
|
return str(config_value)
|
||||||
|
@ -237,7 +239,7 @@ def string_to_config_value(new_value, old_value):
|
||||||
config_type = type(old_value)
|
config_type = type(old_value)
|
||||||
|
|
||||||
if config_type == list:
|
if config_type == list:
|
||||||
return filter(None, [x.strip() for x in new_value.split(',')])
|
return [_f for _f in [x.strip() for x in new_value.split(',')] if _f]
|
||||||
elif config_type == bool:
|
elif config_type == bool:
|
||||||
return (new_value.strip().lower() in ('1', 'true'))
|
return (new_value.strip().lower() in ('1', 'true'))
|
||||||
else:
|
else:
|
||||||
|
@ -366,7 +368,7 @@ class Config(object):
|
||||||
for observer in self.__observers:
|
for observer in self.__observers:
|
||||||
try:
|
try:
|
||||||
observer(name, old_value, value)
|
observer(name, old_value, value)
|
||||||
except Exception, exception:
|
except Exception as exception:
|
||||||
logger.error('Error while calling observer %r: %s',
|
logger.error('Error while calling observer %r: %s',
|
||||||
observer, exception, exc_info=True)
|
observer, exception, exc_info=True)
|
||||||
|
|
||||||
|
|
|
@ -38,12 +38,12 @@ class CoverDownloader(object):
|
||||||
# File name extension dict, lists supported cover art extensions
|
# File name extension dict, lists supported cover art extensions
|
||||||
# Values: functions that check if some data is of that file type
|
# Values: functions that check if some data is of that file type
|
||||||
SUPPORTED_EXTENSIONS = {
|
SUPPORTED_EXTENSIONS = {
|
||||||
'.png': lambda d: d.startswith('\x89PNG\r\n\x1a\n\x00'),
|
'.png': lambda d: d.startswith(b'\x89PNG\r\n\x1a\n\x00'),
|
||||||
'.jpg': lambda d: d.startswith('\xff\xd8'),
|
'.jpg': lambda d: d.startswith(b'\xff\xd8'),
|
||||||
'.gif': lambda d: d.startswith('GIF89a') or d.startswith('GIF87a'),
|
'.gif': lambda d: d.startswith(b'GIF89a') or d.startswith(b'GIF87a'),
|
||||||
}
|
}
|
||||||
|
|
||||||
EXTENSIONS = SUPPORTED_EXTENSIONS.keys()
|
EXTENSIONS = list(SUPPORTED_EXTENSIONS.keys())
|
||||||
ALL_EPISODES_ID = ':gpodder:all-episodes:'
|
ALL_EPISODES_ID = ':gpodder:all-episodes:'
|
||||||
|
|
||||||
# Low timeout to avoid unnecessary hangs of GUIs
|
# Low timeout to avoid unnecessary hangs of GUIs
|
||||||
|
@ -85,14 +85,14 @@ class CoverDownloader(object):
|
||||||
try:
|
try:
|
||||||
logger.info('Downloading cover art: %s', cover_url)
|
logger.info('Downloading cover art: %s', cover_url)
|
||||||
data = util.urlopen(cover_url, timeout=self.TIMEOUT).read()
|
data = util.urlopen(cover_url, timeout=self.TIMEOUT).read()
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
logger.warn('Cover art download failed: %s', e)
|
logger.warn('Cover art download failed: %s', e)
|
||||||
return self._fallback_filename(title)
|
return self._fallback_filename(title)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
extension = None
|
extension = None
|
||||||
|
|
||||||
for filetype, check in self.SUPPORTED_EXTENSIONS.items():
|
for filetype, check in list(self.SUPPORTED_EXTENSIONS.items()):
|
||||||
if check(data):
|
if check(data):
|
||||||
extension = filetype
|
extension = filetype
|
||||||
break
|
break
|
||||||
|
@ -107,7 +107,7 @@ class CoverDownloader(object):
|
||||||
fp.close()
|
fp.close()
|
||||||
|
|
||||||
return filename + extension
|
return filename + extension
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
logger.warn('Cannot save cover art', exc_info=True)
|
logger.warn('Cannot save cover art', exc_info=True)
|
||||||
|
|
||||||
# Fallback to cover art based on the podcast title
|
# Fallback to cover art based on the podcast title
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
# 2010-04-24 Thomas Perl <thp@gpodder.org>
|
# 2010-04-24 Thomas Perl <thp@gpodder.org>
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
import gpodder
|
import gpodder
|
||||||
_ = gpodder.gettext
|
_ = gpodder.gettext
|
||||||
|
@ -55,9 +55,9 @@ class Database(object):
|
||||||
self.commit()
|
self.commit()
|
||||||
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
cur = self.cursor()
|
self.db.isolation_level = None
|
||||||
cur.execute("VACUUM")
|
self.db.execute('VACUUM')
|
||||||
cur.close()
|
self.db.isolation_level = ''
|
||||||
|
|
||||||
self._db.close()
|
self._db.close()
|
||||||
self._db = None
|
self._db = None
|
||||||
|
@ -107,7 +107,7 @@ class Database(object):
|
||||||
try:
|
try:
|
||||||
logger.debug('Commit.')
|
logger.debug('Commit.')
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
logger.error('Cannot commit: %s', e, exc_info=True)
|
logger.error('Cannot commit: %s', e, exc_info=True)
|
||||||
|
|
||||||
def get_content_types(self, id):
|
def get_content_types(self, id):
|
||||||
|
@ -160,7 +160,7 @@ class Database(object):
|
||||||
cur.execute(sql)
|
cur.execute(sql)
|
||||||
|
|
||||||
keys = [desc[0] for desc in cur.description]
|
keys = [desc[0] for desc in cur.description]
|
||||||
result = map(lambda row: factory(dict(zip(keys, row)), self), cur)
|
result = [factory(dict(list(zip(keys, row))), self) for row in cur]
|
||||||
cur.close()
|
cur.close()
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -178,7 +178,7 @@ class Database(object):
|
||||||
cur.execute(sql, args)
|
cur.execute(sql, args)
|
||||||
|
|
||||||
keys = [desc[0] for desc in cur.description]
|
keys = [desc[0] for desc in cur.description]
|
||||||
result = map(lambda row: factory(dict(zip(keys, row))), cur)
|
result = [factory(dict(list(zip(keys, row)))) for row in cur]
|
||||||
cur.close()
|
cur.close()
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -219,7 +219,7 @@ class Database(object):
|
||||||
values.append(o.id)
|
values.append(o.id)
|
||||||
sql = 'UPDATE %s SET %s WHERE id = ?' % (table, qmarks)
|
sql = 'UPDATE %s SET %s WHERE id = ?' % (table, qmarks)
|
||||||
cur.execute(sql, values)
|
cur.execute(sql, values)
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
logger.error('Cannot save %s: %s', o, e, exc_info=True)
|
logger.error('Cannot save %s: %s', o, e, exc_info=True)
|
||||||
|
|
||||||
cur.close()
|
cur.close()
|
||||||
|
|
|
@ -27,7 +27,7 @@ import gpodder
|
||||||
|
|
||||||
_ = gpodder.gettext
|
_ = gpodder.gettext
|
||||||
|
|
||||||
import urllib
|
import urllib.request, urllib.parse, urllib.error
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ class DirectoryTag(object):
|
||||||
|
|
||||||
|
|
||||||
class Provider(object):
|
class Provider(object):
|
||||||
PROVIDER_SEARCH, PROVIDER_URL, PROVIDER_FILE, PROVIDER_TAGCLOUD, PROVIDER_STATIC = range(5)
|
PROVIDER_SEARCH, PROVIDER_URL, PROVIDER_FILE, PROVIDER_TAGCLOUD, PROVIDER_STATIC = list(range(5))
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.name = ''
|
self.name = ''
|
||||||
|
@ -97,7 +97,7 @@ class GPodderNetSearchProvider(Provider):
|
||||||
self.icon = 'directory-gpodder.png'
|
self.icon = 'directory-gpodder.png'
|
||||||
|
|
||||||
def on_search(self, query):
|
def on_search(self, query):
|
||||||
return directory_entry_from_mygpo_json('http://gpodder.net/search.json?q=' + urllib.quote(query))
|
return directory_entry_from_mygpo_json('http://gpodder.net/search.json?q=' + urllib.parse.quote(query))
|
||||||
|
|
||||||
class OpmlWebImportProvider(Provider):
|
class OpmlWebImportProvider(Provider):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -142,7 +142,7 @@ class GPodderNetTagsProvider(Provider):
|
||||||
self.icon = 'directory-tags.png'
|
self.icon = 'directory-tags.png'
|
||||||
|
|
||||||
def on_tag(self, tag):
|
def on_tag(self, tag):
|
||||||
return directory_entry_from_mygpo_json('http://gpodder.net/api/2/tag/%s/50.json' % urllib.quote(tag))
|
return directory_entry_from_mygpo_json('http://gpodder.net/api/2/tag/%s/50.json' % urllib.parse.quote(tag))
|
||||||
|
|
||||||
def get_tags(self):
|
def get_tags(self):
|
||||||
return [DirectoryTag(d['tag'], d['usage']) for d in json.load(util.urlopen('http://gpodder.net/api/2/tags/40.json'))]
|
return [DirectoryTag(d['tag'], d['usage']) for d in json.load(util.urlopen('http://gpodder.net/api/2/tags/40.json'))]
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
# Based on libwget.py (2005-10-29)
|
# Based on libwget.py (2005-10-29)
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -38,8 +38,8 @@ import gpodder
|
||||||
|
|
||||||
import socket
|
import socket
|
||||||
import threading
|
import threading
|
||||||
import urllib
|
import urllib.request, urllib.parse, urllib.error
|
||||||
import urlparse
|
import urllib.parse
|
||||||
import shutil
|
import shutil
|
||||||
import os.path
|
import os.path
|
||||||
import os
|
import os
|
||||||
|
@ -66,13 +66,13 @@ def get_header_param(headers, param, header_name):
|
||||||
"""
|
"""
|
||||||
value = None
|
value = None
|
||||||
try:
|
try:
|
||||||
headers_string = ['%s:%s'%(k,v) for k,v in headers.items()]
|
headers_string = ['%s:%s'%(k,v) for k,v in list(headers.items())]
|
||||||
msg = email.message_from_string('\n'.join(headers_string))
|
msg = email.message_from_string('\n'.join(headers_string))
|
||||||
if header_name in msg:
|
if header_name in msg:
|
||||||
raw_value = msg.get_param(param, header=header_name)
|
raw_value = msg.get_param(param, header=header_name)
|
||||||
if raw_value is not None:
|
if raw_value is not None:
|
||||||
value = email.utils.collapse_rfc2231_value(raw_value)
|
value = email.utils.collapse_rfc2231_value(raw_value)
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
logger.error('Cannot get %s from %s', param, header_name, exc_info=True)
|
logger.error('Cannot get %s from %s', param, header_name, exc_info=True)
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
@ -188,18 +188,18 @@ class gPodderDownloadHTTPError(Exception):
|
||||||
self.error_code = error_code
|
self.error_code = error_code
|
||||||
self.error_message = error_message
|
self.error_message = error_message
|
||||||
|
|
||||||
class DownloadURLOpener(urllib.FancyURLopener):
|
class DownloadURLOpener(urllib.request.FancyURLopener):
|
||||||
version = gpodder.user_agent
|
version = gpodder.user_agent
|
||||||
|
|
||||||
# Sometimes URLs are not escaped correctly - try to fix them
|
# Sometimes URLs are not escaped correctly - try to fix them
|
||||||
# (see RFC2396; Section 2.4.3. Excluded US-ASCII Characters)
|
# (see RFC2396; Section 2.4.3. Excluded US-ASCII Characters)
|
||||||
# FYI: The omission of "%" in the list is to avoid double escaping!
|
# FYI: The omission of "%" in the list is to avoid double escaping!
|
||||||
ESCAPE_CHARS = dict((ord(c), u'%%%x'%ord(c)) for c in u' <>#"{}|\\^[]`')
|
ESCAPE_CHARS = dict((ord(c), '%%%x'%ord(c)) for c in ' <>#"{}|\\^[]`')
|
||||||
|
|
||||||
def __init__( self, channel):
|
def __init__( self, channel):
|
||||||
self.channel = channel
|
self.channel = channel
|
||||||
self._auth_retry_counter = 0
|
self._auth_retry_counter = 0
|
||||||
urllib.FancyURLopener.__init__(self, None)
|
urllib.request.FancyURLopener.__init__(self, None)
|
||||||
|
|
||||||
def http_error_default(self, url, fp, errcode, errmsg, headers):
|
def http_error_default(self, url, fp, errcode, errmsg, headers):
|
||||||
"""
|
"""
|
||||||
|
@ -230,7 +230,7 @@ class DownloadURLOpener(urllib.FancyURLopener):
|
||||||
fp.close()
|
fp.close()
|
||||||
|
|
||||||
# In case the server sent a relative URL, join with original:
|
# In case the server sent a relative URL, join with original:
|
||||||
newurl = urlparse.urljoin(self.type + ":" + url, newurl)
|
newurl = urllib.parse.urljoin(self.type + ":" + url, newurl)
|
||||||
return self.open(newurl)
|
return self.open(newurl)
|
||||||
|
|
||||||
# The following is based on Python's urllib.py "URLopener.retrieve"
|
# The following is based on Python's urllib.py "URLopener.retrieve"
|
||||||
|
@ -266,11 +266,8 @@ class DownloadURLOpener(urllib.FancyURLopener):
|
||||||
tfp = open(filename, 'wb')
|
tfp = open(filename, 'wb')
|
||||||
|
|
||||||
# Fix a problem with bad URLs that are not encoded correctly (bug 549)
|
# Fix a problem with bad URLs that are not encoded correctly (bug 549)
|
||||||
url = url.decode('ascii', 'ignore')
|
|
||||||
url = url.translate(self.ESCAPE_CHARS)
|
url = url.translate(self.ESCAPE_CHARS)
|
||||||
url = url.encode('ascii')
|
|
||||||
|
|
||||||
url = urllib.unwrap(urllib.toBytes(url))
|
|
||||||
fp = self.open(url, data)
|
fp = self.open(url, data)
|
||||||
headers = fp.info()
|
headers = fp.info()
|
||||||
|
|
||||||
|
@ -291,10 +288,10 @@ class DownloadURLOpener(urllib.FancyURLopener):
|
||||||
bs = 1024*8
|
bs = 1024*8
|
||||||
size = -1
|
size = -1
|
||||||
read = current_size
|
read = current_size
|
||||||
blocknum = int(current_size/bs)
|
blocknum = current_size//bs
|
||||||
if reporthook:
|
if reporthook:
|
||||||
if "content-length" in headers:
|
if "content-length" in headers:
|
||||||
size = int(headers.getrawheader("Content-Length")) + current_size
|
size = int(headers['Content-Length']) + current_size
|
||||||
reporthook(blocknum, bs, size)
|
reporthook(blocknum, bs, size)
|
||||||
while read < size or size == -1:
|
while read < size or size == -1:
|
||||||
if size == -1:
|
if size == -1:
|
||||||
|
@ -315,7 +312,7 @@ class DownloadURLOpener(urllib.FancyURLopener):
|
||||||
|
|
||||||
# raise exception if actual size does not match content-length header
|
# raise exception if actual size does not match content-length header
|
||||||
if size >= 0 and read < size:
|
if size >= 0 and read < size:
|
||||||
raise urllib.ContentTooShortError("retrieval incomplete: got only %i out "
|
raise urllib.error.ContentTooShortError("retrieval incomplete: got only %i out "
|
||||||
"of %i bytes" % (read, size), result)
|
"of %i bytes" % (read, size), result)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -337,45 +334,48 @@ class DownloadURLOpener(urllib.FancyURLopener):
|
||||||
|
|
||||||
|
|
||||||
class DownloadQueueWorker(object):
|
class DownloadQueueWorker(object):
|
||||||
def __init__(self, queue, exit_callback, continue_check_callback, minimum_tasks):
|
def __init__(self, queue, exit_callback, continue_check_callback):
|
||||||
self.queue = queue
|
self.queue = queue
|
||||||
self.exit_callback = exit_callback
|
self.exit_callback = exit_callback
|
||||||
self.continue_check_callback = continue_check_callback
|
self.continue_check_callback = continue_check_callback
|
||||||
|
|
||||||
# The minimum amount of tasks that should be downloaded by this worker
|
|
||||||
# before using the continue_check_callback to determine if it might
|
|
||||||
# continue accepting tasks. This can be used to forcefully start a
|
|
||||||
# download, even if a download limit is in effect.
|
|
||||||
self.minimum_tasks = minimum_tasks
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return threading.current_thread().getName()
|
return threading.current_thread().getName()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
logger.info('Starting new thread: %s', self)
|
logger.info('Starting new thread: %s', self)
|
||||||
while True:
|
while True:
|
||||||
# Check if this thread is allowed to continue accepting tasks
|
if not self.continue_check_callback(self):
|
||||||
# (But only after reducing minimum_tasks to zero - see above)
|
|
||||||
if self.minimum_tasks > 0:
|
|
||||||
self.minimum_tasks -= 1
|
|
||||||
elif not self.continue_check_callback(self):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
task = self.queue.pop()
|
task = self.queue.get_next()
|
||||||
logger.info('%s is processing: %s', self, task)
|
logger.info('%s is processing: %s', self, task)
|
||||||
task.run()
|
task.run()
|
||||||
task.recycle()
|
task.recycle()
|
||||||
except IndexError, e:
|
except StopIteration as e:
|
||||||
logger.info('No more tasks for %s to carry out.', self)
|
logger.info('No more tasks for %s to carry out.', self)
|
||||||
break
|
break
|
||||||
self.exit_callback(self)
|
self.exit_callback(self)
|
||||||
|
|
||||||
|
|
||||||
|
class ForceDownloadWorker(object):
|
||||||
|
def __init__(self, task):
|
||||||
|
self.task = task
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return threading.current_thread().getName()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
logger.info('Starting new thread: %s', self)
|
||||||
|
logger.info('%s is processing: %s', self, self.task)
|
||||||
|
self.task.run()
|
||||||
|
|
||||||
|
|
||||||
class DownloadQueueManager(object):
|
class DownloadQueueManager(object):
|
||||||
def __init__(self, config):
|
def __init__(self, config, queue):
|
||||||
self._config = config
|
self._config = config
|
||||||
self.tasks = collections.deque()
|
self.tasks = queue
|
||||||
|
|
||||||
self.worker_threads_access = threading.RLock()
|
self.worker_threads_access = threading.RLock()
|
||||||
self.worker_threads = []
|
self.worker_threads = []
|
||||||
|
@ -393,61 +393,37 @@ class DownloadQueueManager(object):
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def spawn_threads(self, force_start=False):
|
def __spawn_threads(self):
|
||||||
"""Spawn new worker threads if necessary
|
"""Spawn new worker threads if necessary
|
||||||
|
|
||||||
If force_start is True, forcefully spawn a thread and
|
|
||||||
let it process at least one episodes, even if a download
|
|
||||||
limit is in effect at the moment.
|
|
||||||
"""
|
"""
|
||||||
with self.worker_threads_access:
|
with self.worker_threads_access:
|
||||||
if not len(self.tasks):
|
if not self.tasks.has_work():
|
||||||
return
|
return
|
||||||
|
|
||||||
if force_start or len(self.worker_threads) == 0 or \
|
if len(self.worker_threads) == 0 or \
|
||||||
len(self.worker_threads) < self._config.max_downloads or \
|
len(self.worker_threads) < self._config.max_downloads or \
|
||||||
not self._config.max_downloads_enabled:
|
not self._config.max_downloads_enabled:
|
||||||
# We have to create a new thread here, there's work to do
|
# We have to create a new thread here, there's work to do
|
||||||
logger.info('Starting new worker thread.')
|
logger.info('Starting new worker thread.')
|
||||||
|
|
||||||
# The new worker should process at least one task (the one
|
|
||||||
# that we want to forcefully start) if force_start is True.
|
|
||||||
if force_start:
|
|
||||||
minimum_tasks = 1
|
|
||||||
else:
|
|
||||||
minimum_tasks = 0
|
|
||||||
|
|
||||||
worker = DownloadQueueWorker(self.tasks, self.__exit_callback,
|
worker = DownloadQueueWorker(self.tasks, self.__exit_callback,
|
||||||
self.__continue_check_callback, minimum_tasks)
|
self.__continue_check_callback)
|
||||||
self.worker_threads.append(worker)
|
self.worker_threads.append(worker)
|
||||||
util.run_in_background(worker.run)
|
util.run_in_background(worker.run)
|
||||||
|
|
||||||
def are_queued_or_active_tasks(self):
|
def update_max_downloads(self):
|
||||||
with self.worker_threads_access:
|
self.__spawn_threads()
|
||||||
return len(self.worker_threads) > 0
|
|
||||||
|
|
||||||
def add_task(self, task, force_start=False):
|
def force_start_task(self, task):
|
||||||
"""Add a new task to the download queue
|
if self.tasks.set_downloading(task):
|
||||||
|
worker = ForceDownloadWorker(task)
|
||||||
|
util.run_in_background(worker.run)
|
||||||
|
|
||||||
If force_start is True, ignore the download limit
|
def queue_task(self, task):
|
||||||
and forcefully start the download right away.
|
"""Marks a task as queued
|
||||||
"""
|
"""
|
||||||
if task.status != DownloadTask.INIT:
|
|
||||||
# Remove the task from its current position in the
|
|
||||||
# download queue (if any) to avoid race conditions
|
|
||||||
# where two worker threads download the same file
|
|
||||||
try:
|
|
||||||
self.tasks.remove(task)
|
|
||||||
except ValueError, e:
|
|
||||||
pass
|
|
||||||
task.status = DownloadTask.QUEUED
|
task.status = DownloadTask.QUEUED
|
||||||
if force_start:
|
self.__spawn_threads()
|
||||||
# Add the task to be taken on next pop
|
|
||||||
self.tasks.append(task)
|
|
||||||
else:
|
|
||||||
# Add the task to the end of the queue
|
|
||||||
self.tasks.appendleft(task)
|
|
||||||
self.spawn_threads(force_start)
|
|
||||||
|
|
||||||
|
|
||||||
class DownloadTask(object):
|
class DownloadTask(object):
|
||||||
|
@ -526,10 +502,10 @@ class DownloadTask(object):
|
||||||
# Possible states this download task can be in
|
# Possible states this download task can be in
|
||||||
STATUS_MESSAGE = (_('Added'), _('Queued'), _('Downloading'),
|
STATUS_MESSAGE = (_('Added'), _('Queued'), _('Downloading'),
|
||||||
_('Finished'), _('Failed'), _('Cancelled'), _('Paused'))
|
_('Finished'), _('Failed'), _('Cancelled'), _('Paused'))
|
||||||
(INIT, QUEUED, DOWNLOADING, DONE, FAILED, CANCELLED, PAUSED) = range(7)
|
(INIT, QUEUED, DOWNLOADING, DONE, FAILED, CANCELLED, PAUSED) = list(range(7))
|
||||||
|
|
||||||
# Wheter this task represents a file download or a device sync operation
|
# Wheter this task represents a file download or a device sync operation
|
||||||
ACTIVITY_DOWNLOAD, ACTIVITY_SYNCHRONIZE = range(2)
|
ACTIVITY_DOWNLOAD, ACTIVITY_SYNCHRONIZE = list(range(2))
|
||||||
|
|
||||||
# Minimum time between progress updates (in seconds)
|
# Minimum time between progress updates (in seconds)
|
||||||
MIN_TIME_BETWEEN_UPDATES = 1.
|
MIN_TIME_BETWEEN_UPDATES = 1.
|
||||||
|
@ -623,8 +599,8 @@ class DownloadTask(object):
|
||||||
try:
|
try:
|
||||||
already_downloaded = os.path.getsize(self.tempname)
|
already_downloaded = os.path.getsize(self.tempname)
|
||||||
if self.total_size > 0:
|
if self.total_size > 0:
|
||||||
self.progress = max(0.0, min(1.0, float(already_downloaded)/self.total_size))
|
self.progress = max(0.0, min(1.0, already_downloaded/self.total_size))
|
||||||
except OSError, os_error:
|
except OSError as os_error:
|
||||||
logger.error('Cannot get size for %s', os_error)
|
logger.error('Cannot get size for %s', os_error)
|
||||||
else:
|
else:
|
||||||
# "touch self.tempname", so we also get partial
|
# "touch self.tempname", so we also get partial
|
||||||
|
@ -669,7 +645,7 @@ class DownloadTask(object):
|
||||||
self.__episode.save()
|
self.__episode.save()
|
||||||
|
|
||||||
if self.total_size > 0:
|
if self.total_size > 0:
|
||||||
self.progress = max(0.0, min(1.0, float(count*blockSize)/self.total_size))
|
self.progress = max(0.0, min(1.0, count*blockSize/self.total_size))
|
||||||
if self._progress_updated is not None:
|
if self._progress_updated is not None:
|
||||||
diff = time.time() - self._last_progress_updated
|
diff = time.time() - self._last_progress_updated
|
||||||
if diff > self.MIN_TIME_BETWEEN_UPDATES or self.progress == 1.:
|
if diff > self.MIN_TIME_BETWEEN_UPDATES or self.progress == 1.:
|
||||||
|
@ -718,7 +694,7 @@ class DownloadTask(object):
|
||||||
if self._config.limit_rate and speed > self._config.limit_rate_value:
|
if self._config.limit_rate and speed > self._config.limit_rate_value:
|
||||||
# calculate the time that should have passed to reach
|
# calculate the time that should have passed to reach
|
||||||
# the desired download rate and wait if necessary
|
# the desired download rate and wait if necessary
|
||||||
should_have_passed = float((count-self.__start_blocks)*blockSize)/(self._config.limit_rate_value*1024.0)
|
should_have_passed = (count-self.__start_blocks)*blockSize/(self._config.limit_rate_value*1024.0)
|
||||||
if should_have_passed > passed:
|
if should_have_passed > passed:
|
||||||
# sleep a maximum of 10 seconds to not cause time-outs
|
# sleep a maximum of 10 seconds to not cause time-outs
|
||||||
delay = min(10.0, float(should_have_passed-passed))
|
delay = min(10.0, float(should_have_passed-passed))
|
||||||
|
@ -739,8 +715,8 @@ class DownloadTask(object):
|
||||||
self.speed = 0.0
|
self.speed = 0.0
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# We only start this download if its status is "queued"
|
# We only start this download if its status is "downloading"
|
||||||
if self.status != DownloadTask.QUEUED:
|
if self.status != DownloadTask.DOWNLOADING:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# We are downloading this file right now
|
# We are downloading this file right now
|
||||||
|
@ -753,22 +729,23 @@ class DownloadTask(object):
|
||||||
url = youtube.get_real_download_url(self.__episode.url, fmt_ids)
|
url = youtube.get_real_download_url(self.__episode.url, fmt_ids)
|
||||||
url = vimeo.get_real_download_url(url, self._config.vimeo.fileformat)
|
url = vimeo.get_real_download_url(url, self._config.vimeo.fileformat)
|
||||||
url = escapist_videos.get_real_download_url(url)
|
url = escapist_videos.get_real_download_url(url)
|
||||||
|
|
||||||
# We should have properly-escaped characters in the URL, but sometimes
|
|
||||||
# this is not true -- take any characters that are not in ASCII and
|
|
||||||
# convert them to UTF-8 and then percent-encode the UTF-8 string data
|
|
||||||
# Example: https://github.com/gpodder/gpodder/issues/232
|
|
||||||
url_chars = []
|
|
||||||
for char in url:
|
|
||||||
if ord(char) <= 31 or ord(char) >= 127:
|
|
||||||
for char in urllib.quote(char.encode('utf-8')):
|
|
||||||
url_chars.append(char.decode('utf-8'))
|
|
||||||
else:
|
|
||||||
url_chars.append(char)
|
|
||||||
url = u''.join(url_chars)
|
|
||||||
|
|
||||||
url = url.strip()
|
url = url.strip()
|
||||||
|
|
||||||
|
# Properly escapes Unicode characters in the URL path section
|
||||||
|
# TODO: Explore if this should also handle the domain
|
||||||
|
# Based on: http://stackoverflow.com/a/18269491/1072626
|
||||||
|
# In response to issue: https://github.com/gpodder/gpodder/issues/232
|
||||||
|
def iri_to_url(url):
|
||||||
|
url = urllib.parse.urlsplit(url)
|
||||||
|
url = list(url)
|
||||||
|
# First unquote to avoid escaping quoted content
|
||||||
|
url[2] = urllib.parse.unquote(url[2])
|
||||||
|
url[2] = urllib.parse.quote(url[2])
|
||||||
|
url = urllib.parse.urlunsplit(url)
|
||||||
|
return url
|
||||||
|
|
||||||
|
url = iri_to_url(url)
|
||||||
|
|
||||||
downloader = DownloadURLOpener(self.__episode.channel)
|
downloader = DownloadURLOpener(self.__episode.channel)
|
||||||
|
|
||||||
# HTTP Status codes for which we retry the download
|
# HTTP Status codes for which we retry the download
|
||||||
|
@ -786,18 +763,18 @@ class DownloadTask(object):
|
||||||
self.tempname, reporthook=self.status_updated)
|
self.tempname, reporthook=self.status_updated)
|
||||||
# If we arrive here, the download was successful
|
# If we arrive here, the download was successful
|
||||||
break
|
break
|
||||||
except urllib.ContentTooShortError, ctse:
|
except urllib.error.ContentTooShortError as ctse:
|
||||||
if retry < max_retries:
|
if retry < max_retries:
|
||||||
logger.info('Content too short: %s - will retry.',
|
logger.info('Content too short: %s - will retry.',
|
||||||
url)
|
url)
|
||||||
continue
|
continue
|
||||||
raise
|
raise
|
||||||
except socket.timeout, tmout:
|
except socket.timeout as tmout:
|
||||||
if retry < max_retries:
|
if retry < max_retries:
|
||||||
logger.info('Socket timeout: %s - will retry.', url)
|
logger.info('Socket timeout: %s - will retry.', url)
|
||||||
continue
|
continue
|
||||||
raise
|
raise
|
||||||
except gPodderDownloadHTTPError, http:
|
except gPodderDownloadHTTPError as http:
|
||||||
if retry < max_retries and http.error_code in retry_codes:
|
if retry < max_retries and http.error_code in retry_codes:
|
||||||
logger.info('HTTP error %d: %s - will retry.',
|
logger.info('HTTP error %d: %s - will retry.',
|
||||||
http.error_code, url)
|
http.error_code, url)
|
||||||
|
@ -871,23 +848,23 @@ class DownloadTask(object):
|
||||||
util.delete_file(self.tempname)
|
util.delete_file(self.tempname)
|
||||||
self.progress = 0.0
|
self.progress = 0.0
|
||||||
self.speed = 0.0
|
self.speed = 0.0
|
||||||
except urllib.ContentTooShortError, ctse:
|
except urllib.error.ContentTooShortError as ctse:
|
||||||
self.status = DownloadTask.FAILED
|
self.status = DownloadTask.FAILED
|
||||||
self.error_message = _('Missing content from server')
|
self.error_message = _('Missing content from server')
|
||||||
except IOError, ioe:
|
except IOError as ioe:
|
||||||
logger.error('%s while downloading "%s": %s', ioe.strerror,
|
logger.error('%s while downloading "%s": %s', ioe.strerror,
|
||||||
self.__episode.title, ioe.filename, exc_info=True)
|
self.__episode.title, ioe.filename, exc_info=True)
|
||||||
self.status = DownloadTask.FAILED
|
self.status = DownloadTask.FAILED
|
||||||
d = {'error': ioe.strerror, 'filename': ioe.filename}
|
d = {'error': ioe.strerror, 'filename': ioe.filename}
|
||||||
self.error_message = _('I/O Error: %(error)s: %(filename)s') % d
|
self.error_message = _('I/O Error: %(error)s: %(filename)s') % d
|
||||||
except gPodderDownloadHTTPError, gdhe:
|
except gPodderDownloadHTTPError as gdhe:
|
||||||
logger.error('HTTP %s while downloading "%s": %s',
|
logger.error('HTTP %s while downloading "%s": %s',
|
||||||
gdhe.error_code, self.__episode.title, gdhe.error_message,
|
gdhe.error_code, self.__episode.title, gdhe.error_message,
|
||||||
exc_info=True)
|
exc_info=True)
|
||||||
self.status = DownloadTask.FAILED
|
self.status = DownloadTask.FAILED
|
||||||
d = {'code': gdhe.error_code, 'message': gdhe.error_message}
|
d = {'code': gdhe.error_code, 'message': gdhe.error_message}
|
||||||
self.error_message = _('HTTP Error %(code)s: %(message)s') % d
|
self.error_message = _('HTTP Error %(code)s: %(message)s') % d
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
self.status = DownloadTask.FAILED
|
self.status = DownloadTask.FAILED
|
||||||
logger.error('Download failed: %s', str(e), exc_info=True)
|
logger.error('Download failed: %s', str(e), exc_info=True)
|
||||||
self.error_message = _('Error: %s') % (str(e),)
|
self.error_message = _('Error: %s') % (str(e),)
|
||||||
|
|
|
@ -30,15 +30,10 @@ from gpodder import util
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
try:
|
import json
|
||||||
# For Python < 2.6, we use the "simplejson" add-on module
|
|
||||||
import simplejson as json
|
|
||||||
except ImportError:
|
|
||||||
# Python 2.6 already ships with a nice "json" module
|
|
||||||
import json
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import urllib
|
import urllib.request, urllib.parse, urllib.error
|
||||||
|
|
||||||
# This matches the more reliable URL
|
# This matches the more reliable URL
|
||||||
ESCAPIST_NUMBER_RE = re.compile(r'http://www.escapistmagazine.com/videos/view/(\d+)', re.IGNORECASE)
|
ESCAPIST_NUMBER_RE = re.compile(r'http://www.escapistmagazine.com/videos/view/(\d+)', re.IGNORECASE)
|
||||||
|
@ -112,6 +107,7 @@ def get_real_cover(url):
|
||||||
if rss_url is None:
|
if rss_url is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# FIXME: can I be sure to decode it as utf-8?
|
||||||
rss_data = util.urlopen(rss_url).read()
|
rss_data = util.urlopen(rss_url).read()
|
||||||
rss_data_frag = DATA_COVERART_RE.search(rss_data)
|
rss_data_frag = DATA_COVERART_RE.search(rss_data)
|
||||||
|
|
||||||
|
@ -124,6 +120,7 @@ def get_escapist_web(video_id):
|
||||||
if video_id is None:
|
if video_id is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# FIXME: must check if it's utf-8
|
||||||
web_url = 'http://www.escapistmagazine.com/videos/view/%s' % video_id
|
web_url = 'http://www.escapistmagazine.com/videos/view/%s' % video_id
|
||||||
return util.urlopen(web_url).read()
|
return util.urlopen(web_url).read()
|
||||||
|
|
||||||
|
@ -131,7 +128,7 @@ def get_escapist_config_url(data):
|
||||||
if data is None:
|
if data is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
query_string = urllib.urlencode(json.loads(data))
|
query_string = urllib.parse.urlencode(json.loads(data))
|
||||||
|
|
||||||
return 'http://www.escapistmagazine.com/videos/vidconfig.php?%s' % query_string
|
return 'http://www.escapistmagazine.com/videos/vidconfig.php?%s' % query_string
|
||||||
|
|
||||||
|
@ -162,7 +159,7 @@ def get_escapist_real_url(data, config_json):
|
||||||
result_num.append(num_hashes[idx]^hash_n[idx % len(hash_n)])
|
result_num.append(num_hashes[idx]^hash_n[idx % len(hash_n)])
|
||||||
|
|
||||||
# At last, Numbers back into characters
|
# At last, Numbers back into characters
|
||||||
result = ''.join([unichr(x) for x in result_num])
|
result = ''.join([chr(x) for x in result_num])
|
||||||
# A wild JSON appears...
|
# A wild JSON appears...
|
||||||
# You use "Master Ball"...
|
# You use "Master Ball"...
|
||||||
escapist_cfg = json.loads(result)
|
escapist_cfg = json.loads(result)
|
||||||
|
|
|
@ -85,7 +85,7 @@ def call_extensions(func):
|
||||||
result.extend(cb_res)
|
result.extend(cb_res)
|
||||||
elif cb_res is not None:
|
elif cb_res is not None:
|
||||||
result = cb_res
|
result = cb_res
|
||||||
except Exception, exception:
|
except Exception as exception:
|
||||||
logger.error('Error in %s in %s: %s', container.filename,
|
logger.error('Error in %s in %s: %s', container.filename,
|
||||||
method_name, exception, exc_info=True)
|
method_name, exception, exc_info=True)
|
||||||
func(self, *args, **kwargs)
|
func(self, *args, **kwargs)
|
||||||
|
@ -123,12 +123,12 @@ class ExtensionMetadata(object):
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
try:
|
try:
|
||||||
return self.DEFAULTS[name]
|
return self.DEFAULTS[name]
|
||||||
except KeyError, e:
|
except KeyError as e:
|
||||||
raise AttributeError(name, e)
|
raise AttributeError(name, e)
|
||||||
|
|
||||||
def get_sorted(self):
|
def get_sorted(self):
|
||||||
kf = lambda x: self.SORTKEYS.get(x[0], 99)
|
kf = lambda x: self.SORTKEYS.get(x[0], 99)
|
||||||
return sorted([(k, v) for k, v in self.__dict__.items()], key=kf)
|
return sorted([(k, v) for k, v in list(self.__dict__.items())], key=kf)
|
||||||
|
|
||||||
def check_ui(self, target, default):
|
def check_ui(self, target, default):
|
||||||
"""Checks metadata information like
|
"""Checks metadata information like
|
||||||
|
@ -159,7 +159,7 @@ class ExtensionMetadata(object):
|
||||||
if not hasattr(self, target):
|
if not hasattr(self, target):
|
||||||
return default
|
return default
|
||||||
|
|
||||||
uis = filter(None, [x.strip() for x in getattr(self, target).split(',')])
|
uis = [_f for _f in [x.strip() for x in getattr(self, target).split(',')] if _f]
|
||||||
return any(getattr(gpodder.ui, ui.lower(), False) for ui in uis)
|
return any(getattr(gpodder.ui, ui.lower(), False) for ui in uis)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -253,15 +253,14 @@ class ExtensionContainer(object):
|
||||||
self.enabled = True
|
self.enabled = True
|
||||||
if hasattr(self.module, 'on_load'):
|
if hasattr(self.module, 'on_load'):
|
||||||
self.module.on_load()
|
self.module.on_load()
|
||||||
except Exception, exception:
|
except Exception as exception:
|
||||||
logger.error('Cannot load %s from %s: %s', self.name,
|
logger.error('Cannot load %s from %s: %s', self.name,
|
||||||
self.filename, exception, exc_info=True)
|
self.filename, exception, exc_info=True)
|
||||||
if isinstance(exception, ImportError):
|
if isinstance(exception, ImportError):
|
||||||
# Wrap ImportError in MissingCommand for user-friendly
|
# Wrap ImportError in MissingCommand for user-friendly
|
||||||
# message (might be displayed in the GUI)
|
# message (might be displayed in the GUI)
|
||||||
match = re.match('No module named (.*)', exception.message)
|
if exception.name:
|
||||||
if match:
|
module = exception.name
|
||||||
module = match.group(1)
|
|
||||||
msg = _('Python module not found: %(module)s') % {
|
msg = _('Python module not found: %(module)s') % {
|
||||||
'module': module
|
'module': module
|
||||||
}
|
}
|
||||||
|
@ -272,7 +271,7 @@ class ExtensionContainer(object):
|
||||||
try:
|
try:
|
||||||
if hasattr(self.module, 'on_unload'):
|
if hasattr(self.module, 'on_unload'):
|
||||||
self.module.on_unload()
|
self.module.on_unload()
|
||||||
except Exception, exception:
|
except Exception as exception:
|
||||||
logger.error('Failed to on_unload %s: %s', self.name,
|
logger.error('Failed to on_unload %s: %s', self.name,
|
||||||
exception, exc_info=True)
|
exception, exc_info=True)
|
||||||
self.enabled = False
|
self.enabled = False
|
||||||
|
|
|
@ -29,9 +29,9 @@ from gpodder import util
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
from urllib2 import HTTPError
|
from urllib.error import HTTPError
|
||||||
from HTMLParser import HTMLParser
|
from html.parser import HTMLParser
|
||||||
import urlparse
|
import urllib.parse
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Python 2
|
# Python 2
|
||||||
|
@ -67,7 +67,7 @@ class UnknownStatusCode(ExceptionWithData): pass
|
||||||
class AuthenticationRequired(Exception): pass
|
class AuthenticationRequired(Exception): pass
|
||||||
|
|
||||||
# Successful status codes
|
# Successful status codes
|
||||||
UPDATED_FEED, NEW_LOCATION, NOT_MODIFIED, CUSTOM_FEED = range(4)
|
UPDATED_FEED, NEW_LOCATION, NOT_MODIFIED, CUSTOM_FEED = list(range(4))
|
||||||
|
|
||||||
class Result:
|
class Result:
|
||||||
def __init__(self, status, feed=None):
|
def __init__(self, status, feed=None):
|
||||||
|
@ -88,7 +88,7 @@ class FeedAutodiscovery(HTMLParser):
|
||||||
is_feed = attrs.get('type', '') in Fetcher.FEED_TYPES
|
is_feed = attrs.get('type', '') in Fetcher.FEED_TYPES
|
||||||
is_alternate = attrs.get('rel', '') == 'alternate'
|
is_alternate = attrs.get('rel', '') == 'alternate'
|
||||||
url = attrs.get('href', None)
|
url = attrs.get('href', None)
|
||||||
url = urlparse.urljoin(self._base, url)
|
url = urllib.parse.urljoin(self._base, url)
|
||||||
|
|
||||||
if is_feed and is_alternate and url:
|
if is_feed and is_alternate and url:
|
||||||
logger.info('Feed autodiscovery: %s', url)
|
logger.info('Feed autodiscovery: %s', url)
|
||||||
|
@ -175,10 +175,16 @@ class Fetcher(object):
|
||||||
|
|
||||||
data = stream
|
data = stream
|
||||||
if autodiscovery and not is_local and stream.headers.get('content-type', '').startswith('text/html'):
|
if autodiscovery and not is_local and stream.headers.get('content-type', '').startswith('text/html'):
|
||||||
|
# Not very robust attempt to detect encoding: http://stackoverflow.com/a/1495675/1072626
|
||||||
|
charset = stream.headers.get_param('charset')
|
||||||
|
if charset is None:
|
||||||
|
charset = 'utf-8' # utf-8 appears hard-coded elsewhere in this codebase
|
||||||
|
|
||||||
# We use StringIO in case the stream needs to be read again
|
# We use StringIO in case the stream needs to be read again
|
||||||
data = StringIO(stream.read())
|
data = StringIO(stream.read().decode(charset))
|
||||||
ad = FeedAutodiscovery(url)
|
ad = FeedAutodiscovery(url)
|
||||||
ad.feed(data.read())
|
|
||||||
|
ad.feed(data.getvalue())
|
||||||
if ad._resolved_url:
|
if ad._resolved_url:
|
||||||
try:
|
try:
|
||||||
self._parse_feed(ad._resolved_url, None, None, False)
|
self._parse_feed(ad._resolved_url, None, None, False)
|
||||||
|
@ -190,15 +196,16 @@ class Fetcher(object):
|
||||||
url = self._resolve_url(url)
|
url = self._resolve_url(url)
|
||||||
if url:
|
if url:
|
||||||
return Result(NEW_LOCATION, url)
|
return Result(NEW_LOCATION, url)
|
||||||
|
|
||||||
# Reset the stream so podcastparser can give it a go
|
# Reset the stream so podcastparser can give it a go
|
||||||
data.seek(0)
|
data.seek(0)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
feed = podcastparser.parse(url, data)
|
feed = podcastparser.parse(url, data)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise InvalidFeed(u'Could not parse feed: {msg}'.format(msg=e))
|
raise InvalidFeed('Could not parse feed: {msg}'.format(msg=e))
|
||||||
|
|
||||||
if is_local:
|
if is_local:
|
||||||
feed['headers'] = {}
|
feed['headers'] = {}
|
||||||
return Result(UPDATED_FEED, feed)
|
return Result(UPDATED_FEED, feed)
|
||||||
|
|
|
@ -26,10 +26,10 @@ import re
|
||||||
|
|
||||||
import tokenize
|
import tokenize
|
||||||
|
|
||||||
import gtk
|
from gi.repository import Gtk
|
||||||
|
|
||||||
class GtkBuilderWidget(object):
|
class GtkBuilderWidget(object):
|
||||||
def __init__(self, ui_folders, textdomain, **kwargs):
|
def __init__(self, ui_folders, textdomain, parent, **kwargs):
|
||||||
"""
|
"""
|
||||||
Loads the UI file from the specified folder (with translations
|
Loads the UI file from the specified folder (with translations
|
||||||
from the textdomain) and initializes attributes.
|
from the textdomain) and initializes attributes.
|
||||||
|
@ -43,11 +43,16 @@ class GtkBuilderWidget(object):
|
||||||
**kwargs:
|
**kwargs:
|
||||||
Keyword arguments will be set as attributes to this window
|
Keyword arguments will be set as attributes to this window
|
||||||
"""
|
"""
|
||||||
for key, value in kwargs.items():
|
for key, value in list(kwargs.items()):
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
|
|
||||||
self.builder = gtk.Builder()
|
self.builder = Gtk.Builder()
|
||||||
|
if parent is not None:
|
||||||
|
self.builder.expose_object('parent_widget', parent)
|
||||||
self.builder.set_translation_domain(textdomain)
|
self.builder.set_translation_domain(textdomain)
|
||||||
|
if hasattr(self, '_builder_expose'):
|
||||||
|
for (key, value) in list(self._builder_expose.items()):
|
||||||
|
self.builder.expose_object(key, value)
|
||||||
|
|
||||||
#print >>sys.stderr, 'Creating new from file', self.__class__.__name__
|
#print >>sys.stderr, 'Creating new from file', self.__class__.__name__
|
||||||
|
|
||||||
|
@ -74,11 +79,11 @@ class GtkBuilderWidget(object):
|
||||||
"""
|
"""
|
||||||
for widget in self.builder.get_objects():
|
for widget in self.builder.get_objects():
|
||||||
# Just to be safe - every widget from the builder is buildable
|
# Just to be safe - every widget from the builder is buildable
|
||||||
if not isinstance(widget, gtk.Buildable):
|
if not isinstance(widget, Gtk.Buildable):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# The following call looks ugly, but see Gnome bug 591085
|
# The following call looks ugly, but see Gnome bug 591085
|
||||||
widget_name = gtk.Buildable.get_name(widget)
|
widget_name = Gtk.Buildable.get_name(widget)
|
||||||
|
|
||||||
widget_api_name = '_'.join(re.findall(tokenize.Name, widget_name))
|
widget_api_name = '_'.join(re.findall(tokenize.Name, widget_name))
|
||||||
if hasattr(self, widget_api_name):
|
if hasattr(self, widget_api_name):
|
||||||
|
@ -101,27 +106,27 @@ class GtkBuilderWidget(object):
|
||||||
def main(self):
|
def main(self):
|
||||||
"""
|
"""
|
||||||
Starts the main loop of processing events.
|
Starts the main loop of processing events.
|
||||||
The default implementation calls gtk.main()
|
The default implementation calls Gtk.main()
|
||||||
|
|
||||||
Useful for applications that needs a non gtk main loop.
|
Useful for applications that needs a non gtk main loop.
|
||||||
For example, applications based on gstreamer needs to override
|
For example, applications based on gstreamer needs to override
|
||||||
this method with gst.main()
|
this method with Gst.main()
|
||||||
|
|
||||||
Do not directly call this method in your programs.
|
Do not directly call this method in your programs.
|
||||||
Use the method run() instead.
|
Use the method run() instead.
|
||||||
"""
|
"""
|
||||||
gtk.main()
|
Gtk.main()
|
||||||
|
|
||||||
def quit(self):
|
def quit(self):
|
||||||
"""
|
"""
|
||||||
Quit processing events.
|
Quit processing events.
|
||||||
The default implementation calls gtk.main_quit()
|
The default implementation calls Gtk.main_quit()
|
||||||
|
|
||||||
Useful for applications that needs a non gtk main loop.
|
Useful for applications that needs a non gtk main loop.
|
||||||
For example, applications based on gstreamer needs to override
|
For example, applications based on gstreamer needs to override
|
||||||
this method with gst.main_quit()
|
this method with Gst.main_quit()
|
||||||
"""
|
"""
|
||||||
gtk.main_quit()
|
Gtk.main_quit()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -23,8 +23,9 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
import gtk
|
from gi.repository import Gtk
|
||||||
import pango
|
from gi.repository import Gdk
|
||||||
|
from gi.repository import Pango
|
||||||
|
|
||||||
import gpodder
|
import gpodder
|
||||||
from gpodder import util
|
from gpodder import util
|
||||||
|
@ -32,13 +33,12 @@ from gpodder import config
|
||||||
|
|
||||||
_ = gpodder.gettext
|
_ = gpodder.gettext
|
||||||
|
|
||||||
class ConfigModel(gtk.ListStore):
|
class ConfigModel(Gtk.ListStore):
|
||||||
C_NAME, C_TYPE_TEXT, C_VALUE_TEXT, C_TYPE, C_EDITABLE, C_FONT_STYLE, \
|
C_NAME, C_TYPE_TEXT, C_VALUE_TEXT, C_TYPE, C_EDITABLE, C_FONT_STYLE, \
|
||||||
C_IS_BOOLEAN, C_BOOLEAN_VALUE = range(8)
|
C_IS_BOOLEAN, C_BOOLEAN_VALUE = list(range(8))
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
gtk.ListStore.__init__(self, str, str, str, object, \
|
Gtk.ListStore.__init__(self, str, str, str, object, bool, int, bool, bool)
|
||||||
bool, int, bool, bool)
|
|
||||||
|
|
||||||
self._config = config
|
self._config = config
|
||||||
self._fill_model()
|
self._fill_model()
|
||||||
|
@ -65,11 +65,11 @@ class ConfigModel(gtk.ListStore):
|
||||||
value = self._config._lookup(key)
|
value = self._config._lookup(key)
|
||||||
fieldtype = type(value)
|
fieldtype = type(value)
|
||||||
|
|
||||||
style = pango.STYLE_NORMAL
|
style = Pango.Style.NORMAL
|
||||||
#if value == default:
|
#if value == default:
|
||||||
# style = pango.STYLE_NORMAL
|
# style = Pango.Style.NORMAL
|
||||||
#else:
|
#else:
|
||||||
# style = pango.STYLE_ITALIC
|
# style = Pango.Style.ITALIC
|
||||||
|
|
||||||
self.append((key, self._type_as_string(fieldtype),
|
self.append((key, self._type_as_string(fieldtype),
|
||||||
config.config_value_to_string(value),
|
config.config_value_to_string(value),
|
||||||
|
@ -79,11 +79,11 @@ class ConfigModel(gtk.ListStore):
|
||||||
def _on_update(self, name, old_value, new_value):
|
def _on_update(self, name, old_value, new_value):
|
||||||
for row in self:
|
for row in self:
|
||||||
if row[self.C_NAME] == name:
|
if row[self.C_NAME] == name:
|
||||||
style = pango.STYLE_NORMAL
|
style = Pango.Style.NORMAL
|
||||||
#if new_value == self._config.Settings[name]:
|
#if new_value == self._config.Settings[name]:
|
||||||
# style = pango.STYLE_NORMAL
|
# style = Pango.Style.NORMAL
|
||||||
#else:
|
#else:
|
||||||
# style = pango.STYLE_ITALIC
|
# style = Pango.Style.ITALIC
|
||||||
new_value_text = config.config_value_to_string(new_value)
|
new_value_text = config.config_value_to_string(new_value)
|
||||||
self.set(row.iter, \
|
self.set(row.iter, \
|
||||||
self.C_VALUE_TEXT, new_value_text,
|
self.C_VALUE_TEXT, new_value_text,
|
||||||
|
@ -139,11 +139,11 @@ class UIConfig(config.Config):
|
||||||
cfg = getattr(self.ui.gtk.state, config_prefix)
|
cfg = getattr(self.ui.gtk.state, config_prefix)
|
||||||
|
|
||||||
if gpodder.ui.win32:
|
if gpodder.ui.win32:
|
||||||
window.set_gravity(gtk.gdk.GRAVITY_STATIC)
|
window.set_gravity(Gdk.GRAVITY_STATIC)
|
||||||
|
|
||||||
window.resize(cfg.width, cfg.height)
|
window.resize(cfg.width, cfg.height)
|
||||||
if cfg.x == -1 or cfg.y == -1:
|
if cfg.x == -1 or cfg.y == -1:
|
||||||
window.set_position(gtk.WIN_POS_CENTER_ON_PARENT)
|
window.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
|
||||||
else:
|
else:
|
||||||
window.move(cfg.x, cfg.y)
|
window.move(cfg.x, cfg.y)
|
||||||
|
|
||||||
|
@ -153,8 +153,7 @@ class UIConfig(config.Config):
|
||||||
def _receive_configure_event(widget, event):
|
def _receive_configure_event(widget, event):
|
||||||
x_pos, y_pos = event.x, event.y
|
x_pos, y_pos = event.x, event.y
|
||||||
width_size, height_size = event.width, event.height
|
width_size, height_size = event.width, event.height
|
||||||
maximized = bool(event.window.get_state() &
|
maximized = bool(event.window.get_state() & Gdk.WindowState.MAXIMIZED)
|
||||||
gtk.gdk.WINDOW_STATE_MAXIMIZED)
|
|
||||||
if not self.__ignore_window_events and not maximized:
|
if not self.__ignore_window_events and not maximized:
|
||||||
cfg.x = x_pos
|
cfg.x = x_pos
|
||||||
cfg.y = y_pos
|
cfg.y = y_pos
|
||||||
|
@ -164,9 +163,10 @@ class UIConfig(config.Config):
|
||||||
window.connect('configure-event', _receive_configure_event)
|
window.connect('configure-event', _receive_configure_event)
|
||||||
|
|
||||||
def _receive_window_state(widget, event):
|
def _receive_window_state(widget, event):
|
||||||
new_value = bool(event.new_window_state &
|
# ELL: why is it commented out?
|
||||||
gtk.gdk.WINDOW_STATE_MAXIMIZED)
|
#new_value = bool(event.new_window_state & Gdk.WindowState.MAXIMIZED)
|
||||||
cfg.maximized = new_value
|
#cfg.maximized = new_value
|
||||||
|
pass
|
||||||
|
|
||||||
window.connect('window-state-event', _receive_window_state)
|
window.connect('window-state-event', _receive_window_state)
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,9 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
|
||||||
import gtk
|
from gi.repository import Gtk
|
||||||
import gtk.gdk
|
from gi.repository import Gdk
|
||||||
|
from gi.repository import GdkPixbuf
|
||||||
|
|
||||||
import gpodder
|
import gpodder
|
||||||
|
|
||||||
|
@ -41,19 +42,19 @@ class gPodderChannel(BuilderWidget):
|
||||||
self.cbSkipFeedUpdate.set_active(self.channel.pause_subscription)
|
self.cbSkipFeedUpdate.set_active(self.channel.pause_subscription)
|
||||||
self.cbEnableDeviceSync.set_active(self.channel.sync_to_mp3_player)
|
self.cbEnableDeviceSync.set_active(self.channel.sync_to_mp3_player)
|
||||||
|
|
||||||
self.section_list = gtk.ListStore(str)
|
self.section_list = Gtk.ListStore(str)
|
||||||
active_index = 0
|
active_index = 0
|
||||||
for index, section in enumerate(sorted(self.sections)):
|
for index, section in enumerate(sorted(self.sections)):
|
||||||
self.section_list.append([section])
|
self.section_list.append([section])
|
||||||
if section == self.channel.section:
|
if section == self.channel.section:
|
||||||
active_index = index
|
active_index = index
|
||||||
self.combo_section.set_model(self.section_list)
|
self.combo_section.set_model(self.section_list)
|
||||||
cell_renderer = gtk.CellRendererText()
|
cell_renderer = Gtk.CellRendererText()
|
||||||
self.combo_section.pack_start(cell_renderer)
|
self.combo_section.pack_start(cell_renderer, True)
|
||||||
self.combo_section.add_attribute(cell_renderer, 'text', 0)
|
self.combo_section.add_attribute(cell_renderer, 'text', 0)
|
||||||
self.combo_section.set_active(active_index)
|
self.combo_section.set_active(active_index)
|
||||||
|
|
||||||
self.strategy_list = gtk.ListStore(str, int)
|
self.strategy_list = Gtk.ListStore(str, int)
|
||||||
active_index = 0
|
active_index = 0
|
||||||
for index, (checked, strategy_id, strategy) in \
|
for index, (checked, strategy_id, strategy) in \
|
||||||
enumerate(self.channel.get_download_strategies()):
|
enumerate(self.channel.get_download_strategies()):
|
||||||
|
@ -61,8 +62,8 @@ class gPodderChannel(BuilderWidget):
|
||||||
if checked:
|
if checked:
|
||||||
active_index = index
|
active_index = index
|
||||||
self.combo_strategy.set_model(self.strategy_list)
|
self.combo_strategy.set_model(self.strategy_list)
|
||||||
cell_renderer = gtk.CellRendererText()
|
cell_renderer = Gtk.CellRendererText()
|
||||||
self.combo_strategy.pack_start(cell_renderer)
|
self.combo_strategy.pack_start(cell_renderer, True)
|
||||||
self.combo_strategy.add_attribute(cell_renderer, 'text', 0)
|
self.combo_strategy.add_attribute(cell_renderer, 'text', 0)
|
||||||
self.combo_strategy.set_active(active_index)
|
self.combo_strategy.set_active(active_index)
|
||||||
|
|
||||||
|
@ -81,14 +82,14 @@ class gPodderChannel(BuilderWidget):
|
||||||
if not self.channel.link:
|
if not self.channel.link:
|
||||||
self.btn_website.hide_all()
|
self.btn_website.hide_all()
|
||||||
|
|
||||||
b = gtk.TextBuffer()
|
b = Gtk.TextBuffer()
|
||||||
b.set_text( self.channel.description)
|
b.set_text( self.channel.description)
|
||||||
self.channel_description.set_buffer( b)
|
self.channel_description.set_buffer( b)
|
||||||
|
|
||||||
#Add Drag and Drop Support
|
#Add Drag and Drop Support
|
||||||
flags = gtk.DEST_DEFAULT_ALL
|
flags = Gtk.DestDefaults.ALL
|
||||||
targets = [('text/uri-list', 0, 2), ('text/plain', 0, 4)]
|
targets = [Gtk.TargetEntry.new('text/uri-list', 0, 2), Gtk.TargetEntry.new('text/plain', 0, 4)]
|
||||||
actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
|
actions = Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY
|
||||||
self.imgCover.drag_dest_set(flags, targets, actions)
|
self.imgCover.drag_dest_set(flags, targets, actions)
|
||||||
self.imgCover.connect('drag_data_received', self.drag_data_received)
|
self.imgCover.connect('drag_data_received', self.drag_data_received)
|
||||||
border = 6
|
border = 6
|
||||||
|
@ -98,7 +99,7 @@ class gPodderChannel(BuilderWidget):
|
||||||
|
|
||||||
def on_button_add_section_clicked(self, widget):
|
def on_button_add_section_clicked(self, widget):
|
||||||
text = self.show_text_edit_dialog(_('Add section'), _('New section:'),
|
text = self.show_text_edit_dialog(_('Add section'), _('New section:'),
|
||||||
affirmative_text=gtk.STOCK_ADD)
|
affirmative_text=Gtk.STOCK_ADD)
|
||||||
|
|
||||||
if text is not None:
|
if text is not None:
|
||||||
for index, (section,) in enumerate(self.section_list):
|
for index, (section,) in enumerate(self.section_list):
|
||||||
|
@ -110,31 +111,32 @@ class gPodderChannel(BuilderWidget):
|
||||||
self.combo_section.set_active(len(self.section_list)-1)
|
self.combo_section.set_active(len(self.section_list)-1)
|
||||||
|
|
||||||
def on_cover_popup_menu(self, widget, event):
|
def on_cover_popup_menu(self, widget, event):
|
||||||
if event.button != 3:
|
if not event.triggers_context_menu():
|
||||||
return
|
return
|
||||||
|
|
||||||
menu = gtk.Menu()
|
menu = Gtk.Menu()
|
||||||
|
|
||||||
item = gtk.ImageMenuItem(gtk.STOCK_OPEN)
|
item = Gtk.MenuItem.new_with_mnemonic(_('_Open'))
|
||||||
item.connect('activate', self.on_btnDownloadCover_clicked)
|
item.connect('activate', self.on_btnDownloadCover_clicked)
|
||||||
menu.append(item)
|
menu.append(item)
|
||||||
|
|
||||||
item = gtk.ImageMenuItem(gtk.STOCK_REFRESH)
|
item = Gtk.MenuItem.new_with_mnemonic(_('_Refresh'))
|
||||||
item.connect('activate', self.on_btnClearCover_clicked)
|
item.connect('activate', self.on_btnClearCover_clicked)
|
||||||
menu.append(item)
|
menu.append(item)
|
||||||
|
|
||||||
|
menu.attach_to_widget(widget)
|
||||||
menu.show_all()
|
menu.show_all()
|
||||||
menu.popup(None, None, None, event.button, event.time, None)
|
menu.popup(None, None, None, None, event.button, event.time)
|
||||||
|
|
||||||
def on_btn_website_clicked(self, widget):
|
def on_btn_website_clicked(self, widget):
|
||||||
util.open_website(self.channel.link)
|
util.open_website(self.channel.link)
|
||||||
|
|
||||||
def on_btnDownloadCover_clicked(self, widget):
|
def on_btnDownloadCover_clicked(self, widget):
|
||||||
dlg = gtk.FileChooserDialog(title=_('Select new podcast cover artwork'), parent=self.gPodderChannel, action=gtk.FILE_CHOOSER_ACTION_OPEN)
|
dlg = Gtk.FileChooserDialog(title=_('Select new podcast cover artwork'), parent=self.gPodderChannel, action=Gtk.FileChooserAction.OPEN)
|
||||||
dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
|
dlg.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
|
||||||
dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
|
dlg.add_button(Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
|
||||||
|
|
||||||
if dlg.run() == gtk.RESPONSE_OK:
|
if dlg.run() == Gtk.ResponseType.OK:
|
||||||
url = dlg.get_uri()
|
url = dlg.get_uri()
|
||||||
self.clear_cover_cache(self.channel.url)
|
self.clear_cover_cache(self.channel.url)
|
||||||
self.cover_downloader.replace_cover(self.channel, custom_url=url)
|
self.cover_downloader.replace_cover(self.channel, custom_url=url)
|
||||||
|
@ -180,13 +182,13 @@ class gPodderChannel(BuilderWidget):
|
||||||
if pixbuf.get_width() > self.MAX_SIZE:
|
if pixbuf.get_width() > self.MAX_SIZE:
|
||||||
f = float(self.MAX_SIZE)/pixbuf.get_width()
|
f = float(self.MAX_SIZE)/pixbuf.get_width()
|
||||||
(width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
|
(width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
|
||||||
pixbuf = pixbuf.scale_simple(width, height, gtk.gdk.INTERP_BILINEAR)
|
pixbuf = pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR)
|
||||||
|
|
||||||
# Resize if height is too large
|
# Resize if height is too large
|
||||||
if pixbuf.get_height() > self.MAX_SIZE:
|
if pixbuf.get_height() > self.MAX_SIZE:
|
||||||
f = float(self.MAX_SIZE)/pixbuf.get_height()
|
f = float(self.MAX_SIZE)/pixbuf.get_height()
|
||||||
(width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
|
(width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
|
||||||
pixbuf = pixbuf.scale_simple(width, height, gtk.gdk.INTERP_BILINEAR)
|
pixbuf = pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR)
|
||||||
|
|
||||||
return pixbuf
|
return pixbuf
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ class gPodderDevicePlaylist(object):
|
||||||
self.linebreak = '\r\n'
|
self.linebreak = '\r\n'
|
||||||
self.playlist_file=util.sanitize_filename(playlist_name + '.m3u')
|
self.playlist_file=util.sanitize_filename(playlist_name + '.m3u')
|
||||||
self.playlist_folder = os.path.join(self._config.device_sync.device_folder, self._config.device_sync.playlists.folder)
|
self.playlist_folder = os.path.join(self._config.device_sync.device_folder, self._config.device_sync.playlists.folder)
|
||||||
self.mountpoint = util.find_mount_point(util.sanitize_encoding(self.playlist_folder))
|
self.mountpoint = util.find_mount_point(self.playlist_folder)
|
||||||
if self.mountpoint == '/':
|
if self.mountpoint == '/':
|
||||||
self.mountpoint = self.playlist_folder
|
self.mountpoint = self.playlist_folder
|
||||||
logger.warning('MP3 player resides on / - using %s as MP3 player root', self.mountpoint)
|
logger.warning('MP3 player resides on / - using %s as MP3 player root', self.mountpoint)
|
||||||
|
|
|
@ -17,9 +17,8 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
|
||||||
import gtk
|
from gi.repository import Gtk
|
||||||
import pango
|
from gi.repository import Pango
|
||||||
import cgi
|
|
||||||
|
|
||||||
import gpodder
|
import gpodder
|
||||||
|
|
||||||
|
@ -66,7 +65,7 @@ class gPodderEpisodeSelector(BuilderWidget):
|
||||||
- stock_ok_button: (optional) Will replace the "OK" button with
|
- stock_ok_button: (optional) Will replace the "OK" button with
|
||||||
another GTK+ stock item to be used for the
|
another GTK+ stock item to be used for the
|
||||||
affirmative button of the dialog (e.g. can
|
affirmative button of the dialog (e.g. can
|
||||||
be gtk.STOCK_DELETE when the episodes to be
|
be Gtk.STOCK_DELETE when the episodes to be
|
||||||
selected will be deleted after closing the
|
selected will be deleted after closing the
|
||||||
dialog)
|
dialog)
|
||||||
- selection_buttons: (optional) A dictionary with labels as
|
- selection_buttons: (optional) A dictionary with labels as
|
||||||
|
@ -140,25 +139,25 @@ class gPodderEpisodeSelector(BuilderWidget):
|
||||||
|
|
||||||
if hasattr(self, 'stock_ok_button'):
|
if hasattr(self, 'stock_ok_button'):
|
||||||
if self.stock_ok_button == 'gpodder-download':
|
if self.stock_ok_button == 'gpodder-download':
|
||||||
self.btnOK.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_BUTTON))
|
self.btnOK.set_image(Gtk.Image.new_from_icon_name('go-down', Gtk.IconSize.BUTTON))
|
||||||
self.btnOK.set_label(_('Download'))
|
self.btnOK.set_label(_('Download'))
|
||||||
else:
|
else:
|
||||||
self.btnOK.set_label(self.stock_ok_button)
|
self.btnOK.set_label(self.stock_ok_button)
|
||||||
self.btnOK.set_use_stock(True)
|
self.btnOK.set_use_stock(True)
|
||||||
|
|
||||||
# check/uncheck column
|
# check/uncheck column
|
||||||
toggle_cell = gtk.CellRendererToggle()
|
toggle_cell = Gtk.CellRendererToggle()
|
||||||
toggle_cell.connect( 'toggled', self.toggle_cell_handler)
|
toggle_cell.connect( 'toggled', self.toggle_cell_handler)
|
||||||
toggle_column = gtk.TreeViewColumn('', toggle_cell, active=self.COLUMN_TOGGLE)
|
toggle_column = Gtk.TreeViewColumn('', toggle_cell, active=self.COLUMN_TOGGLE)
|
||||||
toggle_column.set_clickable(True)
|
toggle_column.set_clickable(True)
|
||||||
self.treeviewEpisodes.append_column(toggle_column)
|
self.treeviewEpisodes.append_column(toggle_column)
|
||||||
|
|
||||||
next_column = self.COLUMN_ADDITIONAL
|
next_column = self.COLUMN_ADDITIONAL
|
||||||
for name, sort_name, sort_type, caption in self.columns:
|
for name, sort_name, sort_type, caption in self.columns:
|
||||||
renderer = gtk.CellRendererText()
|
renderer = Gtk.CellRendererText()
|
||||||
if next_column < self.COLUMN_ADDITIONAL + 1:
|
if next_column < self.COLUMN_ADDITIONAL + 1:
|
||||||
renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
|
renderer.set_property('ellipsize', Pango.EllipsizeMode.END)
|
||||||
column = gtk.TreeViewColumn(caption, renderer, markup=next_column)
|
column = Gtk.TreeViewColumn(caption, renderer, markup=next_column)
|
||||||
column.set_clickable(False)
|
column.set_clickable(False)
|
||||||
column.set_resizable( True)
|
column.set_resizable( True)
|
||||||
# Only set "expand" on the first column
|
# Only set "expand" on the first column
|
||||||
|
@ -173,7 +172,7 @@ class gPodderEpisodeSelector(BuilderWidget):
|
||||||
|
|
||||||
if sort_name is not None:
|
if sort_name is not None:
|
||||||
# add the sort column
|
# add the sort column
|
||||||
column = gtk.TreeViewColumn()
|
column = Gtk.TreeViewColumn()
|
||||||
column.set_clickable(False)
|
column.set_clickable(False)
|
||||||
column.set_visible(False)
|
column.set_visible(False)
|
||||||
self.treeviewEpisodes.append_column( column)
|
self.treeviewEpisodes.append_column( column)
|
||||||
|
@ -185,7 +184,7 @@ class gPodderEpisodeSelector(BuilderWidget):
|
||||||
column_types.append(str)
|
column_types.append(str)
|
||||||
if sort_name is not None:
|
if sort_name is not None:
|
||||||
column_types.append(sort_type)
|
column_types.append(sort_type)
|
||||||
self.model = gtk.ListStore( *column_types)
|
self.model = Gtk.ListStore( *column_types)
|
||||||
|
|
||||||
tooltip = None
|
tooltip = None
|
||||||
for index, episode in enumerate( self.episodes):
|
for index, episode in enumerate( self.episodes):
|
||||||
|
@ -275,21 +274,21 @@ class gPodderEpisodeSelector(BuilderWidget):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def treeview_episodes_button_pressed(self, treeview, event=None):
|
def treeview_episodes_button_pressed(self, treeview, event=None):
|
||||||
if event is None or event.button == 3:
|
if event is None or event.triggers_context_menu():
|
||||||
menu = gtk.Menu()
|
menu = Gtk.Menu()
|
||||||
|
|
||||||
if len(self.selection_buttons):
|
if len(self.selection_buttons):
|
||||||
for label in self.selection_buttons:
|
for label in self.selection_buttons:
|
||||||
item = gtk.MenuItem(label)
|
item = Gtk.MenuItem(label)
|
||||||
item.connect('activate', self.custom_selection_button_clicked, label)
|
item.connect('activate', self.custom_selection_button_clicked, label)
|
||||||
menu.append(item)
|
menu.append(item)
|
||||||
menu.append(gtk.SeparatorMenuItem())
|
menu.append(Gtk.SeparatorMenuItem())
|
||||||
|
|
||||||
item = gtk.MenuItem(_('Select all'))
|
item = Gtk.MenuItem(_('Select all'))
|
||||||
item.connect('activate', self.on_btnCheckAll_clicked)
|
item.connect('activate', self.on_btnCheckAll_clicked)
|
||||||
menu.append(item)
|
menu.append(item)
|
||||||
|
|
||||||
item = gtk.MenuItem(_('Select none'))
|
item = Gtk.MenuItem(_('Select none'))
|
||||||
item.connect('activate', self.on_btnCheckNone_clicked)
|
item.connect('activate', self.on_btnCheckNone_clicked)
|
||||||
menu.append(item)
|
menu.append(item)
|
||||||
|
|
||||||
|
@ -300,9 +299,9 @@ class gPodderEpisodeSelector(BuilderWidget):
|
||||||
menu.connect('deactivate', lambda menushell: self.episode_list_allow_tooltips())
|
menu.connect('deactivate', lambda menushell: self.episode_list_allow_tooltips())
|
||||||
if event is None:
|
if event is None:
|
||||||
func = TreeViewHelper.make_popup_position_func(treeview)
|
func = TreeViewHelper.make_popup_position_func(treeview)
|
||||||
menu.popup(None, None, func, 3, 0)
|
menu.popup(None, None, func, None, 3, Gtk.get_current_event_time())
|
||||||
else:
|
else:
|
||||||
menu.popup(None, None, None, event.button, event.time)
|
menu.popup(None, None, None, None, event.button, event.time)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -329,9 +328,9 @@ class gPodderEpisodeSelector(BuilderWidget):
|
||||||
self.btnOK.set_sensitive(count>0)
|
self.btnOK.set_sensitive(count>0)
|
||||||
self.btnRemoveAction.set_sensitive(count>0)
|
self.btnRemoveAction.set_sensitive(count>0)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
self.btnCancel.set_label(gtk.STOCK_CANCEL)
|
self.btnCancel.set_label(Gtk.STOCK_CANCEL)
|
||||||
else:
|
else:
|
||||||
self.btnCancel.set_label(gtk.STOCK_CLOSE)
|
self.btnCancel.set_label(Gtk.STOCK_CLOSE)
|
||||||
else:
|
else:
|
||||||
self.btnOK.set_sensitive(False)
|
self.btnOK.set_sensitive(False)
|
||||||
self.btnRemoveAction.set_sensitive(False)
|
self.btnRemoveAction.set_sensitive(False)
|
||||||
|
|
|
@ -24,8 +24,9 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
import gtk
|
from gi.repository import Gtk
|
||||||
import pango
|
from gi.repository import GdkPixbuf
|
||||||
|
from gi.repository import Pango
|
||||||
import cgi
|
import cgi
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
@ -43,11 +44,11 @@ from gpodder.gtkui.interface.common import BuilderWidget
|
||||||
from gpodder.gtkui.interface.progress import ProgressIndicator
|
from gpodder.gtkui.interface.progress import ProgressIndicator
|
||||||
from gpodder.gtkui.interface.tagcloud import TagCloud
|
from gpodder.gtkui.interface.tagcloud import TagCloud
|
||||||
|
|
||||||
class DirectoryPodcastsModel(gtk.ListStore):
|
class DirectoryPodcastsModel(Gtk.ListStore):
|
||||||
C_SELECTED, C_MARKUP, C_TITLE, C_URL = range(4)
|
C_SELECTED, C_MARKUP, C_TITLE, C_URL = list(range(4))
|
||||||
|
|
||||||
def __init__(self, callback_can_subscribe):
|
def __init__(self, callback_can_subscribe):
|
||||||
gtk.ListStore.__init__(self, bool, str, str, str)
|
Gtk.ListStore.__init__(self, bool, str, str, str)
|
||||||
self.callback_can_subscribe = callback_can_subscribe
|
self.callback_can_subscribe = callback_can_subscribe
|
||||||
|
|
||||||
def load(self, directory_entries):
|
def load(self, directory_entries):
|
||||||
|
@ -74,13 +75,13 @@ class DirectoryPodcastsModel(gtk.ListStore):
|
||||||
return [(row[self.C_TITLE], row[self.C_URL]) for row in self if row[self.C_SELECTED]]
|
return [(row[self.C_TITLE], row[self.C_URL]) for row in self if row[self.C_SELECTED]]
|
||||||
|
|
||||||
|
|
||||||
class DirectoryProvidersModel(gtk.ListStore):
|
class DirectoryProvidersModel(Gtk.ListStore):
|
||||||
C_WEIGHT, C_TEXT, C_ICON, C_PROVIDER = range(4)
|
C_WEIGHT, C_TEXT, C_ICON, C_PROVIDER = list(range(4))
|
||||||
|
|
||||||
SEPARATOR = (pango.WEIGHT_NORMAL, '', None, None)
|
SEPARATOR = (Pango.Weight.NORMAL, '', None, None)
|
||||||
|
|
||||||
def __init__(self, providers):
|
def __init__(self, providers):
|
||||||
gtk.ListStore.__init__(self, int, str, gtk.gdk.Pixbuf, object)
|
Gtk.ListStore.__init__(self, int, str, GdkPixbuf.Pixbuf, object)
|
||||||
for provider in providers:
|
for provider in providers:
|
||||||
self.add_provider(provider() if provider else None)
|
self.add_provider(provider() if provider else None)
|
||||||
|
|
||||||
|
@ -89,11 +90,11 @@ class DirectoryProvidersModel(gtk.ListStore):
|
||||||
self.append(self.SEPARATOR)
|
self.append(self.SEPARATOR)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
pixbuf = gtk.gdk.pixbuf_new_from_file(os.path.join(gpodder.images_folder, provider.icon)) if provider.icon else None
|
pixbuf = GdkPixbuf.Pixbuf.new_from_file(os.path.join(gpodder.images_folder, provider.icon)) if provider.icon else None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn('Could not load icon: %s (%s)', provider.icon or '-', e)
|
logger.warn('Could not load icon: %s (%s)', provider.icon or '-', e)
|
||||||
pixbuf = None
|
pixbuf = None
|
||||||
self.append((pango.WEIGHT_NORMAL, provider.name, pixbuf, provider))
|
self.append((Pango.Weight.NORMAL, provider.name, pixbuf, provider))
|
||||||
|
|
||||||
def is_row_separator(self, model, it):
|
def is_row_separator(self, model, it):
|
||||||
return self.get_value(it, self.C_PROVIDER) is None
|
return self.get_value(it, self.C_PROVIDER) is None
|
||||||
|
@ -127,17 +128,17 @@ class gPodderPodcastDirectory(BuilderWidget):
|
||||||
self.tv_providers.set_cursor(len(self.providers_model)-1)
|
self.tv_providers.set_cursor(len(self.providers_model)-1)
|
||||||
|
|
||||||
def setup_podcasts_treeview(self):
|
def setup_podcasts_treeview(self):
|
||||||
column = gtk.TreeViewColumn('')
|
column = Gtk.TreeViewColumn('')
|
||||||
cell = gtk.CellRendererToggle()
|
cell = Gtk.CellRendererToggle()
|
||||||
column.pack_start(cell, False)
|
column.pack_start(cell, False)
|
||||||
column.add_attribute(cell, 'active', DirectoryPodcastsModel.C_SELECTED)
|
column.add_attribute(cell, 'active', DirectoryPodcastsModel.C_SELECTED)
|
||||||
cell.connect('toggled', lambda cell, path: self.podcasts_model.toggle(path))
|
cell.connect('toggled', lambda cell, path: self.podcasts_model.toggle(path))
|
||||||
self.tv_podcasts.append_column(column)
|
self.tv_podcasts.append_column(column)
|
||||||
|
|
||||||
column = gtk.TreeViewColumn('')
|
column = Gtk.TreeViewColumn('')
|
||||||
cell = gtk.CellRendererText()
|
cell = Gtk.CellRendererText()
|
||||||
cell.set_property('ellipsize', pango.ELLIPSIZE_END)
|
cell.set_property('ellipsize', Pango.EllipsizeMode.END)
|
||||||
column.pack_start(cell)
|
column.pack_start(cell, True)
|
||||||
column.add_attribute(cell, 'markup', DirectoryPodcastsModel.C_MARKUP)
|
column.add_attribute(cell, 'markup', DirectoryPodcastsModel.C_MARKUP)
|
||||||
self.tv_podcasts.append_column(column)
|
self.tv_podcasts.append_column(column)
|
||||||
|
|
||||||
|
@ -145,13 +146,13 @@ class gPodderPodcastDirectory(BuilderWidget):
|
||||||
self.podcasts_model.append((False, 'a', 'b', 'c'))
|
self.podcasts_model.append((False, 'a', 'b', 'c'))
|
||||||
|
|
||||||
def setup_providers_treeview(self):
|
def setup_providers_treeview(self):
|
||||||
column = gtk.TreeViewColumn('')
|
column = Gtk.TreeViewColumn('')
|
||||||
cell = gtk.CellRendererPixbuf()
|
cell = Gtk.CellRendererPixbuf()
|
||||||
column.pack_start(cell, False)
|
column.pack_start(cell, False)
|
||||||
column.add_attribute(cell, 'pixbuf', DirectoryProvidersModel.C_ICON)
|
column.add_attribute(cell, 'pixbuf', DirectoryProvidersModel.C_ICON)
|
||||||
cell = gtk.CellRendererText()
|
cell = Gtk.CellRendererText()
|
||||||
#cell.set_property('ellipsize', pango.ELLIPSIZE_END)
|
#cell.set_property('ellipsize', Pango.EllipsizeMode.END)
|
||||||
column.pack_start(cell)
|
column.pack_start(cell, True)
|
||||||
column.add_attribute(cell, 'text', DirectoryProvidersModel.C_TEXT)
|
column.add_attribute(cell, 'text', DirectoryProvidersModel.C_TEXT)
|
||||||
column.add_attribute(cell, 'weight', DirectoryProvidersModel.C_WEIGHT)
|
column.add_attribute(cell, 'weight', DirectoryProvidersModel.C_WEIGHT)
|
||||||
self.tv_providers.append_column(column)
|
self.tv_providers.append_column(column)
|
||||||
|
@ -175,10 +176,10 @@ class gPodderPodcastDirectory(BuilderWidget):
|
||||||
it = self.providers_model.get_iter(path)
|
it = self.providers_model.get_iter(path)
|
||||||
|
|
||||||
for row in self.providers_model:
|
for row in self.providers_model:
|
||||||
row[DirectoryProvidersModel.C_WEIGHT] = pango.WEIGHT_NORMAL
|
row[DirectoryProvidersModel.C_WEIGHT] = Pango.Weight.NORMAL
|
||||||
|
|
||||||
if it:
|
if it:
|
||||||
self.providers_model.set_value(it, DirectoryProvidersModel.C_WEIGHT, pango.WEIGHT_BOLD)
|
self.providers_model.set_value(it, DirectoryProvidersModel.C_WEIGHT, Pango.Weight.BOLD)
|
||||||
provider = self.providers_model.get_value(it, DirectoryProvidersModel.C_PROVIDER)
|
provider = self.providers_model.get_value(it, DirectoryProvidersModel.C_PROVIDER)
|
||||||
self.use_provider(provider)
|
self.use_provider(provider)
|
||||||
|
|
||||||
|
@ -229,7 +230,8 @@ class gPodderPodcastDirectory(BuilderWidget):
|
||||||
|
|
||||||
def on_tv_providers_cursor_changed(self, treeview):
|
def on_tv_providers_cursor_changed(self, treeview):
|
||||||
path, column = treeview.get_cursor()
|
path, column = treeview.get_cursor()
|
||||||
self.on_tv_providers_row_activated(treeview, path, column)
|
if path is not None:
|
||||||
|
self.on_tv_providers_row_activated(treeview, path, column)
|
||||||
|
|
||||||
def obtain_podcasts_with(self, callback):
|
def obtain_podcasts_with(self, callback):
|
||||||
if self.podcasts_progress_indicator is not None:
|
if self.podcasts_progress_indicator is not None:
|
||||||
|
@ -260,15 +262,16 @@ class gPodderPodcastDirectory(BuilderWidget):
|
||||||
self.podcasts_progress_indicator.on_finished()
|
self.podcasts_progress_indicator.on_finished()
|
||||||
self.podcasts_progress_indicator = None
|
self.podcasts_progress_indicator = None
|
||||||
|
|
||||||
if original_provider != self.current_provider:
|
if original_provider == self.current_provider:
|
||||||
|
self.podcasts_model.load(podcasts or [])
|
||||||
|
else:
|
||||||
logger.warn('Ignoring update from old thread')
|
logger.warn('Ignoring update from old thread')
|
||||||
return
|
|
||||||
|
|
||||||
self.podcasts_model.load(podcasts or [])
|
|
||||||
self.en_query.set_sensitive(True)
|
self.en_query.set_sensitive(True)
|
||||||
self.bt_search.set_sensitive(True)
|
self.bt_search.set_sensitive(True)
|
||||||
self.tag_cloud.set_sensitive(True)
|
self.tag_cloud.set_sensitive(True)
|
||||||
self.en_query.grab_focus()
|
if self.en_query.get_realized():
|
||||||
|
self.en_query.grab_focus()
|
||||||
|
|
||||||
def on_bt_search_clicked(self, widget):
|
def on_bt_search_clicked(self, widget):
|
||||||
if self.current_provider is None:
|
if self.current_provider is None:
|
||||||
|
|
|
@ -17,10 +17,11 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
|
||||||
import gtk
|
from gi.repository import Gtk
|
||||||
import pango
|
from gi.repository import Gdk
|
||||||
|
from gi.repository import Pango
|
||||||
import cgi
|
import cgi
|
||||||
import urlparse
|
import urllib.parse
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -40,13 +41,13 @@ from gpodder.gtkui.interface.configeditor import gPodderConfigEditor
|
||||||
|
|
||||||
from gpodder.gtkui.desktopfile import PlayerListModel
|
from gpodder.gtkui.desktopfile import PlayerListModel
|
||||||
|
|
||||||
class NewEpisodeActionList(gtk.ListStore):
|
class NewEpisodeActionList(Gtk.ListStore):
|
||||||
C_CAPTION, C_AUTO_DOWNLOAD = range(2)
|
C_CAPTION, C_AUTO_DOWNLOAD = list(range(2))
|
||||||
|
|
||||||
ACTION_NONE, ACTION_ASK, ACTION_MINIMIZED, ACTION_ALWAYS = range(4)
|
ACTION_NONE, ACTION_ASK, ACTION_MINIMIZED, ACTION_ALWAYS = list(range(4))
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
gtk.ListStore.__init__(self, str, str)
|
Gtk.ListStore.__init__(self, str, str)
|
||||||
self._config = config
|
self._config = config
|
||||||
self.append((_('Do nothing'), 'ignore'))
|
self.append((_('Do nothing'), 'ignore'))
|
||||||
self.append((_('Show episode list'), 'show'))
|
self.append((_('Show episode list'), 'show'))
|
||||||
|
@ -63,11 +64,11 @@ class NewEpisodeActionList(gtk.ListStore):
|
||||||
def set_index(self, index):
|
def set_index(self, index):
|
||||||
self._config.auto_download = self[index][self.C_AUTO_DOWNLOAD]
|
self._config.auto_download = self[index][self.C_AUTO_DOWNLOAD]
|
||||||
|
|
||||||
class DeviceTypeActionList(gtk.ListStore):
|
class DeviceTypeActionList(Gtk.ListStore):
|
||||||
C_CAPTION, C_DEVICE_TYPE = range(2)
|
C_CAPTION, C_DEVICE_TYPE = list(range(2))
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
gtk.ListStore.__init__(self, str, str)
|
Gtk.ListStore.__init__(self, str, str)
|
||||||
self._config = config
|
self._config = config
|
||||||
self.append((_('None'), 'none'))
|
self.append((_('None'), 'none'))
|
||||||
self.append((_('iPod'), 'ipod'))
|
self.append((_('iPod'), 'ipod'))
|
||||||
|
@ -83,12 +84,12 @@ class DeviceTypeActionList(gtk.ListStore):
|
||||||
self._config.device_sync.device_type = self[index][self.C_DEVICE_TYPE]
|
self._config.device_sync.device_type = self[index][self.C_DEVICE_TYPE]
|
||||||
|
|
||||||
|
|
||||||
class OnSyncActionList(gtk.ListStore):
|
class OnSyncActionList(Gtk.ListStore):
|
||||||
C_CAPTION, C_ON_SYNC_DELETE, C_ON_SYNC_MARK_PLAYED = range(3)
|
C_CAPTION, C_ON_SYNC_DELETE, C_ON_SYNC_MARK_PLAYED = list(range(3))
|
||||||
ACTION_NONE, ACTION_ASK, ACTION_MINIMIZED, ACTION_ALWAYS = range(4)
|
ACTION_NONE, ACTION_ASK, ACTION_MINIMIZED, ACTION_ALWAYS = list(range(4))
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
gtk.ListStore.__init__(self, str, bool, bool)
|
Gtk.ListStore.__init__(self, str, bool, bool)
|
||||||
self._config = config
|
self._config = config
|
||||||
self.append((_('Do nothing'), False, False))
|
self.append((_('Do nothing'), False, False))
|
||||||
self.append((_('Mark as played'), False, True))
|
self.append((_('Mark as played'), False, True))
|
||||||
|
@ -111,11 +112,11 @@ class OnSyncActionList(gtk.ListStore):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class YouTubeVideoFormatListModel(gtk.ListStore):
|
class YouTubeVideoFormatListModel(Gtk.ListStore):
|
||||||
C_CAPTION, C_ID = range(2)
|
C_CAPTION, C_ID = list(range(2))
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
gtk.ListStore.__init__(self, str, int)
|
Gtk.ListStore.__init__(self, str, int)
|
||||||
self._config = config
|
self._config = config
|
||||||
self.custom_fmt_ids = self._config.youtube.preferred_fmt_ids
|
self.custom_fmt_ids = self._config.youtube.preferred_fmt_ids
|
||||||
|
|
||||||
|
@ -150,11 +151,11 @@ class YouTubeVideoFormatListModel(gtk.ListStore):
|
||||||
self._config.youtube.preferred_fmt_ids = self.custom_fmt_ids
|
self._config.youtube.preferred_fmt_ids = self.custom_fmt_ids
|
||||||
|
|
||||||
|
|
||||||
class VimeoVideoFormatListModel(gtk.ListStore):
|
class VimeoVideoFormatListModel(Gtk.ListStore):
|
||||||
C_CAPTION, C_ID = range(2)
|
C_CAPTION, C_ID = list(range(2))
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
gtk.ListStore.__init__(self, str, str)
|
Gtk.ListStore.__init__(self, str, str)
|
||||||
self._config = config
|
self._config = config
|
||||||
|
|
||||||
for fileformat, description in vimeo.FORMATS:
|
for fileformat, description in vimeo.FORMATS:
|
||||||
|
@ -168,20 +169,20 @@ class VimeoVideoFormatListModel(gtk.ListStore):
|
||||||
|
|
||||||
def set_index(self, index):
|
def set_index(self, index):
|
||||||
value = self[index][self.C_ID]
|
value = self[index][self.C_ID]
|
||||||
if value > 0:
|
if value is not None:
|
||||||
self._config.vimeo.fileformat = value
|
self._config.vimeo.fileformat = value
|
||||||
|
|
||||||
|
|
||||||
class gPodderPreferences(BuilderWidget):
|
class gPodderPreferences(BuilderWidget):
|
||||||
C_TOGGLE, C_LABEL, C_EXTENSION, C_SHOW_TOGGLE = range(4)
|
C_TOGGLE, C_LABEL, C_EXTENSION, C_SHOW_TOGGLE = list(range(4))
|
||||||
|
|
||||||
def new(self):
|
def new(self):
|
||||||
for cb in (self.combo_audio_player_app, self.combo_video_player_app):
|
for cb in (self.combo_audio_player_app, self.combo_video_player_app):
|
||||||
cellrenderer = gtk.CellRendererPixbuf()
|
cellrenderer = Gtk.CellRendererPixbuf()
|
||||||
cb.pack_start(cellrenderer, False)
|
cb.pack_start(cellrenderer, False)
|
||||||
cb.add_attribute(cellrenderer, 'pixbuf', PlayerListModel.C_ICON)
|
cb.add_attribute(cellrenderer, 'pixbuf', PlayerListModel.C_ICON)
|
||||||
cellrenderer = gtk.CellRendererText()
|
cellrenderer = Gtk.CellRendererText()
|
||||||
cellrenderer.set_property('ellipsize', pango.ELLIPSIZE_END)
|
cellrenderer.set_property('ellipsize', Pango.EllipsizeMode.END)
|
||||||
cb.pack_start(cellrenderer, True)
|
cb.pack_start(cellrenderer, True)
|
||||||
cb.add_attribute(cellrenderer, 'markup', PlayerListModel.C_NAME)
|
cb.add_attribute(cellrenderer, 'markup', PlayerListModel.C_NAME)
|
||||||
cb.set_row_separator_func(PlayerListModel.is_separator)
|
cb.set_row_separator_func(PlayerListModel.is_separator)
|
||||||
|
@ -198,14 +199,14 @@ class gPodderPreferences(BuilderWidget):
|
||||||
|
|
||||||
self.preferred_youtube_format_model = YouTubeVideoFormatListModel(self._config)
|
self.preferred_youtube_format_model = YouTubeVideoFormatListModel(self._config)
|
||||||
self.combobox_preferred_youtube_format.set_model(self.preferred_youtube_format_model)
|
self.combobox_preferred_youtube_format.set_model(self.preferred_youtube_format_model)
|
||||||
cellrenderer = gtk.CellRendererText()
|
cellrenderer = Gtk.CellRendererText()
|
||||||
self.combobox_preferred_youtube_format.pack_start(cellrenderer, True)
|
self.combobox_preferred_youtube_format.pack_start(cellrenderer, True)
|
||||||
self.combobox_preferred_youtube_format.add_attribute(cellrenderer, 'text', self.preferred_youtube_format_model.C_CAPTION)
|
self.combobox_preferred_youtube_format.add_attribute(cellrenderer, 'text', self.preferred_youtube_format_model.C_CAPTION)
|
||||||
self.combobox_preferred_youtube_format.set_active(self.preferred_youtube_format_model.get_index())
|
self.combobox_preferred_youtube_format.set_active(self.preferred_youtube_format_model.get_index())
|
||||||
|
|
||||||
self.preferred_vimeo_format_model = VimeoVideoFormatListModel(self._config)
|
self.preferred_vimeo_format_model = VimeoVideoFormatListModel(self._config)
|
||||||
self.combobox_preferred_vimeo_format.set_model(self.preferred_vimeo_format_model)
|
self.combobox_preferred_vimeo_format.set_model(self.preferred_vimeo_format_model)
|
||||||
cellrenderer = gtk.CellRendererText()
|
cellrenderer = Gtk.CellRendererText()
|
||||||
self.combobox_preferred_vimeo_format.pack_start(cellrenderer, True)
|
self.combobox_preferred_vimeo_format.pack_start(cellrenderer, True)
|
||||||
self.combobox_preferred_vimeo_format.add_attribute(cellrenderer, 'text', self.preferred_vimeo_format_model.C_CAPTION)
|
self.combobox_preferred_vimeo_format.add_attribute(cellrenderer, 'text', self.preferred_vimeo_format_model.C_CAPTION)
|
||||||
self.combobox_preferred_vimeo_format.set_active(self.preferred_vimeo_format_model.get_index())
|
self.combobox_preferred_vimeo_format.set_active(self.preferred_vimeo_format_model.get_index())
|
||||||
|
@ -217,7 +218,7 @@ class gPodderPreferences(BuilderWidget):
|
||||||
|
|
||||||
self.update_interval_presets = [0, 10, 30, 60, 2*60, 6*60, 12*60]
|
self.update_interval_presets = [0, 10, 30, 60, 2*60, 6*60, 12*60]
|
||||||
adjustment_update_interval = self.hscale_update_interval.get_adjustment()
|
adjustment_update_interval = self.hscale_update_interval.get_adjustment()
|
||||||
adjustment_update_interval.upper = len(self.update_interval_presets)-1
|
adjustment_update_interval.set_upper(len(self.update_interval_presets)-1)
|
||||||
if self._config.auto_update_frequency in self.update_interval_presets:
|
if self._config.auto_update_frequency in self.update_interval_presets:
|
||||||
index = self.update_interval_presets.index(self._config.auto_update_frequency)
|
index = self.update_interval_presets.index(self._config.auto_update_frequency)
|
||||||
self.hscale_update_interval.set_value(index)
|
self.hscale_update_interval.set_value(index)
|
||||||
|
@ -226,7 +227,7 @@ class gPodderPreferences(BuilderWidget):
|
||||||
self.update_interval_presets.append(self._config.auto_update_frequency)
|
self.update_interval_presets.append(self._config.auto_update_frequency)
|
||||||
self.update_interval_presets.sort()
|
self.update_interval_presets.sort()
|
||||||
|
|
||||||
adjustment_update_interval.upper = len(self.update_interval_presets)-1
|
adjustment_update_interval.set_upper(len(self.update_interval_presets)-1)
|
||||||
index = self.update_interval_presets.index(self._config.auto_update_frequency)
|
index = self.update_interval_presets.index(self._config.auto_update_frequency)
|
||||||
self.hscale_update_interval.set_value(index)
|
self.hscale_update_interval.set_value(index)
|
||||||
|
|
||||||
|
@ -234,7 +235,7 @@ class gPodderPreferences(BuilderWidget):
|
||||||
|
|
||||||
self.auto_download_model = NewEpisodeActionList(self._config)
|
self.auto_download_model = NewEpisodeActionList(self._config)
|
||||||
self.combo_auto_download.set_model(self.auto_download_model)
|
self.combo_auto_download.set_model(self.auto_download_model)
|
||||||
cellrenderer = gtk.CellRendererText()
|
cellrenderer = Gtk.CellRendererText()
|
||||||
self.combo_auto_download.pack_start(cellrenderer, True)
|
self.combo_auto_download.pack_start(cellrenderer, True)
|
||||||
self.combo_auto_download.add_attribute(cellrenderer, 'text', NewEpisodeActionList.C_CAPTION)
|
self.combo_auto_download.add_attribute(cellrenderer, 'text', NewEpisodeActionList.C_CAPTION)
|
||||||
self.combo_auto_download.set_active(self.auto_download_model.get_index())
|
self.combo_auto_download.set_active(self.auto_download_model.get_index())
|
||||||
|
@ -243,7 +244,7 @@ class gPodderPreferences(BuilderWidget):
|
||||||
adjustment_expiration = self.hscale_expiration.get_adjustment()
|
adjustment_expiration = self.hscale_expiration.get_adjustment()
|
||||||
if self._config.episode_old_age > adjustment_expiration.get_upper():
|
if self._config.episode_old_age > adjustment_expiration.get_upper():
|
||||||
# Patch the adjustment to include the higher current value
|
# Patch the adjustment to include the higher current value
|
||||||
adjustment_expiration.upper = self._config.episode_old_age
|
adjustment_expiration.set_upper(self._config.episode_old_age)
|
||||||
|
|
||||||
self.hscale_expiration.set_value(self._config.episode_old_age)
|
self.hscale_expiration.set_value(self._config.episode_old_age)
|
||||||
else:
|
else:
|
||||||
|
@ -256,7 +257,7 @@ class gPodderPreferences(BuilderWidget):
|
||||||
|
|
||||||
self.device_type_model = DeviceTypeActionList(self._config)
|
self.device_type_model = DeviceTypeActionList(self._config)
|
||||||
self.combobox_device_type.set_model(self.device_type_model)
|
self.combobox_device_type.set_model(self.device_type_model)
|
||||||
cellrenderer = gtk.CellRendererText()
|
cellrenderer = Gtk.CellRendererText()
|
||||||
self.combobox_device_type.pack_start(cellrenderer, True)
|
self.combobox_device_type.pack_start(cellrenderer, True)
|
||||||
self.combobox_device_type.add_attribute(cellrenderer, 'text',
|
self.combobox_device_type.add_attribute(cellrenderer, 'text',
|
||||||
DeviceTypeActionList.C_CAPTION)
|
DeviceTypeActionList.C_CAPTION)
|
||||||
|
@ -264,7 +265,7 @@ class gPodderPreferences(BuilderWidget):
|
||||||
|
|
||||||
self.on_sync_model = OnSyncActionList(self._config)
|
self.on_sync_model = OnSyncActionList(self._config)
|
||||||
self.combobox_on_sync.set_model(self.on_sync_model)
|
self.combobox_on_sync.set_model(self.on_sync_model)
|
||||||
cellrenderer = gtk.CellRendererText()
|
cellrenderer = Gtk.CellRendererText()
|
||||||
self.combobox_on_sync.pack_start(cellrenderer, True)
|
self.combobox_on_sync.pack_start(cellrenderer, True)
|
||||||
self.combobox_on_sync.add_attribute(cellrenderer, 'text', OnSyncActionList.C_CAPTION)
|
self.combobox_on_sync.add_attribute(cellrenderer, 'text', OnSyncActionList.C_CAPTION)
|
||||||
self.combobox_on_sync.set_active(self.on_sync_model.get_index())
|
self.combobox_on_sync.set_active(self.on_sync_model.get_index())
|
||||||
|
@ -292,12 +293,16 @@ class gPodderPreferences(BuilderWidget):
|
||||||
|
|
||||||
# Configure the extensions manager GUI
|
# Configure the extensions manager GUI
|
||||||
self.set_extension_preferences()
|
self.set_extension_preferences()
|
||||||
|
self.main_window.show()
|
||||||
|
|
||||||
|
def _extensions_select_function(self, selection, model, path, path_currently_selected):
|
||||||
|
return model.get_value(model.get_iter(path), self.C_SHOW_TOGGLE)
|
||||||
|
|
||||||
def set_extension_preferences(self):
|
def set_extension_preferences(self):
|
||||||
def search_equal_func(model, column, key, it):
|
def search_equal_func(model, column, key, it):
|
||||||
label = model.get_value(it, self.C_LABEL)
|
label = model.get_value(it, self.C_LABEL)
|
||||||
if key.lower() in label.lower():
|
if key.lower() in label.lower():
|
||||||
# from http://www.pygtk.org/docs/pygtk/class-gtktreeview.html:
|
# from http://www.pyGtk.org/docs/pygtk/class-gtktreeview.html:
|
||||||
# "func should return False to indicate that the row matches
|
# "func should return False to indicate that the row matches
|
||||||
# the search criteria."
|
# the search criteria."
|
||||||
return False
|
return False
|
||||||
|
@ -305,24 +310,27 @@ class gPodderPreferences(BuilderWidget):
|
||||||
return True
|
return True
|
||||||
self.treeviewExtensions.set_search_equal_func(search_equal_func)
|
self.treeviewExtensions.set_search_equal_func(search_equal_func)
|
||||||
|
|
||||||
toggle_cell = gtk.CellRendererToggle()
|
selection = self.treeviewExtensions.get_selection()
|
||||||
|
selection.set_select_function(self._extensions_select_function)
|
||||||
|
|
||||||
|
toggle_cell = Gtk.CellRendererToggle()
|
||||||
toggle_cell.connect('toggled', self.on_extensions_cell_toggled)
|
toggle_cell.connect('toggled', self.on_extensions_cell_toggled)
|
||||||
toggle_column = gtk.TreeViewColumn('')
|
toggle_column = Gtk.TreeViewColumn('')
|
||||||
toggle_column.pack_start(toggle_cell, True)
|
toggle_column.pack_start(toggle_cell, True)
|
||||||
toggle_column.add_attribute(toggle_cell, 'active', self.C_TOGGLE)
|
toggle_column.add_attribute(toggle_cell, 'active', self.C_TOGGLE)
|
||||||
toggle_column.add_attribute(toggle_cell, 'visible', self.C_SHOW_TOGGLE)
|
toggle_column.add_attribute(toggle_cell, 'visible', self.C_SHOW_TOGGLE)
|
||||||
toggle_column.set_property('min-width', 32)
|
toggle_column.set_property('min-width', 32)
|
||||||
self.treeviewExtensions.append_column(toggle_column)
|
self.treeviewExtensions.append_column(toggle_column)
|
||||||
|
|
||||||
name_cell = gtk.CellRendererText()
|
name_cell = Gtk.CellRendererText()
|
||||||
name_cell.set_property('ellipsize', pango.ELLIPSIZE_END)
|
name_cell.set_property('ellipsize', Pango.EllipsizeMode.END)
|
||||||
extension_column = gtk.TreeViewColumn(_('Name'))
|
extension_column = Gtk.TreeViewColumn(_('Name'))
|
||||||
extension_column.pack_start(name_cell, True)
|
extension_column.pack_start(name_cell, True)
|
||||||
extension_column.add_attribute(name_cell, 'markup', self.C_LABEL)
|
extension_column.add_attribute(name_cell, 'markup', self.C_LABEL)
|
||||||
extension_column.set_expand(True)
|
extension_column.set_expand(True)
|
||||||
self.treeviewExtensions.append_column(extension_column)
|
self.treeviewExtensions.append_column(extension_column)
|
||||||
|
|
||||||
self.extensions_model = gtk.ListStore(bool, str, object, bool)
|
self.extensions_model = Gtk.ListStore(bool, str, object, bool)
|
||||||
|
|
||||||
def key_func(pair):
|
def key_func(pair):
|
||||||
category, container = pair
|
category, container = pair
|
||||||
|
@ -351,7 +359,7 @@ class gPodderPreferences(BuilderWidget):
|
||||||
if event.window != treeview.get_bin_window():
|
if event.window != treeview.get_bin_window():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if event.type == gtk.gdk.BUTTON_RELEASE and event.button == 3:
|
if event.type == Gdk.EventType.BUTTON_RELEASE and event.button == 3:
|
||||||
return self.on_treeview_extension_show_context_menu(treeview, event)
|
return self.on_treeview_extension_show_context_menu(treeview, event)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
@ -364,29 +372,30 @@ class gPodderPreferences(BuilderWidget):
|
||||||
if not container:
|
if not container:
|
||||||
return
|
return
|
||||||
|
|
||||||
menu = gtk.Menu()
|
menu = Gtk.Menu()
|
||||||
|
|
||||||
if container.metadata.doc:
|
if container.metadata.doc:
|
||||||
menu_item = gtk.MenuItem(_('Documentation'))
|
menu_item = Gtk.MenuItem(_('Documentation'))
|
||||||
menu_item.connect('activate', self.open_weblink,
|
menu_item.connect('activate', self.open_weblink,
|
||||||
container.metadata.doc)
|
container.metadata.doc)
|
||||||
menu.append(menu_item)
|
menu.append(menu_item)
|
||||||
|
|
||||||
menu_item = gtk.MenuItem(_('Extension info'))
|
menu_item = Gtk.MenuItem(_('Extension info'))
|
||||||
menu_item.connect('activate', self.show_extension_info, model, container)
|
menu_item.connect('activate', self.show_extension_info, model, container)
|
||||||
menu.append(menu_item)
|
menu.append(menu_item)
|
||||||
|
|
||||||
if container.metadata.payment:
|
if container.metadata.payment:
|
||||||
menu_item = gtk.MenuItem(_('Support the author'))
|
menu_item = Gtk.MenuItem(_('Support the author'))
|
||||||
menu_item.connect('activate', self.open_weblink, container.metadata.payment)
|
menu_item.connect('activate', self.open_weblink, container.metadata.payment)
|
||||||
menu.append(menu_item)
|
menu.append(menu_item)
|
||||||
|
|
||||||
menu.show_all()
|
menu.show_all()
|
||||||
if event is None:
|
if event is None:
|
||||||
func = TreeViewHelper.make_popup_position_func(treeview)
|
func = TreeViewHelper.make_popup_position_func(treeview)
|
||||||
menu.popup(None, None, func, 3, 0)
|
menu.popup(None, None, func, None, 3, Gtk.get_current_event_time())
|
||||||
else:
|
else:
|
||||||
menu.popup(None, None, None, 3, 0)
|
menu.popup(None, None, None, None, 3, Gtk.get_current_event_time())
|
||||||
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -414,7 +423,11 @@ class gPodderPreferences(BuilderWidget):
|
||||||
else:
|
else:
|
||||||
self.on_extension_disabled(container.module)
|
self.on_extension_disabled(container.module)
|
||||||
elif container.error is not None:
|
elif container.error is not None:
|
||||||
self.show_message(container.error.message,
|
if hasattr(container.error, 'message'):
|
||||||
|
error_msg = container.error.message
|
||||||
|
else:
|
||||||
|
error_msg = str(container.error)
|
||||||
|
self.show_message(error_msg,
|
||||||
_('Extension cannot be activated'), important=True)
|
_('Extension cannot be activated'), important=True)
|
||||||
model.set_value(it, self.C_TOGGLE, False)
|
model.set_value(it, self.C_TOGGLE, False)
|
||||||
|
|
||||||
|
@ -425,7 +438,7 @@ class gPodderPreferences(BuilderWidget):
|
||||||
# This is one ugly hack, but it displays the attributes of
|
# This is one ugly hack, but it displays the attributes of
|
||||||
# the metadata object of the container..
|
# the metadata object of the container..
|
||||||
info = '\n'.join('<b>%s:</b> %s' %
|
info = '\n'.join('<b>%s:</b> %s' %
|
||||||
tuple(map(cgi.escape, map(str, (key, value))))
|
tuple(map(cgi.escape, list(map(str, (key, value)))))
|
||||||
for key, value in container.metadata.get_sorted())
|
for key, value in container.metadata.get_sorted())
|
||||||
|
|
||||||
self.show_message(info, _('Extension module info'), important=True)
|
self.show_message(info, _('Extension module info'), important=True)
|
||||||
|
@ -486,12 +499,19 @@ class gPodderPreferences(BuilderWidget):
|
||||||
|
|
||||||
def format_update_interval_value(self, scale, value):
|
def format_update_interval_value(self, scale, value):
|
||||||
value = int(value)
|
value = int(value)
|
||||||
|
ret = None
|
||||||
if value == 0:
|
if value == 0:
|
||||||
return _('manually')
|
ret = _('manually')
|
||||||
elif value > 0 and len(self.update_interval_presets) > value:
|
elif value > 0 and len(self.update_interval_presets) > value:
|
||||||
return util.format_seconds_to_hour_min_sec(self.update_interval_presets[value]*60)
|
ret = util.format_seconds_to_hour_min_sec(self.update_interval_presets[value]*60)
|
||||||
else:
|
else:
|
||||||
return str(value)
|
ret = str(value)
|
||||||
|
# bug in gtk3: value representation (pixels) must be smaller than value for highest value.
|
||||||
|
# this makes sense when formatting e.g. 0 to 1000 where '1000' is the longest
|
||||||
|
# string, but not when '10 minutes' is longer than '12 hours'
|
||||||
|
# so we replace spaces with non breaking spaces otherwise '10 minutes' is displayed as '10'
|
||||||
|
ret = ret.replace(' ', '\xa0')
|
||||||
|
return ret
|
||||||
|
|
||||||
def on_update_interval_value_changed(self, range):
|
def on_update_interval_value_changed(self, range):
|
||||||
value = int(range.get_value())
|
value = int(range.get_value())
|
||||||
|
@ -626,12 +646,12 @@ class gPodderPreferences(BuilderWidget):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def on_btn_device_mountpoint_clicked(self, widget):
|
def on_btn_device_mountpoint_clicked(self, widget):
|
||||||
fs = gtk.FileChooserDialog(title=_('Select folder for mount point'),
|
fs = Gtk.FileChooserDialog(title=_('Select folder for mount point'),
|
||||||
action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
|
action=Gtk.FileChooserAction.SELECT_FOLDER)
|
||||||
fs.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
|
fs.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
|
||||||
fs.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
|
fs.add_button(Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
|
||||||
fs.set_current_folder(self.btn_filesystemMountpoint.get_label())
|
fs.set_current_folder(self.btn_filesystemMountpoint.get_label())
|
||||||
if fs.run() == gtk.RESPONSE_OK:
|
if fs.run() == Gtk.ResponseType.OK:
|
||||||
filename = fs.get_filename()
|
filename = fs.get_filename()
|
||||||
if self._config.device_sync.device_type == 'filesystem':
|
if self._config.device_sync.device_type == 'filesystem':
|
||||||
self._config.device_sync.device_folder = filename
|
self._config.device_sync.device_folder = filename
|
||||||
|
@ -643,12 +663,12 @@ class gPodderPreferences(BuilderWidget):
|
||||||
fs.destroy()
|
fs.destroy()
|
||||||
|
|
||||||
def on_btn_playlist_folder_clicked(self, widget):
|
def on_btn_playlist_folder_clicked(self, widget):
|
||||||
fs = gtk.FileChooserDialog(title=_('Select folder for playlists'),
|
fs = Gtk.FileChooserDialog(title=_('Select folder for playlists'),
|
||||||
action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
|
action=Gtk.FileChooserAction.SELECT_FOLDER)
|
||||||
fs.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
|
fs.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
|
||||||
fs.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
|
fs.add_button(Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
|
||||||
fs.set_current_folder(self.btn_playlistfolder.get_label())
|
fs.set_current_folder(self.btn_playlistfolder.get_label())
|
||||||
if fs.run() == gtk.RESPONSE_OK:
|
if fs.run() == Gtk.ResponseType.OK:
|
||||||
filename = util.relpath(self._config.device_sync.device_folder,
|
filename = util.relpath(self._config.device_sync.device_folder,
|
||||||
fs.get_filename())
|
fs.get_filename())
|
||||||
if self._config.device_sync.device_type == 'filesystem':
|
if self._config.device_sync.device_type == 'filesystem':
|
||||||
|
|
|
@ -37,7 +37,7 @@ logger = logging.getLogger(__name__)
|
||||||
class gPodderSyncUI(object):
|
class gPodderSyncUI(object):
|
||||||
def __init__(self, config, notification, parent_window,
|
def __init__(self, config, notification, parent_window,
|
||||||
show_confirmation,
|
show_confirmation,
|
||||||
preferences_widget,
|
show_preferences,
|
||||||
channels,
|
channels,
|
||||||
download_status_model,
|
download_status_model,
|
||||||
download_queue_manager,
|
download_queue_manager,
|
||||||
|
@ -51,7 +51,7 @@ class gPodderSyncUI(object):
|
||||||
self.parent_window = parent_window
|
self.parent_window = parent_window
|
||||||
self.show_confirmation = show_confirmation
|
self.show_confirmation = show_confirmation
|
||||||
|
|
||||||
self.preferences_widget = preferences_widget
|
self.show_preferences = show_preferences
|
||||||
self.channels=channels
|
self.channels=channels
|
||||||
self.download_status_model = download_status_model
|
self.download_status_model = download_status_model
|
||||||
self.download_queue_manager = download_queue_manager
|
self.download_queue_manager = download_queue_manager
|
||||||
|
@ -81,12 +81,13 @@ class gPodderSyncUI(object):
|
||||||
def _show_message_unconfigured(self):
|
def _show_message_unconfigured(self):
|
||||||
title = _('No device configured')
|
title = _('No device configured')
|
||||||
message = _('Please set up your device in the preferences dialog.')
|
message = _('Please set up your device in the preferences dialog.')
|
||||||
self.notification(message, title, widget=self.preferences_widget, important=True)
|
if self.show_confirmation(message, title):
|
||||||
|
self.show_preferences(self.parent_window, None)
|
||||||
|
|
||||||
def _show_message_cannot_open(self):
|
def _show_message_cannot_open(self):
|
||||||
title = _('Cannot open device')
|
title = _('Cannot open device')
|
||||||
message = _('Please check the settings in the preferences dialog.')
|
message = _('Please check the settings in the preferences dialog.')
|
||||||
self.notification(message, title, widget=self.preferences_widget, important=True)
|
self.notification(message, title, important=True)
|
||||||
|
|
||||||
def on_synchronize_episodes(self, channels, episodes=None, force_played=True):
|
def on_synchronize_episodes(self, channels, episodes=None, force_played=True):
|
||||||
device = sync.open_device(self)
|
device = sync.open_device(self)
|
||||||
|
@ -185,7 +186,7 @@ class gPodderSyncUI(object):
|
||||||
key=lambda ep: ep.published)
|
key=lambda ep: ep.published)
|
||||||
#don't add played episodes to playlist if skip_played_episodes is True
|
#don't add played episodes to playlist if skip_played_episodes is True
|
||||||
if self._config.device_sync.skip_played_episodes:
|
if self._config.device_sync.skip_played_episodes:
|
||||||
episodes_for_playlist=filter(lambda ep: ep.is_new, episodes_for_playlist)
|
episodes_for_playlist=[ep for ep in episodes_for_playlist if ep.is_new]
|
||||||
playlist.write_m3u(episodes_for_playlist)
|
playlist.write_m3u(episodes_for_playlist)
|
||||||
|
|
||||||
#enable updating of UI
|
#enable updating of UI
|
||||||
|
@ -194,7 +195,7 @@ class gPodderSyncUI(object):
|
||||||
if (self._config.device_sync.device_type=='filesystem' and self._config.device_sync.playlists.create):
|
if (self._config.device_sync.device_type=='filesystem' and self._config.device_sync.playlists.create):
|
||||||
title = _('Update successful')
|
title = _('Update successful')
|
||||||
message = _('The playlist on your MP3 player has been updated.')
|
message = _('The playlist on your MP3 player has been updated.')
|
||||||
self.notification(message, title, widget=self.preferences_widget)
|
self.notification(message, title)
|
||||||
|
|
||||||
# Finally start the synchronization process
|
# Finally start the synchronization process
|
||||||
@util.run_in_background
|
@util.run_in_background
|
||||||
|
@ -216,10 +217,10 @@ class gPodderSyncUI(object):
|
||||||
#get episodes to be written to playlist
|
#get episodes to be written to playlist
|
||||||
episodes_for_playlist=sorted(current_channel.get_episodes(gpodder.STATE_DOWNLOADED),
|
episodes_for_playlist=sorted(current_channel.get_episodes(gpodder.STATE_DOWNLOADED),
|
||||||
key=lambda ep: ep.published)
|
key=lambda ep: ep.published)
|
||||||
episode_keys=map(playlist.get_absolute_filename_for_playlist,
|
episode_keys=list(map(playlist.get_absolute_filename_for_playlist,
|
||||||
episodes_for_playlist)
|
episodes_for_playlist))
|
||||||
|
|
||||||
episode_dict=dict(zip(episode_keys, episodes_for_playlist))
|
episode_dict=dict(list(zip(episode_keys, episodes_for_playlist)))
|
||||||
|
|
||||||
#then get episodes in playlist (if it exists) already on device
|
#then get episodes in playlist (if it exists) already on device
|
||||||
episodes_in_playlists = playlist.read_m3u()
|
episodes_in_playlists = playlist.read_m3u()
|
||||||
|
@ -233,7 +234,7 @@ class gPodderSyncUI(object):
|
||||||
#i.e. must have been deleted by user, so delete from gpodder
|
#i.e. must have been deleted by user, so delete from gpodder
|
||||||
try:
|
try:
|
||||||
episodes_to_delete.append(episode_dict[episode_filename])
|
episodes_to_delete.append(episode_dict[episode_filename])
|
||||||
except KeyError, ioe:
|
except KeyError as ioe:
|
||||||
logger.warn('Episode %s, removed from device has already been deleted from gpodder',
|
logger.warn('Episode %s, removed from device has already been deleted from gpodder',
|
||||||
episode_filename)
|
episode_filename)
|
||||||
|
|
||||||
|
@ -277,10 +278,10 @@ class gPodderSyncUI(object):
|
||||||
logger.warning("Starting sync - no episodes to delete")
|
logger.warning("Starting sync - no episodes to delete")
|
||||||
resume_sync([],[],None)
|
resume_sync([],[],None)
|
||||||
|
|
||||||
except IOError, ioe:
|
except IOError as ioe:
|
||||||
title = _('Error writing playlist files')
|
title = _('Error writing playlist files')
|
||||||
message = _(str(ioe))
|
message = _(str(ioe))
|
||||||
self.notification(message, title, widget=self.preferences_widget)
|
self.notification(message, title)
|
||||||
else:
|
else:
|
||||||
logger.info ('Not creating playlists - starting sync')
|
logger.info ('Not creating playlists - starting sync')
|
||||||
resume_sync([],[],None)
|
resume_sync([],[],None)
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
|
||||||
import gtk
|
from gi.repository import Gtk
|
||||||
|
|
||||||
import gpodder
|
import gpodder
|
||||||
|
|
||||||
|
@ -31,12 +31,12 @@ class gPodderWelcome(BuilderWidget):
|
||||||
def new(self):
|
def new(self):
|
||||||
for widget in self.vbox_buttons.get_children():
|
for widget in self.vbox_buttons.get_children():
|
||||||
for child in widget.get_children():
|
for child in widget.get_children():
|
||||||
if isinstance(child, gtk.Alignment):
|
if isinstance(child, Gtk.Alignment):
|
||||||
child.set_padding(self.PADDING, self.PADDING,
|
child.set_padding(self.PADDING, self.PADDING,
|
||||||
self.PADDING, self.PADDING)
|
self.PADDING, self.PADDING)
|
||||||
else:
|
else:
|
||||||
child.set_padding(self.PADDING, self.PADDING)
|
child.set_padding(self.PADDING, self.PADDING)
|
||||||
|
|
||||||
def on_btnCancel_clicked(self, button):
|
def on_btnCancel_clicked(self, button):
|
||||||
self.main_window.response(gtk.RESPONSE_CANCEL)
|
self.main_window.response(Gtk.ResponseType.CANCEL)
|
||||||
|
|
||||||
|
|
|
@ -30,11 +30,11 @@ import os
|
||||||
import os.path
|
import os.path
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from ConfigParser import RawConfigParser
|
from configparser import RawConfigParser
|
||||||
|
|
||||||
import gobject
|
from gi.repository import GObject
|
||||||
import gtk
|
from gi.repository import GdkPixbuf
|
||||||
import gtk.gdk
|
from gi.repository import Gtk
|
||||||
|
|
||||||
import gpodder
|
import gpodder
|
||||||
|
|
||||||
|
@ -49,11 +49,11 @@ userappsdirs = [ '/usr/share/applications/', '/usr/local/share/applications/', '
|
||||||
# the name of the section in the .desktop files
|
# the name of the section in the .desktop files
|
||||||
sect = 'Desktop Entry'
|
sect = 'Desktop Entry'
|
||||||
|
|
||||||
class PlayerListModel(gtk.ListStore):
|
class PlayerListModel(Gtk.ListStore):
|
||||||
C_ICON, C_NAME, C_COMMAND, C_CUSTOM = range(4)
|
C_ICON, C_NAME, C_COMMAND, C_CUSTOM = list(range(4))
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
gtk.ListStore.__init__(self, gtk.gdk.Pixbuf, str, str, bool)
|
Gtk.ListStore.__init__(self, GdkPixbuf.Pixbuf, str, str, bool)
|
||||||
|
|
||||||
def insert_app(self, pixbuf, name, command):
|
def insert_app(self, pixbuf, name, command):
|
||||||
self.append((pixbuf, name, command, False))
|
self.append((pixbuf, name, command, False))
|
||||||
|
@ -92,13 +92,13 @@ class UserApplication(object):
|
||||||
# Load it from an absolute filename
|
# Load it from an absolute filename
|
||||||
if os.path.exists(self.icon):
|
if os.path.exists(self.icon):
|
||||||
try:
|
try:
|
||||||
return gtk.gdk.pixbuf_new_from_file_at_size(self.icon, 24, 24)
|
return GdkPixbuf.Pixbuf.new_from_file_at_size(self.icon, 24, 24)
|
||||||
except gobject.GError, ge:
|
except GObject.GError as ge:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Load it from the current icon theme
|
# Load it from the current icon theme
|
||||||
(icon_name, extension) = os.path.splitext(os.path.basename(self.icon))
|
(icon_name, extension) = os.path.splitext(os.path.basename(self.icon))
|
||||||
theme = gtk.IconTheme()
|
theme = Gtk.IconTheme()
|
||||||
if theme.has_icon(icon_name):
|
if theme.has_icon(icon_name):
|
||||||
return theme.load_icon(icon_name, 24, 0)
|
return theme.load_icon(icon_name, 24, 0)
|
||||||
|
|
||||||
|
@ -116,23 +116,23 @@ WIN32_APP_REG_KEYS = [
|
||||||
|
|
||||||
|
|
||||||
def win32_read_registry_key(path):
|
def win32_read_registry_key(path):
|
||||||
import _winreg
|
import winreg
|
||||||
|
|
||||||
rootmap = {
|
rootmap = {
|
||||||
'HKEY_CLASSES_ROOT': _winreg.HKEY_CLASSES_ROOT,
|
'HKEY_CLASSES_ROOT': winreg.HKEY_CLASSES_ROOT,
|
||||||
}
|
}
|
||||||
|
|
||||||
parts = path.split('\\')
|
parts = path.split('\\')
|
||||||
root = parts.pop(0)
|
root = parts.pop(0)
|
||||||
key = _winreg.OpenKey(rootmap[root], parts.pop(0))
|
key = winreg.OpenKey(rootmap[root], parts.pop(0))
|
||||||
|
|
||||||
while parts:
|
while parts:
|
||||||
key = _winreg.OpenKey(key, parts.pop(0))
|
key = winreg.OpenKey(key, parts.pop(0))
|
||||||
|
|
||||||
value, type_ = _winreg.QueryValueEx(key, '')
|
value, type_ = winreg.QueryValueEx(key, '')
|
||||||
if type_ == _winreg.REG_EXPAND_SZ:
|
if type_ == winreg.REG_EXPAND_SZ:
|
||||||
cmdline = re.sub(r'%([^%]+)%', lambda m: os.environ[m.group(1)], value)
|
cmdline = re.sub(r'%([^%]+)%', lambda m: os.environ[m.group(1)], value)
|
||||||
elif type_ == _winreg.REG_SZ:
|
elif type_ == winreg.REG_SZ:
|
||||||
cmdline = value
|
cmdline = value
|
||||||
else:
|
else:
|
||||||
raise ValueError('Not a string: ' + path)
|
raise ValueError('Not a string: ' + path)
|
||||||
|
@ -151,7 +151,7 @@ class UserAppsReader(object):
|
||||||
self.__has_read = False
|
self.__has_read = False
|
||||||
self.__finished = threading.Event()
|
self.__finished = threading.Event()
|
||||||
self.__has_sep = False
|
self.__has_sep = False
|
||||||
self.apps.append(UserApplication(_('Default application'), 'default', ';'.join((mime+'/*' for mime in self.mimetypes)), gtk.STOCK_OPEN))
|
self.apps.append(UserApplication(_('Default application'), 'default', ';'.join((mime+'/*' for mime in self.mimetypes)), Gtk.STOCK_OPEN))
|
||||||
|
|
||||||
def add_separator(self):
|
def add_separator(self):
|
||||||
self.apps.append(UserApplication('', '', ';'.join((mime+'/*' for mime in self.mimetypes)), ''))
|
self.apps.append(UserApplication('', '', ';'.join((mime+'/*' for mime in self.mimetypes)), ''))
|
||||||
|
@ -163,7 +163,7 @@ class UserAppsReader(object):
|
||||||
|
|
||||||
self.__has_read = True
|
self.__has_read = True
|
||||||
if gpodder.ui.win32:
|
if gpodder.ui.win32:
|
||||||
import _winreg
|
import winreg
|
||||||
for caption, types, hkey in WIN32_APP_REG_KEYS:
|
for caption, types, hkey in WIN32_APP_REG_KEYS:
|
||||||
try:
|
try:
|
||||||
cmdline = win32_read_registry_key(hkey)
|
cmdline = win32_read_registry_key(hkey)
|
||||||
|
|
|
@ -23,35 +23,40 @@
|
||||||
# Based on code from gpodder.services (thp, 2007-08-24)
|
# Based on code from gpodder.services (thp, 2007-08-24)
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import gpodder
|
import gpodder
|
||||||
|
|
||||||
from gpodder import util
|
from gpodder import util
|
||||||
from gpodder import download
|
from gpodder import download
|
||||||
|
|
||||||
import gtk
|
from gi.repository import Gtk
|
||||||
import cgi
|
import cgi
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
|
import threading
|
||||||
|
|
||||||
_ = gpodder.gettext
|
_ = gpodder.gettext
|
||||||
|
|
||||||
|
|
||||||
class DownloadStatusModel(gtk.ListStore):
|
class DownloadStatusModel(Gtk.ListStore):
|
||||||
# Symbolic names for our columns, so we know what we're up to
|
# Symbolic names for our columns, so we know what we're up to
|
||||||
C_TASK, C_NAME, C_URL, C_PROGRESS, C_PROGRESS_TEXT, C_ICON_NAME = range(6)
|
C_TASK, C_NAME, C_URL, C_PROGRESS, C_PROGRESS_TEXT, C_ICON_NAME = list(range(6))
|
||||||
|
|
||||||
SEARCH_COLUMNS = (C_NAME, C_URL)
|
SEARCH_COLUMNS = (C_NAME, C_URL)
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
gtk.ListStore.__init__(self, object, str, str, int, str, str)
|
Gtk.ListStore.__init__(self, object, str, str, int, str, str)
|
||||||
|
|
||||||
|
self.set_downloading_access = threading.RLock()
|
||||||
|
|
||||||
# Set up stock icon IDs for tasks
|
# Set up stock icon IDs for tasks
|
||||||
self._status_ids = collections.defaultdict(lambda: None)
|
self._status_ids = collections.defaultdict(lambda: None)
|
||||||
self._status_ids[download.DownloadTask.DOWNLOADING] = gtk.STOCK_GO_DOWN
|
self._status_ids[download.DownloadTask.DOWNLOADING] = 'go-down'
|
||||||
self._status_ids[download.DownloadTask.DONE] = gtk.STOCK_APPLY
|
self._status_ids[download.DownloadTask.DONE] = Gtk.STOCK_APPLY
|
||||||
self._status_ids[download.DownloadTask.FAILED] = gtk.STOCK_STOP
|
self._status_ids[download.DownloadTask.FAILED] = 'dialog-error'
|
||||||
self._status_ids[download.DownloadTask.CANCELLED] = gtk.STOCK_CANCEL
|
self._status_ids[download.DownloadTask.CANCELLED] = 'media-playback-stop'
|
||||||
self._status_ids[download.DownloadTask.PAUSED] = gtk.STOCK_MEDIA_PAUSE
|
self._status_ids[download.DownloadTask.PAUSED] = 'media-playback-pause'
|
||||||
|
|
||||||
def _format_message(self, episode, message, podcast):
|
def _format_message(self, episode, message, podcast):
|
||||||
episode = cgi.escape(episode)
|
episode = cgi.escape(episode)
|
||||||
|
@ -138,6 +143,27 @@ class DownloadStatusModel(gtk.ListStore):
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def has_work(self):
|
||||||
|
return any(task for task in
|
||||||
|
(row[DownloadStatusModel.C_TASK] for row in self)
|
||||||
|
if task.status == task.QUEUED)
|
||||||
|
|
||||||
|
def get_next(self):
|
||||||
|
with self.set_downloading_access:
|
||||||
|
result = next(task for task in
|
||||||
|
(row[DownloadStatusModel.C_TASK] for row in self)
|
||||||
|
if task.status == task.QUEUED)
|
||||||
|
self.set_downloading(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def set_downloading(self, task):
|
||||||
|
with self.set_downloading_access:
|
||||||
|
if task.status is task.DOWNLOADING:
|
||||||
|
# Task was already set as DOWNLOADING by get_next
|
||||||
|
return False
|
||||||
|
task.status = task.DOWNLOADING
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class DownloadTaskMonitor(object):
|
class DownloadTaskMonitor(object):
|
||||||
"""A helper class that abstracts download events"""
|
"""A helper class that abstracts download events"""
|
||||||
|
|
|
@ -25,11 +25,18 @@
|
||||||
|
|
||||||
import gpodder
|
import gpodder
|
||||||
|
|
||||||
import gtk
|
import gi
|
||||||
import pango
|
gi.require_version('PangoCairo', '1.0')
|
||||||
import pangocairo
|
|
||||||
|
from gi.repository import Gtk
|
||||||
|
from gi.repository import Gdk
|
||||||
|
from gi.repository import GdkPixbuf
|
||||||
|
from gi.repository import Pango
|
||||||
|
from gi.repository import PangoCairo
|
||||||
|
|
||||||
import cairo
|
import cairo
|
||||||
import StringIO
|
|
||||||
|
import io
|
||||||
import math
|
import math
|
||||||
|
|
||||||
|
|
||||||
|
@ -87,25 +94,25 @@ def rounded_rectangle(ctx, x, y, width, height, radius=4.):
|
||||||
|
|
||||||
|
|
||||||
def draw_text_box_centered(ctx, widget, w_width, w_height, text, font_desc=None, add_progress=None):
|
def draw_text_box_centered(ctx, widget, w_width, w_height, text, font_desc=None, add_progress=None):
|
||||||
style = widget.rc_get_style()
|
style_context = widget.get_style_context()
|
||||||
text_color = style.text[gtk.STATE_PRELIGHT]
|
text_color = style_context.get_color(Gtk.StateFlags.PRELIGHT)
|
||||||
red, green, blue = text_color.red, text_color.green, text_color.blue
|
red, green, blue = text_color.red, text_color.green, text_color.blue
|
||||||
text_color = [float(x)/65535. for x in (red, green, blue)]
|
text_color = [x/65535. for x in (red, green, blue)]
|
||||||
text_color.append(.5)
|
text_color.append(.5)
|
||||||
|
|
||||||
if font_desc is None:
|
if font_desc is None:
|
||||||
font_desc = style.font_desc
|
font_desc = style_context.get_font(Gtk.StateFlags.NORMAL)
|
||||||
font_desc.set_size(14*pango.SCALE)
|
font_desc.set_size(14*Pango.SCALE)
|
||||||
|
|
||||||
pango_context = widget.create_pango_context()
|
pango_context = widget.create_pango_context()
|
||||||
layout = pango.Layout(pango_context)
|
layout = Pango.Layout(pango_context)
|
||||||
layout.set_font_description(font_desc)
|
layout.set_font_description(font_desc)
|
||||||
layout.set_text(text)
|
layout.set_text(text, -1)
|
||||||
width, height = layout.get_pixel_size()
|
width, height = layout.get_pixel_size()
|
||||||
|
|
||||||
ctx.move_to(w_width/2-width/2, w_height/2-height/2)
|
ctx.move_to(w_width/2-width/2, w_height/2-height/2)
|
||||||
ctx.set_source_rgba(*text_color)
|
ctx.set_source_rgba(*text_color)
|
||||||
ctx.show_layout(layout)
|
PangoCairo.show_layout(ctx, layout)
|
||||||
|
|
||||||
# Draw an optional progress bar below the text (same width)
|
# Draw an optional progress bar below the text (same width)
|
||||||
if add_progress is not None:
|
if add_progress is not None:
|
||||||
|
@ -126,13 +133,17 @@ def draw_cake(percentage, text=None, emblem=None, size=None):
|
||||||
size = EPISODE_LIST_ICON_SIZE
|
size = EPISODE_LIST_ICON_SIZE
|
||||||
|
|
||||||
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size, size)
|
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size, size)
|
||||||
ctx = pangocairo.CairoContext(cairo.Context(surface))
|
ctx = cairo.Context(surface)
|
||||||
|
|
||||||
widget = gtk.ProgressBar()
|
# ELL: get all black
|
||||||
style = widget.rc_get_style()
|
#widget = Gtk.ProgressBar()
|
||||||
bgc = style.bg[gtk.STATE_NORMAL]
|
#style_context = widget.get_style_context()
|
||||||
fgc = style.bg[gtk.STATE_SELECTED]
|
bgc = Gdk.RGBA() #style_context.get_background_color(Gtk.StateFlags.NORMAL)
|
||||||
txc = style.text[gtk.STATE_NORMAL]
|
bgc.parse('white')
|
||||||
|
fgc = Gdk.RGBA() #style_context.get_background_color(Gtk.StateFlags.SELECTED)
|
||||||
|
fgc.parse('#4a90d9')
|
||||||
|
txc = Gdk.RGBA() #style_context.get_color(Gtk.StateFlags.NORMAL)
|
||||||
|
txc.parse('#333333')
|
||||||
|
|
||||||
border = 1.5
|
border = 1.5
|
||||||
height = int(size*.4)
|
height = int(size*.4)
|
||||||
|
@ -142,19 +153,19 @@ def draw_cake(percentage, text=None, emblem=None, size=None):
|
||||||
|
|
||||||
# Background
|
# Background
|
||||||
ctx.rectangle(x, y, width, height)
|
ctx.rectangle(x, y, width, height)
|
||||||
ctx.set_source_rgb(bgc.red_float, bgc.green_float, bgc.blue_float)
|
ctx.set_source_rgb(bgc.red, bgc.green, bgc.blue)
|
||||||
ctx.fill()
|
ctx.fill()
|
||||||
|
|
||||||
# Filling
|
# Filling
|
||||||
if percentage > 0:
|
if percentage > 0:
|
||||||
fill_width = max(1, min(width-2, (width-2)*percentage+.5))
|
fill_width = max(1, min(width-2, (width-2)*percentage+.5))
|
||||||
ctx.rectangle(x+1, y+1, fill_width, height-2)
|
ctx.rectangle(x+1, y+1, fill_width, height-2)
|
||||||
ctx.set_source_rgb(fgc.red_float, fgc.green_float, fgc.blue_float)
|
ctx.set_source_rgb(0.289, 0.5625, 0.84765625)
|
||||||
ctx.fill()
|
ctx.fill()
|
||||||
|
|
||||||
# Border
|
# Border
|
||||||
ctx.rectangle(x, y, width, height)
|
ctx.rectangle(x, y, width, height)
|
||||||
ctx.set_source_rgb(txc.red_float, txc.green_float, txc.blue_float)
|
ctx.set_source_rgb(txc.red, txc.green, txc.blue)
|
||||||
ctx.set_line_width(1)
|
ctx.set_line_width(1)
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
|
|
||||||
|
@ -162,12 +173,10 @@ def draw_cake(percentage, text=None, emblem=None, size=None):
|
||||||
return surface
|
return surface
|
||||||
|
|
||||||
def draw_text_pill(left_text, right_text, x=0, y=0, border=2, radius=14, font_desc=None):
|
def draw_text_pill(left_text, right_text, x=0, y=0, border=2, radius=14, font_desc=None):
|
||||||
# Create temporary context to calculate the text size
|
|
||||||
ctx = cairo.Context(cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1))
|
|
||||||
|
|
||||||
# Use GTK+ style of a normal Button
|
# Use GTK+ style of a normal Button
|
||||||
widget = gtk.Label()
|
widget = Gtk.Label()
|
||||||
style = widget.rc_get_style()
|
style_context = widget.get_style_context()
|
||||||
|
|
||||||
# Padding (in px) at the right edge of the image (for Ubuntu; bug 1533)
|
# Padding (in px) at the right edge of the image (for Ubuntu; bug 1533)
|
||||||
padding_right = 7
|
padding_right = 7
|
||||||
|
@ -175,16 +184,16 @@ def draw_text_pill(left_text, right_text, x=0, y=0, border=2, radius=14, font_de
|
||||||
x_border = border*2
|
x_border = border*2
|
||||||
|
|
||||||
if font_desc is None:
|
if font_desc is None:
|
||||||
font_desc = style.font_desc
|
font_desc = style_context.get_font(Gtk.StateFlags.NORMAL)
|
||||||
font_desc.set_weight(pango.WEIGHT_BOLD)
|
font_desc.set_weight(Pango.Weight.BOLD)
|
||||||
|
|
||||||
pango_context = widget.create_pango_context()
|
pango_context = widget.create_pango_context()
|
||||||
layout_left = pango.Layout(pango_context)
|
layout_left = Pango.Layout(pango_context)
|
||||||
layout_left.set_font_description(font_desc)
|
layout_left.set_font_description(font_desc)
|
||||||
layout_left.set_text(left_text)
|
layout_left.set_text(left_text, -1)
|
||||||
layout_right = pango.Layout(pango_context)
|
layout_right = Pango.Layout(pango_context)
|
||||||
layout_right.set_font_description(font_desc)
|
layout_right.set_font_description(font_desc)
|
||||||
layout_right.set_text(right_text)
|
layout_right.set_text(right_text, -1)
|
||||||
|
|
||||||
width_left, height_left = layout_left.get_pixel_size()
|
width_left, height_left = layout_left.get_pixel_size()
|
||||||
width_right, height_right = layout_right.get_pixel_size()
|
width_right, height_right = layout_right.get_pixel_size()
|
||||||
|
@ -193,9 +202,9 @@ def draw_text_pill(left_text, right_text, x=0, y=0, border=2, radius=14, font_de
|
||||||
|
|
||||||
image_height = int(y+text_height+border*2)
|
image_height = int(y+text_height+border*2)
|
||||||
image_width = int(x+width_left+width_right+x_border*4+padding_right)
|
image_width = int(x+width_left+width_right+x_border*4+padding_right)
|
||||||
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, image_width, image_height)
|
|
||||||
|
|
||||||
ctx = pangocairo.CairoContext(cairo.Context(surface))
|
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, image_width, image_height)
|
||||||
|
ctx = cairo.Context(surface)
|
||||||
|
|
||||||
# Clip so as to not draw on the right padding (for Ubuntu; bug 1533)
|
# Clip so as to not draw on the right padding (for Ubuntu; bug 1533)
|
||||||
ctx.rectangle(0, 0, image_width - padding_right, image_height)
|
ctx.rectangle(0, 0, image_width - padding_right, image_height)
|
||||||
|
@ -235,39 +244,39 @@ def draw_text_pill(left_text, right_text, x=0, y=0, border=2, radius=14, font_de
|
||||||
|
|
||||||
ctx.move_to(x+x_border, y+1+border)
|
ctx.move_to(x+x_border, y+1+border)
|
||||||
ctx.set_source_rgba( 0, 0, 0, 1)
|
ctx.set_source_rgba( 0, 0, 0, 1)
|
||||||
ctx.show_layout(layout_left)
|
PangoCairo.show_layout(ctx, layout_left)
|
||||||
ctx.move_to(x-1+x_border, y+border)
|
ctx.move_to(x-1+x_border, y+border)
|
||||||
ctx.set_source_rgba( 1, 1, 1, 1)
|
ctx.set_source_rgba( 1, 1, 1, 1)
|
||||||
ctx.show_layout(layout_left)
|
PangoCairo.show_layout(ctx, layout_left)
|
||||||
|
|
||||||
if right_text is not None:
|
if right_text is not None:
|
||||||
draw_rounded_rectangle(ctx, x, y, rect_width, rect_height, radius, left_side_width, RRECT_RIGHT_SIDE, left_text is None)
|
draw_rounded_rectangle(ctx, x, y, rect_width, rect_height, radius, left_side_width, RRECT_RIGHT_SIDE, left_text is None)
|
||||||
linear = cairo.LinearGradient(x+left_side_width, y, x+left_side_width+right_side_width/2, y+rect_height)
|
linear = cairo.LinearGradient(x+left_side_width, y, x+left_side_width+right_side_width/2, y+rect_height)
|
||||||
linear.add_color_stop_rgba(0, .2, .2, .2, .9)
|
linear.add_color_stop_rgba(0, .2, .2, .2, .9)
|
||||||
linear.add_color_stop_rgba(.4, .2, .2, .2, .8)
|
linear.add_color_stop_rgba(.4, .2, .2, .2, .8)
|
||||||
linear.add_color_stop_rgba(.6, .2, .2, .2, .6)
|
linear.add_color_stop_rgba(.6, .2, .2, .2, .6)
|
||||||
linear.add_color_stop_rgba(.9, .2, .2, .2, .7)
|
linear.add_color_stop_rgba(.9, .2, .2, .2, .7)
|
||||||
linear.add_color_stop_rgba(1, .2, .2, .2, .5)
|
linear.add_color_stop_rgba(1, .2, .2, .2, .5)
|
||||||
ctx.set_source(linear)
|
ctx.set_source(linear)
|
||||||
ctx.fill()
|
ctx.fill()
|
||||||
xpos, ypos, width, height = x, y+1, rect_width-1, rect_height-2
|
xpos, ypos, width, height = x, y+1, rect_width-1, rect_height-2
|
||||||
if left_text is None:
|
if left_text is None:
|
||||||
xpos, width = x+1, rect_width-2
|
xpos, width = x+1, rect_width-2
|
||||||
draw_rounded_rectangle(ctx, xpos, ypos, width, height, radius, left_side_width, RRECT_RIGHT_SIDE, left_text is None)
|
draw_rounded_rectangle(ctx, xpos, ypos, width, height, radius, left_side_width, RRECT_RIGHT_SIDE, left_text is None)
|
||||||
ctx.set_source_rgba(1., 1., 1., .3)
|
ctx.set_source_rgba(1., 1., 1., .3)
|
||||||
ctx.set_line_width(1)
|
ctx.set_line_width(1)
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
draw_rounded_rectangle(ctx, x, y, rect_width, rect_height, radius, left_side_width, RRECT_RIGHT_SIDE, left_text is None)
|
draw_rounded_rectangle(ctx, x, y, rect_width, rect_height, radius, left_side_width, RRECT_RIGHT_SIDE, left_text is None)
|
||||||
ctx.set_source_rgba(.1, .1, .1, .6)
|
ctx.set_source_rgba(.1, .1, .1, .6)
|
||||||
ctx.set_line_width(1)
|
ctx.set_line_width(1)
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
|
|
||||||
ctx.move_to(x+left_side_width+x_border, y+1+border)
|
ctx.move_to(x+left_side_width+x_border, y+1+border)
|
||||||
ctx.set_source_rgba( 0, 0, 0, 1)
|
ctx.set_source_rgba( 0, 0, 0, 1)
|
||||||
ctx.show_layout(layout_right)
|
PangoCairo.show_layout(ctx, layout_right)
|
||||||
ctx.move_to(x-1+left_side_width+x_border, y+border)
|
ctx.move_to(x-1+left_side_width+x_border, y+border)
|
||||||
ctx.set_source_rgba( 1, 1, 1, 1)
|
ctx.set_source_rgba( 1, 1, 1, 1)
|
||||||
ctx.show_layout(layout_right)
|
PangoCairo.show_layout(ctx, layout_right)
|
||||||
|
|
||||||
return surface
|
return surface
|
||||||
|
|
||||||
|
@ -284,19 +293,19 @@ def cairo_surface_to_pixbuf(s):
|
||||||
Converts a Cairo surface to a Gtk Pixbuf by
|
Converts a Cairo surface to a Gtk Pixbuf by
|
||||||
encoding it as PNG and using the PixbufLoader.
|
encoding it as PNG and using the PixbufLoader.
|
||||||
"""
|
"""
|
||||||
sio = StringIO.StringIO()
|
bio = io.BytesIO()
|
||||||
try:
|
try:
|
||||||
s.write_to_png(sio)
|
s.write_to_png(bio)
|
||||||
except:
|
except:
|
||||||
# Write an empty PNG file to the StringIO, so
|
# Write an empty PNG file to the StringIO, so
|
||||||
# in case of an error we have "something" to
|
# in case of an error we have "something" to
|
||||||
# load. This happens in PyCairo < 1.1.6, see:
|
# load. This happens in PyCairo < 1.1.6, see:
|
||||||
# http://webcvs.cairographics.org/pycairo/NEWS?view=markup
|
# http://webcvs.cairographics.org/pycairo/NEWS?view=markup
|
||||||
# Thanks to Chris Arnold for reporting this bug
|
# Thanks to Chris Arnold for reporting this bug
|
||||||
sio.write('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A\n/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9cMEQkqIyxn3RkAAAAZdEVYdENv\nbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAADUlEQVQI12NgYGBgAAAABQABXvMqOgAAAABJ\nRU5ErkJggg==\n'.decode('base64'))
|
bio.write('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A\n/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9cMEQkqIyxn3RkAAAAZdEVYdENv\nbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAADUlEQVQI12NgYGBgAAAABQABXvMqOgAAAABJ\nRU5ErkJggg==\n'.decode('base64'))
|
||||||
|
|
||||||
pbl = gtk.gdk.PixbufLoader()
|
pbl = GdkPixbuf.PixbufLoader()
|
||||||
pbl.write(sio.getvalue())
|
pbl.write(bio.getvalue())
|
||||||
pbl.close()
|
pbl.close()
|
||||||
|
|
||||||
pixbuf = pbl.get_pixbuf()
|
pixbuf = pbl.get_pixbuf()
|
||||||
|
@ -312,7 +321,7 @@ def progressbar_pixbuf(width, height, percentage):
|
||||||
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
|
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
|
||||||
ctx = cairo.Context(surface)
|
ctx = cairo.Context(surface)
|
||||||
|
|
||||||
padding = int(float(width)/8.0)
|
padding = int(width/8.0)
|
||||||
bar_width = 2*padding
|
bar_width = 2*padding
|
||||||
bar_height = height - 2*padding
|
bar_height = height - 2*padding
|
||||||
bar_height_fill = bar_height*percentage
|
bar_height_fill = bar_height*percentage
|
||||||
|
@ -337,4 +346,3 @@ def progressbar_pixbuf(width, height, percentage):
|
||||||
ctx.stroke()
|
ctx.stroke()
|
||||||
|
|
||||||
return cairo_surface_to_pixbuf(surface)
|
return cairo_surface_to_pixbuf(surface)
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,8 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
|
||||||
import gtk
|
from gi.repository import Gtk
|
||||||
|
from gi.repository import Gdk
|
||||||
|
|
||||||
import gpodder
|
import gpodder
|
||||||
|
|
||||||
|
@ -43,8 +44,11 @@ class gPodderAddPodcast(BuilderWidget):
|
||||||
|
|
||||||
if not hasattr(self, 'preset_url'):
|
if not hasattr(self, 'preset_url'):
|
||||||
# Fill the entry if a valid URL is in the clipboard, but
|
# Fill the entry if a valid URL is in the clipboard, but
|
||||||
# only if there's no preset_url available (see bug 1132)
|
# only if there's no preset_url available (see bug 1132).
|
||||||
clipboard = gtk.Clipboard(selection='PRIMARY')
|
# First try from CLIPBOARD (everyday copy-paste),
|
||||||
|
# then from SELECTION (text selected and pasted via
|
||||||
|
# middle mouse button).
|
||||||
|
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||||
def receive_clipboard_text(clipboard, text, second_try):
|
def receive_clipboard_text(clipboard, text, second_try):
|
||||||
# Heuristic: If there is a space in the clipboard
|
# Heuristic: If there is a space in the clipboard
|
||||||
# text, assume it's some arbitrary text, and no URL
|
# text, assume it's some arbitrary text, and no URL
|
||||||
|
@ -56,7 +60,7 @@ class gPodderAddPodcast(BuilderWidget):
|
||||||
return
|
return
|
||||||
|
|
||||||
if not second_try:
|
if not second_try:
|
||||||
clipboard = gtk.Clipboard()
|
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_PRIMARY)
|
||||||
clipboard.request_text(receive_clipboard_text, True)
|
clipboard.request_text(receive_clipboard_text, True)
|
||||||
clipboard.request_text(receive_clipboard_text, False)
|
clipboard.request_text(receive_clipboard_text, False)
|
||||||
|
|
||||||
|
@ -64,7 +68,7 @@ class gPodderAddPodcast(BuilderWidget):
|
||||||
self.gPodderAddPodcast.destroy()
|
self.gPodderAddPodcast.destroy()
|
||||||
|
|
||||||
def on_btn_paste_clicked(self, widget):
|
def on_btn_paste_clicked(self, widget):
|
||||||
clipboard = gtk.Clipboard()
|
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||||
clipboard.request_text(self.receive_clipboard_text)
|
clipboard.request_text(self.receive_clipboard_text)
|
||||||
|
|
||||||
def receive_clipboard_text(self, clipboard, text, data=None):
|
def receive_clipboard_text(self, clipboard, text, data=None):
|
||||||
|
|
|
@ -17,7 +17,9 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
|
||||||
import gtk
|
from gi.repository import Gtk
|
||||||
|
from gi.repository import Gdk
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
@ -33,40 +35,17 @@ from gpodder.gtkui.base import GtkBuilderWidget
|
||||||
class BuilderWidget(GtkBuilderWidget):
|
class BuilderWidget(GtkBuilderWidget):
|
||||||
def __init__(self, parent, **kwargs):
|
def __init__(self, parent, **kwargs):
|
||||||
self._window_iconified = False
|
self._window_iconified = False
|
||||||
self._window_visible = False
|
|
||||||
|
|
||||||
GtkBuilderWidget.__init__(self, gpodder.ui_folders, gpodder.textdomain, **kwargs)
|
GtkBuilderWidget.__init__(self, gpodder.ui_folders, gpodder.textdomain, parent, **kwargs)
|
||||||
|
|
||||||
# Enable support for tracking iconified state
|
# Enable support for tracking iconified state
|
||||||
if hasattr(self, 'on_iconify') and hasattr(self, 'on_uniconify'):
|
if hasattr(self, 'on_iconify') and hasattr(self, 'on_uniconify'):
|
||||||
self.main_window.connect('window-state-event', \
|
self.main_window.connect('window-state-event', \
|
||||||
self._on_window_state_event_iconified)
|
self._on_window_state_event_iconified)
|
||||||
|
|
||||||
# Enable support for tracking visibility state
|
|
||||||
self.main_window.connect('visibility-notify-event', \
|
|
||||||
self._on_window_state_event_visibility)
|
|
||||||
|
|
||||||
if parent is not None:
|
|
||||||
self.main_window.set_transient_for(parent)
|
|
||||||
|
|
||||||
if hasattr(self, 'center_on_widget'):
|
|
||||||
(x, y) = parent.get_position()
|
|
||||||
a = self.center_on_widget.allocation
|
|
||||||
(x, y) = (x + a.x, y + a.y)
|
|
||||||
(w, h) = (a.width, a.height)
|
|
||||||
(pw, ph) = self.main_window.get_size()
|
|
||||||
self.main_window.move(x + w/2 - pw/2, y + h/2 - ph/2)
|
|
||||||
|
|
||||||
def _on_window_state_event_visibility(self, widget, event):
|
|
||||||
if event.state & gtk.gdk.VISIBILITY_FULLY_OBSCURED:
|
|
||||||
self._window_visible = False
|
|
||||||
else:
|
|
||||||
self._window_visible = True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _on_window_state_event_iconified(self, widget, event):
|
def _on_window_state_event_iconified(self, widget, event):
|
||||||
if event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED:
|
if event.new_window_state & Gdk.WindowState.ICONIFIED:
|
||||||
if not self._window_iconified:
|
if not self._window_iconified:
|
||||||
self._window_iconified = True
|
self._window_iconified = True
|
||||||
self.on_iconify()
|
self.on_iconify()
|
||||||
|
@ -84,12 +63,12 @@ class BuilderWidget(GtkBuilderWidget):
|
||||||
util.idle_add(self.show_message, message, title, important, widget)
|
util.idle_add(self.show_message, message, title, important, widget)
|
||||||
|
|
||||||
def get_dialog_parent(self):
|
def get_dialog_parent(self):
|
||||||
"""Return a gtk.Window that should be the parent of dialogs"""
|
"""Return a Gtk.Window that should be the parent of dialogs"""
|
||||||
return self.main_window
|
return self.main_window
|
||||||
|
|
||||||
def show_message(self, message, title=None, important=False, widget=None):
|
def show_message(self, message, title=None, important=False, widget=None):
|
||||||
if important:
|
if important:
|
||||||
dlg = gtk.MessageDialog(self.main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK)
|
dlg = Gtk.MessageDialog(self.main_window, Gtk.DialogFlags.MODAL, Gtk.MessageType.INFO, Gtk.ButtonsType.OK)
|
||||||
if title:
|
if title:
|
||||||
dlg.set_title(str(title))
|
dlg.set_title(str(title))
|
||||||
dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
|
dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
|
||||||
|
@ -101,7 +80,7 @@ class BuilderWidget(GtkBuilderWidget):
|
||||||
gpodder.user_extensions.on_notification_show(title, message)
|
gpodder.user_extensions.on_notification_show(title, message)
|
||||||
|
|
||||||
def show_confirmation(self, message, title=None):
|
def show_confirmation(self, message, title=None):
|
||||||
dlg = gtk.MessageDialog(self.main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO)
|
dlg = Gtk.MessageDialog(self.main_window, Gtk.DialogFlags.MODAL, Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO)
|
||||||
if title:
|
if title:
|
||||||
dlg.set_title(str(title))
|
dlg.set_title(str(title))
|
||||||
dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
|
dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
|
||||||
|
@ -109,21 +88,20 @@ class BuilderWidget(GtkBuilderWidget):
|
||||||
dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
|
dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
|
||||||
response = dlg.run()
|
response = dlg.run()
|
||||||
dlg.destroy()
|
dlg.destroy()
|
||||||
return response == gtk.RESPONSE_YES
|
return response == Gtk.ResponseType.YES
|
||||||
|
|
||||||
def show_text_edit_dialog(self, title, prompt, text=None, empty=False, \
|
def show_text_edit_dialog(self, title, prompt, text=None, empty=False, \
|
||||||
is_url=False, affirmative_text=gtk.STOCK_OK):
|
is_url=False, affirmative_text=Gtk.STOCK_OK):
|
||||||
dialog = gtk.Dialog(title, self.get_dialog_parent(), \
|
dialog = Gtk.Dialog(title, self.get_dialog_parent(), \
|
||||||
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT)
|
Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT)
|
||||||
|
|
||||||
dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
|
dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
|
||||||
dialog.add_button(affirmative_text, gtk.RESPONSE_OK)
|
dialog.add_button(affirmative_text, Gtk.ResponseType.OK)
|
||||||
|
|
||||||
dialog.set_has_separator(False)
|
|
||||||
dialog.set_default_size(300, -1)
|
dialog.set_default_size(300, -1)
|
||||||
dialog.set_default_response(gtk.RESPONSE_OK)
|
dialog.set_default_response(Gtk.ResponseType.OK)
|
||||||
|
|
||||||
text_entry = gtk.Entry()
|
text_entry = Gtk.Entry()
|
||||||
text_entry.set_activates_default(True)
|
text_entry.set_activates_default(True)
|
||||||
if text is not None:
|
if text is not None:
|
||||||
text_entry.set_text(text)
|
text_entry.set_text(text)
|
||||||
|
@ -132,24 +110,24 @@ class BuilderWidget(GtkBuilderWidget):
|
||||||
if not empty:
|
if not empty:
|
||||||
def on_text_changed(editable):
|
def on_text_changed(editable):
|
||||||
can_confirm = (editable.get_text() != '')
|
can_confirm = (editable.get_text() != '')
|
||||||
dialog.set_response_sensitive(gtk.RESPONSE_OK, can_confirm)
|
dialog.set_response_sensitive(Gtk.ResponseType.OK, can_confirm)
|
||||||
text_entry.connect('changed', on_text_changed)
|
text_entry.connect('changed', on_text_changed)
|
||||||
if text is None:
|
if text is None:
|
||||||
dialog.set_response_sensitive(gtk.RESPONSE_OK, False)
|
dialog.set_response_sensitive(Gtk.ResponseType.OK, False)
|
||||||
|
|
||||||
hbox = gtk.HBox()
|
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
||||||
hbox.set_border_width(10)
|
hbox.set_border_width(10)
|
||||||
hbox.set_spacing(10)
|
hbox.set_spacing(10)
|
||||||
hbox.pack_start(gtk.Label(prompt), False, False)
|
hbox.pack_start(Gtk.Label(prompt, True, True, 0), False, False, 0)
|
||||||
hbox.pack_start(text_entry, True, True)
|
hbox.pack_start(text_entry, True, True, 0)
|
||||||
dialog.vbox.pack_start(hbox, True, True)
|
dialog.vbox.pack_start(hbox, True, True, 0)
|
||||||
|
|
||||||
dialog.show_all()
|
dialog.show_all()
|
||||||
response = dialog.run()
|
response = dialog.run()
|
||||||
result = text_entry.get_text()
|
result = text_entry.get_text()
|
||||||
dialog.destroy()
|
dialog.destroy()
|
||||||
|
|
||||||
if response == gtk.RESPONSE_OK:
|
if response == Gtk.ResponseType.OK:
|
||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
@ -162,25 +140,25 @@ class BuilderWidget(GtkBuilderWidget):
|
||||||
if register_text is None:
|
if register_text is None:
|
||||||
register_text = _('New user')
|
register_text = _('New user')
|
||||||
|
|
||||||
dialog = gtk.MessageDialog(
|
dialog = Gtk.MessageDialog(
|
||||||
self.main_window,
|
self.main_window,
|
||||||
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
|
Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
|
||||||
gtk.MESSAGE_QUESTION,
|
Gtk.MessageType.QUESTION,
|
||||||
gtk.BUTTONS_CANCEL)
|
Gtk.ButtonsType.CANCEL)
|
||||||
dialog.add_button(_('Login'), gtk.RESPONSE_OK)
|
dialog.add_button(_('Login'), Gtk.ResponseType.OK)
|
||||||
dialog.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG))
|
dialog.set_image(Gtk.Image.new_from_icon_name('dialog-password', Gtk.IconSize.DIALOG))
|
||||||
dialog.set_title(_('Authentication required'))
|
dialog.set_title(_('Authentication required'))
|
||||||
dialog.set_markup('<span weight="bold" size="larger">' + title + '</span>')
|
dialog.set_markup('<span weight="bold" size="larger">' + title + '</span>')
|
||||||
dialog.format_secondary_markup(message)
|
dialog.format_secondary_markup(message)
|
||||||
dialog.set_default_response(gtk.RESPONSE_OK)
|
dialog.set_default_response(Gtk.ResponseType.OK)
|
||||||
|
|
||||||
if register_callback is not None:
|
if register_callback is not None:
|
||||||
dialog.add_button(register_text, gtk.RESPONSE_HELP)
|
dialog.add_button(register_text, Gtk.ResponseType.HELP)
|
||||||
|
|
||||||
server_entry = gtk.Entry()
|
server_entry = Gtk.Entry()
|
||||||
server_entry.set_tooltip_text(_('hostname or root URL (e.g. https://gpodder.net)'))
|
server_entry.set_tooltip_text(_('hostname or root URL (e.g. https://gpodder.net)'))
|
||||||
username_entry = gtk.Entry()
|
username_entry = Gtk.Entry()
|
||||||
password_entry = gtk.Entry()
|
password_entry = Gtk.Entry()
|
||||||
|
|
||||||
server_entry.connect('activate', lambda w: username_entry.grab_focus())
|
server_entry.connect('activate', lambda w: username_entry.grab_focus())
|
||||||
username_entry.connect('activate', lambda w: password_entry.grab_focus())
|
username_entry.connect('activate', lambda w: password_entry.grab_focus())
|
||||||
|
@ -194,17 +172,17 @@ class BuilderWidget(GtkBuilderWidget):
|
||||||
if password is not None:
|
if password is not None:
|
||||||
password_entry.set_text(password)
|
password_entry.set_text(password)
|
||||||
|
|
||||||
table = gtk.Table(3, 2)
|
table = Gtk.Table(3, 2)
|
||||||
table.set_row_spacings(6)
|
table.set_row_spacings(6)
|
||||||
table.set_col_spacings(6)
|
table.set_col_spacings(6)
|
||||||
|
|
||||||
server_label = gtk.Label()
|
server_label = Gtk.Label()
|
||||||
server_label.set_markup('<b>' + _('Server') + ':</b>')
|
server_label.set_markup('<b>' + _('Server') + ':</b>')
|
||||||
|
|
||||||
username_label = gtk.Label()
|
username_label = Gtk.Label()
|
||||||
username_label.set_markup('<b>' + username_prompt + ':</b>')
|
username_label.set_markup('<b>' + username_prompt + ':</b>')
|
||||||
|
|
||||||
password_label = gtk.Label()
|
password_label = Gtk.Label()
|
||||||
password_label.set_markup('<b>' + _('Password') + ':</b>')
|
password_label.set_markup('<b>' + _('Password') + ':</b>')
|
||||||
|
|
||||||
label_entries = [(username_label, username_entry),
|
label_entries = [(username_label, username_entry),
|
||||||
|
@ -215,7 +193,7 @@ class BuilderWidget(GtkBuilderWidget):
|
||||||
|
|
||||||
for i, (label, entry) in enumerate(label_entries):
|
for i, (label, entry) in enumerate(label_entries):
|
||||||
label.set_alignment(0.0, 0.5)
|
label.set_alignment(0.0, 0.5)
|
||||||
table.attach(label, 0, 1, i, i + 1, gtk.FILL, 0)
|
table.attach(label, 0, 1, i, i + 1, Gtk.AttachOptions.FILL, 0)
|
||||||
table.attach(entry, 1, 2, i, i + 1)
|
table.attach(entry, 1, 2, i, i + 1)
|
||||||
|
|
||||||
dialog.vbox.pack_end(table, True, True, 0)
|
dialog.vbox.pack_end(table, True, True, 0)
|
||||||
|
@ -223,7 +201,7 @@ class BuilderWidget(GtkBuilderWidget):
|
||||||
username_entry.grab_focus()
|
username_entry.grab_focus()
|
||||||
response = dialog.run()
|
response = dialog.run()
|
||||||
|
|
||||||
while response == gtk.RESPONSE_HELP:
|
while response == Gtk.ResponseType.HELP:
|
||||||
register_callback()
|
register_callback()
|
||||||
response = dialog.run()
|
response = dialog.run()
|
||||||
|
|
||||||
|
@ -231,7 +209,7 @@ class BuilderWidget(GtkBuilderWidget):
|
||||||
root_url = server_entry.get_text()
|
root_url = server_entry.get_text()
|
||||||
username = username_entry.get_text()
|
username = username_entry.get_text()
|
||||||
password = password_entry.get_text()
|
password = password_entry.get_text()
|
||||||
success = (response == gtk.RESPONSE_OK)
|
success = (response == Gtk.ResponseType.OK)
|
||||||
|
|
||||||
dialog.destroy()
|
dialog.destroy()
|
||||||
|
|
||||||
|
@ -240,36 +218,22 @@ class BuilderWidget(GtkBuilderWidget):
|
||||||
else:
|
else:
|
||||||
return (success, (username, password))
|
return (success, (username, password))
|
||||||
|
|
||||||
def show_copy_dialog(self, src_filename, dst_filename=None, dst_directory=None, title=_('Select destination')):
|
def show_folder_select_dialog(self, initial_directory=None, title=_('Select destination')):
|
||||||
if dst_filename is None:
|
if initial_directory is None:
|
||||||
dst_filename = src_filename
|
initial_directory = os.path.expanduser('~')
|
||||||
|
|
||||||
if dst_directory is None:
|
dlg = Gtk.FileChooserDialog(title=title, parent=self.main_window, action=Gtk.FileChooserAction.SELECT_FOLDER)
|
||||||
dst_directory = os.path.expanduser('~')
|
dlg.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
|
||||||
|
dlg.add_button(Gtk.STOCK_SAVE, Gtk.ResponseType.OK)
|
||||||
base, extension = os.path.splitext(src_filename)
|
|
||||||
|
|
||||||
if not dst_filename.endswith(extension):
|
|
||||||
dst_filename += extension
|
|
||||||
|
|
||||||
dlg = gtk.FileChooserDialog(title=title, parent=self.main_window, action=gtk.FILE_CHOOSER_ACTION_SAVE)
|
|
||||||
dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
|
|
||||||
dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
|
|
||||||
|
|
||||||
dlg.set_do_overwrite_confirmation(True)
|
dlg.set_do_overwrite_confirmation(True)
|
||||||
dlg.set_current_name(os.path.basename(dst_filename))
|
dlg.set_current_folder(initial_directory)
|
||||||
dlg.set_current_folder(dst_directory)
|
|
||||||
|
|
||||||
result = False
|
result = False
|
||||||
folder = dst_directory
|
folder = initial_directory
|
||||||
if dlg.run() == gtk.RESPONSE_OK:
|
if dlg.run() == Gtk.ResponseType.OK:
|
||||||
result = True
|
result = True
|
||||||
dst_filename = dlg.get_filename()
|
|
||||||
folder = dlg.get_current_folder()
|
folder = dlg.get_current_folder()
|
||||||
if not dst_filename.endswith(extension):
|
|
||||||
dst_filename += extension
|
|
||||||
|
|
||||||
shutil.copyfile(src_filename, dst_filename)
|
|
||||||
|
|
||||||
dlg.destroy()
|
dlg.destroy()
|
||||||
return (result, folder)
|
return (result, folder)
|
||||||
|
@ -282,7 +246,7 @@ class TreeViewHelper(object):
|
||||||
COLUMNS = '_gpodder_columns'
|
COLUMNS = '_gpodder_columns'
|
||||||
|
|
||||||
# Enum for the role attribute
|
# Enum for the role attribute
|
||||||
ROLE_PODCASTS, ROLE_EPISODES, ROLE_DOWNLOADS = range(3)
|
ROLE_PODCASTS, ROLE_EPISODES, ROLE_DOWNLOADS = list(range(3))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def set(cls, treeview, role):
|
def set(cls, treeview, role):
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
|
||||||
import gtk
|
from gi.repository import Gtk
|
||||||
import cgi
|
import cgi
|
||||||
|
|
||||||
import gpodder
|
import gpodder
|
||||||
|
@ -31,28 +31,30 @@ from gpodder.gtkui.interface.common import BuilderWidget
|
||||||
|
|
||||||
class gPodderConfigEditor(BuilderWidget):
|
class gPodderConfigEditor(BuilderWidget):
|
||||||
def new(self):
|
def new(self):
|
||||||
name_column = gtk.TreeViewColumn(_('Setting'))
|
name_column = Gtk.TreeViewColumn(_('Setting'))
|
||||||
name_renderer = gtk.CellRendererText()
|
name_renderer = Gtk.CellRendererText()
|
||||||
name_column.pack_start(name_renderer)
|
name_column.pack_start(name_renderer, True)
|
||||||
name_column.add_attribute(name_renderer, 'text', 0)
|
name_column.add_attribute(name_renderer, 'text', 0)
|
||||||
name_column.add_attribute(name_renderer, 'style', 5)
|
name_column.add_attribute(name_renderer, 'style', 5)
|
||||||
|
name_column.set_expand(True)
|
||||||
self.configeditor.append_column(name_column)
|
self.configeditor.append_column(name_column)
|
||||||
|
|
||||||
value_column = gtk.TreeViewColumn(_('Set to'))
|
value_column = Gtk.TreeViewColumn(_('Set to'))
|
||||||
value_check_renderer = gtk.CellRendererToggle()
|
value_check_renderer = Gtk.CellRendererToggle()
|
||||||
value_column.pack_start(value_check_renderer, expand=False)
|
value_column.pack_start(value_check_renderer, False)
|
||||||
value_column.add_attribute(value_check_renderer, 'active', 7)
|
value_column.add_attribute(value_check_renderer, 'active', 7)
|
||||||
value_column.add_attribute(value_check_renderer, 'visible', 6)
|
value_column.add_attribute(value_check_renderer, 'visible', 6)
|
||||||
value_column.add_attribute(value_check_renderer, 'activatable', 6)
|
value_column.add_attribute(value_check_renderer, 'activatable', 6)
|
||||||
value_check_renderer.connect('toggled', self.value_toggled)
|
value_check_renderer.connect('toggled', self.value_toggled)
|
||||||
|
|
||||||
value_renderer = gtk.CellRendererText()
|
value_renderer = Gtk.CellRendererText()
|
||||||
value_column.pack_start(value_renderer)
|
value_column.pack_start(value_renderer, True)
|
||||||
value_column.add_attribute(value_renderer, 'text', 2)
|
value_column.add_attribute(value_renderer, 'text', 2)
|
||||||
value_column.add_attribute(value_renderer, 'visible', 4)
|
value_column.add_attribute(value_renderer, 'visible', 4)
|
||||||
value_column.add_attribute(value_renderer, 'editable', 4)
|
value_column.add_attribute(value_renderer, 'editable', 4)
|
||||||
value_column.add_attribute(value_renderer, 'style', 5)
|
value_column.add_attribute(value_renderer, 'style', 5)
|
||||||
value_renderer.connect('edited', self.value_edited)
|
value_renderer.connect('edited', self.value_edited)
|
||||||
|
value_column.set_expand(False)
|
||||||
self.configeditor.append_column(value_column)
|
self.configeditor.append_column(value_column)
|
||||||
|
|
||||||
self.model = ConfigModel(self._config)
|
self.model = ConfigModel(self._config)
|
||||||
|
|
|
@ -17,9 +17,9 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
|
||||||
import gtk
|
from gi.repository import Gtk
|
||||||
import gobject
|
from gi.repository import GObject
|
||||||
import pango
|
from gi.repository import Pango
|
||||||
|
|
||||||
import gpodder
|
import gpodder
|
||||||
|
|
||||||
|
@ -45,16 +45,16 @@ class ProgressIndicator(object):
|
||||||
self._initial_message = None
|
self._initial_message = None
|
||||||
self._initial_progress = None
|
self._initial_progress = None
|
||||||
self._progress_set = False
|
self._progress_set = False
|
||||||
self.source_id = gobject.timeout_add(self.DELAY, self._create_progress)
|
self.source_id = GObject.timeout_add(self.DELAY, self._create_progress)
|
||||||
|
|
||||||
def _on_delete_event(self, window, event):
|
def _on_delete_event(self, window, event):
|
||||||
if self.cancellable:
|
if self.cancellable:
|
||||||
self.dialog.response(gtk.RESPONSE_CANCEL)
|
self.dialog.response(Gtk.ResponseType.CANCEL)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _create_progress(self):
|
def _create_progress(self):
|
||||||
self.dialog = gtk.MessageDialog(self.parent, \
|
self.dialog = Gtk.MessageDialog(self.parent, \
|
||||||
0, 0, gtk.BUTTONS_CANCEL, self.subtitle or self.title)
|
0, 0, Gtk.ButtonsType.CANCEL, self.subtitle or self.title)
|
||||||
self.dialog.set_modal(True)
|
self.dialog.set_modal(True)
|
||||||
self.dialog.connect('delete-event', self._on_delete_event)
|
self.dialog.connect('delete-event', self._on_delete_event)
|
||||||
self.dialog.set_title(self.title)
|
self.dialog.set_title(self.title)
|
||||||
|
@ -63,14 +63,15 @@ class ProgressIndicator(object):
|
||||||
# Avoid selectable text (requires PyGTK >= 2.22)
|
# Avoid selectable text (requires PyGTK >= 2.22)
|
||||||
if hasattr(self.dialog, 'get_message_area'):
|
if hasattr(self.dialog, 'get_message_area'):
|
||||||
for label in self.dialog.get_message_area():
|
for label in self.dialog.get_message_area():
|
||||||
if isinstance(label, gtk.Label):
|
if isinstance(label, Gtk.Label):
|
||||||
label.set_selectable(False)
|
label.set_selectable(False)
|
||||||
|
|
||||||
self.dialog.set_response_sensitive(gtk.RESPONSE_CANCEL, \
|
self.dialog.set_response_sensitive(Gtk.ResponseType.CANCEL, \
|
||||||
self.cancellable)
|
self.cancellable)
|
||||||
|
|
||||||
self.progressbar = gtk.ProgressBar()
|
self.progressbar = Gtk.ProgressBar()
|
||||||
self.progressbar.set_ellipsize(pango.ELLIPSIZE_END)
|
self.progressbar.set_show_text(True)
|
||||||
|
self.progressbar.set_ellipsize(Pango.EllipsizeMode.END)
|
||||||
|
|
||||||
# If the window is shown after the first update, set the progress
|
# If the window is shown after the first update, set the progress
|
||||||
# info so that when the window appears, data is there already
|
# info so that when the window appears, data is there already
|
||||||
|
@ -84,8 +85,8 @@ class ProgressIndicator(object):
|
||||||
self.dialog.set_image(self.indicator)
|
self.dialog.set_image(self.indicator)
|
||||||
self.dialog.show_all()
|
self.dialog.show_all()
|
||||||
|
|
||||||
gobject.source_remove(self.source_id)
|
GObject.source_remove(self.source_id)
|
||||||
self.source_id = gobject.timeout_add(self.INTERVAL, self._update_gui)
|
self.source_id = GObject.timeout_add(self.INTERVAL, self._update_gui)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _update_gui(self):
|
def _update_gui(self):
|
||||||
|
@ -111,5 +112,5 @@ class ProgressIndicator(object):
|
||||||
def on_finished(self):
|
def on_finished(self):
|
||||||
if self.dialog is not None:
|
if self.dialog is not None:
|
||||||
self.dialog.destroy()
|
self.dialog.destroy()
|
||||||
gobject.source_remove(self.source_id)
|
GObject.source_remove(self.source_id)
|
||||||
|
|
||||||
|
|
|
@ -18,19 +18,18 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
import gtk
|
from gi.repository import Gtk
|
||||||
import gobject
|
from gi.repository import GObject
|
||||||
import cgi
|
import cgi
|
||||||
|
|
||||||
class TagCloud(gtk.Layout):
|
class TagCloud(Gtk.Layout):
|
||||||
__gsignals__ = {
|
__gsignals__ = {
|
||||||
'selected': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
|
'selected': (GObject.SignalFlags.RUN_LAST, None,
|
||||||
(gobject.TYPE_STRING,))
|
(GObject.TYPE_STRING,))
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, min_size=20, max_size=36):
|
def __init__(self, min_size=20, max_size=36):
|
||||||
self.__gobject_init__()
|
Gtk.Layout.__init__(self)
|
||||||
gtk.Layout.__init__(self)
|
|
||||||
self._min_weight = 0
|
self._min_weight = 0
|
||||||
self._max_weight = 0
|
self._max_weight = 0
|
||||||
self._min_size = min_size
|
self._min_size = min_size
|
||||||
|
@ -49,10 +48,10 @@ class TagCloud(gtk.Layout):
|
||||||
self._max_weight = max(weight for tag, weight in tags)
|
self._max_weight = max(weight for tag, weight in tags)
|
||||||
|
|
||||||
for tag, weight in tags:
|
for tag, weight in tags:
|
||||||
label = gtk.Label()
|
label = Gtk.Label()
|
||||||
markup = '<span size="%d">%s</span>' % (1000*self._scale(weight), cgi.escape(tag))
|
markup = '<span size="%d">%s</span>' % (1000*self._scale(weight), cgi.escape(tag))
|
||||||
label.set_markup(markup)
|
label.set_markup(markup)
|
||||||
button = gtk.ToolButton(label)
|
button = Gtk.ToolButton(label)
|
||||||
button.connect('clicked', lambda b, t: self.emit('selected', t), tag)
|
button.connect('clicked', lambda b, t: self.emit('selected', t), tag)
|
||||||
self.put(button, 1, 1)
|
self.put(button, 1, 1)
|
||||||
button.show_all()
|
button.show_all()
|
||||||
|
@ -65,8 +64,8 @@ class TagCloud(gtk.Layout):
|
||||||
self.relayout()
|
self.relayout()
|
||||||
|
|
||||||
def _scale(self, weight):
|
def _scale(self, weight):
|
||||||
weight_range = float(self._max_weight-self._min_weight)
|
weight_range = self._max_weight-self._min_weight
|
||||||
ratio = float(weight-self._min_weight)/weight_range
|
ratio = (weight-self._min_weight)/weight_range
|
||||||
return int(self._min_size + (self._max_size-self._min_size)*ratio)
|
return int(self._min_size + (self._max_size-self._min_size)*ratio)
|
||||||
|
|
||||||
def relayout(self):
|
def relayout(self):
|
||||||
|
@ -76,10 +75,10 @@ class TagCloud(gtk.Layout):
|
||||||
pw, ph = self._size
|
pw, ph = self._size
|
||||||
def fixup_row(widgets, x, y, max_h):
|
def fixup_row(widgets, x, y, max_h):
|
||||||
residue = (pw - x)
|
residue = (pw - x)
|
||||||
x = int(residue/2)
|
x = int(residue//2)
|
||||||
for widget in widgets:
|
for widget in widgets:
|
||||||
cw, ch = widget.size_request()
|
cw, ch = widget.size_request()
|
||||||
self.move(widget, x, y+max(0, int((max_h-ch)/2)))
|
self.move(widget, x, y+max(0, int(max_h-ch)//2))
|
||||||
x += cw + 10
|
x += cw + 10
|
||||||
for child in self.get_children():
|
for child in self.get_children():
|
||||||
w, h = child.size_request()
|
w, h = child.size_request()
|
||||||
|
@ -98,6 +97,6 @@ class TagCloud(gtk.Layout):
|
||||||
def unrelayout():
|
def unrelayout():
|
||||||
self._in_relayout = False
|
self._in_relayout = False
|
||||||
return False
|
return False
|
||||||
gobject.idle_add(unrelayout)
|
GObject.idle_add(unrelayout)
|
||||||
|
|
||||||
gobject.type_register(TagCloud)
|
GObject.type_register(TagCloud)
|
||||||
|
|
|
@ -29,7 +29,10 @@ def aeKeyword(fourCharCode):
|
||||||
|
|
||||||
# for the kCoreEventClass, kAEOpenDocuments, ... constants
|
# for the kCoreEventClass, kAEOpenDocuments, ... constants
|
||||||
# comes with macpython
|
# comes with macpython
|
||||||
from Carbon.AppleEvents import *
|
try:
|
||||||
|
from Carbon.AppleEvents import *
|
||||||
|
except ImportError:
|
||||||
|
...
|
||||||
|
|
||||||
# all this depends on pyObjc (http://pyobjc.sourceforge.net/).
|
# all this depends on pyObjc (http://pyobjc.sourceforge.net/).
|
||||||
# There may be a way to achieve something equivalent with only
|
# There may be a way to achieve something equivalent with only
|
||||||
|
@ -78,7 +81,7 @@ try:
|
||||||
util.idle_add(self.gp.on_item_import_from_file_activate, None,url)
|
util.idle_add(self.gp.on_item_import_from_file_activate, None,url)
|
||||||
urls.append(str(url))
|
urls.append(str(url))
|
||||||
|
|
||||||
print >>sys.stderr,("open Files :",urls)
|
print(("open Files :",urls), file=sys.stderr)
|
||||||
result = NSAppleEventDescriptor.descriptorWithInt32_(42)
|
result = NSAppleEventDescriptor.descriptorWithInt32_(42)
|
||||||
reply.setParamDescriptor_forKeyword_(result, aeKeyword('----'))
|
reply.setParamDescriptor_forKeyword_(result, aeKeyword('----'))
|
||||||
|
|
||||||
|
@ -88,7 +91,7 @@ try:
|
||||||
fileURLData = filelist.data()
|
fileURLData = filelist.data()
|
||||||
url = buffer(fileURLData.bytes(),0,fileURLData.length())
|
url = buffer(fileURLData.bytes(),0,fileURLData.length())
|
||||||
url = str(url)
|
url = str(url)
|
||||||
print >>sys.stderr,("Subscribe to :"+url)
|
print(("Subscribe to :"+url), file=sys.stderr)
|
||||||
util.idle_add(self.gp.subscribe_to_url, url)
|
util.idle_add(self.gp.subscribe_to_url, url)
|
||||||
|
|
||||||
result = NSAppleEventDescriptor.descriptorWithInt32_(42)
|
result = NSAppleEventDescriptor.descriptorWithInt32_(42)
|
||||||
|
@ -97,9 +100,9 @@ try:
|
||||||
# global reference to the handler (mustn't be destroyed)
|
# global reference to the handler (mustn't be destroyed)
|
||||||
handler = gPodderEventHandler.alloc().init()
|
handler = gPodderEventHandler.alloc().init()
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print >> sys.stderr, """
|
print("""
|
||||||
Warning: pyobjc not found. Disabling "Subscribe with" events handling
|
Warning: pyobjc not found. Disabling "Subscribe with" events handling
|
||||||
"""
|
""", file=sys.stderr)
|
||||||
handler = None
|
handler = None
|
||||||
|
|
||||||
def register_handlers(gp):
|
def register_handlers(gp):
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -38,14 +38,15 @@ logger = logging.getLogger(__name__)
|
||||||
from gpodder.gtkui import draw
|
from gpodder.gtkui import draw
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import gtk
|
from gi.repository import Gtk
|
||||||
import gobject
|
from gi.repository import GObject
|
||||||
|
from gi.repository import GdkPixbuf
|
||||||
import cgi
|
import cgi
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import gio
|
from gi.repository import Gio
|
||||||
have_gio = True
|
have_gio = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
have_gio = False
|
have_gio = False
|
||||||
|
@ -140,15 +141,17 @@ class BackgroundUpdate(object):
|
||||||
return bool(self.episodes)
|
return bool(self.episodes)
|
||||||
|
|
||||||
|
|
||||||
class EpisodeListModel(gtk.ListStore):
|
class EpisodeListModel(Gtk.ListStore):
|
||||||
C_URL, C_TITLE, C_FILESIZE_TEXT, C_EPISODE, C_STATUS_ICON, \
|
C_URL, C_TITLE, C_FILESIZE_TEXT, C_EPISODE, C_STATUS_ICON, \
|
||||||
C_PUBLISHED_TEXT, C_DESCRIPTION, C_TOOLTIP, \
|
C_PUBLISHED_TEXT, C_DESCRIPTION, C_TOOLTIP, \
|
||||||
C_VIEW_SHOW_UNDELETED, C_VIEW_SHOW_DOWNLOADED, \
|
C_VIEW_SHOW_UNDELETED, C_VIEW_SHOW_DOWNLOADED, \
|
||||||
C_VIEW_SHOW_UNPLAYED, C_FILESIZE, C_PUBLISHED, \
|
C_VIEW_SHOW_UNPLAYED, C_FILESIZE, C_PUBLISHED, \
|
||||||
C_TIME, C_TIME_VISIBLE, C_TOTAL_TIME, \
|
C_TIME, C_TIME_VISIBLE, C_TOTAL_TIME, \
|
||||||
C_LOCKED = range(17)
|
C_LOCKED = list(range(17))
|
||||||
|
|
||||||
VIEW_ALL, VIEW_UNDELETED, VIEW_DOWNLOADED, VIEW_UNPLAYED = range(4)
|
VIEW_ALL, VIEW_UNDELETED, VIEW_DOWNLOADED, VIEW_UNPLAYED = list(range(4))
|
||||||
|
|
||||||
|
VIEWS = ['VIEW_ALL', 'VIEW_UNDELETED', 'VIEW_DOWNLOADED', 'VIEW_UNPLAYED']
|
||||||
|
|
||||||
# In which steps the UI is updated for "loading" animations
|
# In which steps the UI is updated for "loading" animations
|
||||||
_UI_UPDATE_STEP = .03
|
_UI_UPDATE_STEP = .03
|
||||||
|
@ -157,9 +160,9 @@ class EpisodeListModel(gtk.ListStore):
|
||||||
PROGRESS_STEPS = 20
|
PROGRESS_STEPS = 20
|
||||||
|
|
||||||
def __init__(self, config, on_filter_changed=lambda has_episodes: None):
|
def __init__(self, config, on_filter_changed=lambda has_episodes: None):
|
||||||
gtk.ListStore.__init__(self, str, str, str, object, \
|
Gtk.ListStore.__init__(self, str, str, str, object, \
|
||||||
str, str, str, str, bool, bool, bool, \
|
str, str, str, str, bool, bool, bool, \
|
||||||
gobject.TYPE_INT64, gobject.TYPE_INT64, str, bool, gobject.TYPE_INT64, bool)
|
GObject.TYPE_INT64, GObject.TYPE_INT64, str, bool, GObject.TYPE_INT64, bool)
|
||||||
|
|
||||||
self._config = config
|
self._config = config
|
||||||
|
|
||||||
|
@ -169,7 +172,7 @@ class EpisodeListModel(gtk.ListStore):
|
||||||
|
|
||||||
# Filter to allow hiding some episodes
|
# Filter to allow hiding some episodes
|
||||||
self._filter = self.filter_new()
|
self._filter = self.filter_new()
|
||||||
self._sorter = gtk.TreeModelSort(self._filter)
|
self._sorter = Gtk.TreeModelSort(self._filter)
|
||||||
self._view_mode = self.VIEW_ALL
|
self._view_mode = self.VIEW_ALL
|
||||||
self._search_term = None
|
self._search_term = None
|
||||||
self._search_term_eql = None
|
self._search_term_eql = None
|
||||||
|
@ -182,8 +185,8 @@ class EpisodeListModel(gtk.ListStore):
|
||||||
self.ICON_VIDEO_FILE = 'video-x-generic'
|
self.ICON_VIDEO_FILE = 'video-x-generic'
|
||||||
self.ICON_IMAGE_FILE = 'image-x-generic'
|
self.ICON_IMAGE_FILE = 'image-x-generic'
|
||||||
self.ICON_GENERIC_FILE = 'text-x-generic'
|
self.ICON_GENERIC_FILE = 'text-x-generic'
|
||||||
self.ICON_DOWNLOADING = gtk.STOCK_GO_DOWN
|
self.ICON_DOWNLOADING = Gtk.STOCK_GO_DOWN
|
||||||
self.ICON_DELETED = gtk.STOCK_DELETE
|
self.ICON_DELETED = Gtk.STOCK_DELETE
|
||||||
|
|
||||||
self.background_update = None
|
self.background_update = None
|
||||||
self.background_update_tag = None
|
self.background_update_tag = None
|
||||||
|
@ -201,7 +204,7 @@ class EpisodeListModel(gtk.ListStore):
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _filter_visible_func(self, model, iter):
|
def _filter_visible_func(self, model, iter, misc):
|
||||||
# If searching is active, set visibility based on search text
|
# If searching is active, set visibility based on search text
|
||||||
if self._search_term is not None:
|
if self._search_term is not None:
|
||||||
episode = model.get_value(iter, self.C_EPISODE)
|
episode = model.get_value(iter, self.C_EPISODE)
|
||||||
|
@ -210,7 +213,7 @@ class EpisodeListModel(gtk.ListStore):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self._search_term_eql.match(episode)
|
return self._search_term_eql.match(episode)
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if self._view_mode == self.VIEW_ALL:
|
if self._view_mode == self.VIEW_ALL:
|
||||||
|
@ -314,10 +317,10 @@ class EpisodeListModel(gtk.ListStore):
|
||||||
|
|
||||||
def _update_from_episodes(self, episodes, include_description):
|
def _update_from_episodes(self, episodes, include_description):
|
||||||
if self.background_update_tag is not None:
|
if self.background_update_tag is not None:
|
||||||
gobject.source_remove(self.background_update_tag)
|
GObject.source_remove(self.background_update_tag)
|
||||||
|
|
||||||
self.background_update = BackgroundUpdate(self, episodes, include_description)
|
self.background_update = BackgroundUpdate(self, episodes, include_description)
|
||||||
self.background_update_tag = gobject.idle_add(self._update_background)
|
self.background_update_tag = GObject.idle_add(self._update_background)
|
||||||
|
|
||||||
def _update_background(self):
|
def _update_background(self):
|
||||||
if self.background_update is not None:
|
if self.background_update is not None:
|
||||||
|
@ -349,7 +352,7 @@ class EpisodeListModel(gtk.ListStore):
|
||||||
def update_by_filter_iter(self, iter, include_description=False):
|
def update_by_filter_iter(self, iter, include_description=False):
|
||||||
# Convenience function for use by "outside" methods that use iters
|
# Convenience function for use by "outside" methods that use iters
|
||||||
# from the filtered episode list model (i.e. all UI things normally)
|
# from the filtered episode list model (i.e. all UI things normally)
|
||||||
iter = self._sorter.convert_iter_to_child_iter(None, iter)
|
iter = self._sorter.convert_iter_to_child_iter(iter)
|
||||||
self.update_by_iter(self._filter.convert_iter_to_child_iter(iter),
|
self.update_by_iter(self._filter.convert_iter_to_child_iter(iter),
|
||||||
include_description)
|
include_description)
|
||||||
|
|
||||||
|
@ -362,7 +365,7 @@ class EpisodeListModel(gtk.ListStore):
|
||||||
view_show_undeleted = True
|
view_show_undeleted = True
|
||||||
view_show_downloaded = False
|
view_show_downloaded = False
|
||||||
view_show_unplayed = False
|
view_show_unplayed = False
|
||||||
icon_theme = gtk.icon_theme_get_default()
|
icon_theme = Gtk.IconTheme.get_default()
|
||||||
|
|
||||||
if episode.downloading:
|
if episode.downloading:
|
||||||
tooltip.append('%s %d%%' % (_('Downloading'),
|
tooltip.append('%s %d%%' % (_('Downloading'),
|
||||||
|
@ -408,9 +411,9 @@ class EpisodeListModel(gtk.ListStore):
|
||||||
|
|
||||||
# Try to find a themed icon for this file
|
# Try to find a themed icon for this file
|
||||||
if filename is not None and have_gio:
|
if filename is not None and have_gio:
|
||||||
file = gio.File(filename)
|
file = Gio.File.new_for_path(filename)
|
||||||
if file.query_exists():
|
if file.query_exists():
|
||||||
file_info = file.query_info('*')
|
file_info = file.query_info('*', Gio.FileQueryInfoFlags.NONE, None)
|
||||||
icon = file_info.get_icon()
|
icon = file_info.get_icon()
|
||||||
for icon_name in icon.get_names():
|
for icon_name in icon.get_names():
|
||||||
if icon_theme.has_icon(icon_name):
|
if icon_theme.has_icon(icon_name):
|
||||||
|
@ -504,12 +507,12 @@ class PodcastChannelProxy(object):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PodcastListModel(gtk.ListStore):
|
class PodcastListModel(Gtk.ListStore):
|
||||||
C_URL, C_TITLE, C_DESCRIPTION, C_PILL, C_CHANNEL, \
|
C_URL, C_TITLE, C_DESCRIPTION, C_PILL, C_CHANNEL, \
|
||||||
C_COVER, C_ERROR, C_PILL_VISIBLE, \
|
C_COVER, C_ERROR, C_PILL_VISIBLE, \
|
||||||
C_VIEW_SHOW_UNDELETED, C_VIEW_SHOW_DOWNLOADED, \
|
C_VIEW_SHOW_UNDELETED, C_VIEW_SHOW_DOWNLOADED, \
|
||||||
C_VIEW_SHOW_UNPLAYED, C_HAS_EPISODES, C_SEPARATOR, \
|
C_VIEW_SHOW_UNPLAYED, C_HAS_EPISODES, C_SEPARATOR, \
|
||||||
C_DOWNLOADS, C_COVER_VISIBLE, C_SECTION = range(16)
|
C_DOWNLOADS, C_COVER_VISIBLE, C_SECTION = list(range(16))
|
||||||
|
|
||||||
SEARCH_COLUMNS = (C_TITLE, C_DESCRIPTION, C_SECTION)
|
SEARCH_COLUMNS = (C_TITLE, C_DESCRIPTION, C_SECTION)
|
||||||
|
|
||||||
|
@ -518,8 +521,8 @@ class PodcastListModel(gtk.ListStore):
|
||||||
return model.get_value(iter, cls.C_SEPARATOR)
|
return model.get_value(iter, cls.C_SEPARATOR)
|
||||||
|
|
||||||
def __init__(self, cover_downloader):
|
def __init__(self, cover_downloader):
|
||||||
gtk.ListStore.__init__(self, str, str, str, gtk.gdk.Pixbuf, \
|
Gtk.ListStore.__init__(self, str, str, str, GdkPixbuf.Pixbuf, \
|
||||||
object, gtk.gdk.Pixbuf, str, bool, bool, bool, bool, \
|
object, GdkPixbuf.Pixbuf, str, bool, bool, bool, bool, \
|
||||||
bool, bool, int, bool, str)
|
bool, bool, int, bool, str)
|
||||||
|
|
||||||
# Filter to allow hiding some episodes
|
# Filter to allow hiding some episodes
|
||||||
|
@ -534,7 +537,7 @@ class PodcastListModel(gtk.ListStore):
|
||||||
|
|
||||||
self.ICON_DISABLED = 'gtk-media-pause'
|
self.ICON_DISABLED = 'gtk-media-pause'
|
||||||
|
|
||||||
def _filter_visible_func(self, model, iter):
|
def _filter_visible_func(self, model, iter, misc):
|
||||||
# If searching is active, set visibility based on search text
|
# If searching is active, set visibility based on search text
|
||||||
if self._search_term is not None:
|
if self._search_term is not None:
|
||||||
if model.get_value(iter, self.C_CHANNEL) == SectionMarker:
|
if model.get_value(iter, self.C_CHANNEL) == SectionMarker:
|
||||||
|
@ -608,14 +611,14 @@ class PodcastListModel(gtk.ListStore):
|
||||||
if pixbuf.get_width() > self._max_image_side:
|
if pixbuf.get_width() > self._max_image_side:
|
||||||
f = float(self._max_image_side)/pixbuf.get_width()
|
f = float(self._max_image_side)/pixbuf.get_width()
|
||||||
(width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
|
(width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
|
||||||
pixbuf = pixbuf.scale_simple(width, height, gtk.gdk.INTERP_BILINEAR)
|
pixbuf = pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR)
|
||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
# Resize if too high
|
# Resize if too high
|
||||||
if pixbuf.get_height() > self._max_image_side:
|
if pixbuf.get_height() > self._max_image_side:
|
||||||
f = float(self._max_image_side)/pixbuf.get_height()
|
f = float(self._max_image_side)/pixbuf.get_height()
|
||||||
(width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
|
(width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
|
||||||
pixbuf = pixbuf.scale_simple(width, height, gtk.gdk.INTERP_BILINEAR)
|
pixbuf = pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR)
|
||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
if changed:
|
if changed:
|
||||||
|
@ -632,7 +635,7 @@ class PodcastListModel(gtk.ListStore):
|
||||||
|
|
||||||
def _overlay_pixbuf(self, pixbuf, icon):
|
def _overlay_pixbuf(self, pixbuf, icon):
|
||||||
try:
|
try:
|
||||||
icon_theme = gtk.icon_theme_get_default()
|
icon_theme = Gtk.IconTheme.get_default()
|
||||||
emblem = icon_theme.load_icon(icon, self._max_image_side/2, 0)
|
emblem = icon_theme.load_icon(icon, self._max_image_side/2, 0)
|
||||||
(width, height) = (emblem.get_width(), emblem.get_height())
|
(width, height) = (emblem.get_width(), emblem.get_height())
|
||||||
xpos = pixbuf.get_width() - width
|
xpos = pixbuf.get_width() - width
|
||||||
|
@ -643,7 +646,7 @@ class PodcastListModel(gtk.ListStore):
|
||||||
(width, height) = (emblem.get_width(), emblem.get_height())
|
(width, height) = (emblem.get_width(), emblem.get_height())
|
||||||
xpos = pixbuf.get_width() - width
|
xpos = pixbuf.get_width() - width
|
||||||
ypos = pixbuf.get_height() - height
|
ypos = pixbuf.get_height() - height
|
||||||
emblem.composite(pixbuf, xpos, ypos, width, height, xpos, ypos, 1, 1, gtk.gdk.INTERP_BILINEAR, 255)
|
emblem.composite(pixbuf, xpos, ypos, width, height, xpos, ypos, 1, 1, GdkPixbuf.InterpType.BILINEAR, 255)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -654,11 +657,11 @@ class PodcastListModel(gtk.ListStore):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
loader = gtk.gdk.PixbufLoader('png')
|
loader = GdkPixbuf.PixbufLoader()
|
||||||
loader.write(channel.cover_thumb)
|
loader.write(channel.cover_thumb)
|
||||||
loader.close()
|
loader.close()
|
||||||
return loader.get_pixbuf()
|
return loader.get_pixbuf()
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
logger.warn('Could not load cached cover art for %s', channel.url, exc_info=True)
|
logger.warn('Could not load cached cover art for %s', channel.url, exc_info=True)
|
||||||
channel.cover_thumb = None
|
channel.cover_thumb = None
|
||||||
channel.save()
|
channel.save()
|
||||||
|
@ -666,8 +669,11 @@ class PodcastListModel(gtk.ListStore):
|
||||||
|
|
||||||
def _save_cached_thumb(self, channel, pixbuf):
|
def _save_cached_thumb(self, channel, pixbuf):
|
||||||
bufs = []
|
bufs = []
|
||||||
pixbuf.save_to_callback(lambda buf, data: data.append(buf), 'png', {}, bufs)
|
def save_callback(buf, length, user_data):
|
||||||
channel.cover_thumb = buffer(''.join(bufs))
|
user_data.append(buf)
|
||||||
|
return True
|
||||||
|
pixbuf.save_to_callbackv(save_callback, bufs, 'png', [None], [])
|
||||||
|
channel.cover_thumb = bytes(b''.join(bufs))
|
||||||
channel.save()
|
channel.save()
|
||||||
|
|
||||||
def _get_cover_image(self, channel, add_overlay=False):
|
def _get_cover_image(self, channel, add_overlay=False):
|
||||||
|
@ -799,7 +805,7 @@ class PodcastListModel(gtk.ListStore):
|
||||||
def iter_is_first_row(self, iter):
|
def iter_is_first_row(self, iter):
|
||||||
iter = self._filter.convert_iter_to_child_iter(iter)
|
iter = self._filter.convert_iter_to_child_iter(iter)
|
||||||
path = self.get_path(iter)
|
path = self.get_path(iter)
|
||||||
return (path == (0,))
|
return (path == Gtk.TreePath.new_first())
|
||||||
|
|
||||||
def update_by_filter_iter(self, iter):
|
def update_by_filter_iter(self, iter):
|
||||||
self.update_by_iter(self._filter.convert_iter_to_child_iter(iter))
|
self.update_by_iter(self._filter.convert_iter_to_child_iter(iter))
|
||||||
|
@ -828,8 +834,11 @@ class PodcastListModel(gtk.ListStore):
|
||||||
if isinstance(c, GPodcast) and c.section == section]
|
if isinstance(c, GPodcast) and c.section == section]
|
||||||
|
|
||||||
# Calculate the stats over all podcasts of this section
|
# Calculate the stats over all podcasts of this section
|
||||||
total, deleted, new, downloaded, unplayed = map(sum,
|
if len(channels) is 0:
|
||||||
zip(*[c.get_statistics() for c in channels]))
|
total = deleted = new = downloaded = unplayed = 0
|
||||||
|
else:
|
||||||
|
total, deleted, new, downloaded, unplayed = list(map(sum,
|
||||||
|
list(zip(*[c.get_statistics() for c in channels]))))
|
||||||
|
|
||||||
# We could customized the section header here with the list
|
# We could customized the section header here with the list
|
||||||
# of channels and their stats (i.e. add some "new" indicator)
|
# of channels and their stats (i.e. add some "new" indicator)
|
||||||
|
|
|
@ -34,7 +34,8 @@ logger = logging.getLogger(__name__)
|
||||||
from gpodder import util
|
from gpodder import util
|
||||||
from gpodder import coverart
|
from gpodder import coverart
|
||||||
|
|
||||||
import gtk
|
from gi.repository import Gtk
|
||||||
|
from gi.repository import GdkPixbuf
|
||||||
|
|
||||||
|
|
||||||
class CoverDownloader(ObservableService):
|
class CoverDownloader(ObservableService):
|
||||||
|
@ -117,15 +118,15 @@ class CoverDownloader(ObservableService):
|
||||||
pixbuf = None
|
pixbuf = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
pixbuf = gtk.gdk.pixbuf_new_from_file(filename)
|
pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename)
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
logger.warn('Cannot load cover art', exc_info=True)
|
logger.warn('Cannot load cover art', exc_info=True)
|
||||||
if pixbuf is None and filename.startswith(channel.cover_file):
|
if pixbuf is None and filename.startswith(channel.cover_file):
|
||||||
logger.info('Deleting broken cover: %s', filename)
|
logger.info('Deleting broken cover: %s', filename)
|
||||||
util.delete_file(filename)
|
util.delete_file(filename)
|
||||||
filename = get_filename()
|
filename = get_filename()
|
||||||
try:
|
try:
|
||||||
pixbuf = gtk.gdk.pixbuf_new_from_file(filename)
|
pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn('Corrupt cover art on server, deleting', exc_info=True)
|
logger.warn('Corrupt cover art on server, deleting', exc_info=True)
|
||||||
util.delete_file(filename)
|
util.delete_file(filename)
|
||||||
|
|
|
@ -16,39 +16,54 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import gtk
|
from gi.repository import Gtk
|
||||||
import gtk.gdk
|
from gi.repository import Gdk
|
||||||
import gobject
|
from gi.repository import Pango
|
||||||
import pango
|
|
||||||
import os
|
|
||||||
import cgi
|
|
||||||
|
|
||||||
|
import html
|
||||||
|
import logging
|
||||||
|
|
||||||
import gpodder
|
import gpodder
|
||||||
|
|
||||||
_ = gpodder.gettext
|
|
||||||
|
|
||||||
import logging
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
from gpodder import util
|
from gpodder import util
|
||||||
from gpodder.gtkui.draw import draw_text_box_centered
|
from gpodder.gtkui.draw import draw_text_box_centered
|
||||||
|
|
||||||
|
_ = gpodder.gettext
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
has_webkit2 = False
|
||||||
|
try:
|
||||||
|
import gi
|
||||||
|
gi.require_version('WebKit2', '4.0')
|
||||||
|
from gi.repository import WebKit2
|
||||||
|
has_webkit2 = True
|
||||||
|
except (ImportError, ValueError):
|
||||||
|
logger.info('No WebKit2 gobject bindings, so no HTML shownotes')
|
||||||
|
|
||||||
|
|
||||||
|
def get_shownotes(enable_html, pane):
|
||||||
|
if enable_html and has_webkit2:
|
||||||
|
return gPodderShownotesHTML(pane)
|
||||||
|
else:
|
||||||
|
return gPodderShownotesText(pane)
|
||||||
|
|
||||||
|
|
||||||
class gPodderShownotes:
|
class gPodderShownotes:
|
||||||
def __init__(self, shownotes_pane):
|
def __init__(self, shownotes_pane):
|
||||||
self.shownotes_pane = shownotes_pane
|
self.shownotes_pane = shownotes_pane
|
||||||
|
|
||||||
self.scrolled_window = gtk.ScrolledWindow()
|
self.scrolled_window = Gtk.ScrolledWindow()
|
||||||
self.scrolled_window.set_shadow_type(gtk.SHADOW_IN)
|
self.scrolled_window.set_shadow_type(Gtk.ShadowType.IN)
|
||||||
self.scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
|
self.scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||||
self.scrolled_window.add(self.init())
|
self.scrolled_window.add(self.init())
|
||||||
self.scrolled_window.show_all()
|
self.scrolled_window.show_all()
|
||||||
|
|
||||||
self.da_message = gtk.DrawingArea()
|
self.da_message = Gtk.DrawingArea()
|
||||||
self.da_message.connect('expose-event', \
|
self.da_message.set_property('expand', True)
|
||||||
self.on_shownotes_message_expose_event)
|
self.da_message.connect('draw', self.on_shownotes_message_expose_event)
|
||||||
self.shownotes_pane.add(self.da_message)
|
self.shownotes_pane.add(self.da_message)
|
||||||
self.shownotes_pane.add(self.scrolled_window)
|
self.shownotes_pane.add(self.scrolled_window)
|
||||||
|
|
||||||
|
@ -90,19 +105,14 @@ class gPodderShownotes:
|
||||||
else:
|
else:
|
||||||
self.show_pane(selected_episodes)
|
self.show_pane(selected_episodes)
|
||||||
|
|
||||||
def on_shownotes_message_expose_event(self, drawingarea, event):
|
def on_shownotes_message_expose_event(self, drawingarea, ctx):
|
||||||
ctx = event.window.cairo_create()
|
|
||||||
ctx.rectangle(event.area.x, event.area.y, \
|
|
||||||
event.area.width, event.area.height)
|
|
||||||
ctx.clip()
|
|
||||||
|
|
||||||
# paint the background white
|
# paint the background white
|
||||||
colormap = event.window.get_colormap()
|
ctx.set_source_rgba(1, 1, 1)
|
||||||
gc = event.window.new_gc(foreground=colormap.alloc_color('white'))
|
x1, y1, x2, y2 = ctx.clip_extents()
|
||||||
event.window.draw_rectangle(gc, True, event.area.x, event.area.y, \
|
ctx.rectangle(x1, y1, x2 - x1, y2 - y1)
|
||||||
event.area.width, event.area.height)
|
ctx.fill()
|
||||||
|
|
||||||
x, y, width, height, depth = event.window.get_geometry()
|
width, height = drawingarea.get_allocated_width(), drawingarea.get_allocated_height(),
|
||||||
text = _('Please select an episode')
|
text = _('Please select an episode')
|
||||||
draw_text_box_centered(ctx, drawingarea, width, height, text, None, None)
|
draw_text_box_centered(ctx, drawingarea, width, height, text, None, None)
|
||||||
return False
|
return False
|
||||||
|
@ -110,19 +120,18 @@ class gPodderShownotes:
|
||||||
|
|
||||||
class gPodderShownotesText(gPodderShownotes):
|
class gPodderShownotesText(gPodderShownotes):
|
||||||
def init(self):
|
def init(self):
|
||||||
self.text_view = gtk.TextView()
|
self.text_view = Gtk.TextView()
|
||||||
self.text_view.set_wrap_mode(gtk.WRAP_WORD_CHAR)
|
self.text_view.set_property('expand', True)
|
||||||
|
self.text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
|
||||||
self.text_view.set_border_width(10)
|
self.text_view.set_border_width(10)
|
||||||
self.text_view.set_editable(False)
|
self.text_view.set_editable(False)
|
||||||
self.text_view.connect('button-release-event', self.on_button_release)
|
self.text_view.connect('button-release-event', self.on_button_release)
|
||||||
self.text_view.connect('key-press-event', self.on_key_press)
|
self.text_view.connect('key-press-event', self.on_key_press)
|
||||||
self.text_buffer = gtk.TextBuffer()
|
self.text_buffer = Gtk.TextBuffer()
|
||||||
self.text_buffer.create_tag('heading', scale=pango.SCALE_LARGE, weight=pango.WEIGHT_BOLD)
|
self.text_buffer.create_tag('heading', scale=2, weight=Pango.Weight.BOLD)
|
||||||
self.text_buffer.create_tag('subheading', scale=pango.SCALE_SMALL)
|
self.text_buffer.create_tag('subheading', scale=1.5)
|
||||||
self.text_buffer.create_tag('hyperlink', foreground="#0000FF", underline=pango.UNDERLINE_SINGLE)
|
self.text_buffer.create_tag('hyperlink', foreground="#0000FF", underline=Pango.Underline.SINGLE)
|
||||||
self.text_view.set_buffer(self.text_buffer)
|
self.text_view.set_buffer(self.text_buffer)
|
||||||
self.text_view.modify_bg(gtk.STATE_NORMAL,
|
|
||||||
gtk.gdk.color_parse('#ffffff'))
|
|
||||||
return self.text_view
|
return self.text_view
|
||||||
|
|
||||||
def update(self, heading, subheading, episode):
|
def update(self, heading, subheading, episode):
|
||||||
|
@ -149,7 +158,7 @@ class gPodderShownotesText(gPodderShownotes):
|
||||||
self.activate_links()
|
self.activate_links()
|
||||||
|
|
||||||
def on_key_press(self, widget, event):
|
def on_key_press(self, widget, event):
|
||||||
if gtk.gdk.keyval_name(event.keyval) == 'Return':
|
if event.keyval == Gdk.KEY_Return:
|
||||||
self.activate_links()
|
self.activate_links()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -161,3 +170,123 @@ class gPodderShownotesText(gPodderShownotes):
|
||||||
target = next((url for start, end, url in self.hyperlinks if start < pos < end), None)
|
target = next((url for start, end, url in self.hyperlinks if start < pos < end), None)
|
||||||
if target is not None:
|
if target is not None:
|
||||||
util.open_website(target)
|
util.open_website(target)
|
||||||
|
|
||||||
|
|
||||||
|
class gPodderShownotesHTML(gPodderShownotes):
|
||||||
|
def init(self):
|
||||||
|
# basic restrictions
|
||||||
|
settings = WebKit2.Settings()
|
||||||
|
settings.set_enable_java(False)
|
||||||
|
settings.set_enable_plugins(False)
|
||||||
|
settings.set_enable_javascript(False)
|
||||||
|
self.html_view = WebKit2.WebView.new_with_settings(settings)
|
||||||
|
self.html_view.set_property('expand', True)
|
||||||
|
self.html_view.connect('mouse-target-changed', self.on_mouse_over)
|
||||||
|
self.html_view.connect('context-menu', self.on_context_menu)
|
||||||
|
self.html_view.connect('decide-policy', self.on_decide_policy)
|
||||||
|
self.header = Gtk.Label.new()
|
||||||
|
self.header.set_halign(Gtk.Align.START)
|
||||||
|
self.header.set_valign(Gtk.Align.START)
|
||||||
|
self.header.set_property('margin', 10)
|
||||||
|
self.header.set_selectable(True)
|
||||||
|
self.status = Gtk.Label.new()
|
||||||
|
self.status.set_halign(Gtk.Align.START)
|
||||||
|
self.status.set_valign(Gtk.Align.END)
|
||||||
|
self.set_status(None)
|
||||||
|
grid = Gtk.Grid()
|
||||||
|
grid.attach(self.header, 0, 0, 1, 1)
|
||||||
|
grid.attach(self.html_view, 0, 1, 1, 1)
|
||||||
|
grid.attach(self.status, 0, 2, 1, 1)
|
||||||
|
return grid
|
||||||
|
|
||||||
|
def update(self, heading, subheading, episode):
|
||||||
|
tmpl = '<span size="x-large" font_weight="bold">%s</span>\n' \
|
||||||
|
+ '<span size="medium">%s</span>'
|
||||||
|
self.header.set_markup(tmpl % (html.escape(heading), html.escape(subheading)))
|
||||||
|
|
||||||
|
if episode.has_website_link:
|
||||||
|
self._base_uri = episode.link
|
||||||
|
else:
|
||||||
|
self._base_uri = episode.channel.url
|
||||||
|
|
||||||
|
# for incomplete base URI (e.g. http://919.noagendanotes.com)
|
||||||
|
baseURI = urlparse(self._base_uri)
|
||||||
|
if baseURI.path == '':
|
||||||
|
self._base_uri += '/'
|
||||||
|
self._loaded = False
|
||||||
|
|
||||||
|
description_html = episode.description_html
|
||||||
|
if description_html:
|
||||||
|
self.html_view.load_html(description_html, self._base_uri)
|
||||||
|
else:
|
||||||
|
self.html_view.load_plain_text(episode.description)
|
||||||
|
|
||||||
|
def on_mouse_over(self, webview, hit_test_result, modifiers):
|
||||||
|
if hit_test_result.context_is_link():
|
||||||
|
self.set_status(hit_test_result.get_link_uri())
|
||||||
|
else:
|
||||||
|
self.set_status(None)
|
||||||
|
|
||||||
|
def on_context_menu(self, webview, context_menu, event, hit_test_result):
|
||||||
|
whitelist_actions = [
|
||||||
|
WebKit2.ContextMenuAction.NO_ACTION,
|
||||||
|
WebKit2.ContextMenuAction.STOP,
|
||||||
|
WebKit2.ContextMenuAction.RELOAD,
|
||||||
|
WebKit2.ContextMenuAction.COPY,
|
||||||
|
WebKit2.ContextMenuAction.CUT,
|
||||||
|
WebKit2.ContextMenuAction.PASTE,
|
||||||
|
WebKit2.ContextMenuAction.DELETE,
|
||||||
|
WebKit2.ContextMenuAction.SELECT_ALL,
|
||||||
|
WebKit2.ContextMenuAction.INPUT_METHODS,
|
||||||
|
WebKit2.ContextMenuAction.COPY_VIDEO_LINK_TO_CLIPBOARD,
|
||||||
|
WebKit2.ContextMenuAction.COPY_AUDIO_LINK_TO_CLIPBOARD,
|
||||||
|
WebKit2.ContextMenuAction.COPY_LINK_TO_CLIPBOARD,
|
||||||
|
WebKit2.ContextMenuAction.COPY_IMAGE_TO_CLIPBOARD,
|
||||||
|
WebKit2.ContextMenuAction.COPY_IMAGE_URL_TO_CLIPBOARD
|
||||||
|
]
|
||||||
|
items = context_menu.get_items()
|
||||||
|
for item in items:
|
||||||
|
if item.get_stock_action() not in whitelist_actions:
|
||||||
|
context_menu.remove(item)
|
||||||
|
if hit_test_result.get_context() == WebKit2.HitTestResultContext.DOCUMENT:
|
||||||
|
item = self.create_open_item(
|
||||||
|
'shownotes-in-browser',
|
||||||
|
_('Open shownotes in web browser'),
|
||||||
|
self._base_uri)
|
||||||
|
context_menu.insert(item, -1)
|
||||||
|
elif hit_test_result.context_is_link():
|
||||||
|
item = self.create_open_item(
|
||||||
|
'link-in-browser',
|
||||||
|
_('Open link in web browser'),
|
||||||
|
hit_test_result.get_link_uri())
|
||||||
|
context_menu.insert(item, -1)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def on_decide_policy(self, webview, decision, decision_type):
|
||||||
|
if decision_type == WebKit2.PolicyDecisionType.NEW_WINDOW_ACTION:
|
||||||
|
decision.ignore()
|
||||||
|
return False
|
||||||
|
elif decision_type == WebKit2.PolicyDecisionType.NAVIGATION_ACTION:
|
||||||
|
req = decision.get_request()
|
||||||
|
# about:blank is for plain text shownotes
|
||||||
|
if req.get_uri() in (self._base_uri, 'about:blank'):
|
||||||
|
decision.use()
|
||||||
|
else:
|
||||||
|
logger.debug("refusing to go to %s (base URI=%s)", req.get_uri(), self._base_uri)
|
||||||
|
decision.ignore()
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
decision.use()
|
||||||
|
return False
|
||||||
|
|
||||||
|
def on_open_in_browser(self, action):
|
||||||
|
util.open_website(action.url)
|
||||||
|
|
||||||
|
def create_open_item(self, name, label, url):
|
||||||
|
action = Gtk.Action.new(name, label, None, Gtk.STOCK_OPEN)
|
||||||
|
action.url = url
|
||||||
|
action.connect('activate', self.on_open_in_browser)
|
||||||
|
return WebKit2.ContextMenuItem.new(action)
|
||||||
|
|
||||||
|
def set_status(self, text):
|
||||||
|
self.status.set_label(text or " ")
|
||||||
|
|
|
@ -24,36 +24,37 @@
|
||||||
# Thomas Perl <thp@gpodder.org> 2009-03-31
|
# Thomas Perl <thp@gpodder.org> 2009-03-31
|
||||||
#
|
#
|
||||||
|
|
||||||
import gtk
|
from gi.repository import Gdk
|
||||||
import gobject
|
from gi.repository import Gtk
|
||||||
import pango
|
from gi.repository import GObject
|
||||||
|
from gi.repository import Pango
|
||||||
|
|
||||||
import cgi
|
import cgi
|
||||||
|
|
||||||
class SimpleMessageArea(gtk.HBox):
|
class SimpleMessageArea(Gtk.HBox):
|
||||||
"""A simple, yellow message area. Inspired by gedit.
|
"""A simple, yellow message area. Inspired by gedit.
|
||||||
|
|
||||||
Original C source code:
|
Original C source code:
|
||||||
http://svn.gnome.org/viewvc/gedit/trunk/gedit/gedit-message-area.c
|
http://svn.gnome.org/viewvc/gedit/trunk/gedit/gedit-message-area.c
|
||||||
"""
|
"""
|
||||||
def __init__(self, message, buttons=()):
|
def __init__(self, message, buttons=()):
|
||||||
gtk.HBox.__init__(self, spacing=6)
|
Gtk.HBox.__init__(self, spacing=6)
|
||||||
self.set_border_width(6)
|
self.set_border_width(6)
|
||||||
self.__in_style_set = False
|
self.__in_style_updated = False
|
||||||
self.connect('style-set', self.__style_set)
|
self.connect('style-updated', self.__style_updated)
|
||||||
self.connect('expose-event', self.__expose_event)
|
self.connect('draw', self.__on_draw)
|
||||||
|
|
||||||
self.__label = gtk.Label()
|
self.__label = Gtk.Label()
|
||||||
self.__label.set_alignment(0.0, 0.5)
|
self.__label.set_alignment(0.0, 0.5)
|
||||||
self.__label.set_line_wrap(False)
|
self.__label.set_line_wrap(False)
|
||||||
self.__label.set_ellipsize(pango.ELLIPSIZE_END)
|
self.__label.set_ellipsize(Pango.EllipsizeMode.END)
|
||||||
self.__label.set_markup('<b>%s</b>' % cgi.escape(message))
|
self.__label.set_markup('<b>%s</b>' % cgi.escape(message))
|
||||||
self.pack_start(self.__label, expand=True, fill=True)
|
self.pack_start(self.__label, True, True, 0)
|
||||||
|
|
||||||
hbox = gtk.HBox()
|
hbox = Gtk.HBox()
|
||||||
for button in buttons:
|
for button in buttons:
|
||||||
hbox.pack_start(button, expand=True, fill=False)
|
hbox.pack_start(button, True, False, 0)
|
||||||
self.pack_start(hbox, expand=False, fill=False)
|
self.pack_start(hbox, False, False, 0)
|
||||||
|
|
||||||
def set_markup(self, markup, line_wrap=True, min_width=3, max_width=100):
|
def set_markup(self, markup, line_wrap=True, min_width=3, max_width=100):
|
||||||
# The longest line should determine the size of the label
|
# The longest line should determine the size of the label
|
||||||
|
@ -66,11 +67,11 @@ class SimpleMessageArea(gtk.HBox):
|
||||||
self.__label.set_markup(markup)
|
self.__label.set_markup(markup)
|
||||||
self.__label.set_line_wrap(line_wrap)
|
self.__label.set_line_wrap(line_wrap)
|
||||||
|
|
||||||
def __style_set(self, widget, previous_style):
|
def __style_updated(self, widget):
|
||||||
if self.__in_style_set:
|
if self.__in_style_updated:
|
||||||
return
|
return
|
||||||
|
|
||||||
w = gtk.Window(gtk.WINDOW_POPUP)
|
w = Gtk.Window(Gtk.WindowType.POPUP)
|
||||||
w.set_name('gtk-tooltip')
|
w.set_name('gtk-tooltip')
|
||||||
w.ensure_style()
|
w.ensure_style()
|
||||||
style = w.get_style()
|
style = w.get_style()
|
||||||
|
@ -84,33 +85,33 @@ class SimpleMessageArea(gtk.HBox):
|
||||||
|
|
||||||
self.queue_draw()
|
self.queue_draw()
|
||||||
|
|
||||||
def __expose_event(self, widget, event):
|
def __on_draw(self, widget, cr):
|
||||||
style = widget.get_style()
|
style = widget.get_style()
|
||||||
rect = widget.get_allocation()
|
x, rect = Gdk.cairo_get_clip_rectangle(cr)
|
||||||
style.paint_flat_box(widget.window, gtk.STATE_NORMAL,
|
Gtk.paint_flat_box(style, cr, Gtk.StateType.NORMAL,
|
||||||
gtk.SHADOW_OUT, None, widget, "tooltip",
|
Gtk.ShadowType.OUT, widget, "tooltip",
|
||||||
rect.x, rect.y, rect.width, rect.height)
|
rect.x, rect.y, rect.width, rect.height)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class SpinningProgressIndicator(gtk.Image):
|
class SpinningProgressIndicator(Gtk.Image):
|
||||||
# Progress indicator loading inspired by glchess from gnome-games-clutter
|
# Progress indicator loading inspired by glchess from gnome-games-clutter
|
||||||
def __init__(self, size=32):
|
def __init__(self, size=32):
|
||||||
gtk.Image.__init__(self)
|
Gtk.Image.__init__(self)
|
||||||
|
|
||||||
self._frames = []
|
self._frames = []
|
||||||
self._frame_id = 0
|
self._frame_id = 0
|
||||||
|
|
||||||
# Load the progress indicator
|
# Load the progress indicator
|
||||||
icon_theme = gtk.icon_theme_get_default()
|
icon_theme = Gtk.IconTheme.get_default()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
icon = icon_theme.load_icon('process-working', size, 0)
|
icon = icon_theme.load_icon('process-working', size, 0)
|
||||||
width, height = icon.get_width(), icon.get_height()
|
width, height = icon.get_width(), icon.get_height()
|
||||||
if width < size or height < size:
|
if width < size or height < size:
|
||||||
size = min(width, height)
|
size = min(width, height)
|
||||||
for row in range(height/size):
|
for row in range(height//size):
|
||||||
for column in range(width/size):
|
for column in range(width//size):
|
||||||
frame = icon.subpixbuf(column*size, row*size, size, size)
|
frame = icon.subpixbuf(column*size, row*size, size, size)
|
||||||
self._frames.append(frame)
|
self._frames.append(frame)
|
||||||
# Remove the first frame (the "idle" icon)
|
# Remove the first frame (the "idle" icon)
|
||||||
|
@ -119,7 +120,7 @@ class SpinningProgressIndicator(gtk.Image):
|
||||||
self.step_animation()
|
self.step_animation()
|
||||||
except:
|
except:
|
||||||
# FIXME: This is not very beautiful :/
|
# FIXME: This is not very beautiful :/
|
||||||
self.set_from_stock(gtk.STOCK_EXECUTE, gtk.ICON_SIZE_BUTTON)
|
self.set_from_icon_name('system-run', Gtk.IconSize.BUTTON)
|
||||||
|
|
||||||
def step_animation(self):
|
def step_animation(self):
|
||||||
if len(self._frames) > 1:
|
if len(self._frames) > 1:
|
||||||
|
|
|
@ -24,13 +24,9 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
from functools import reduce
|
||||||
|
|
||||||
try:
|
import json
|
||||||
# For Python < 2.6, we use the "simplejson" add-on module
|
|
||||||
import simplejson as json
|
|
||||||
except ImportError:
|
|
||||||
# Python 2.6 already ships with a nice "json" module
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
class JsonConfigSubtree(object):
|
class JsonConfigSubtree(object):
|
||||||
|
@ -89,7 +85,7 @@ class JsonConfig(object):
|
||||||
For newly-set keys, on_key_changed is also called. In this case,
|
For newly-set keys, on_key_changed is also called. In this case,
|
||||||
None will be the old_value:
|
None will be the old_value:
|
||||||
|
|
||||||
>>> def callback(*args): print 'callback:', args
|
>>> def callback(*args): print('callback:', args)
|
||||||
>>> c = JsonConfig(on_key_changed=callback)
|
>>> c = JsonConfig(on_key_changed=callback)
|
||||||
>>> c.a.b = 10
|
>>> c.a.b = 10
|
||||||
callback: ('a.b', None, 10)
|
callback: ('a.b', None, 10)
|
||||||
|
@ -102,7 +98,7 @@ class JsonConfig(object):
|
||||||
|
|
||||||
Please note that dict-style access will not call on_key_changed:
|
Please note that dict-style access will not call on_key_changed:
|
||||||
|
|
||||||
>>> def callback(*args): print 'callback:', args
|
>>> def callback(*args): print('callback:', args)
|
||||||
>>> c = JsonConfig(on_key_changed=callback)
|
>>> c = JsonConfig(on_key_changed=callback)
|
||||||
>>> c.a.b = 1 # This works as expected
|
>>> c.a.b = 1 # This works as expected
|
||||||
callback: ('a.b', None, 1)
|
callback: ('a.b', None, 1)
|
||||||
|
@ -129,14 +125,14 @@ class JsonConfig(object):
|
||||||
>>> c = JsonConfig()
|
>>> c = JsonConfig()
|
||||||
>>> c.a.b = 10
|
>>> c.a.b = 10
|
||||||
>>> backup = repr(c)
|
>>> backup = repr(c)
|
||||||
>>> print c.a.b
|
>>> print(c.a.b)
|
||||||
10
|
10
|
||||||
>>> c.a.b = 11
|
>>> c.a.b = 11
|
||||||
>>> print c.a.b
|
>>> print(c.a.b)
|
||||||
11
|
11
|
||||||
>>> c._restore(backup)
|
>>> c._restore(backup)
|
||||||
False
|
False
|
||||||
>>> print c.a.b
|
>>> print(c.a.b)
|
||||||
10
|
10
|
||||||
"""
|
"""
|
||||||
self._data = json.loads(backup)
|
self._data = json.loads(backup)
|
||||||
|
@ -156,7 +152,7 @@ class JsonConfig(object):
|
||||||
work_queue = [(self._data, merge_source)]
|
work_queue = [(self._data, merge_source)]
|
||||||
while work_queue:
|
while work_queue:
|
||||||
data, default = work_queue.pop()
|
data, default = work_queue.pop()
|
||||||
for key, value in default.iteritems():
|
for key, value in default.items():
|
||||||
if key not in data:
|
if key not in data:
|
||||||
# Copy defaults for missing key
|
# Copy defaults for missing key
|
||||||
data[key] = copy.deepcopy(value)
|
data[key] = copy.deepcopy(value)
|
||||||
|
@ -175,7 +171,7 @@ class JsonConfig(object):
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
"""
|
"""
|
||||||
>>> c = JsonConfig('{"a": 1}')
|
>>> c = JsonConfig('{"a": 1}')
|
||||||
>>> print c
|
>>> print(c)
|
||||||
{
|
{
|
||||||
"a": 1
|
"a": 1
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
|
|
||||||
|
|
||||||
# For Python 2.5, we need to request the "with" statement
|
# For Python 2.5, we need to request the "with" statement
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import sqlite3.dbapi2 as sqlite
|
import sqlite3.dbapi2 as sqlite
|
||||||
|
@ -55,7 +55,7 @@ class Store(object):
|
||||||
# necessary. The value None is special-cased and never cast.
|
# necessary. The value None is special-cased and never cast.
|
||||||
cls = o.__class__.__slots__[slot]
|
cls = o.__class__.__slots__[slot]
|
||||||
if value is not None:
|
if value is not None:
|
||||||
if isinstance(value, unicode):
|
if isinstance(value, bytes):
|
||||||
value = value.decode('utf-8')
|
value = value.decode('utf-8')
|
||||||
value = cls(value)
|
value = cls(value)
|
||||||
setattr(o, slot, value)
|
setattr(o, slot, value)
|
||||||
|
@ -66,7 +66,9 @@ class Store(object):
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
|
self.db.isolation_level = None
|
||||||
self.db.execute('VACUUM')
|
self.db.execute('VACUUM')
|
||||||
|
self.db.isolation_level = ''
|
||||||
self.db.close()
|
self.db.close()
|
||||||
|
|
||||||
def _register(self, class_):
|
def _register(self, class_):
|
||||||
|
@ -86,7 +88,7 @@ class Store(object):
|
||||||
', '.join('%s TEXT'%s for s in slots)))
|
', '.join('%s TEXT'%s for s in slots)))
|
||||||
|
|
||||||
def convert(self, v):
|
def convert(self, v):
|
||||||
if isinstance(v, unicode):
|
if isinstance(v, str):
|
||||||
return v
|
return v
|
||||||
elif isinstance(v, str):
|
elif isinstance(v, str):
|
||||||
# XXX: Rewrite ^^^ as "isinstance(v, bytes)" in Python 3
|
# XXX: Rewrite ^^^ as "isinstance(v, bytes)" in Python 3
|
||||||
|
@ -96,7 +98,7 @@ class Store(object):
|
||||||
|
|
||||||
def update(self, o, **kwargs):
|
def update(self, o, **kwargs):
|
||||||
self.remove(o)
|
self.remove(o)
|
||||||
for k, v in kwargs.items():
|
for k, v in list(kwargs.items()):
|
||||||
setattr(o, k, v)
|
setattr(o, k, v)
|
||||||
self.save(o)
|
self.save(o)
|
||||||
|
|
||||||
|
@ -134,9 +136,9 @@ class Store(object):
|
||||||
if kwargs:
|
if kwargs:
|
||||||
sql += ' WHERE %s' % (' AND '.join('%s=?' % k for k in kwargs))
|
sql += ' WHERE %s' % (' AND '.join('%s=?' % k for k in kwargs))
|
||||||
try:
|
try:
|
||||||
self.db.execute(sql, kwargs.values())
|
self.db.execute(sql, list(kwargs.values()))
|
||||||
return True
|
return True
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def remove(self, o):
|
def remove(self, o):
|
||||||
|
@ -164,18 +166,18 @@ class Store(object):
|
||||||
if kwargs:
|
if kwargs:
|
||||||
sql += ' WHERE %s' % (' AND '.join('%s=?' % k for k in kwargs))
|
sql += ' WHERE %s' % (' AND '.join('%s=?' % k for k in kwargs))
|
||||||
try:
|
try:
|
||||||
cur = self.db.execute(sql, kwargs.values())
|
cur = self.db.execute(sql, list(kwargs.values()))
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
raise
|
raise
|
||||||
def apply(row):
|
def apply(row):
|
||||||
o = class_.__new__(class_)
|
o = class_.__new__(class_)
|
||||||
for attr, value in zip(slots, row):
|
for attr, value in zip(slots, row):
|
||||||
try:
|
try:
|
||||||
self._set(o, attr, value)
|
self._set(o, attr, value)
|
||||||
except ValueError, ve:
|
except ValueError as ve:
|
||||||
return None
|
return None
|
||||||
return o
|
return o
|
||||||
return filter(lambda x: x is not None, [apply(row) for row in cur])
|
return [x for x in [apply(row) for row in cur] if x is not None]
|
||||||
|
|
||||||
def get(self, class_, **kwargs):
|
def get(self, class_, **kwargs):
|
||||||
result = self.load(class_, **kwargs)
|
result = self.load(class_, **kwargs)
|
||||||
|
@ -199,7 +201,7 @@ if __name__ == '__main__':
|
||||||
m.save(Person('User %d' % x, x*20) for x in range(50))
|
m.save(Person('User %d' % x, x*20) for x in range(50))
|
||||||
|
|
||||||
p = m.get(Person, id=200)
|
p = m.get(Person, id=200)
|
||||||
print p
|
print(p)
|
||||||
m.remove(p)
|
m.remove(p)
|
||||||
p = m.get(Person, id=200)
|
p = m.get(Person, id=200)
|
||||||
|
|
||||||
|
@ -219,5 +221,5 @@ if __name__ == '__main__':
|
||||||
|
|
||||||
# A schema update takes place here
|
# A schema update takes place here
|
||||||
m.save(Person('User %d' % x, x*20, 'user@home.com') for x in range(50))
|
m.save(Person('User %d' % x, x*20, 'user@home.com') for x in range(50))
|
||||||
print m.load(Person)
|
print(m.load(Person))
|
||||||
|
|
||||||
|
|
|
@ -112,7 +112,7 @@ class PodcastModelObject(object):
|
||||||
o = cls(*args)
|
o = cls(*args)
|
||||||
|
|
||||||
# XXX: all(map(lambda k: hasattr(o, k), d))?
|
# XXX: all(map(lambda k: hasattr(o, k), d))?
|
||||||
for k, v in d.iteritems():
|
for k, v in d.items():
|
||||||
setattr(o, k, v)
|
setattr(o, k, v)
|
||||||
|
|
||||||
return o
|
return o
|
||||||
|
@ -433,7 +433,7 @@ class PodcastEpisode(PodcastModelObject):
|
||||||
if self.download_filename is None and (check_only or not create):
|
if self.download_filename is None and (check_only or not create):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
ext = self.extension(may_call_local_filename=False).encode('utf-8', 'ignore')
|
ext = self.extension(may_call_local_filename=False)
|
||||||
|
|
||||||
if not check_only and (force_update or not self.download_filename):
|
if not check_only and (force_update or not self.download_filename):
|
||||||
# Avoid and catch gPodder bug 1440 and similar situations
|
# Avoid and catch gPodder bug 1440 and similar situations
|
||||||
|
@ -500,8 +500,7 @@ class PodcastEpisode(PodcastModelObject):
|
||||||
self.download_filename = wanted_filename
|
self.download_filename = wanted_filename
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
return os.path.join(util.sanitize_encoding(self.channel.save_dir),
|
return os.path.join(self.channel.save_dir, self.download_filename)
|
||||||
util.sanitize_encoding(self.download_filename))
|
|
||||||
|
|
||||||
def extension(self, may_call_local_filename=True):
|
def extension(self, may_call_local_filename=True):
|
||||||
filename, ext = util.filename_from_url(self.url)
|
filename, ext = util.filename_from_url(self.url)
|
||||||
|
@ -640,10 +639,10 @@ class PodcastEpisode(PodcastModelObject):
|
||||||
class PodcastChannel(PodcastModelObject):
|
class PodcastChannel(PodcastModelObject):
|
||||||
__slots__ = schema.PodcastColumns + ('_common_prefix',)
|
__slots__ = schema.PodcastColumns + ('_common_prefix',)
|
||||||
|
|
||||||
UNICODE_TRANSLATE = {ord(u'ö'): u'o', ord(u'ä'): u'a', ord(u'ü'): u'u'}
|
UNICODE_TRANSLATE = {ord('ö'): 'o', ord('ä'): 'a', ord('ü'): 'u'}
|
||||||
|
|
||||||
# Enumerations for download strategy
|
# Enumerations for download strategy
|
||||||
STRATEGY_DEFAULT, STRATEGY_LATEST = range(2)
|
STRATEGY_DEFAULT, STRATEGY_LATEST = list(range(2))
|
||||||
|
|
||||||
# Description and ordering of strategies
|
# Description and ordering of strategies
|
||||||
STRATEGIES = [
|
STRATEGIES = [
|
||||||
|
@ -812,12 +811,8 @@ class PodcastChannel(PodcastModelObject):
|
||||||
return re.sub('^the ', '', key).translate(cls.UNICODE_TRANSLATE)
|
return re.sub('^the ', '', key).translate(cls.UNICODE_TRANSLATE)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, model, url, create=True, authentication_tokens=None,\
|
def load(cls, model, url, create=True, authentication_tokens=None, max_episodes=0):
|
||||||
max_episodes=0):
|
existing = [p for p in model.get_podcasts() if p.url == url]
|
||||||
if isinstance(url, unicode):
|
|
||||||
url = url.encode('utf-8')
|
|
||||||
|
|
||||||
existing = filter(lambda p: p.url == url, model.get_podcasts())
|
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
return existing[0]
|
return existing[0]
|
||||||
|
@ -835,7 +830,7 @@ class PodcastChannel(PodcastModelObject):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tmp.update(max_episodes)
|
tmp.update(max_episodes)
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
logger.debug('Fetch failed. Removing buggy feed.')
|
logger.debug('Fetch failed. Removing buggy feed.')
|
||||||
tmp.remove_downloaded()
|
tmp.remove_downloaded()
|
||||||
tmp.delete()
|
tmp.delete()
|
||||||
|
@ -1034,7 +1029,7 @@ class PodcastChannel(PodcastModelObject):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.save()
|
self.save()
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
# "Not really" errors
|
# "Not really" errors
|
||||||
#feedcore.AuthenticationRequired
|
#feedcore.AuthenticationRequired
|
||||||
# Temporary errors
|
# Temporary errors
|
||||||
|
@ -1153,7 +1148,7 @@ class PodcastChannel(PodcastModelObject):
|
||||||
return self.children
|
return self.children
|
||||||
|
|
||||||
def get_episodes(self, state):
|
def get_episodes(self, state):
|
||||||
return filter(lambda e: e.state == state, self.get_all_episodes())
|
return [e for e in self.get_all_episodes() if e.state == state]
|
||||||
|
|
||||||
def find_unique_folder_name(self, download_folder):
|
def find_unique_folder_name(self, download_folder):
|
||||||
# Remove trailing dots to avoid errors on Windows (bug 600)
|
# Remove trailing dots to avoid errors on Windows (bug 600)
|
||||||
|
@ -1191,9 +1186,6 @@ class PodcastChannel(PodcastModelObject):
|
||||||
|
|
||||||
save_dir = os.path.join(gpodder.downloads, self.download_folder)
|
save_dir = os.path.join(gpodder.downloads, self.download_folder)
|
||||||
|
|
||||||
# Avoid encoding errors for OS-specific functions (bug 1570)
|
|
||||||
save_dir = util.sanitize_encoding(save_dir)
|
|
||||||
|
|
||||||
# Create save_dir if it does not yet exist
|
# Create save_dir if it does not yet exist
|
||||||
if not util.make_directory(save_dir):
|
if not util.make_directory(save_dir):
|
||||||
logger.error('Could not create save_dir: %s', save_dir)
|
logger.error('Could not create save_dir: %s', save_dir)
|
||||||
|
|
|
@ -49,13 +49,13 @@ MYGPOCLIENT_REQUIRED = '1.4'
|
||||||
|
|
||||||
if not hasattr(mygpoclient, 'require_version') or \
|
if not hasattr(mygpoclient, 'require_version') or \
|
||||||
not mygpoclient.require_version(MYGPOCLIENT_REQUIRED):
|
not mygpoclient.require_version(MYGPOCLIENT_REQUIRED):
|
||||||
print >>sys.stderr, """
|
print("""
|
||||||
Please upgrade your mygpoclient library.
|
Please upgrade your mygpoclient library.
|
||||||
See http://thp.io/2010/mygpoclient/
|
See http://thp.io/2010/mygpoclient/
|
||||||
|
|
||||||
Required version: %s
|
Required version: %s
|
||||||
Installed version: %s
|
Installed version: %s
|
||||||
""" % (MYGPOCLIENT_REQUIRED, mygpoclient.__version__)
|
""" % (MYGPOCLIENT_REQUIRED, mygpoclient.__version__), file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -79,7 +79,7 @@ class SinceValue(object):
|
||||||
__slots__ = {'host': str, 'device_id': str, 'category': int, 'since': int}
|
__slots__ = {'host': str, 'device_id': str, 'category': int, 'since': int}
|
||||||
|
|
||||||
# Possible values for the "category" field
|
# Possible values for the "category" field
|
||||||
PODCASTS, EPISODES = range(2)
|
PODCASTS, EPISODES = list(range(2))
|
||||||
|
|
||||||
def __init__(self, host, device_id, category, since=0):
|
def __init__(self, host, device_id, category, since=0):
|
||||||
self.host = host
|
self.host = host
|
||||||
|
@ -91,7 +91,7 @@ class SubscribeAction(object):
|
||||||
__slots__ = {'action_type': int, 'url': str}
|
__slots__ = {'action_type': int, 'url': str}
|
||||||
|
|
||||||
# Possible values for the "action_type" field
|
# Possible values for the "action_type" field
|
||||||
ADD, REMOVE = range(2)
|
ADD, REMOVE = list(range(2))
|
||||||
|
|
||||||
def __init__(self, action_type, url):
|
def __init__(self, action_type, url):
|
||||||
self.action_type = action_type
|
self.action_type = action_type
|
||||||
|
@ -506,7 +506,7 @@ class MygPoClient(object):
|
||||||
# handle outside
|
# handle outside
|
||||||
raise
|
raise
|
||||||
|
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
logger.warn('Exception while polling for episodes.', exc_info=True)
|
logger.warn('Exception while polling for episodes.', exc_info=True)
|
||||||
|
|
||||||
# Step 2: Upload Episode actions
|
# Step 2: Upload Episode actions
|
||||||
|
@ -533,7 +533,7 @@ class MygPoClient(object):
|
||||||
self._config.mygpo.enabled = False
|
self._config.mygpo.enabled = False
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
logger.error('Cannot upload episode actions: %s', str(e), exc_info=True)
|
logger.error('Cannot upload episode actions: %s', str(e), exc_info=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -598,7 +598,7 @@ class MygPoClient(object):
|
||||||
self._config.mygpo.enabled = False
|
self._config.mygpo.enabled = False
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
logger.error('Cannot upload subscriptions: %s', str(e), exc_info=True)
|
logger.error('Cannot upload subscriptions: %s', str(e), exc_info=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -615,7 +615,7 @@ class MygPoClient(object):
|
||||||
self._config.mygpo.enabled = False
|
self._config.mygpo.enabled = False
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
logger.error('Cannot update device %s: %s', self.device_id,
|
logger.error('Cannot update device %s: %s', self.device_id,
|
||||||
str(e), exc_info=True)
|
str(e), exc_info=True)
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -71,6 +71,7 @@ class Importer(object):
|
||||||
if os.path.exists(url):
|
if os.path.exists(url):
|
||||||
doc = xml.dom.minidom.parse(url)
|
doc = xml.dom.minidom.parse(url)
|
||||||
else:
|
else:
|
||||||
|
# FIXME: is it ok to pass bytes to parseString?
|
||||||
doc = xml.dom.minidom.parseString(util.urlopen(url).read())
|
doc = xml.dom.minidom.parseString(util.urlopen(url).read())
|
||||||
|
|
||||||
for outline in doc.getElementsByTagName('outline'):
|
for outline in doc.getElementsByTagName('outline'):
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
|
|
||||||
|
|
||||||
import gpodder
|
import gpodder
|
||||||
import urllib
|
import urllib.request, urllib.parse, urllib.error
|
||||||
|
|
||||||
class MediaPlayerDBusReceiver(object):
|
class MediaPlayerDBusReceiver(object):
|
||||||
INTERFACE = 'org.gpodder.player'
|
INTERFACE = 'org.gpodder.player'
|
||||||
|
@ -77,12 +77,7 @@ class MediaPlayerDBusReceiver(object):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def on_playback_stopped(self, start, end, total, file_uri):
|
def on_playback_stopped(self, start, end, total, file_uri):
|
||||||
# Assume the URI comes as quoted UTF-8 string, so decode
|
|
||||||
# it first to utf-8 (should be no problem) for unquoting
|
|
||||||
# to work correctly on this later on (Maemo bug 11811)
|
|
||||||
if isinstance(file_uri, unicode):
|
|
||||||
file_uri = file_uri.encode('utf-8')
|
|
||||||
if file_uri.startswith('/'):
|
if file_uri.startswith('/'):
|
||||||
file_uri = 'file://' + urllib.quote(file_uri)
|
file_uri = 'file://' + urllib.parse.quote(file_uri)
|
||||||
self.on_play_event(start, end, total, file_uri)
|
self.on_play_event(start, end, total, file_uri)
|
||||||
|
|
||||||
|
|
|
@ -28,12 +28,7 @@ _ = gpodder.gettext
|
||||||
from gpodder import model
|
from gpodder import model
|
||||||
from gpodder import util
|
from gpodder import util
|
||||||
|
|
||||||
try:
|
import json
|
||||||
# For Python < 2.6, we use the "simplejson" add-on module
|
|
||||||
import simplejson as json
|
|
||||||
except ImportError:
|
|
||||||
# Python 2.6 already ships with a nice "json" module
|
|
||||||
import json
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
@ -41,7 +36,7 @@ import time
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import email
|
import email
|
||||||
import urllib
|
import urllib.request, urllib.parse, urllib.error
|
||||||
|
|
||||||
|
|
||||||
# gPodder's consumer key for the Soundcloud API
|
# gPodder's consumer key for the Soundcloud API
|
||||||
|
@ -58,7 +53,7 @@ def soundcloud_parsedate(s):
|
||||||
parsed with this function (2009/11/03 13:37:00).
|
parsed with this function (2009/11/03 13:37:00).
|
||||||
"""
|
"""
|
||||||
m = re.match(r'(\d{4})/(\d{2})/(\d{2}) (\d{2}):(\d{2}):(\d{2})', s)
|
m = re.match(r'(\d{4})/(\d{2})/(\d{2}) (\d{2}):(\d{2}):(\d{2})', s)
|
||||||
return time.mktime([int(x) for x in m.groups()]+[0, 0, -1])
|
return time.mktime(tuple([int(x) for x in m.groups()]+[0, 0, -1]))
|
||||||
|
|
||||||
def get_param(s, param='filename', header='content-disposition'):
|
def get_param(s, param='filename', header='content-disposition'):
|
||||||
"""Get a parameter from a string of headers
|
"""Get a parameter from a string of headers
|
||||||
|
@ -76,8 +71,8 @@ def get_param(s, param='filename', header='content-disposition'):
|
||||||
if encoding:
|
if encoding:
|
||||||
value.append(part.decode(encoding))
|
value.append(part.decode(encoding))
|
||||||
else:
|
else:
|
||||||
value.append(unicode(part))
|
value.append(str(part))
|
||||||
return u''.join(value)
|
return ''.join(value)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -92,7 +87,7 @@ def get_metadata(url):
|
||||||
headers = track_fp.info()
|
headers = track_fp.info()
|
||||||
filesize = headers['content-length'] or '0'
|
filesize = headers['content-length'] or '0'
|
||||||
filetype = headers['content-type'] or 'application/octet-stream'
|
filetype = headers['content-type'] or 'application/octet-stream'
|
||||||
headers_s = '\n'.join('%s:%s'%(k,v) for k, v in headers.items())
|
headers_s = '\n'.join('%s:%s'%(k,v) for k, v in list(headers.items()))
|
||||||
filename = get_param(headers_s) or os.path.basename(os.path.dirname(url))
|
filename = get_param(headers_s) or os.path.basename(os.path.dirname(url))
|
||||||
track_fp.close()
|
track_fp.close()
|
||||||
return filesize, filetype, filename
|
return filesize, filetype, filename
|
||||||
|
@ -121,7 +116,7 @@ class SoundcloudUser(object):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
json_url = 'https://api.soundcloud.com/users/%s.json?consumer_key=%s' % (self.username, CONSUMER_KEY)
|
json_url = 'https://api.soundcloud.com/users/%s.json?consumer_key=%s' % (self.username, CONSUMER_KEY)
|
||||||
user_info = json.load(util.urlopen(json_url))
|
user_info = json.loads(util.urlopen(json_url).read().decode('utf-8'))
|
||||||
self.cache[key] = user_info
|
self.cache[key] = user_info
|
||||||
finally:
|
finally:
|
||||||
self.commit_cache()
|
self.commit_cache()
|
||||||
|
@ -252,5 +247,5 @@ model.register_custom_handler(SoundcloudFeed)
|
||||||
model.register_custom_handler(SoundcloudFavFeed)
|
model.register_custom_handler(SoundcloudFavFeed)
|
||||||
|
|
||||||
def search_for_user(query):
|
def search_for_user(query):
|
||||||
json_url = 'https://api.soundcloud.com/users.json?q=%s&consumer_key=%s' % (urllib.quote(query), CONSUMER_KEY)
|
json_url = 'https://api.soundcloud.com/users.json?q=%s&consumer_key=%s' % (urllib.parse.quote(query), CONSUMER_KEY)
|
||||||
return json.load(util.urlopen(json_url))
|
return json.loads(util.urlopen(json_url).read().decode('utf-8'))
|
||||||
|
|
|
@ -40,8 +40,8 @@ class Matcher(object):
|
||||||
def match(self, term):
|
def match(self, term):
|
||||||
try:
|
try:
|
||||||
return bool(eval(term, {'__builtins__': None}, self))
|
return bool(eval(term, {'__builtins__': None}, self))
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
print e
|
print(e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def __getitem__(self, k):
|
def __getitem__(self, k):
|
||||||
|
@ -69,7 +69,7 @@ class Matcher(object):
|
||||||
|
|
||||||
# Nouns (for comparisons)
|
# Nouns (for comparisons)
|
||||||
if k in ('megabytes', 'mb'):
|
if k in ('megabytes', 'mb'):
|
||||||
return float(episode.file_size) / (1024*1024)
|
return episode.file_size / (1024*1024)
|
||||||
elif k == 'title':
|
elif k == 'title':
|
||||||
return episode.title
|
return episode.title
|
||||||
elif k == 'description':
|
elif k == 'description':
|
||||||
|
@ -79,9 +79,9 @@ class Matcher(object):
|
||||||
elif k == 'age':
|
elif k == 'age':
|
||||||
return episode.age_in_days()
|
return episode.age_in_days()
|
||||||
elif k in ('minutes', 'min'):
|
elif k in ('minutes', 'min'):
|
||||||
return float(episode.total_time) / 60
|
return episode.total_time / 60
|
||||||
elif k in ('remaining', 'rem'):
|
elif k in ('remaining', 'rem'):
|
||||||
return float(episode.total_time - episode.current_position) / 60
|
return episode.total_time - episode.current_position / 60
|
||||||
|
|
||||||
raise KeyError(k)
|
raise KeyError(k)
|
||||||
|
|
||||||
|
@ -140,8 +140,8 @@ class EQL(object):
|
||||||
if not self._regex and not self._string:
|
if not self._regex and not self._string:
|
||||||
try:
|
try:
|
||||||
self._query = compile(query, '<eql-string>', 'eval')
|
self._query = compile(query, '<eql-string>', 'eval')
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
print e
|
print(e)
|
||||||
self._query = None
|
self._query = None
|
||||||
|
|
||||||
|
|
||||||
|
@ -157,7 +157,7 @@ class EQL(object):
|
||||||
return Matcher(episode).match(self._query)
|
return Matcher(episode).match(self._query)
|
||||||
|
|
||||||
def filter(self, episodes):
|
def filter(self, episodes):
|
||||||
return filter(self.match, episodes)
|
return list(filter(self.match, episodes))
|
||||||
|
|
||||||
|
|
||||||
def UserEQL(query):
|
def UserEQL(query):
|
||||||
|
|
|
@ -210,7 +210,7 @@ def upgrade(db, filename):
|
||||||
backup = '%s_upgraded-v%d_%d' % (filename, int(version), int(time.time()))
|
backup = '%s_upgraded-v%d_%d' % (filename, int(version), int(time.time()))
|
||||||
try:
|
try:
|
||||||
shutil.copy(filename, backup)
|
shutil.copy(filename, backup)
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
raise Exception('Cannot create DB backup before upgrade: ' + e)
|
raise Exception('Cannot create DB backup before upgrade: ' + e)
|
||||||
|
|
||||||
db.execute("DELETE FROM version")
|
db.execute("DELETE FROM version")
|
||||||
|
@ -246,7 +246,7 @@ def convert_gpodder2_db(old_db, new_db):
|
||||||
old_cur = old_db.cursor()
|
old_cur = old_db.cursor()
|
||||||
columns = [x[1] for x in old_cur.execute('PRAGMA table_info(channels)')]
|
columns = [x[1] for x in old_cur.execute('PRAGMA table_info(channels)')]
|
||||||
for row in old_cur.execute('SELECT * FROM channels'):
|
for row in old_cur.execute('SELECT * FROM channels'):
|
||||||
row = dict(zip(columns, row))
|
row = dict(list(zip(columns, row)))
|
||||||
values = (
|
values = (
|
||||||
row['id'],
|
row['id'],
|
||||||
row['override_title'] or row['title'],
|
row['override_title'] or row['title'],
|
||||||
|
@ -276,7 +276,7 @@ def convert_gpodder2_db(old_db, new_db):
|
||||||
old_cur = old_db.cursor()
|
old_cur = old_db.cursor()
|
||||||
columns = [x[1] for x in old_cur.execute('PRAGMA table_info(episodes)')]
|
columns = [x[1] for x in old_cur.execute('PRAGMA table_info(episodes)')]
|
||||||
for row in old_cur.execute('SELECT * FROM episodes'):
|
for row in old_cur.execute('SELECT * FROM episodes'):
|
||||||
row = dict(zip(columns, row))
|
row = dict(list(zip(columns, row)))
|
||||||
values = (
|
values = (
|
||||||
row['id'],
|
row['id'],
|
||||||
row['channel_id'],
|
row['channel_id'],
|
||||||
|
|
|
@ -85,8 +85,8 @@ if pymtp_available:
|
||||||
folder = folder.contents
|
folder = folder.contents
|
||||||
name = self.sep.join([path, folder.name]).lstrip(self.sep)
|
name = self.sep.join([path, folder.name]).lstrip(self.sep)
|
||||||
result[name] = folder.folder_id
|
result[name] = folder.folder_id
|
||||||
if folder.child:
|
if folder.get_child():
|
||||||
result.update(self.unfold(folder.child, name))
|
result.update(self.unfold(folder.get_child(), name))
|
||||||
folder = folder.sibling
|
folder = folder.sibling
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -97,7 +97,7 @@ if pymtp_available:
|
||||||
while parts:
|
while parts:
|
||||||
prefix.append(parts[0])
|
prefix.append(parts[0])
|
||||||
tmpath = self.sep.join(prefix)
|
tmpath = self.sep.join(prefix)
|
||||||
if self.folders.has_key(tmpath):
|
if tmpath in self.folders:
|
||||||
folder_id = self.folders[tmpath]
|
folder_id = self.folders[tmpath]
|
||||||
else:
|
else:
|
||||||
folder_id = self.create_folder(parts[0], parent=folder_id)
|
folder_id = self.create_folder(parts[0], parent=folder_id)
|
||||||
|
@ -136,7 +136,7 @@ def get_track_length(filename):
|
||||||
try:
|
try:
|
||||||
mp3file = eyed3.mp3.Mp3AudioFile(filename)
|
mp3file = eyed3.mp3.Mp3AudioFile(filename)
|
||||||
return int(mp3file.info.time_secs * 1000)
|
return int(mp3file.info.time_secs * 1000)
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
logger.warn('Could not determine length: %s', filename, exc_info=True)
|
logger.warn('Could not determine length: %s', filename, exc_info=True)
|
||||||
|
|
||||||
return int(60*60*1000*3) # Default is three hours (to be on the safe side)
|
return int(60*60*1000*3) # Default is three hours (to be on the safe side)
|
||||||
|
@ -225,16 +225,16 @@ class Device(services.ObservableService):
|
||||||
|
|
||||||
sync_task.status=sync_task.QUEUED
|
sync_task.status=sync_task.QUEUED
|
||||||
sync_task.device=self
|
sync_task.device=self
|
||||||
|
# New Task, we must wait on the GTK Loop
|
||||||
self.download_status_model.register_task(sync_task)
|
self.download_status_model.register_task(sync_task)
|
||||||
self.download_queue_manager.add_task(sync_task)
|
# Executes after task has been registered
|
||||||
|
util.idle_add(self.download_queue_manager.queue_task, sync_task)
|
||||||
else:
|
else:
|
||||||
logger.warning("No episodes to sync")
|
logger.warning("No episodes to sync")
|
||||||
|
|
||||||
if done_callback:
|
if done_callback:
|
||||||
done_callback()
|
done_callback()
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def remove_tracks(self, tracklist):
|
def remove_tracks(self, tracklist):
|
||||||
for idx, track in enumerate(tracklist):
|
for idx, track in enumerate(tracklist):
|
||||||
if self.cancelled:
|
if self.cancelled:
|
||||||
|
@ -379,7 +379,7 @@ class iPodDevice(Device):
|
||||||
try:
|
try:
|
||||||
released = gpod.itdb_time_mac_to_host(track.time_released)
|
released = gpod.itdb_time_mac_to_host(track.time_released)
|
||||||
released = util.format_date(released)
|
released = util.format_date(released)
|
||||||
except ValueError, ve:
|
except ValueError as ve:
|
||||||
# timestamp out of range for platform time_t (bug 418)
|
# timestamp out of range for platform time_t (bug 418)
|
||||||
logger.info('Cannot convert track time: %s', ve)
|
logger.info('Cannot convert track time: %s', ve)
|
||||||
released = 0
|
released = 0
|
||||||
|
@ -502,7 +502,7 @@ class MP3PlayerDevice(Device):
|
||||||
download_status_model,
|
download_status_model,
|
||||||
download_queue_manager):
|
download_queue_manager):
|
||||||
Device.__init__(self, config)
|
Device.__init__(self, config)
|
||||||
self.destination = util.sanitize_encoding(self._config.device_sync.device_folder)
|
self.destination = self._config.device_sync.device_folder
|
||||||
self.buffer_size = 1024*1024 # 1 MiB
|
self.buffer_size = 1024*1024 # 1 MiB
|
||||||
self.download_status_model = download_status_model
|
self.download_status_model = download_status_model
|
||||||
self.download_queue_manager = download_queue_manager
|
self.download_queue_manager = download_queue_manager
|
||||||
|
@ -532,11 +532,11 @@ class MP3PlayerDevice(Device):
|
||||||
else:
|
else:
|
||||||
folder = self.destination
|
folder = self.destination
|
||||||
|
|
||||||
return util.sanitize_encoding(folder)
|
return folder
|
||||||
|
|
||||||
def get_episode_file_on_device(self, episode):
|
def get_episode_file_on_device(self, episode):
|
||||||
# get the local file
|
# get the local file
|
||||||
from_file = util.sanitize_encoding(episode.local_filename(create=False))
|
from_file = episode.local_filename(create=False)
|
||||||
# get the formated base name
|
# get the formated base name
|
||||||
filename_base = util.sanitize_filename(episode.sync_filename(
|
filename_base = util.sanitize_filename(episode.sync_filename(
|
||||||
self._config.device_sync.custom_sync_name_enabled,
|
self._config.device_sync.custom_sync_name_enabled,
|
||||||
|
@ -554,7 +554,7 @@ class MP3PlayerDevice(Device):
|
||||||
return to_file
|
return to_file
|
||||||
|
|
||||||
def add_track(self, episode,reporthook=None):
|
def add_track(self, episode,reporthook=None):
|
||||||
self.notify('status', _('Adding %s') % episode.title.decode('utf-8', 'ignore'))
|
self.notify('status', _('Adding %s') % episode.title)
|
||||||
|
|
||||||
# get the folder on the device
|
# get the folder on the device
|
||||||
folder = self.get_episode_folder_on_device(episode)
|
folder = self.get_episode_folder_on_device(episode)
|
||||||
|
@ -564,7 +564,7 @@ class MP3PlayerDevice(Device):
|
||||||
# local_filename(create=False) must never return None as filename
|
# local_filename(create=False) must never return None as filename
|
||||||
assert filename is not None
|
assert filename is not None
|
||||||
|
|
||||||
from_file = util.sanitize_encoding(filename)
|
from_file = filename
|
||||||
|
|
||||||
# verify free space
|
# verify free space
|
||||||
needed = util.calculate_size(from_file)
|
needed = util.calculate_size(from_file)
|
||||||
|
@ -578,7 +578,7 @@ class MP3PlayerDevice(Device):
|
||||||
|
|
||||||
# get the filename that will be used on the device
|
# get the filename that will be used on the device
|
||||||
to_file = self.get_episode_file_on_device(episode)
|
to_file = self.get_episode_file_on_device(episode)
|
||||||
to_file = util.sanitize_encoding(os.path.join(folder, to_file))
|
to_file = os.path.join(folder, to_file)
|
||||||
|
|
||||||
if not os.path.exists(folder):
|
if not os.path.exists(folder):
|
||||||
try:
|
try:
|
||||||
|
@ -590,7 +590,7 @@ class MP3PlayerDevice(Device):
|
||||||
if not os.path.exists(to_file):
|
if not os.path.exists(to_file):
|
||||||
logger.info('Copying %s => %s',
|
logger.info('Copying %s => %s',
|
||||||
os.path.basename(from_file),
|
os.path.basename(from_file),
|
||||||
to_file.decode(util.encoding))
|
to_file)
|
||||||
self.copy_file_progress(from_file, to_file, reporthook)
|
self.copy_file_progress(from_file, to_file, reporthook)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -598,7 +598,7 @@ class MP3PlayerDevice(Device):
|
||||||
def copy_file_progress(self, from_file, to_file, reporthook=None):
|
def copy_file_progress(self, from_file, to_file, reporthook=None):
|
||||||
try:
|
try:
|
||||||
out_file = open(to_file, 'wb')
|
out_file = open(to_file, 'wb')
|
||||||
except IOError, ioerror:
|
except IOError as ioerror:
|
||||||
d = {'filename': ioerror.filename, 'message': ioerror.strerror}
|
d = {'filename': ioerror.filename, 'message': ioerror.strerror}
|
||||||
self.errors.append(_('Error opening %(filename)s: %(message)s') % d)
|
self.errors.append(_('Error opening %(filename)s: %(message)s') % d)
|
||||||
self.cancel()
|
self.cancel()
|
||||||
|
@ -606,7 +606,7 @@ class MP3PlayerDevice(Device):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
in_file = open(from_file, 'rb')
|
in_file = open(from_file, 'rb')
|
||||||
except IOError, ioerror:
|
except IOError as ioerror:
|
||||||
d = {'filename': ioerror.filename, 'message': ioerror.strerror}
|
d = {'filename': ioerror.filename, 'message': ioerror.strerror}
|
||||||
self.errors.append(_('Error opening %(filename)s: %(message)s') % d)
|
self.errors.append(_('Error opening %(filename)s: %(message)s') % d)
|
||||||
self.cancel()
|
self.cancel()
|
||||||
|
@ -622,7 +622,7 @@ class MP3PlayerDevice(Device):
|
||||||
bytes_read += len(s)
|
bytes_read += len(s)
|
||||||
try:
|
try:
|
||||||
out_file.write(s)
|
out_file.write(s)
|
||||||
except IOError, ioerror:
|
except IOError as ioerror:
|
||||||
self.errors.append(ioerror.strerror)
|
self.errors.append(ioerror.strerror)
|
||||||
try:
|
try:
|
||||||
out_file.close()
|
out_file.close()
|
||||||
|
@ -697,7 +697,7 @@ class MTPDevice(Device):
|
||||||
self.__model_name = None
|
self.__model_name = None
|
||||||
try:
|
try:
|
||||||
self.__MTPDevice = MTP()
|
self.__MTPDevice = MTP()
|
||||||
except NameError, e:
|
except NameError as e:
|
||||||
# pymtp not available / not installed (see bug 924)
|
# pymtp not available / not installed (see bug 924)
|
||||||
logger.error('pymtp not found: %s', str(e))
|
logger.error('pymtp not found: %s', str(e))
|
||||||
self.__MTPDevice = None
|
self.__MTPDevice = None
|
||||||
|
@ -705,7 +705,7 @@ class MTPDevice(Device):
|
||||||
def __callback(self, sent, total):
|
def __callback(self, sent, total):
|
||||||
if self.cancelled:
|
if self.cancelled:
|
||||||
return -1
|
return -1
|
||||||
percentage = round(float(sent)/float(total)*100)
|
percentage = round(sent/total*100)
|
||||||
text = ('%i%%' % percentage)
|
text = ('%i%%' % percentage)
|
||||||
self.notify('progress', sent, total, text)
|
self.notify('progress', sent, total, text)
|
||||||
|
|
||||||
|
@ -722,7 +722,7 @@ class MTPDevice(Device):
|
||||||
try:
|
try:
|
||||||
d = time.gmtime(date)
|
d = time.gmtime(date)
|
||||||
return time.strftime("%Y%m%d-%H%M%S.0Z", d)
|
return time.strftime("%Y%m%d-%H%M%S.0Z", d)
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
logger.error('ERROR: An error has happend while trying to convert date to an mtp string')
|
logger.error('ERROR: An error has happend while trying to convert date to an mtp string')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -752,10 +752,10 @@ class MTPDevice(Device):
|
||||||
_date -= shift_in_sec
|
_date -= shift_in_sec
|
||||||
else:
|
else:
|
||||||
raise ValueError("Expected + or -")
|
raise ValueError("Expected + or -")
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
logger.warning('WARNING: ignoring invalid time zone information for %s (%s)')
|
logger.warning('WARNING: ignoring invalid time zone information for %s (%s)')
|
||||||
return max( 0, _date )
|
return max( 0, _date )
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
logger.warning('WARNING: the mtp date "%s" can not be parsed against mtp specification (%s)')
|
logger.warning('WARNING: the mtp date "%s" can not be parsed against mtp specification (%s)')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -796,7 +796,7 @@ class MTPDevice(Device):
|
||||||
self.__MTPDevice.connect()
|
self.__MTPDevice.connect()
|
||||||
# build the initial tracks_list
|
# build the initial tracks_list
|
||||||
self.tracks_list = self.get_all_tracks()
|
self.tracks_list = self.get_all_tracks()
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
logger.error('unable to find an MTP device (%s)')
|
logger.error('unable to find an MTP device (%s)')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -809,7 +809,7 @@ class MTPDevice(Device):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.__MTPDevice.disconnect()
|
self.__MTPDevice.disconnect()
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
logger.error('unable to close %s (%s)', self.get_name())
|
logger.error('unable to close %s (%s)', self.get_name())
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -876,7 +876,7 @@ class MTPDevice(Device):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.__MTPDevice.delete_object(sync_track.mtptrack.item_id)
|
self.__MTPDevice.delete_object(sync_track.mtptrack.item_id)
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
logger.error('unable remove file %s (%s)', sync_track.mtptrack.filename)
|
logger.error('unable remove file %s (%s)', sync_track.mtptrack.filename)
|
||||||
|
|
||||||
logger.info('%s removed', sync_track.mtptrack.title)
|
logger.info('%s removed', sync_track.mtptrack.title)
|
||||||
|
@ -884,7 +884,7 @@ class MTPDevice(Device):
|
||||||
def get_all_tracks(self):
|
def get_all_tracks(self):
|
||||||
try:
|
try:
|
||||||
listing = self.__MTPDevice.get_tracklisting(callback=self.__callback)
|
listing = self.__MTPDevice.get_tracklisting(callback=self.__callback)
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
logger.error('unable to get file listing %s (%s)')
|
logger.error('unable to get file listing %s (%s)')
|
||||||
|
|
||||||
tracks = []
|
tracks = []
|
||||||
|
@ -923,7 +923,7 @@ class SyncTask(download.DownloadTask):
|
||||||
# Possible states this sync task can be in
|
# Possible states this sync task can be in
|
||||||
STATUS_MESSAGE = (_('Added'), _('Queued'), _('Synchronizing'),
|
STATUS_MESSAGE = (_('Added'), _('Queued'), _('Synchronizing'),
|
||||||
_('Finished'), _('Failed'), _('Cancelled'), _('Paused'))
|
_('Finished'), _('Failed'), _('Cancelled'), _('Paused'))
|
||||||
(INIT, QUEUED, DOWNLOADING, DONE, FAILED, CANCELLED, PAUSED) = range(7)
|
(INIT, QUEUED, DOWNLOADING, DONE, FAILED, CANCELLED, PAUSED) = list(range(7))
|
||||||
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -1040,7 +1040,7 @@ class SyncTask(download.DownloadTask):
|
||||||
self.total_size = float(totalSize)
|
self.total_size = float(totalSize)
|
||||||
|
|
||||||
if self.total_size > 0:
|
if self.total_size > 0:
|
||||||
self.progress = max(0.0, min(1.0, float(count*blockSize)/self.total_size))
|
self.progress = max(0.0, min(1.0, (count*blockSize)/self.total_size))
|
||||||
self._progress_updated(self.progress)
|
self._progress_updated(self.progress)
|
||||||
|
|
||||||
if self.status == SyncTask.CANCELLED:
|
if self.status == SyncTask.CANCELLED:
|
||||||
|
@ -1064,8 +1064,8 @@ class SyncTask(download.DownloadTask):
|
||||||
self.speed = 0.0
|
self.speed = 0.0
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# We only start this download if its status is "queued"
|
# We only start this download if its status is "downloading"
|
||||||
if self.status != SyncTask.QUEUED:
|
if self.status != SyncTask.DOWNLOADING:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# We are synching this file right now
|
# We are synching this file right now
|
||||||
|
@ -1075,7 +1075,7 @@ class SyncTask(download.DownloadTask):
|
||||||
try:
|
try:
|
||||||
logger.info('Starting SyncTask')
|
logger.info('Starting SyncTask')
|
||||||
self.device.add_track(self.episode, reporthook=self.status_updated)
|
self.device.add_track(self.episode, reporthook=self.status_updated)
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
self.status = SyncTask.FAILED
|
self.status = SyncTask.FAILED
|
||||||
logger.error('Sync failed: %s', str(e), exc_info=True)
|
logger.error('Sync failed: %s', str(e), exc_info=True)
|
||||||
self.error_message = _('Error: %s') % (str(e),)
|
self.error_message = _('Error: %s') % (str(e),)
|
||||||
|
|
|
@ -30,11 +30,11 @@ try:
|
||||||
# Unused here locally, but we import it to be able to give an early
|
# Unused here locally, but we import it to be able to give an early
|
||||||
# warning about this missing dependency in order to avoid bogus errors.
|
# warning about this missing dependency in order to avoid bogus errors.
|
||||||
import minimock
|
import minimock
|
||||||
except ImportError, e:
|
except ImportError as e:
|
||||||
print >>sys.stderr, """
|
print("""
|
||||||
Error: Unit tests require the "minimock" module (python-minimock).
|
Error: Unit tests require the "minimock" module (python-minimock).
|
||||||
Please install it before running the unit tests.
|
Please install it before running the unit tests.
|
||||||
"""
|
""", file=sys.stderr)
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
# Main package and test package (for modules in main package)
|
# Main package and test package (for modules in main package)
|
||||||
|
@ -73,9 +73,9 @@ try:
|
||||||
import HTMLTestRunner
|
import HTMLTestRunner
|
||||||
REPORT_FILENAME = 'test_report.html'
|
REPORT_FILENAME = 'test_report.html'
|
||||||
runner = HTMLTestRunner.HTMLTestRunner(stream=open(REPORT_FILENAME, 'w'))
|
runner = HTMLTestRunner.HTMLTestRunner(stream=open(REPORT_FILENAME, 'w'))
|
||||||
print """
|
print("""
|
||||||
HTML Test Report will be written to %s
|
HTML Test Report will be written to %s
|
||||||
""" % REPORT_FILENAME
|
""" % REPORT_FILENAME)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
runner = unittest.TextTestRunner(verbosity=2)
|
runner = unittest.TextTestRunner(verbosity=2)
|
||||||
|
|
||||||
|
@ -100,7 +100,7 @@ if __name__ == '__main__':
|
||||||
cov.report(coverage_modules)
|
cov.report(coverage_modules)
|
||||||
cov.erase()
|
cov.erase()
|
||||||
else:
|
else:
|
||||||
print >>sys.stderr, """
|
print("""
|
||||||
No coverage reporting done (Python module "coverage" is missing)
|
No coverage reporting done (Python module "coverage" is missing)
|
||||||
Please install the python-coverage package to get coverage reporting.
|
Please install the python-coverage package to get coverage reporting.
|
||||||
"""
|
""", file=sys.stderr)
|
||||||
|
|
|
@ -49,28 +49,28 @@ import string
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
from htmlentitydefs import entitydefs
|
from html.entities import entitydefs
|
||||||
import time
|
import time
|
||||||
import gzip
|
import gzip
|
||||||
import datetime
|
import datetime
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
import urlparse
|
import urllib.parse
|
||||||
import urllib
|
import urllib.request, urllib.parse, urllib.error
|
||||||
import urllib2
|
import urllib.request, urllib.error, urllib.parse
|
||||||
import httplib
|
import http.client
|
||||||
import webbrowser
|
import webbrowser
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
import StringIO
|
import io
|
||||||
import xml.dom.minidom
|
import xml.dom.minidom
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
|
|
||||||
if sys.hexversion < 0x03000000:
|
if sys.hexversion < 0x03000000:
|
||||||
from HTMLParser import HTMLParser
|
from html.parser import HTMLParser
|
||||||
from htmlentitydefs import name2codepoint
|
from html.entities import name2codepoint
|
||||||
else:
|
else:
|
||||||
from html.parser import HTMLParser
|
from html.parser import HTMLParser
|
||||||
from html.entities import name2codepoint
|
from html.entities import name2codepoint
|
||||||
|
@ -95,7 +95,7 @@ N_ = gpodder.ngettext
|
||||||
import locale
|
import locale
|
||||||
try:
|
try:
|
||||||
locale.setlocale(locale.LC_ALL, '')
|
locale.setlocale(locale.LC_ALL, '')
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
logger.warn('Cannot set locale (%s)', e, exc_info=True)
|
logger.warn('Cannot set locale (%s)', e, exc_info=True)
|
||||||
|
|
||||||
# Native filesystem encoding detection
|
# Native filesystem encoding detection
|
||||||
|
@ -119,15 +119,15 @@ if encoding is None:
|
||||||
# Filename / folder name sanitization
|
# Filename / folder name sanitization
|
||||||
def _sanitize_char(c):
|
def _sanitize_char(c):
|
||||||
if c in string.whitespace:
|
if c in string.whitespace:
|
||||||
return ' '
|
return b' '
|
||||||
elif c in ',-.()':
|
elif c in ',-.()':
|
||||||
return c
|
return c.encode('utf-8')
|
||||||
elif c in string.punctuation or ord(c) <= 31:
|
elif c in string.punctuation or ord(c) <= 31 or ord(c) >= 127:
|
||||||
return '_'
|
return b'_'
|
||||||
|
|
||||||
return c
|
return c.encode('utf-8')
|
||||||
|
|
||||||
SANITIZATION_TABLE = ''.join(map(_sanitize_char, map(chr, range(256))))
|
SANITIZATION_TABLE = b''.join(map(_sanitize_char, list(map(chr, list(range(256))))))
|
||||||
del _sanitize_char
|
del _sanitize_char
|
||||||
|
|
||||||
_MIME_TYPE_LIST = [
|
_MIME_TYPE_LIST = [
|
||||||
|
@ -232,7 +232,7 @@ def normalize_feed_url(url):
|
||||||
'ytpl:': 'http://gdata.youtube.com/feeds/api/playlists/%s',
|
'ytpl:': 'http://gdata.youtube.com/feeds/api/playlists/%s',
|
||||||
}
|
}
|
||||||
|
|
||||||
for prefix, expansion in PREFIXES.iteritems():
|
for prefix, expansion in PREFIXES.items():
|
||||||
if url.startswith(prefix):
|
if url.startswith(prefix):
|
||||||
url = expansion % (url[len(prefix):],)
|
url = expansion % (url[len(prefix):],)
|
||||||
break
|
break
|
||||||
|
@ -241,7 +241,7 @@ def normalize_feed_url(url):
|
||||||
if not '://' in url:
|
if not '://' in url:
|
||||||
url = 'http://' + url
|
url = 'http://' + url
|
||||||
|
|
||||||
scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
|
scheme, netloc, path, query, fragment = urllib.parse.urlsplit(url)
|
||||||
|
|
||||||
# Domain name is case insensitive, but username/password is not (bug 1942)
|
# Domain name is case insensitive, but username/password is not (bug 1942)
|
||||||
if '@' in netloc:
|
if '@' in netloc:
|
||||||
|
@ -265,7 +265,7 @@ def normalize_feed_url(url):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# urlunsplit might return "a slighty different, but equivalent URL"
|
# urlunsplit might return "a slighty different, but equivalent URL"
|
||||||
return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
|
return urllib.parse.urlunsplit((scheme, netloc, path, query, fragment))
|
||||||
|
|
||||||
|
|
||||||
def username_password_from_url(url):
|
def username_password_from_url(url):
|
||||||
|
@ -287,11 +287,11 @@ def username_password_from_url(url):
|
||||||
>>> username_password_from_url(1)
|
>>> username_password_from_url(1)
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
ValueError: URL has to be a string or unicode object.
|
ValueError: URL has to be a string.
|
||||||
>>> username_password_from_url(None)
|
>>> username_password_from_url(None)
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
ValueError: URL has to be a string or unicode object.
|
ValueError: URL has to be a string.
|
||||||
>>> username_password_from_url('http://a@b:c@host.com/')
|
>>> username_password_from_url('http://a@b:c@host.com/')
|
||||||
('a@b', 'c')
|
('a@b', 'c')
|
||||||
>>> username_password_from_url('ftp://a:b:c@host.com/')
|
>>> username_password_from_url('ftp://a:b:c@host.com/')
|
||||||
|
@ -299,18 +299,18 @@ def username_password_from_url(url):
|
||||||
>>> username_password_from_url('http://i%2Fo:P%40ss%3A@host.com/')
|
>>> username_password_from_url('http://i%2Fo:P%40ss%3A@host.com/')
|
||||||
('i/o', 'P@ss:')
|
('i/o', 'P@ss:')
|
||||||
>>> username_password_from_url('ftp://%C3%B6sterreich@host.com/')
|
>>> username_password_from_url('ftp://%C3%B6sterreich@host.com/')
|
||||||
('\xc3\xb6sterreich', None)
|
('österreich', None)
|
||||||
>>> username_password_from_url('http://w%20x:y%20z@example.org/')
|
>>> username_password_from_url('http://w%20x:y%20z@example.org/')
|
||||||
('w x', 'y z')
|
('w x', 'y z')
|
||||||
>>> username_password_from_url('http://example.com/x@y:z@test.com/')
|
>>> username_password_from_url('http://example.com/x@y:z@test.com/')
|
||||||
(None, None)
|
(None, None)
|
||||||
"""
|
"""
|
||||||
if type(url) not in (str, unicode):
|
if not isinstance(url, str):
|
||||||
raise ValueError('URL has to be a string or unicode object.')
|
raise ValueError('URL has to be a string.')
|
||||||
|
|
||||||
(username, password) = (None, None)
|
(username, password) = (None, None)
|
||||||
|
|
||||||
(scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url)
|
(scheme, netloc, path, params, query, fragment) = urllib.parse.urlparse(url)
|
||||||
|
|
||||||
if '@' in netloc:
|
if '@' in netloc:
|
||||||
(authentication, netloc) = netloc.rsplit('@', 1)
|
(authentication, netloc) = netloc.rsplit('@', 1)
|
||||||
|
@ -330,10 +330,10 @@ def username_password_from_url(url):
|
||||||
# is handled by the authentication.split(':', 1) above, and
|
# is handled by the authentication.split(':', 1) above, and
|
||||||
# will cause any extraneous ':'s to be part of the password.
|
# will cause any extraneous ':'s to be part of the password.
|
||||||
|
|
||||||
username = urllib.unquote(username)
|
username = urllib.parse.unquote(username)
|
||||||
password = urllib.unquote(password)
|
password = urllib.parse.unquote(password)
|
||||||
else:
|
else:
|
||||||
username = urllib.unquote(authentication)
|
username = urllib.parse.unquote(authentication)
|
||||||
|
|
||||||
return (username, password)
|
return (username, password)
|
||||||
|
|
||||||
|
@ -353,10 +353,10 @@ def calculate_size( path):
|
||||||
to list all subdirectories of the given path.
|
to list all subdirectories of the given path.
|
||||||
"""
|
"""
|
||||||
if path is None:
|
if path is None:
|
||||||
return 0L
|
return 0
|
||||||
|
|
||||||
if os.path.dirname( path) == '/':
|
if os.path.dirname( path) == '/':
|
||||||
return 0L
|
return 0
|
||||||
|
|
||||||
if os.path.isfile( path):
|
if os.path.isfile( path):
|
||||||
return os.path.getsize( path)
|
return os.path.getsize( path)
|
||||||
|
@ -375,7 +375,7 @@ def calculate_size( path):
|
||||||
|
|
||||||
return sum
|
return sum
|
||||||
|
|
||||||
return 0L
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def file_modification_datetime(filename):
|
def file_modification_datetime(filename):
|
||||||
|
@ -433,9 +433,9 @@ def file_age_to_string(days):
|
||||||
>>> file_age_to_string(0)
|
>>> file_age_to_string(0)
|
||||||
''
|
''
|
||||||
>>> file_age_to_string(1)
|
>>> file_age_to_string(1)
|
||||||
u'1 day ago'
|
'1 day ago'
|
||||||
>>> file_age_to_string(2)
|
>>> file_age_to_string(2)
|
||||||
u'2 days ago'
|
'2 days ago'
|
||||||
"""
|
"""
|
||||||
if days < 1:
|
if days < 1:
|
||||||
return ''
|
return ''
|
||||||
|
@ -511,10 +511,10 @@ def format_date(timestamp):
|
||||||
yesterday = time.localtime(time.time() - seconds_in_a_day)[:3]
|
yesterday = time.localtime(time.time() - seconds_in_a_day)[:3]
|
||||||
try:
|
try:
|
||||||
timestamp_date = time.localtime(timestamp)[:3]
|
timestamp_date = time.localtime(timestamp)[:3]
|
||||||
except ValueError, ve:
|
except ValueError as ve:
|
||||||
logger.warn('Cannot convert timestamp', exc_info=True)
|
logger.warn('Cannot convert timestamp', exc_info=True)
|
||||||
return None
|
return None
|
||||||
except TypeError, te:
|
except TypeError as te:
|
||||||
logger.warn('Cannot convert timestamp', exc_info=True)
|
logger.warn('Cannot convert timestamp', exc_info=True)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -536,10 +536,10 @@ def format_date(timestamp):
|
||||||
|
|
||||||
if diff < 7:
|
if diff < 7:
|
||||||
# Weekday name
|
# Weekday name
|
||||||
return str(timestamp.strftime('%A').decode(encoding))
|
return timestamp.strftime('%A')
|
||||||
else:
|
else:
|
||||||
# Locale's appropriate date representation
|
# Locale's appropriate date representation
|
||||||
return str(timestamp.strftime('%x'))
|
return timestamp.strftime('%x')
|
||||||
|
|
||||||
|
|
||||||
def format_filesize(bytesize, use_si_units=False, digits=2):
|
def format_filesize(bytesize, use_si_units=False, digits=2):
|
||||||
|
@ -636,10 +636,10 @@ def remove_html_tags(html):
|
||||||
result = re_strip_tags.sub('', result)
|
result = re_strip_tags.sub('', result)
|
||||||
|
|
||||||
# Convert numeric XML entities to their unicode character
|
# Convert numeric XML entities to their unicode character
|
||||||
result = re_unicode_entities.sub(lambda x: unichr(int(x.group(1))), result)
|
result = re_unicode_entities.sub(lambda x: chr(int(x.group(1))), result)
|
||||||
|
|
||||||
# Convert named HTML entities to their unicode character
|
# Convert named HTML entities to their unicode character
|
||||||
result = re_html_entities.sub(lambda x: unicode(entitydefs.get(x.group(1),''), 'iso-8859-1'), result)
|
result = re_html_entities.sub(lambda x: entitydefs.get(x.group(1),''), result)
|
||||||
|
|
||||||
# Convert more than two newlines to two newlines
|
# Convert more than two newlines to two newlines
|
||||||
result = re.sub('([\r\n]{2})([\r\n])+', '\\1', result)
|
result = re.sub('([\r\n]{2})([\r\n])+', '\\1', result)
|
||||||
|
@ -658,7 +658,7 @@ class HyperlinkExtracter(object):
|
||||||
group_it = itertools.groupby(self.parts, key=lambda x: x[0])
|
group_it = itertools.groupby(self.parts, key=lambda x: x[0])
|
||||||
result = []
|
result = []
|
||||||
for target, parts in group_it:
|
for target, parts in group_it:
|
||||||
t = u''.join(text for _, text in parts if text is not None)
|
t = ''.join(text for _, text in parts if text is not None)
|
||||||
# Remove trailing spaces
|
# Remove trailing spaces
|
||||||
t = re.sub(' +\n', '\n', t)
|
t = re.sub(' +\n', '\n', t)
|
||||||
# Convert more than two newlines to two newlines
|
# Convert more than two newlines to two newlines
|
||||||
|
@ -705,14 +705,14 @@ class HyperlinkExtracter(object):
|
||||||
self.output(self.htmlws(data))
|
self.output(self.htmlws(data))
|
||||||
|
|
||||||
def handle_entityref(self, name):
|
def handle_entityref(self, name):
|
||||||
c = unichr(name2codepoint[name])
|
c = chr(name2codepoint[name])
|
||||||
self.output(c)
|
self.output(c)
|
||||||
|
|
||||||
def handle_charref(self, name):
|
def handle_charref(self, name):
|
||||||
if name.startswith('x'):
|
if name.startswith('x'):
|
||||||
c = unichr(int(name[1:], 16))
|
c = chr(int(name[1:], 16))
|
||||||
else:
|
else:
|
||||||
c = unichr(int(name))
|
c = chr(int(name))
|
||||||
self.output(c)
|
self.output(c)
|
||||||
|
|
||||||
def output_newline(self, attrs=None):
|
def output_newline(self, attrs=None):
|
||||||
|
@ -740,7 +740,7 @@ class ExtractHyperlinkedText(object):
|
||||||
def visit(self, element):
|
def visit(self, element):
|
||||||
NS = '{http://www.w3.org/1999/xhtml}'
|
NS = '{http://www.w3.org/1999/xhtml}'
|
||||||
tag_name = (element.tag[len(NS):] if element.tag.startswith(NS) else element.tag).lower()
|
tag_name = (element.tag[len(NS):] if element.tag.startswith(NS) else element.tag).lower()
|
||||||
self.extracter.handle_starttag(tag_name, element.items())
|
self.extracter.handle_starttag(tag_name, list(element.items()))
|
||||||
|
|
||||||
if element.text is not None:
|
if element.text is not None:
|
||||||
self.extracter.handle_data(element.text)
|
self.extracter.handle_data(element.text)
|
||||||
|
@ -940,8 +940,8 @@ def filename_from_url(url):
|
||||||
http://server/get.jsp?file=/episode0815.MOV => ("episode0815", ".mov")
|
http://server/get.jsp?file=/episode0815.MOV => ("episode0815", ".mov")
|
||||||
http://s/redirect.mp4?http://serv2/test.mp4 => ("test", ".mp4")
|
http://s/redirect.mp4?http://serv2/test.mp4 => ("test", ".mp4")
|
||||||
"""
|
"""
|
||||||
(scheme, netloc, path, para, query, fragid) = urlparse.urlparse(url)
|
(scheme, netloc, path, para, query, fragid) = urllib.parse.urlparse(url)
|
||||||
(filename, extension) = os.path.splitext(os.path.basename( urllib.unquote(path)))
|
(filename, extension) = os.path.splitext(os.path.basename( urllib.parse.unquote(path)))
|
||||||
|
|
||||||
if file_type_by_extension(extension) is not None and not \
|
if file_type_by_extension(extension) is not None and not \
|
||||||
query.startswith(scheme+'://'):
|
query.startswith(scheme+'://'):
|
||||||
|
@ -951,7 +951,7 @@ def filename_from_url(url):
|
||||||
|
|
||||||
# If the query string looks like a possible URL, try that first
|
# If the query string looks like a possible URL, try that first
|
||||||
if len(query.strip()) > 0 and query.find('/') != -1:
|
if len(query.strip()) > 0 and query.find('/') != -1:
|
||||||
query_url = '://'.join((scheme, urllib.unquote(query)))
|
query_url = '://'.join((scheme, urllib.parse.unquote(query)))
|
||||||
(query_filename, query_extension) = filename_from_url(query_url)
|
(query_filename, query_extension) = filename_from_url(query_url)
|
||||||
|
|
||||||
if file_type_by_extension(query_extension) is not None:
|
if file_type_by_extension(query_extension) is not None:
|
||||||
|
@ -1033,7 +1033,7 @@ def object_string_formatter(s, **kwargs):
|
||||||
'Hi 123 456'
|
'Hi 123 456'
|
||||||
"""
|
"""
|
||||||
result = s
|
result = s
|
||||||
for key, o in kwargs.iteritems():
|
for key, o in kwargs.items():
|
||||||
matches = re.findall(r'\{%s\.([^\}]+)\}' % key, s)
|
matches = re.findall(r'\{%s\.([^\}]+)\}' % key, s)
|
||||||
for attr in matches:
|
for attr in matches:
|
||||||
if hasattr(o, attr):
|
if hasattr(o, attr):
|
||||||
|
@ -1116,14 +1116,14 @@ def url_strip_authentication(url):
|
||||||
>>> url_strip_authentication('http://x@x.com:s3cret@example.com/')
|
>>> url_strip_authentication('http://x@x.com:s3cret@example.com/')
|
||||||
'http://example.com/'
|
'http://example.com/'
|
||||||
"""
|
"""
|
||||||
url_parts = list(urlparse.urlsplit(url))
|
url_parts = list(urllib.parse.urlsplit(url))
|
||||||
# url_parts[1] is the HOST part of the URL
|
# url_parts[1] is the HOST part of the URL
|
||||||
|
|
||||||
# Remove existing authentication data
|
# Remove existing authentication data
|
||||||
if '@' in url_parts[1]:
|
if '@' in url_parts[1]:
|
||||||
url_parts[1] = url_parts[1].rsplit('@', 1)[1]
|
url_parts[1] = url_parts[1].rsplit('@', 1)[1]
|
||||||
|
|
||||||
return urlparse.urlunsplit(url_parts)
|
return urllib.parse.urlunsplit(url_parts)
|
||||||
|
|
||||||
|
|
||||||
def url_add_authentication(url, username, password):
|
def url_add_authentication(url, username, password):
|
||||||
|
@ -1158,21 +1158,21 @@ def url_add_authentication(url, username, password):
|
||||||
# Relaxations of the strict quoting rules (bug 1521):
|
# Relaxations of the strict quoting rules (bug 1521):
|
||||||
# 1. Accept '@' in username and password
|
# 1. Accept '@' in username and password
|
||||||
# 2. Acecpt ':' in password only
|
# 2. Acecpt ':' in password only
|
||||||
username = urllib.quote(username, safe='@')
|
username = urllib.parse.quote(username, safe='@')
|
||||||
|
|
||||||
if password is not None:
|
if password is not None:
|
||||||
password = urllib.quote(password, safe='@:')
|
password = urllib.parse.quote(password, safe='@:')
|
||||||
auth_string = ':'.join((username, password))
|
auth_string = ':'.join((username, password))
|
||||||
else:
|
else:
|
||||||
auth_string = username
|
auth_string = username
|
||||||
|
|
||||||
url = url_strip_authentication(url)
|
url = url_strip_authentication(url)
|
||||||
|
|
||||||
url_parts = list(urlparse.urlsplit(url))
|
url_parts = list(urllib.parse.urlsplit(url))
|
||||||
# url_parts[1] is the HOST part of the URL
|
# url_parts[1] is the HOST part of the URL
|
||||||
url_parts[1] = '@'.join((auth_string, url_parts[1]))
|
url_parts[1] = '@'.join((auth_string, url_parts[1]))
|
||||||
|
|
||||||
return urlparse.urlunsplit(url_parts)
|
return urllib.parse.urlunsplit(url_parts)
|
||||||
|
|
||||||
|
|
||||||
def urlopen(url, headers=None, data=None, timeout=None):
|
def urlopen(url, headers=None, data=None, timeout=None):
|
||||||
|
@ -1182,12 +1182,12 @@ def urlopen(url, headers=None, data=None, timeout=None):
|
||||||
username, password = username_password_from_url(url)
|
username, password = username_password_from_url(url)
|
||||||
if username is not None or password is not None:
|
if username is not None or password is not None:
|
||||||
url = url_strip_authentication(url)
|
url = url_strip_authentication(url)
|
||||||
password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
|
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
|
||||||
password_mgr.add_password(None, url, username, password)
|
password_mgr.add_password(None, url, username, password)
|
||||||
handler = urllib2.HTTPBasicAuthHandler(password_mgr)
|
handler = urllib.request.HTTPBasicAuthHandler(password_mgr)
|
||||||
opener = urllib2.build_opener(handler)
|
opener = urllib.request.build_opener(handler)
|
||||||
else:
|
else:
|
||||||
opener = urllib2.build_opener()
|
opener = urllib.request.build_opener()
|
||||||
|
|
||||||
if headers is None:
|
if headers is None:
|
||||||
headers = {}
|
headers = {}
|
||||||
|
@ -1195,7 +1195,7 @@ def urlopen(url, headers=None, data=None, timeout=None):
|
||||||
headers = dict(headers)
|
headers = dict(headers)
|
||||||
|
|
||||||
headers.update({'User-agent': gpodder.user_agent})
|
headers.update({'User-agent': gpodder.user_agent})
|
||||||
request = urllib2.Request(url, data=data, headers=headers)
|
request = urllib.request.Request(url, data=data, headers=headers)
|
||||||
if timeout is None:
|
if timeout is None:
|
||||||
return opener.open(request)
|
return opener.open(request)
|
||||||
else:
|
else:
|
||||||
|
@ -1251,8 +1251,8 @@ def idle_add(func, *args):
|
||||||
as possible from the main UI thread.
|
as possible from the main UI thread.
|
||||||
"""
|
"""
|
||||||
if gpodder.ui.gtk:
|
if gpodder.ui.gtk:
|
||||||
import gobject
|
from gi.repository import GObject
|
||||||
gobject.idle_add(func, *args)
|
GObject.idle_add(func, *args)
|
||||||
else:
|
else:
|
||||||
func(*args)
|
func(*args)
|
||||||
|
|
||||||
|
@ -1358,11 +1358,11 @@ def format_seconds_to_hour_min_sec(seconds):
|
||||||
human-readable string (duration).
|
human-readable string (duration).
|
||||||
|
|
||||||
>>> format_seconds_to_hour_min_sec(3834)
|
>>> format_seconds_to_hour_min_sec(3834)
|
||||||
u'1 hour, 3 minutes and 54 seconds'
|
'1 hour, 3 minutes and 54 seconds'
|
||||||
>>> format_seconds_to_hour_min_sec(3600)
|
>>> format_seconds_to_hour_min_sec(3600)
|
||||||
u'1 hour'
|
'1 hour'
|
||||||
>>> format_seconds_to_hour_min_sec(62)
|
>>> format_seconds_to_hour_min_sec(62)
|
||||||
u'1 minute and 2 seconds'
|
'1 minute and 2 seconds'
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if seconds < 1:
|
if seconds < 1:
|
||||||
|
@ -1372,10 +1372,10 @@ def format_seconds_to_hour_min_sec(seconds):
|
||||||
|
|
||||||
seconds = int(seconds)
|
seconds = int(seconds)
|
||||||
|
|
||||||
hours = seconds/3600
|
hours = seconds//3600
|
||||||
seconds = seconds%3600
|
seconds = seconds%3600
|
||||||
|
|
||||||
minutes = seconds/60
|
minutes = seconds//60
|
||||||
seconds = seconds%60
|
seconds = seconds%60
|
||||||
|
|
||||||
if hours:
|
if hours:
|
||||||
|
@ -1393,8 +1393,8 @@ def format_seconds_to_hour_min_sec(seconds):
|
||||||
return result[0]
|
return result[0]
|
||||||
|
|
||||||
def http_request(url, method='HEAD'):
|
def http_request(url, method='HEAD'):
|
||||||
(scheme, netloc, path, parms, qry, fragid) = urlparse.urlparse(url)
|
(scheme, netloc, path, parms, qry, fragid) = urllib.parse.urlparse(url)
|
||||||
conn = httplib.HTTPConnection(netloc)
|
conn = http.client.HTTPConnection(netloc)
|
||||||
start = len(scheme) + len('://') + len(netloc)
|
start = len(scheme) + len('://') + len(netloc)
|
||||||
conn.request(method, url[start:])
|
conn.request(method, url[start:])
|
||||||
return conn.getresponse()
|
return conn.getresponse()
|
||||||
|
@ -1437,75 +1437,38 @@ def convert_bytes(d):
|
||||||
strings. Any other data types will be left alone.
|
strings. Any other data types will be left alone.
|
||||||
|
|
||||||
>>> convert_bytes(None)
|
>>> convert_bytes(None)
|
||||||
>>> convert_bytes(1)
|
>>> convert_bytes(4711)
|
||||||
1
|
4711
|
||||||
>>> convert_bytes(4711L)
|
|
||||||
4711L
|
|
||||||
>>> convert_bytes(True)
|
>>> convert_bytes(True)
|
||||||
True
|
True
|
||||||
>>> convert_bytes(3.1415)
|
>>> convert_bytes(3.1415)
|
||||||
3.1415
|
3.1415
|
||||||
>>> convert_bytes('Hello')
|
>>> convert_bytes('Hello')
|
||||||
u'Hello'
|
'Hello'
|
||||||
>>> convert_bytes(u'Hey')
|
>>> type(convert_bytes(b'hoho'))
|
||||||
u'Hey'
|
<class 'bytes'>
|
||||||
>>> type(convert_bytes(buffer('hoho')))
|
|
||||||
<type 'buffer'>
|
|
||||||
"""
|
"""
|
||||||
if d is None:
|
if d is None:
|
||||||
return d
|
return d
|
||||||
if isinstance(d, buffer):
|
elif isinstance(d, bytes):
|
||||||
return d
|
return d
|
||||||
elif any(isinstance(d, t) for t in (int, long, bool, float)):
|
elif any(isinstance(d, t) for t in (int, int, bool, float)):
|
||||||
return d
|
return d
|
||||||
elif not isinstance(d, unicode):
|
elif not isinstance(d, str):
|
||||||
return d.decode('utf-8', 'ignore')
|
return d.decode('utf-8', 'ignore')
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def sanitize_encoding(filename):
|
|
||||||
r"""
|
|
||||||
Generate a sanitized version of a string (i.e.
|
|
||||||
remove invalid characters and encode in the
|
|
||||||
detected native language encoding).
|
|
||||||
|
|
||||||
>>> sanitize_encoding('\x80')
|
def sanitize_filename(filename, max_length=0):
|
||||||
''
|
|
||||||
>>> sanitize_encoding(u'unicode')
|
|
||||||
'unicode'
|
|
||||||
"""
|
"""
|
||||||
# The encoding problem goes away in Python 3.. hopefully!
|
Generate a sanitized version of a filename; trim filename
|
||||||
if sys.version_info >= (3, 0):
|
if greater than max_length (0 = no limit).
|
||||||
return filename
|
|
||||||
|
|
||||||
global encoding
|
|
||||||
if not isinstance(filename, unicode):
|
|
||||||
filename = filename.decode(encoding, 'ignore')
|
|
||||||
return filename.encode(encoding, 'ignore')
|
|
||||||
|
|
||||||
|
|
||||||
def sanitize_filename(filename, max_length=0, use_ascii=False):
|
|
||||||
"""
|
"""
|
||||||
Generate a sanitized version of a filename that can
|
|
||||||
be written on disk (i.e. remove/replace invalid
|
|
||||||
characters and encode in the native language) and
|
|
||||||
trim filename if greater than max_length (0 = no limit).
|
|
||||||
|
|
||||||
If use_ascii is True, don't encode in the native language,
|
|
||||||
but use only characters from the ASCII character set.
|
|
||||||
"""
|
|
||||||
if not isinstance(filename, unicode):
|
|
||||||
filename = filename.decode(encoding, 'ignore')
|
|
||||||
|
|
||||||
if max_length > 0 and len(filename) > max_length:
|
if max_length > 0 and len(filename) > max_length:
|
||||||
logger.info('Limiting file/folder name "%s" to %d characters.',
|
logger.info('Limiting file/folder name "%s" to %d characters.', filename, max_length)
|
||||||
filename, max_length)
|
|
||||||
filename = filename[:max_length]
|
filename = filename[:max_length]
|
||||||
|
|
||||||
filename = filename.encode('ascii' if use_ascii else encoding, 'ignore')
|
return filename.strip('.' + string.whitespace)
|
||||||
filename = filename.translate(SANITIZATION_TABLE)
|
|
||||||
filename = filename.strip('.' + string.whitespace)
|
|
||||||
|
|
||||||
return filename
|
|
||||||
|
|
||||||
|
|
||||||
def find_mount_point(directory):
|
def find_mount_point(directory):
|
||||||
|
@ -1519,10 +1482,10 @@ def find_mount_point(directory):
|
||||||
>>> find_mount_point('/')
|
>>> find_mount_point('/')
|
||||||
'/'
|
'/'
|
||||||
|
|
||||||
>>> find_mount_point(u'/something')
|
>>> find_mount_point(b'/something')
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
...
|
...
|
||||||
ValueError: Convert unicode objects to str first.
|
ValueError: Convert bytes objects to str first.
|
||||||
|
|
||||||
>>> find_mount_point(None)
|
>>> find_mount_point(None)
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
|
@ -1573,15 +1536,14 @@ def find_mount_point(directory):
|
||||||
'/media/usbdisk'
|
'/media/usbdisk'
|
||||||
>>> restore()
|
>>> restore()
|
||||||
"""
|
"""
|
||||||
if isinstance(directory, unicode):
|
if isinstance(directory, bytes):
|
||||||
# XXX: This is only valid for Python 2 - misleading error in Python 3?
|
# We do not accept byte strings, because they could fail when
|
||||||
# We do not accept unicode strings, because they could fail when
|
|
||||||
# trying to be converted to some native encoding, so fail loudly
|
# trying to be converted to some native encoding, so fail loudly
|
||||||
# and leave it up to the callee to encode into the proper encoding.
|
# and leave it up to the callee to decode from the proper encoding.
|
||||||
raise ValueError('Convert unicode objects to str first.')
|
raise ValueError('Convert bytes objects to str first.')
|
||||||
|
|
||||||
if not isinstance(directory, str):
|
if not isinstance(directory, str):
|
||||||
# In Python 2, we assume it's a byte str; in Python 3, we assume
|
# In Python 2, we assumed it's a byte str; in Python 3, we assume
|
||||||
# that it's a unicode str. The abspath/ismount/split functions of
|
# that it's a unicode str. The abspath/ismount/split functions of
|
||||||
# os.path work with unicode str in Python 3, but not in Python 2.
|
# os.path work with unicode str in Python 3, but not in Python 2.
|
||||||
raise ValueError('Directory names should be of type str.')
|
raise ValueError('Directory names should be of type str.')
|
||||||
|
@ -1751,7 +1713,6 @@ def atomic_rename(old_name, new_name):
|
||||||
def check_command(self, cmd):
|
def check_command(self, cmd):
|
||||||
"""Check if a command line command/program exists"""
|
"""Check if a command line command/program exists"""
|
||||||
# Prior to Python 2.7.3, this module (shlex) did not support Unicode input.
|
# Prior to Python 2.7.3, this module (shlex) did not support Unicode input.
|
||||||
cmd = sanitize_encoding(cmd)
|
|
||||||
program = shlex.split(cmd)[0]
|
program = shlex.split(cmd)[0]
|
||||||
return (find_command(program) is not None)
|
return (find_command(program) is not None)
|
||||||
|
|
||||||
|
@ -1818,7 +1779,7 @@ def linux_get_active_interfaces():
|
||||||
"""
|
"""
|
||||||
process = subprocess.Popen(['ip', 'link'], stdout=subprocess.PIPE)
|
process = subprocess.Popen(['ip', 'link'], stdout=subprocess.PIPE)
|
||||||
data, _ = process.communicate()
|
data, _ = process.communicate()
|
||||||
for interface, _ in re.findall(r'\d+: ([^:]+):.*state (UP|UNKNOWN)', data):
|
for interface, _ in re.findall(r'\d+: ([^:]+):.*state (UP|UNKNOWN)', data.decode(locale.getpreferredencoding())):
|
||||||
if interface != 'lo':
|
if interface != 'lo':
|
||||||
yield interface
|
yield interface
|
||||||
|
|
||||||
|
@ -1832,7 +1793,7 @@ def osx_get_active_interfaces():
|
||||||
"""
|
"""
|
||||||
process = subprocess.Popen(['ifconfig'], stdout=subprocess.PIPE)
|
process = subprocess.Popen(['ifconfig'], stdout=subprocess.PIPE)
|
||||||
stdout, _ = process.communicate()
|
stdout, _ = process.communicate()
|
||||||
for i in re.split('\n(?!\t)', stdout, re.MULTILINE):
|
for i in re.split('\n(?!\t)', stdout.decode('utf-8'), re.MULTILINE):
|
||||||
b = re.match('(\\w+):.*status: (active|associated)$', i, re.MULTILINE | re.DOTALL)
|
b = re.match('(\\w+):.*status: (active|associated)$', i, re.MULTILINE | re.DOTALL)
|
||||||
if b:
|
if b:
|
||||||
yield b.group(1)
|
yield b.group(1)
|
||||||
|
@ -1846,7 +1807,7 @@ def unix_get_active_interfaces():
|
||||||
"""
|
"""
|
||||||
process = subprocess.Popen(['ifconfig'], stdout=subprocess.PIPE)
|
process = subprocess.Popen(['ifconfig'], stdout=subprocess.PIPE)
|
||||||
stdout, _ = process.communicate()
|
stdout, _ = process.communicate()
|
||||||
for i in re.split('\n(?!\t)', stdout, re.MULTILINE):
|
for i in re.split('\n(?!\t)', stdout.decode(locale.getpreferredencoding()), re.MULTILINE):
|
||||||
b = re.match('(\\w+):.*status: (active|associated)$', i, re.MULTILINE | re.DOTALL)
|
b = re.match('(\\w+):.*status: (active|associated)$', i, re.MULTILINE | re.DOTALL)
|
||||||
if b:
|
if b:
|
||||||
yield b.group(1)
|
yield b.group(1)
|
||||||
|
@ -1885,7 +1846,7 @@ def connection_available():
|
||||||
return not offline
|
return not offline
|
||||||
|
|
||||||
return False
|
return False
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
logger.warn('Cannot get connection status: %s', e, exc_info=True)
|
logger.warn('Cannot get connection status: %s', e, exc_info=True)
|
||||||
# When we can't determine the connection status, act as if we're online (bug 1730)
|
# When we can't determine the connection status, act as if we're online (bug 1730)
|
||||||
return True
|
return True
|
||||||
|
@ -1900,9 +1861,9 @@ def website_reachable(url):
|
||||||
return (False, None)
|
return (False, None)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = urllib2.urlopen(url, timeout=1)
|
response = urllib.request.urlopen(url, timeout=1)
|
||||||
return (True, response)
|
return (True, response)
|
||||||
except urllib2.URLError as err:
|
except urllib.error.URLError as err:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return (False, None)
|
return (False, None)
|
||||||
|
|
|
@ -32,12 +32,7 @@ from gpodder import util
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
try:
|
import json
|
||||||
# For Python < 2.6, we use the "simplejson" add-on module
|
|
||||||
import simplejson as json
|
|
||||||
except ImportError:
|
|
||||||
# Python 2.6 already ships with a nice "json" module
|
|
||||||
import json
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
@ -64,7 +59,7 @@ def get_real_download_url(url, preferred_fileformat=None):
|
||||||
def get_urls(data_config_url):
|
def get_urls(data_config_url):
|
||||||
data_config_data = util.urlopen(data_config_url).read().decode('utf-8')
|
data_config_data = util.urlopen(data_config_url).read().decode('utf-8')
|
||||||
data_config = json.loads(data_config_data)
|
data_config = json.loads(data_config_data)
|
||||||
for fileinfo in data_config['request']['files'].values():
|
for fileinfo in list(data_config['request']['files'].values()):
|
||||||
if not isinstance(fileinfo, list):
|
if not isinstance(fileinfo, list):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
|
@ -30,20 +30,12 @@ import os.path
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
try:
|
import json
|
||||||
import simplejson as json
|
|
||||||
except ImportError:
|
|
||||||
import json
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import urllib
|
import urllib.request, urllib.parse, urllib.error
|
||||||
|
|
||||||
try:
|
from urllib.parse import parse_qs
|
||||||
# Python >= 2.6
|
|
||||||
from urlparse import parse_qs
|
|
||||||
except ImportError:
|
|
||||||
# Python < 2.6
|
|
||||||
from cgi import parse_qs
|
|
||||||
|
|
||||||
# http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs
|
# http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs
|
||||||
# format id, (preferred ids, path(?), description) # video bitrate, audio bitrate
|
# format id, (preferred ids, path(?), description) # video bitrate, audio bitrate
|
||||||
|
@ -117,7 +109,7 @@ def get_real_download_url(url, preferred_fmt_ids=None):
|
||||||
def find_urls(page):
|
def find_urls(page):
|
||||||
r4 = re.search('url_encoded_fmt_stream_map=([^&]+)', page)
|
r4 = re.search('url_encoded_fmt_stream_map=([^&]+)', page)
|
||||||
if r4 is not None:
|
if r4 is not None:
|
||||||
fmt_url_map = urllib.unquote(r4.group(1))
|
fmt_url_map = urllib.parse.unquote(r4.group(1))
|
||||||
for fmt_url_encoded in fmt_url_map.split(','):
|
for fmt_url_encoded in fmt_url_map.split(','):
|
||||||
video_info = parse_qs(fmt_url_encoded)
|
video_info = parse_qs(fmt_url_encoded)
|
||||||
yield int(video_info['itag'][0]), video_info['url'][0]
|
yield int(video_info['itag'][0]), video_info['url'][0]
|
||||||
|
@ -212,7 +204,7 @@ def get_real_cover(url):
|
||||||
def return_user_cover(url, channel):
|
def return_user_cover(url, channel):
|
||||||
try:
|
try:
|
||||||
api_url = 'https://www.youtube.com/channel/{0}'.format(channel)
|
api_url = 'https://www.youtube.com/channel/{0}'.format(channel)
|
||||||
data = util.urlopen(api_url).read()
|
data = util.urlopen(api_url).read().decode('utf-8')
|
||||||
# Look for 900x900px image first.
|
# Look for 900x900px image first.
|
||||||
m = re.search('<link rel="image_src"[^>]* href=[\'"]([^\'"]+)[\'"][^>]*>', data)
|
m = re.search('<link rel="image_src"[^>]* href=[\'"]([^\'"]+)[\'"][^>]*>', data)
|
||||||
if m is None:
|
if m is None:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
# create-desktop-icon.py: Create a Desktop icon
|
# create-desktop-icon.py: Create a Desktop icon
|
||||||
# 2016-12-22 Thomas Perl <m@thp.io>
|
# 2016-12-22 Thomas Perl <m@thp.io>
|
||||||
|
|
||||||
|
@ -19,11 +19,11 @@ Type=Application
|
||||||
DESTINATION = os.path.expanduser('~/Desktop/gpodder-git.desktop')
|
DESTINATION = os.path.expanduser('~/Desktop/gpodder-git.desktop')
|
||||||
|
|
||||||
if os.path.exists(DESTINATION):
|
if os.path.exists(DESTINATION):
|
||||||
print '%(DESTINATION)s already exists, not overwriting'
|
print('%(DESTINATION)s already exists, not overwriting')
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
with open(DESTINATION, 'w') as fp:
|
with open(DESTINATION, 'w') as fp:
|
||||||
fp.write(TEMPLATE)
|
fp.write(TEMPLATE)
|
||||||
os.chmod(DESTINATION, 0755)
|
os.chmod(DESTINATION, 0o755)
|
||||||
|
|
||||||
print 'Wrote %(DESTINATION)s' % locals()
|
print('Wrote %(DESTINATION)s' % locals())
|
||||||
|
|
|
@ -4,4 +4,4 @@ import re
|
||||||
here = os.path.dirname(__file__) or '.'
|
here = os.path.dirname(__file__) or '.'
|
||||||
main_module = open(os.path.join(here, '../src/gpodder/__init__.py')).read()
|
main_module = open(os.path.join(here, '../src/gpodder/__init__.py')).read()
|
||||||
metadata = dict(re.findall("__([a-z_]+)__\s*=\s*'([^']+)'", main_module))
|
metadata = dict(re.findall("__([a-z_]+)__\s*=\s*'([^']+)'", main_module))
|
||||||
print metadata['version']
|
print(metadata['version'])
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/python
|
#!/usr/bin/python3
|
||||||
# summary.py - Text-based visual translation completeness summary
|
# summary.py - Text-based visual translation completeness summary
|
||||||
# Thomas Perl <thp@gpodder.org>, 2009-01-03
|
# Thomas Perl <thp@gpodder.org>, 2009-01-03
|
||||||
#
|
#
|
||||||
|
@ -22,13 +22,13 @@ class Language(object):
|
||||||
self.untranslated = int(untranslated)
|
self.untranslated = int(untranslated)
|
||||||
|
|
||||||
def get_translated_ratio(self):
|
def get_translated_ratio(self):
|
||||||
return float(self.translated)/float(self.translated+self.fuzzy+self.untranslated)
|
return self.translated/(self.translated+self.fuzzy+self.untranslated)
|
||||||
|
|
||||||
def get_fuzzy_ratio(self):
|
def get_fuzzy_ratio(self):
|
||||||
return float(self.fuzzy)/float(self.translated+self.fuzzy+self.untranslated)
|
return self.fuzzy/(self.translated+self.fuzzy+self.untranslated)
|
||||||
|
|
||||||
def get_untranslated_ratio(self):
|
def get_untranslated_ratio(self):
|
||||||
return float(self.untranslated)/float(self.translated+self.fuzzy+self.untranslated)
|
return self.untranslated/(self.translated+self.fuzzy+self.untranslated)
|
||||||
|
|
||||||
def __cmp__(self, other):
|
def __cmp__(self, other):
|
||||||
return cmp(self.get_translated_ratio(), other.get_translated_ratio())
|
return cmp(self.get_translated_ratio(), other.get_translated_ratio())
|
||||||
|
@ -47,15 +47,15 @@ for filename in glob.glob(os.path.join(po_folder, '*.po')):
|
||||||
match = re.match(COUNTS_RE, stderr).groups()
|
match = re.match(COUNTS_RE, stderr).groups()
|
||||||
languages.append(Language(language, match[1] or '0', match[3] or '0', match[5] or '0'))
|
languages.append(Language(language, match[1] or '0', match[3] or '0', match[5] or '0'))
|
||||||
|
|
||||||
print ''
|
print('')
|
||||||
for language in sorted(languages):
|
for language in sorted(languages):
|
||||||
tc = '#'*(int(math.floor(width*language.get_translated_ratio())))
|
tc = '#'*(int(math.floor(width*language.get_translated_ratio())))
|
||||||
fc = '~'*(int(math.floor(width*language.get_fuzzy_ratio())))
|
fc = '~'*(int(math.floor(width*language.get_fuzzy_ratio())))
|
||||||
uc = ' '*(width-len(tc)-len(fc))
|
uc = ' '*(width-len(tc)-len(fc))
|
||||||
|
|
||||||
print ' %5s [%s%s%s] -- %3.0f %% translated' % (language.language, tc, fc, uc, language.get_translated_ratio()*100)
|
print(' %5s [%s%s%s] -- %3.0f %% translated' % (language.language, tc, fc, uc, language.get_translated_ratio()*100))
|
||||||
|
|
||||||
print """
|
print("""
|
||||||
Total translations: %s
|
Total translations: %s
|
||||||
""" % (len(languages))
|
""" % (len(languages)))
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/python
|
#!/usr/bin/python3
|
||||||
#
|
#
|
||||||
# gPodder dependency installer for running the CLI from the source tree
|
# gPodder dependency installer for running the CLI from the source tree
|
||||||
#
|
#
|
||||||
|
@ -8,10 +8,10 @@
|
||||||
# Thomas Perl <thp.io/about>; 2012-02-11
|
# Thomas Perl <thp.io/about>; 2012-02-11
|
||||||
#
|
#
|
||||||
|
|
||||||
import urllib2
|
import urllib.request, urllib.error, urllib.parse
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import StringIO
|
import io
|
||||||
import tarfile
|
import tarfile
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
@ -30,33 +30,33 @@ MODULES = [
|
||||||
|
|
||||||
def get_tarball_url(modulename):
|
def get_tarball_url(modulename):
|
||||||
url = 'http://pypi.python.org/pypi/' + modulename
|
url = 'http://pypi.python.org/pypi/' + modulename
|
||||||
html = urllib2.urlopen(url).read()
|
html = urllib.request.urlopen(url).read().decode('utf-8')
|
||||||
match = re.search(r'(http[s]?://[^>]*%s-([0-9.]*)(?:\.post\d+)?\.tar\.gz)' % modulename, html)
|
match = re.search(r'(http[s]?://[^>]*%s-([0-9.]*)(?:\.post\d+)?\.tar\.gz)' % modulename, html)
|
||||||
return match.group(0) if match is not None else None
|
return match.group(0) if match is not None else None
|
||||||
|
|
||||||
for module, required_files in MODULES:
|
for module, required_files in MODULES:
|
||||||
print 'Fetching', module, '...',
|
print('Fetching', module, '...', end=' ')
|
||||||
tarball_url = get_tarball_url(module)
|
tarball_url = get_tarball_url(module)
|
||||||
if tarball_url is None:
|
if tarball_url is None:
|
||||||
print 'Cannot determine download URL for', module, '- aborting!'
|
print('Cannot determine download URL for', module, '- aborting!')
|
||||||
break
|
break
|
||||||
data = urllib2.urlopen(tarball_url).read()
|
data = urllib.request.urlopen(tarball_url).read()
|
||||||
print '%d KiB' % (len(data)/1024)
|
print('%d KiB' % (len(data)//1024))
|
||||||
tar = tarfile.open(fileobj=StringIO.StringIO(data))
|
tar = tarfile.open(fileobj=io.BytesIO(data))
|
||||||
for name in tar.getnames():
|
for name in tar.getnames():
|
||||||
match = re.match(required_files, name)
|
match = re.match(required_files, name)
|
||||||
if match is not None:
|
if match is not None:
|
||||||
target_name = match.group(1)
|
target_name = match.group(1)
|
||||||
target_file = os.path.join(src_dir, target_name)
|
target_file = os.path.join(src_dir, target_name)
|
||||||
if os.path.exists(target_file):
|
if os.path.exists(target_file):
|
||||||
print 'Skipping:', target_file
|
print('Skipping:', target_file)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
target_dir = os.path.dirname(target_file)
|
target_dir = os.path.dirname(target_file)
|
||||||
if not os.path.isdir(target_dir):
|
if not os.path.isdir(target_dir):
|
||||||
os.mkdir(target_dir)
|
os.mkdir(target_dir)
|
||||||
|
|
||||||
print 'Extracting:', target_name
|
print('Extracting:', target_name)
|
||||||
tar.extract(name, tmp_dir)
|
tar.extract(name, tmp_dir)
|
||||||
shutil.move(os.path.join(tmp_dir, name), target_file)
|
shutil.move(os.path.join(tmp_dir, name), target_file)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/python
|
#!/usr/bin/python3
|
||||||
# Progressbar icon tester
|
# Progressbar icon tester
|
||||||
# Thomas Perl <thp.io/about>; 2012-02-05
|
# Thomas Perl <thp.io/about>; 2012-02-05
|
||||||
#
|
#
|
||||||
|
@ -8,26 +8,26 @@
|
||||||
import sys
|
import sys
|
||||||
sys.path.insert(0, 'src')
|
sys.path.insert(0, 'src')
|
||||||
|
|
||||||
import gtk
|
from gi.repository import Gtk
|
||||||
|
|
||||||
from gpodder.gtkui.draw import draw_cake_pixbuf
|
from gpodder.gtkui.draw import draw_cake_pixbuf
|
||||||
|
|
||||||
def gen(percentage):
|
def gen(percentage):
|
||||||
pixbuf = draw_cake_pixbuf(percentage)
|
pixbuf = draw_cake_pixbuf(percentage)
|
||||||
return gtk.image_new_from_pixbuf(pixbuf)
|
return Gtk.Image.new_from_pixbuf(pixbuf)
|
||||||
|
|
||||||
w = gtk.Window()
|
w = Gtk.Window()
|
||||||
w.connect('destroy', gtk.main_quit)
|
w.connect('destroy', Gtk.main_quit)
|
||||||
v = gtk.VBox()
|
v = Gtk.VBox()
|
||||||
w.add(v)
|
w.add(v)
|
||||||
for y in xrange(1):
|
for y in range(1):
|
||||||
h = gtk.HBox()
|
h = Gtk.HBox()
|
||||||
h.set_homogeneous(True)
|
h.set_homogeneous(True)
|
||||||
v.add(h)
|
v.add(h)
|
||||||
PARTS = 20
|
PARTS = 20
|
||||||
for x in xrange(PARTS + 1):
|
for x in range(PARTS + 1):
|
||||||
h.add(gen(float(x)/float(PARTS)))
|
h.add(gen(x/PARTS))
|
||||||
w.set_default_size(400, 100)
|
w.set_default_size(400, 100)
|
||||||
w.show_all()
|
w.show_all()
|
||||||
gtk.main()
|
Gtk.main()
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
#!/usr/bin/python
|
#!/usr/bin/python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Simple HTTP web server for testing HTTP Authentication (see bug 1539)
|
# Simple HTTP web server for testing HTTP Authentication (see bug 1539)
|
||||||
# from our crappy-but-does-the-job department
|
# from our crappy-but-does-the-job department
|
||||||
# Thomas Perl <thp.io/about>; 2012-01-20
|
# Thomas Perl <thp.io/about>; 2012-01-20
|
||||||
|
|
||||||
import BaseHTTPServer
|
import http.server
|
||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
import hashlib
|
import hashlib
|
||||||
|
@ -47,7 +47,7 @@ def mkrss(items=EP_COUNT):
|
||||||
type="%(EPISODES_MIME)s"
|
type="%(EPISODES_MIME)s"
|
||||||
length="%(SIZE)s"/>
|
length="%(SIZE)s"/>
|
||||||
</item>
|
</item>
|
||||||
""" % dict(locals().items()+globals().items())
|
""" % dict(list(locals().items())+list(globals().items()))
|
||||||
for INDEX, PUBDATE in enumerate(mkpubdates(items)))
|
for INDEX, PUBDATE in enumerate(mkpubdates(items)))
|
||||||
|
|
||||||
return """
|
return """
|
||||||
|
@ -56,13 +56,13 @@ def mkrss(items=EP_COUNT):
|
||||||
%(ITEMS)s
|
%(ITEMS)s
|
||||||
</channel>
|
</channel>
|
||||||
</rss>
|
</rss>
|
||||||
""" % dict(locals().items()+globals().items())
|
""" % dict(list(locals().items())+list(globals().items()))
|
||||||
|
|
||||||
def mkdata(size=SIZE):
|
def mkdata(size=SIZE):
|
||||||
"""Generate dummy data of a given size (in bytes)"""
|
"""Generate dummy data of a given size (in bytes)"""
|
||||||
return ''.join(chr(32+(i%(127-32))) for i in range(size))
|
return ''.join(chr(32+(i%(127-32))) for i in range(size))
|
||||||
|
|
||||||
class AuthRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
class AuthRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||||
FEEDFILE_PATH = '/%s' % FEEDFILE
|
FEEDFILE_PATH = '/%s' % FEEDFILE
|
||||||
EPISODES_PATH = '/%s' % EPISODES
|
EPISODES_PATH = '/%s' % EPISODES
|
||||||
|
|
||||||
|
@ -77,21 +77,21 @@ class AuthRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||||
auth_data = m.group(1).decode('base64').split(':', 1)
|
auth_data = m.group(1).decode('base64').split(':', 1)
|
||||||
if len(auth_data) == 2:
|
if len(auth_data) == 2:
|
||||||
username, password = auth_data
|
username, password = auth_data
|
||||||
print 'Got username:', username
|
print('Got username:', username)
|
||||||
print 'Got password:', password
|
print('Got password:', password)
|
||||||
if (username, password) == (USERNAME, PASSWORD):
|
if (username, password) == (USERNAME, PASSWORD):
|
||||||
print 'Valid credentials provided.'
|
print('Valid credentials provided.')
|
||||||
authorized = True
|
authorized = True
|
||||||
|
|
||||||
if self.path == self.FEEDFILE_PATH:
|
if self.path == self.FEEDFILE_PATH:
|
||||||
print 'Feed request.'
|
print('Feed request.')
|
||||||
is_feed = True
|
is_feed = True
|
||||||
elif self.path.startswith(self.EPISODES_PATH):
|
elif self.path.startswith(self.EPISODES_PATH):
|
||||||
print 'Episode request.'
|
print('Episode request.')
|
||||||
is_episode = True
|
is_episode = True
|
||||||
|
|
||||||
if not authorized:
|
if not authorized:
|
||||||
print 'Not authorized - sending WWW-Authenticate header.'
|
print('Not authorized - sending WWW-Authenticate header.')
|
||||||
self.send_response(401)
|
self.send_response(401)
|
||||||
self.send_header('WWW-Authenticate',
|
self.send_header('WWW-Authenticate',
|
||||||
'Basic realm="%s"' % sys.argv[0])
|
'Basic realm="%s"' % sys.argv[0])
|
||||||
|
@ -108,12 +108,12 @@ class AuthRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
httpd = BaseHTTPServer.HTTPServer((HOST, PORT), AuthRequestHandler)
|
httpd = http.server.HTTPServer((HOST, PORT), AuthRequestHandler)
|
||||||
print """
|
print("""
|
||||||
Feed URL: %(URL)s/%(FEEDFILE)s
|
Feed URL: %(URL)s/%(FEEDFILE)s
|
||||||
Username: %(USERNAME)s
|
Username: %(USERNAME)s
|
||||||
Password: %(PASSWORD)s
|
Password: %(PASSWORD)s
|
||||||
""" % locals()
|
""" % locals())
|
||||||
while True:
|
while True:
|
||||||
httpd.handle_request()
|
httpd.handle_request()
|
||||||
|
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
The Win32 launcher can now be cross-compiled on Linux for Windows.
|
|
||||||
|
|
||||||
On Fedora, install:
|
|
||||||
|
|
||||||
mingw32-gcc-c++
|
|
||||||
|
|
||||||
Then, to build, use:
|
|
||||||
|
|
||||||
mingw32-make
|
|
||||||
|
|
||||||
This should result in gpo.exe and gpodder.exe that can be used for
|
|
||||||
the .zip and the .exe release.
|
|
|
@ -1,222 +0,0 @@
|
||||||
|
|
||||||
#include "gpodder.h"
|
|
||||||
#include "folderselector.h"
|
|
||||||
|
|
||||||
#include <windows.h>
|
|
||||||
#include <shlobj.h>
|
|
||||||
#include <shellapi.h>
|
|
||||||
|
|
||||||
|
|
||||||
/* Private function declarations */
|
|
||||||
|
|
||||||
void
|
|
||||||
UseFolderSelector();
|
|
||||||
|
|
||||||
int
|
|
||||||
FolderExists(const char *folder);
|
|
||||||
|
|
||||||
void
|
|
||||||
UseFolder(const char *folder);
|
|
||||||
|
|
||||||
void
|
|
||||||
SaveFolder(const char *folder);
|
|
||||||
|
|
||||||
const char *
|
|
||||||
RegistryFolder();
|
|
||||||
|
|
||||||
const char *
|
|
||||||
DefaultFolder();
|
|
||||||
|
|
||||||
int
|
|
||||||
AskUserFolder(const char *folder);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void
|
|
||||||
DetermineHomeFolder(int force_select)
|
|
||||||
{
|
|
||||||
if (force_select) {
|
|
||||||
/* Forced selection of (new) download folder */
|
|
||||||
UseFolderSelector();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getenv("GPODDER_HOME") != NULL) {
|
|
||||||
/* GPODDER_HOME already set - don't modify */
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (FolderExists(RegistryFolder())) {
|
|
||||||
/* Use folder in registry */
|
|
||||||
UseFolder(RegistryFolder());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (FolderExists(DefaultFolder())) {
|
|
||||||
/* Save default in registry and use it */
|
|
||||||
SaveFolder(DefaultFolder());
|
|
||||||
UseFolder(DefaultFolder());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (AskUserFolder(DefaultFolder())) {
|
|
||||||
/* User wants to use the default folder */
|
|
||||||
SaveFolder(DefaultFolder());
|
|
||||||
UseFolder(DefaultFolder());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* If everything else fails, use folder selector */
|
|
||||||
UseFolderSelector();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void
|
|
||||||
UseFolderSelector()
|
|
||||||
{
|
|
||||||
BROWSEINFO browseInfo = {
|
|
||||||
0, /* hwndOwner */
|
|
||||||
NULL, /* pidlRoot */
|
|
||||||
NULL, /* pszDisplayName */
|
|
||||||
"Select the data folder where gPodder will "
|
|
||||||
"store the database and downloaded episodes:", /* lpszTitle */
|
|
||||||
BIF_USENEWUI | BIF_RETURNONLYFSDIRS, /* ulFlags */
|
|
||||||
NULL, /* lpfn */
|
|
||||||
0, /* lParam */
|
|
||||||
0, /* iImage */
|
|
||||||
};
|
|
||||||
LPITEMIDLIST pidList;
|
|
||||||
static char path[MAX_PATH];
|
|
||||||
|
|
||||||
pidList = SHBrowseForFolder(&browseInfo);
|
|
||||||
if (pidList == NULL) {
|
|
||||||
/* User clicked on "Cancel" */
|
|
||||||
exit(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
memset(path, 0, sizeof(path));
|
|
||||||
if (!SHGetPathFromIDList(pidList, path)) {
|
|
||||||
BAILOUT("Could not determine filesystem path from selection.");
|
|
||||||
}
|
|
||||||
|
|
||||||
CoTaskMemFree(pidList);
|
|
||||||
|
|
||||||
SaveFolder(path);
|
|
||||||
UseFolder(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
int
|
|
||||||
FolderExists(const char *folder)
|
|
||||||
{
|
|
||||||
DWORD attrs;
|
|
||||||
|
|
||||||
if (folder == NULL) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
attrs = GetFileAttributes(folder);
|
|
||||||
return ((attrs != INVALID_FILE_ATTRIBUTES) &&
|
|
||||||
(attrs & FILE_ATTRIBUTE_DIRECTORY));
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
UseFolder(const char *folder)
|
|
||||||
{
|
|
||||||
if (folder == NULL) {
|
|
||||||
BAILOUT("Folder is NULL in UseFolder(). Exiting.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SetEnvironmentVariable("GPODDER_HOME", folder) == 0) {
|
|
||||||
BAILOUT("SetEnvironmentVariable for GPODDER_HOME failed.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
SaveFolder(const char *folder)
|
|
||||||
{
|
|
||||||
HKEY regKey;
|
|
||||||
|
|
||||||
if (folder == NULL) {
|
|
||||||
BAILOUT("Folder is NULL in SaveFolder(). Exiting.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (RegCreateKey(HKEY_CURRENT_USER, GPODDER_REGISTRY_KEY,
|
|
||||||
®Key) != ERROR_SUCCESS) {
|
|
||||||
BAILOUT("Cannot create registry key:\n\n"
|
|
||||||
"HKEY_CURRENT_USER\\" GPODDER_REGISTRY_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (RegSetValueEx(regKey,
|
|
||||||
"GPODDER_HOME",
|
|
||||||
0,
|
|
||||||
REG_SZ,
|
|
||||||
folder,
|
|
||||||
strlen(folder)+1) != ERROR_SUCCESS) {
|
|
||||||
BAILOUT("Cannot set value in registry:\n\n"
|
|
||||||
"HKEY_CURRENT_USER\\" GPODDER_REGISTRY_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
RegCloseKey(regKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
const char *
|
|
||||||
RegistryFolder()
|
|
||||||
{
|
|
||||||
static char folder[MAX_PATH] = {0};
|
|
||||||
DWORD folderSize = MAX_PATH;
|
|
||||||
HKEY regKey;
|
|
||||||
char *result = NULL;
|
|
||||||
|
|
||||||
if (strlen(folder)) {
|
|
||||||
return folder;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (RegOpenKeyEx(HKEY_CURRENT_USER, GPODDER_REGISTRY_KEY,
|
|
||||||
0, KEY_READ, ®Key) != ERROR_SUCCESS) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (RegQueryValueEx(regKey, "GPODDER_HOME", NULL, NULL,
|
|
||||||
folder, &folderSize) == ERROR_SUCCESS) {
|
|
||||||
result = folder;
|
|
||||||
}
|
|
||||||
|
|
||||||
RegCloseKey(regKey);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char *
|
|
||||||
DefaultFolder()
|
|
||||||
{
|
|
||||||
static char defaultFolder[MAX_PATH] = {0};
|
|
||||||
|
|
||||||
if (!strlen(defaultFolder)) {
|
|
||||||
if (SHGetFolderPath(NULL,
|
|
||||||
CSIDL_PERSONAL | CSIDL_FLAG_CREATE,
|
|
||||||
NULL,
|
|
||||||
0,
|
|
||||||
defaultFolder) != S_OK) {
|
|
||||||
BAILOUT("Cannot determine your home directory (SHGetFolderPath).");
|
|
||||||
}
|
|
||||||
strncat(defaultFolder, "\\gPodder\\", MAX_PATH);
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
int
|
|
||||||
AskUserFolder(const char *folder)
|
|
||||||
{
|
|
||||||
char tmp[MAX_PATH+100];
|
|
||||||
|
|
||||||
if (folder == NULL) return 0;
|
|
||||||
|
|
||||||
strcpy(tmp, PROGNAME " requires a download folder.\n"
|
|
||||||
"Use the default download folder?\n\n");
|
|
||||||
strcat(tmp, folder);
|
|
||||||
|
|
||||||
return (MessageBox(NULL, tmp, "No download folder selected",
|
|
||||||
MB_YESNO | MB_ICONQUESTION) == IDYES);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
#ifndef _FOLDERSELECTOR_H
|
|
||||||
#define _FOLDERSELECTOR_H
|
|
||||||
|
|
||||||
void
|
|
||||||
DetermineHomeFolder(int force_select);
|
|
||||||
|
|
||||||
#endif
|
|
Binary file not shown.
Before Width: | Height: | Size: 97 KiB |
Binary file not shown.
|
@ -1,267 +0,0 @@
|
||||||
|
|
||||||
/**
|
|
||||||
* gPodder - A media aggregator and podcast client
|
|
||||||
* Copyright (c) 2005-2017 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/>.
|
|
||||||
**/
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* gPodder for Windows
|
|
||||||
* Thomas Perl <thp@gpodder.org>; 2011-11-06
|
|
||||||
**/
|
|
||||||
|
|
||||||
|
|
||||||
#include <windows.h>
|
|
||||||
#include <shlobj.h>
|
|
||||||
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <shellapi.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <sys/stat.h>
|
|
||||||
#include <stdbool.h>
|
|
||||||
|
|
||||||
#include "gpodder.h"
|
|
||||||
#include "folderselector.h"
|
|
||||||
|
|
||||||
#if defined(GPODDER_GUI)
|
|
||||||
# define MAIN_MODULE "bin\\gpodder"
|
|
||||||
#else
|
|
||||||
# define MAIN_MODULE "bin\\gpo"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define LOOKUP_FUNCTION(x) {x = GetProcAddress(python_dll, #x); \
|
|
||||||
if(x == NULL) {BAILOUT("Cannot find function: " #x);}}
|
|
||||||
|
|
||||||
|
|
||||||
static char *
|
|
||||||
get_python_install_path()
|
|
||||||
{
|
|
||||||
static char InstallPath[MAX_PATH];
|
|
||||||
DWORD InstallPathSize = MAX_PATH;
|
|
||||||
HKEY RegKey;
|
|
||||||
char *result = NULL;
|
|
||||||
|
|
||||||
/* Try to detect "just for me"-installed Python version (bug 1480) */
|
|
||||||
if (RegOpenKeyEx(HKEY_CURRENT_USER,
|
|
||||||
"Software\\Python\\PythonCore\\2.7\\InstallPath",
|
|
||||||
0, KEY_READ, &RegKey) != ERROR_SUCCESS) {
|
|
||||||
/* Try to detect "for all users" Python (bug 1480, comment 9) */
|
|
||||||
if (RegOpenKeyEx(HKEY_LOCAL_MACHINE,
|
|
||||||
"Software\\Python\\PythonCore\\2.7\\InstallPath",
|
|
||||||
0, KEY_READ, &RegKey) != ERROR_SUCCESS) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (RegQueryValueEx(RegKey, NULL, NULL, NULL,
|
|
||||||
InstallPath, &InstallPathSize) == ERROR_SUCCESS) {
|
|
||||||
result = strdup(InstallPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
RegCloseKey(RegKey);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
char *FindPythonDLL()
|
|
||||||
{
|
|
||||||
char *path = get_python_install_path();
|
|
||||||
if (path == NULL) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const char *python27_dll = "\\python27.dll";
|
|
||||||
char *result = malloc(strlen(path) + strlen(python27_dll) + 1);
|
|
||||||
sprintf(result, "%s%s", path, python27_dll);
|
|
||||||
free(path);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool contains_system_dll(const char *path, const char *filename)
|
|
||||||
{
|
|
||||||
bool result = false;
|
|
||||||
struct stat st;
|
|
||||||
|
|
||||||
char *fn = malloc(strlen(path) + 1 + strlen(filename) + 1);
|
|
||||||
sprintf(fn, "%s\\%s", path, filename);
|
|
||||||
if (stat(fn, &st) == 0) {
|
|
||||||
result = true;
|
|
||||||
}
|
|
||||||
free(fn);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
char *clean_path_variable(const char *path)
|
|
||||||
{
|
|
||||||
char *old_path = strdup(path);
|
|
||||||
int length = strlen(path) + 1;
|
|
||||||
char *new_path = (char *)malloc(length);
|
|
||||||
memset(new_path, 0, length);
|
|
||||||
|
|
||||||
char *tok = strtok(old_path, ";");
|
|
||||||
while (tok != NULL) {
|
|
||||||
// Only add the path component if it doesn't contain msvcr90.dll
|
|
||||||
if (!contains_system_dll(tok, "msvcr90.dll")) {
|
|
||||||
if (strlen(new_path) > 0) {
|
|
||||||
strcat(new_path, ";");
|
|
||||||
}
|
|
||||||
|
|
||||||
strcat(new_path, tok);
|
|
||||||
}
|
|
||||||
|
|
||||||
tok = strtok(NULL, ";");
|
|
||||||
}
|
|
||||||
|
|
||||||
free(old_path);
|
|
||||||
return new_path;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char** argv)
|
|
||||||
{
|
|
||||||
char path_env[MAX_PATH];
|
|
||||||
char current_dir[MAX_PATH];
|
|
||||||
char *endmarker = NULL;
|
|
||||||
const char *target_folder = NULL;
|
|
||||||
char tmp[MAX_PATH];
|
|
||||||
int force_select = 0;
|
|
||||||
int i;
|
|
||||||
void *MainPy;
|
|
||||||
void *GtkModule;
|
|
||||||
int _argc = 1;
|
|
||||||
char *_argv[] = { MAIN_MODULE };
|
|
||||||
TCHAR gPodder_Home[MAX_PATH];
|
|
||||||
TCHAR Temp_Download_Filename[MAX_PATH];
|
|
||||||
|
|
||||||
HMODULE python_dll;
|
|
||||||
FARPROC Py_Initialize;
|
|
||||||
FARPROC PySys_SetArgvEx;
|
|
||||||
FARPROC PyImport_ImportModule;
|
|
||||||
FARPROC PyFile_FromString;
|
|
||||||
FARPROC PyFile_AsFile;
|
|
||||||
FARPROC PyRun_SimpleFile;
|
|
||||||
FARPROC Py_Finalize;
|
|
||||||
|
|
||||||
#if defined(GPODDER_CLI)
|
|
||||||
SetConsoleTitle(PROGNAME);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
for (i=1; i<argc; i++) {
|
|
||||||
if (strcmp(argv[i], "--select-folder") == 0) {
|
|
||||||
force_select = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DetermineHomeFolder(force_select);
|
|
||||||
|
|
||||||
if (GetEnvironmentVariable("GPODDER_HOME",
|
|
||||||
gPodder_Home, sizeof(gPodder_Home)) == 0) {
|
|
||||||
BAILOUT("Cannot determine download folder (GPODDER_HOME). Exiting.");
|
|
||||||
}
|
|
||||||
CreateDirectory(gPodder_Home, NULL);
|
|
||||||
|
|
||||||
/* Set current directory to directory of launcher */
|
|
||||||
strncpy(current_dir, argv[0], MAX_PATH);
|
|
||||||
endmarker = strrchr(current_dir, '\\');
|
|
||||||
if (endmarker == NULL) {
|
|
||||||
endmarker = strrchr(current_dir, '/');
|
|
||||||
}
|
|
||||||
if (endmarker != NULL) {
|
|
||||||
*endmarker = '\0';
|
|
||||||
/* We know the folder where the launcher sits - cd into it */
|
|
||||||
if (SetCurrentDirectory(current_dir) == 0) {
|
|
||||||
BAILOUT("Cannot set current directory.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Workaround for error R6034 (need to do this before Python DLL
|
|
||||||
* is loaded, otherwise the runtime error will still show up)
|
|
||||||
**/
|
|
||||||
char *new_path = clean_path_variable(getenv("PATH"));
|
|
||||||
SetEnvironmentVariable("PATH", new_path);
|
|
||||||
free(new_path);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Workaround import issues with Python 2.7.11.
|
|
||||||
**/
|
|
||||||
if (getenv("PYTHONHOME") == NULL) {
|
|
||||||
char *python_home = get_python_install_path();
|
|
||||||
if (python_home) {
|
|
||||||
SetEnvironmentVariable("PYTHONHOME", python_home);
|
|
||||||
free(python_home);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Only load the Python DLL after we've set up the environment */
|
|
||||||
python_dll = LoadLibrary("python27.dll");
|
|
||||||
|
|
||||||
if (python_dll == NULL) {
|
|
||||||
char *dll_path = FindPythonDLL();
|
|
||||||
if (dll_path != NULL) {
|
|
||||||
python_dll = LoadLibrary(dll_path);
|
|
||||||
free(dll_path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (python_dll == NULL) {
|
|
||||||
MessageBox(NULL,
|
|
||||||
PROGNAME " requires Python 2.7.\n"
|
|
||||||
"See http://gpodder.org/dependencies",
|
|
||||||
"Python 2.7 installation not found",
|
|
||||||
MB_OK | MB_ICONQUESTION);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOOKUP_FUNCTION(Py_Initialize);
|
|
||||||
LOOKUP_FUNCTION(PySys_SetArgvEx);
|
|
||||||
LOOKUP_FUNCTION(PyImport_ImportModule);
|
|
||||||
LOOKUP_FUNCTION(PyFile_FromString);
|
|
||||||
LOOKUP_FUNCTION(PyFile_AsFile);
|
|
||||||
LOOKUP_FUNCTION(PyRun_SimpleFile);
|
|
||||||
LOOKUP_FUNCTION(Py_Finalize);
|
|
||||||
|
|
||||||
Py_Initialize();
|
|
||||||
argv[0] = MAIN_MODULE;
|
|
||||||
PySys_SetArgvEx(argc, argv, 0);
|
|
||||||
|
|
||||||
#if defined(GPODDER_GUI)
|
|
||||||
/* Check for GTK, but not if we are running the CLI */
|
|
||||||
GtkModule = (void*)PyImport_ImportModule("gtk");
|
|
||||||
if (GtkModule == NULL) {
|
|
||||||
MessageBox(NULL,
|
|
||||||
PROGNAME " requires PyGTK.\n"
|
|
||||||
"See http://gpodder.org/dependencies",
|
|
||||||
"PyGTK installation not found",
|
|
||||||
MB_OK | MB_ICONQUESTION);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
// decref GtkModule
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// XXX: Test for podcastparser, mygpoclient, dbus
|
|
||||||
|
|
||||||
MainPy = (void*)PyFile_FromString(MAIN_MODULE, "r");
|
|
||||||
if (MainPy == NULL) { BAILOUT("Cannot load main file") }
|
|
||||||
if (PyRun_SimpleFile(PyFile_AsFile(MainPy), MAIN_MODULE) != 0) {
|
|
||||||
BAILOUT("There was an error running " MAIN_MODULE " in Python.");
|
|
||||||
}
|
|
||||||
// decref MainPy
|
|
||||||
Py_Finalize();
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
#ifndef _GPODDER_H
|
|
||||||
#define _GPODDER_H
|
|
||||||
|
|
||||||
#define PROGNAME "gPodder"
|
|
||||||
|
|
||||||
#define BAILOUT(s) { \
|
|
||||||
MessageBox(NULL, s, "Error launching " PROGNAME, MB_OK); \
|
|
||||||
exit(1); \
|
|
||||||
}
|
|
||||||
|
|
||||||
#define DEBUG(a, b) { \
|
|
||||||
MessageBox(NULL, a, b, MB_OK); \
|
|
||||||
}
|
|
||||||
|
|
||||||
#define GPODDER_REGISTRY_KEY \
|
|
||||||
"Software\\gpodder.org\\gPodder"
|
|
||||||
|
|
||||||
#endif
|
|
Binary file not shown.
Before Width: | Height: | Size: 97 KiB |
Binary file not shown.
|
@ -1,64 +0,0 @@
|
||||||
#
|
|
||||||
# gPodder - A media aggregator and podcast client
|
|
||||||
# Copyright (c) 2005-2017 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/>.
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
#
|
|
||||||
# Makefile for building the Win32 Launcher for gPodder
|
|
||||||
# Thomas Perl <thp@gpodder.org>; 2009-04-29
|
|
||||||
#
|
|
||||||
|
|
||||||
CROSS ?= i686-w64-mingw32
|
|
||||||
|
|
||||||
CC := $(CROSS)-gcc
|
|
||||||
CXX := $(CROSS)-g++
|
|
||||||
CPP := $(CROSS)-cpp
|
|
||||||
RANLIB := $(CROSS)-ranlib
|
|
||||||
STRIP := $(CROSS)-strip
|
|
||||||
WINDRES := $(CROSS)-windres
|
|
||||||
|
|
||||||
LDFLAGS += -lkernel32 -lshell32 -lole32
|
|
||||||
MODULES := gpodder folderselector
|
|
||||||
TARGETS := gpodder.exe gpo.exe
|
|
||||||
|
|
||||||
all: $(TARGETS)
|
|
||||||
|
|
||||||
gpodder.exe: gpodder-res.o $(addsuffix _gui.o, $(MODULES))
|
|
||||||
$(CC) -o $@ -mwindows $^ $(LDFLAGS)
|
|
||||||
$(STRIP) -s $@
|
|
||||||
|
|
||||||
gpo.exe: gpo-res.o $(addsuffix _cli.o, $(MODULES))
|
|
||||||
$(CC) -o $@ $^ $(LDFLAGS)
|
|
||||||
$(STRIP) -s $@
|
|
||||||
|
|
||||||
%_gui.o: %.c
|
|
||||||
$(CC) -c -o $@ $(CFLAGS) -DGPODDER_GUI $^
|
|
||||||
|
|
||||||
%_cli.o: %.c
|
|
||||||
$(CC) -c -o $@ $(CFLAGS) -DGPODDER_CLI $^
|
|
||||||
|
|
||||||
%-res.o: %.res
|
|
||||||
$(WINDRES) $^ $@
|
|
||||||
|
|
||||||
clean:
|
|
||||||
$(RM) *.o
|
|
||||||
|
|
||||||
distclean: clean
|
|
||||||
$(RM) $(TARGETS)
|
|
||||||
|
|
||||||
.PHONY: distclean clean all
|
|
||||||
.DEFAULT: all
|
|
|
@ -1,54 +0,0 @@
|
||||||
# gPodder Win32 Portable Cross-Build script
|
|
||||||
# 2014-10-21 Thomas Perl <m@thp.io>
|
|
||||||
|
|
||||||
PYTHON ?= python
|
|
||||||
|
|
||||||
VERSION := $(shell $(PYTHON) ../getversion.py)
|
|
||||||
|
|
||||||
MKDIR := mkdir
|
|
||||||
CP := cp
|
|
||||||
|
|
||||||
PORTABLE_OUTPUT_DIR := gpodder-$(VERSION)-win32
|
|
||||||
PORTABLE_OUTPUT := $(PORTABLE_OUTPUT_DIR).zip
|
|
||||||
|
|
||||||
SOURCE_ROOT := ../../
|
|
||||||
|
|
||||||
LAUNCHER_ROOT := ../win32-launcher
|
|
||||||
GPODDER_EXE := gpodder.exe
|
|
||||||
GPO_EXE := gpo.exe
|
|
||||||
|
|
||||||
all: $(PORTABLE_OUTPUT)
|
|
||||||
|
|
||||||
$(PORTABLE_OUTPUT_DIR): $(LAUNCHER_ROOT)/$(GPODDER_EXE) $(LAUNCHER_ROOT)/$(GPO_EXE)
|
|
||||||
$(PYTHON) ../localdepends.py
|
|
||||||
cp -rpv ../fake-dbus-module/dbus ../../src/
|
|
||||||
$(MAKE) -C $(SOURCE_ROOT) messages
|
|
||||||
$(RM) -r $(PORTABLE_OUTPUT_DIR)
|
|
||||||
$(MKDIR) -p $(PORTABLE_OUTPUT_DIR)
|
|
||||||
$(CP) $(LAUNCHER_ROOT)/$(GPODDER_EXE) $(LAUNCHER_ROOT)/$(GPO_EXE) $(PORTABLE_OUTPUT_DIR)
|
|
||||||
$(CP) $(SOURCE_ROOT)/README $(SOURCE_ROOT)/COPYING $(PORTABLE_OUTPUT_DIR)
|
|
||||||
$(CP) -r $(SOURCE_ROOT)/bin $(SOURCE_ROOT)/share $(SOURCE_ROOT)/src $(PORTABLE_OUTPUT_DIR)
|
|
||||||
$(RM) -r $(PORTABLE_OUTPUT_DIR)/share/applications
|
|
||||||
$(RM) -r $(PORTABLE_OUTPUT_DIR)/share/dbus-1
|
|
||||||
$(RM) -r $(PORTABLE_OUTPUT_DIR)/share/man
|
|
||||||
find $(PORTABLE_OUTPUT_DIR) -name '*.pyc' -exec $(RM) {} +
|
|
||||||
find $(PORTABLE_OUTPUT_DIR) -name '*.ui.h' -exec $(RM) {} +
|
|
||||||
|
|
||||||
$(PORTABLE_OUTPUT): $(PORTABLE_OUTPUT_DIR)
|
|
||||||
zip -r $(PORTABLE_OUTPUT) $(PORTABLE_OUTPUT_DIR)
|
|
||||||
|
|
||||||
$(LAUNCHER_ROOT)/$(GPODDER_EXE):
|
|
||||||
$(MAKE) -C $(LAUNCHER_ROOT) $(GPODDER_EXE)
|
|
||||||
|
|
||||||
$(LAUNCHER_ROOT)/$(GPO_EXE):
|
|
||||||
$(MAKE) -C $(LAUNCHER_ROOT) $(GPO_EXE)
|
|
||||||
|
|
||||||
clean:
|
|
||||||
$(RM) -r $(PORTABLE_OUTPUT_DIR)
|
|
||||||
$(MAKE) -C $(LAUNCHER_ROOT) distclean
|
|
||||||
|
|
||||||
distclean: clean
|
|
||||||
$(RM) -r $(PORTABLE_OUTPUT)
|
|
||||||
|
|
||||||
.PHONY: all clean distclean $(PORTABLE_OUTPUT_DIR) $(LAUNCHER_ROOT)/$(GPODDER_EXE) $(LAUNCHER_ROOT)/$(GPO_EXE)
|
|
||||||
.DEFAULT: all
|
|
|
@ -1,56 +0,0 @@
|
||||||
[Setup]
|
|
||||||
; NOTE: The value of AppId uniquely identifies this application.
|
|
||||||
; Do not use the same AppId value in installers for other applications.
|
|
||||||
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
|
|
||||||
AppId={{ABE123A1-41D1-4917-8E1E-C7E37991B673}
|
|
||||||
AppName=gPodder
|
|
||||||
AppVersion=%VERSION%
|
|
||||||
AppPublisher=Thomas Perl
|
|
||||||
AppPublisherURL=http://gpodder.org/
|
|
||||||
AppSupportURL=http://gpodder.org/documentation
|
|
||||||
AppUpdatesURL=http://gpodder.org/downloads
|
|
||||||
DefaultDirName={pf}\gPodder
|
|
||||||
DefaultGroupName=gPodder
|
|
||||||
LicenseFile=..\..\COPYING
|
|
||||||
InfoBeforeFile=..\..\README
|
|
||||||
OutputDir=.
|
|
||||||
OutputBaseFilename=gpodder-%VERSION%-setup
|
|
||||||
Compression=lzma
|
|
||||||
SolidCompression=yes
|
|
||||||
WizardSmallImageFile=wizard-small-image.bmp
|
|
||||||
WizardImageFile=wizard-image.bmp
|
|
||||||
|
|
||||||
[Languages]
|
|
||||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
|
||||||
|
|
||||||
[Tasks]
|
|
||||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
|
||||||
Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 0,6.1
|
|
||||||
|
|
||||||
[Files]
|
|
||||||
Source: "..\win32-launcher\gpodder.exe"; DestDir: "{app}"; Flags: ignoreversion
|
|
||||||
Source: "..\win32-launcher\gpo.exe"; DestDir: "{app}"; Flags: ignoreversion
|
|
||||||
Source: "..\..\COPYING"; DestDir: "{app}"; Flags: ignoreversion
|
|
||||||
Source: "..\..\README"; DestDir: "{app}"; Flags: ignoreversion
|
|
||||||
Source: "..\..\bin\*"; DestDir: "{app}\bin"; Flags: ignoreversion recursesubdirs createallsubdirs
|
|
||||||
Source: "..\..\share\*"; DestDir: "{app}\share"; Excludes: "*.h"; Flags: ignoreversion recursesubdirs createallsubdirs
|
|
||||||
Source: "..\..\src\*"; DestDir: "{app}\src"; Excludes: "*.pyc,*.py~"; Flags: ignoreversion recursesubdirs createallsubdirs
|
|
||||||
|
|
||||||
[Icons]
|
|
||||||
Name: "{group}\gPodder"; Filename: "{app}\gpodder.exe"
|
|
||||||
Name: "{group}\gPodder (set download folder)"; Filename: "{app}\gpodder.exe"; Parameters: "--select-folder"
|
|
||||||
Name: "{group}\gPodder (CLI)"; Filename: "{app}\gpo.exe"
|
|
||||||
Name: "{group}\{cm:ProgramOnTheWeb,gPodder}"; Filename: "http://gpodder.org/"
|
|
||||||
Name: "{group}\{cm:UninstallProgram,gPodder}"; Filename: "{uninstallexe}"
|
|
||||||
Name: "{commondesktop}\gPodder"; Filename: "{app}\gpodder.exe"; Tasks: desktopicon
|
|
||||||
Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\gPodder"; Filename: "{app}\gpodder.exe"; Tasks: quicklaunchicon
|
|
||||||
|
|
||||||
[Registry]
|
|
||||||
Root: HKCR; Subkey: "gpodder"; ValueType: string; ValueData: "gPodder Protocol Handler"; Flags: uninsdeletekey
|
|
||||||
Root: HKCR; Subkey: "gpodder"; ValueType: string; ValueName: "URL Protocol"; ValueData: ""
|
|
||||||
Root: HKCR; Subkey: "gpodder\DefaultIcon"; ValueType: string; ValueData: "{app}\gpodder.exe"
|
|
||||||
Root: HKCR; Subkey: "gpodder\shell\open\command"; ValueType: string; ValueData: """{app}\gpodder.exe"" -s ""%1"""
|
|
||||||
|
|
||||||
[Run]
|
|
||||||
Filename: "{app}\gpodder.exe"; Description: "{cm:LaunchProgram,gPodder}"; Flags: nowait postinstall skipifsilent
|
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
# gPodder Win32 Setup Cross-Build script
|
|
||||||
# 2014-10-21 Thomas Perl <m@thp.io>
|
|
||||||
|
|
||||||
PYTHON ?= python
|
|
||||||
|
|
||||||
VERSION := $(shell $(PYTHON) ../getversion.py)
|
|
||||||
|
|
||||||
SED := sed
|
|
||||||
CHMOD := chmod
|
|
||||||
|
|
||||||
# Simple script that calls ISCC.exe via Wine, and passes arguments
|
|
||||||
|
|
||||||
ISCC ?= "$(HOME)/.wine/drive_c/Program Files (x86)/Inno Setup 5/ISCC.exe"
|
|
||||||
|
|
||||||
SETUP_SCRIPT := gpodder-setup.iss
|
|
||||||
SETUP_SCRIPT_IN := $(SETUP_SCRIPT).in
|
|
||||||
SETUP_OUTPUT := gpodder-$(VERSION)-setup.exe
|
|
||||||
|
|
||||||
SOURCE_ROOT := ../../
|
|
||||||
|
|
||||||
LAUNCHER_ROOT := ../win32-launcher
|
|
||||||
GPODDER_EXE := gpodder.exe
|
|
||||||
GPO_EXE := gpo.exe
|
|
||||||
|
|
||||||
all: $(SETUP_OUTPUT)
|
|
||||||
|
|
||||||
$(SETUP_OUTPUT): $(SETUP_SCRIPT) $(LAUNCHER_ROOT)/$(GPODDER_EXE) $(LAUNCHER_ROOT)/$(GPO_EXE)
|
|
||||||
$(PYTHON) ../localdepends.py
|
|
||||||
cp -rpv ../fake-dbus-module/dbus ../../src/
|
|
||||||
$(MAKE) -C $(SOURCE_ROOT) messages
|
|
||||||
if [ ! -f $(ISCC) ]; then \
|
|
||||||
wget -O innosetup-installer.exe http://www.jrsoftware.org/download.php/is.exe; \
|
|
||||||
xvfb-run wine innosetup-installer.exe /verysilent; \
|
|
||||||
rm -f innosetup-installer.exe; \
|
|
||||||
fi
|
|
||||||
xvfb-run wine $(ISCC) $<
|
|
||||||
$(CHMOD) -x $@
|
|
||||||
|
|
||||||
$(LAUNCHER_ROOT)/$(GPODDER_EXE):
|
|
||||||
$(MAKE) -C $(LAUNCHER_ROOT) $(GPODDER_EXE)
|
|
||||||
|
|
||||||
$(LAUNCHER_ROOT)/$(GPO_EXE):
|
|
||||||
$(MAKE) -C $(LAUNCHER_ROOT) $(GPO_EXE)
|
|
||||||
|
|
||||||
$(SETUP_SCRIPT): $(SETUP_SCRIPT_IN)
|
|
||||||
$(RM) $@
|
|
||||||
$(SED) -e "s#%VERSION%#$(VERSION)#g" $< >$@
|
|
||||||
|
|
||||||
clean:
|
|
||||||
$(RM) $(SETUP_SCRIPT)
|
|
||||||
$(MAKE) -C $(LAUNCHER_ROOT) distclean
|
|
||||||
|
|
||||||
distclean: clean
|
|
||||||
$(RM) $(SETUP_OUTPUT)
|
|
||||||
|
|
||||||
.PHONY: all clean distclean $(LAUNCHER_ROOT)/$(GPODDER_EXE) $(LAUNCHER_ROOT)/$(GPO_EXE)
|
|
||||||
.DEFAULT: all
|
|
Binary file not shown.
Before Width: | Height: | Size: 201 KiB |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue