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
|
||||
sudo: required
|
||||
python:
|
||||
- "2.7"
|
||||
- "3.5"
|
||||
install:
|
||||
- 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
|
||||
- pip install coverage minimock
|
||||
- python tools/localdepends.py
|
||||
- sudo apt-get install intltool desktop-file-utils
|
||||
- pip3 install coverage minimock
|
||||
- python3 tools/localdepends.py
|
||||
script:
|
||||
- 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 po *
|
||||
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 -*-
|
||||
|
||||
#
|
||||
|
@ -63,7 +63,7 @@
|
|||
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
|
||||
import sys
|
||||
import collections
|
||||
|
@ -142,38 +142,6 @@ def incolor(color_id, s):
|
|||
return '\033[9%dm%s\033[0m' % (color_id, 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
|
||||
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)
|
||||
# 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"
|
||||
is_unique = lambda p: len([n for n in names if n.startswith(p)]) == 1
|
||||
|
@ -276,13 +244,13 @@ class gPodderCli(object):
|
|||
else:
|
||||
line = line + (' '*(self.COLUMNS-7-len(line)))
|
||||
self._current_action = line
|
||||
safe_print(self._current_action, end='')
|
||||
print(self._current_action, end='')
|
||||
|
||||
def _update_action(self, progress):
|
||||
if have_ansi:
|
||||
progress = '%3.0f%%' % (progress*100.,)
|
||||
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):
|
||||
if skip:
|
||||
|
@ -293,9 +261,9 @@ class gPodderCli(object):
|
|||
result = '['+inred('FAIL')+']'
|
||||
|
||||
if have_ansi:
|
||||
safe_print('\r' + self._current_action + result)
|
||||
print('\r' + self._current_action + result)
|
||||
else:
|
||||
safe_print(result)
|
||||
print(result)
|
||||
self._current_action = ''
|
||||
|
||||
def _atexit(self):
|
||||
|
@ -354,7 +322,7 @@ class gPodderCli(object):
|
|||
if title is not None:
|
||||
podcast.rename(title)
|
||||
podcast.save()
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
logger.warn('Cannot subscribe: %s', e, exc_info=True)
|
||||
if hasattr(e, 'strerror'):
|
||||
self._error(e.strerror)
|
||||
|
@ -371,7 +339,7 @@ class gPodderCli(object):
|
|||
for key in self._config.all_keys():
|
||||
if search_for is None or search_for.lower() in key.lower():
|
||||
value = config_value_to_string(self._config._lookup(key))
|
||||
safe_print(key, '=', value)
|
||||
print(key, '=', value)
|
||||
|
||||
def set(self, key=None, value=None):
|
||||
if value is None:
|
||||
|
@ -427,17 +395,17 @@ class gPodderCli(object):
|
|||
def status_str(episode):
|
||||
# is new
|
||||
if self.is_episode_new(episode):
|
||||
return u' * '
|
||||
return ' * '
|
||||
# is downloaded
|
||||
if (episode.state == gpodder.STATE_DOWNLOADED):
|
||||
return u' ▉ '
|
||||
return ' ▉ '
|
||||
# is 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()))
|
||||
return episodes
|
||||
|
||||
|
@ -456,8 +424,8 @@ class gPodderCli(object):
|
|||
title, url, status = podcast.title, podcast.url, \
|
||||
feed_update_status_msg(podcast)
|
||||
episodes = self._episodesList(podcast)
|
||||
episodes = u'\n '.join(episodes)
|
||||
self._pager(u"""
|
||||
episodes = '\n '.join(episodes)
|
||||
self._pager("""
|
||||
Title: %(title)s
|
||||
URL: %(url)s
|
||||
Feed update is %(status)s
|
||||
|
@ -475,24 +443,24 @@ class gPodderCli(object):
|
|||
podcast_printed = False
|
||||
if url is None or podcast.url == url:
|
||||
episodes = self._episodesList(podcast)
|
||||
episodes = u'\n '.join(episodes)
|
||||
output.append(u"""
|
||||
episodes = '\n '.join(episodes)
|
||||
output.append("""
|
||||
Episodes from %s:
|
||||
%s
|
||||
""" % (podcast.url, episodes))
|
||||
|
||||
self._pager(u'\n'.join(output))
|
||||
self._pager('\n'.join(output))
|
||||
return True
|
||||
|
||||
def list(self):
|
||||
for podcast in self._model.get_podcasts():
|
||||
if not podcast.pause_subscription:
|
||||
safe_print('#', ingreen(podcast.title))
|
||||
print('#', ingreen(podcast.title))
|
||||
else:
|
||||
safe_print('#', inred(podcast.title),
|
||||
print('#', inred(podcast.title),
|
||||
'-', _('Updates disabled'))
|
||||
|
||||
safe_print(podcast.url)
|
||||
print(podcast.url)
|
||||
|
||||
return True
|
||||
|
||||
|
@ -507,7 +475,7 @@ class gPodderCli(object):
|
|||
@FirstArgumentIsPodcastURL
|
||||
def update(self, url=None):
|
||||
count = 0
|
||||
safe_print(_('Checking for new episodes'))
|
||||
print(_('Checking for new episodes'))
|
||||
for podcast in self._model.get_podcasts():
|
||||
if url is not None and podcast.url != url:
|
||||
continue
|
||||
|
@ -521,7 +489,7 @@ class gPodderCli(object):
|
|||
self._finish_action(skip=True)
|
||||
|
||||
util.delete_empty_folders(gpodder.downloads)
|
||||
safe_print(inblue(self._pending_message(count)))
|
||||
print(inblue(self._pending_message(count)))
|
||||
return True
|
||||
|
||||
@FirstArgumentIsPodcastURL
|
||||
|
@ -533,13 +501,13 @@ class gPodderCli(object):
|
|||
for episode in podcast.get_all_episodes():
|
||||
if self.is_episode_new(episode):
|
||||
if not podcast_printed:
|
||||
safe_print('#', ingreen(podcast.title))
|
||||
print('#', ingreen(podcast.title))
|
||||
podcast_printed = True
|
||||
safe_print(' ', episode.title)
|
||||
print(' ', episode.title)
|
||||
count += 1
|
||||
|
||||
util.delete_empty_folders(gpodder.downloads)
|
||||
safe_print(inblue(self._pending_message(count)))
|
||||
print(inblue(self._pending_message(count)))
|
||||
return True
|
||||
|
||||
def _download_episode(self, episode):
|
||||
|
@ -565,12 +533,12 @@ class gPodderCli(object):
|
|||
last_podcast = None
|
||||
for episode in episodes:
|
||||
if episode.channel != last_podcast:
|
||||
safe_print(inblue(episode.channel.title))
|
||||
print(inblue(episode.channel.title))
|
||||
last_podcast = episode.channel
|
||||
self._download_episode(episode)
|
||||
|
||||
util.delete_empty_folders(gpodder.downloads)
|
||||
safe_print(len(episodes), 'episodes downloaded.')
|
||||
print(len(episodes), 'episodes downloaded.')
|
||||
return True
|
||||
|
||||
@FirstArgumentIsPodcastURL
|
||||
|
@ -606,7 +574,7 @@ class gPodderCli(object):
|
|||
def youtube(self, url):
|
||||
fmt_ids = youtube.get_fmt_ids(self._config.youtube)
|
||||
yurl = youtube.get_real_download_url(url, fmt_ids)
|
||||
safe_print(yurl)
|
||||
print(yurl)
|
||||
|
||||
return True
|
||||
|
||||
|
@ -672,11 +640,11 @@ class gPodderCli(object):
|
|||
return
|
||||
|
||||
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
|
||||
|
||||
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 '')
|
||||
for index, (title, url) in enumerate(results)))
|
||||
|
||||
|
@ -684,7 +652,7 @@ class gPodderCli(object):
|
|||
|
||||
msg = _('Enter index to subscribe, ? for list')
|
||||
while True:
|
||||
index = raw_input(msg + ': ')
|
||||
index = input(msg + ': ')
|
||||
|
||||
if not index:
|
||||
return
|
||||
|
@ -728,7 +696,7 @@ class gPodderCli(object):
|
|||
return True
|
||||
|
||||
def help(self):
|
||||
safe_print(stylize(__doc__), file=sys.stderr, end='')
|
||||
print(stylize(__doc__), file=sys.stderr, end='')
|
||||
return True
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
|
@ -739,14 +707,14 @@ class gPodderCli(object):
|
|||
rows_needed = len(output.splitlines()) + 2
|
||||
rows, cols = get_terminal_size()
|
||||
if rows_needed < rows:
|
||||
safe_print(output)
|
||||
print(output)
|
||||
else:
|
||||
pydoc.pager(util.sanitize_encoding(output))
|
||||
pydoc.pager(output)
|
||||
else:
|
||||
safe_print(output)
|
||||
print(output)
|
||||
|
||||
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
|
||||
%(__copyright__)s
|
||||
License: %(__license__)s
|
||||
|
@ -764,12 +732,12 @@ class gPodderCli(object):
|
|||
|
||||
while True:
|
||||
try:
|
||||
line = raw_input('gpo> ')
|
||||
line = input('gpo> ')
|
||||
except EOFError:
|
||||
safe_print('')
|
||||
print('')
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
safe_print('')
|
||||
print('')
|
||||
continue
|
||||
|
||||
if self._prefixes.get(line, line) in self.EXIT_COMMANDS:
|
||||
|
@ -777,7 +745,7 @@ class gPodderCli(object):
|
|||
|
||||
try:
|
||||
args = shlex.split(line)
|
||||
except ValueError, value_error:
|
||||
except ValueError as value_error:
|
||||
self._error(_('Syntax error: %(error)s') %
|
||||
{'error': value_error})
|
||||
continue
|
||||
|
@ -792,13 +760,13 @@ class gPodderCli(object):
|
|||
self._atexit()
|
||||
|
||||
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
|
||||
_warn = _error
|
||||
|
||||
def _info(self, *args):
|
||||
safe_print(*args)
|
||||
print(*args)
|
||||
|
||||
def _checkargs(self, func, command_line):
|
||||
args, varargs, keywords, defaults = inspect.getargspec(func)
|
||||
|
@ -872,9 +840,9 @@ class gPodderCli(object):
|
|||
return self._checkargs(func, command_line)
|
||||
|
||||
if command in self._expansions:
|
||||
safe_print(_('Ambiguous command. Did you mean..'))
|
||||
print(_('Ambiguous command. Did you mean..'))
|
||||
for cmd in self._expansions[command]:
|
||||
safe_print(' ', inblue(cmd))
|
||||
print(' ', inblue(cmd))
|
||||
else:
|
||||
self._error(_('The requested function is not available.'))
|
||||
|
||||
|
@ -897,5 +865,5 @@ if __name__ == '__main__':
|
|||
elif interactive_console:
|
||||
cli._shell()
|
||||
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 -*-
|
||||
|
||||
#
|
||||
|
@ -43,9 +43,9 @@ try:
|
|||
import dbus.glib
|
||||
have_dbus = True
|
||||
except ImportError:
|
||||
print >>sys.stderr, """
|
||||
print("""
|
||||
Warning: python-dbus not found. Disabling D-Bus support.
|
||||
"""
|
||||
""", file=sys.stderr)
|
||||
have_dbus = False
|
||||
|
||||
from optparse import OptionParser
|
||||
|
@ -73,9 +73,9 @@ def main():
|
|||
process = subprocess.Popen(locale_cmd, stdout=subprocess.PIPE)
|
||||
output, error_output = process.communicate()
|
||||
# the output is a string like 'fr_FR', and we need 'fr_FR.utf-8'
|
||||
user_locale = output.strip() + '.UTF-8'
|
||||
user_locale = output.decode('utf-8').strip() + '.UTF-8'
|
||||
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
|
||||
gettext.bindtextdomain('gpodder', locale_dir)
|
||||
|
@ -112,7 +112,7 @@ def main():
|
|||
options, args = parser.parse_args(sys.argv)
|
||||
|
||||
gpodder.ui.gtk = True
|
||||
gpodder.ui.python2 = True
|
||||
gpodder.ui.python3 = True
|
||||
|
||||
gpodder.ui.unity = (os.environ.get('DESKTOP_SESSION', 'unknown').lower() in
|
||||
('ubuntu', 'ubuntu-2d'))
|
||||
|
@ -140,7 +140,7 @@ def main():
|
|||
remote_object.subscribe_to_url(options.subscribe)
|
||||
|
||||
return
|
||||
except dbus.exceptions.DBusException, dbus_exception:
|
||||
except dbus.exceptions.DBusException as dbus_exception:
|
||||
logger.info('Cannot connect to remote object.', exc_info=True)
|
||||
|
||||
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 -*-
|
||||
|
||||
#
|
||||
|
@ -27,7 +27,7 @@
|
|||
import sys
|
||||
import os
|
||||
import re
|
||||
import ConfigParser
|
||||
import configparser
|
||||
import shutil
|
||||
|
||||
gpodder_script = sys.argv[0]
|
||||
|
@ -56,25 +56,25 @@ old_config = os.path.expanduser('~/.config/gpodder/gpodder.conf')
|
|||
new_config = gpodder.config_file
|
||||
|
||||
if not os.path.exists(old_database):
|
||||
print >>sys.stderr, """
|
||||
print("""
|
||||
Turns out that you never ran gPodder 2.
|
||||
Can't find this required file:
|
||||
|
||||
%(old_database)s
|
||||
""" % locals()
|
||||
""" % locals(), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
old_downloads = None
|
||||
|
||||
if os.path.exists(old_config):
|
||||
parser = ConfigParser.RawConfigParser()
|
||||
parser = configparser.RawConfigParser()
|
||||
parser.read(old_config)
|
||||
try:
|
||||
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
|
||||
pass
|
||||
except ConfigParser.NoOptionError:
|
||||
except configparser.NoOptionError:
|
||||
# The section is available, but the key (download_dir) is not
|
||||
pass
|
||||
|
||||
|
@ -87,22 +87,22 @@ if old_downloads is None:
|
|||
new_downloads = gpodder.downloads
|
||||
|
||||
if not os.path.exists(old_downloads):
|
||||
print >>sys.stderr, """
|
||||
print("""
|
||||
Old download directory does not exist. Creating empty one.
|
||||
"""
|
||||
""", file=sys.stderr)
|
||||
os.makedirs(old_downloads)
|
||||
|
||||
if any(os.path.exists(x) for x in (new_database, new_downloads)):
|
||||
print >>sys.stderr, """
|
||||
print("""
|
||||
Existing gPodder 3 user data found.
|
||||
To continue, please remove:
|
||||
|
||||
%(new_database)s
|
||||
%(new_downloads)s
|
||||
""" % locals()
|
||||
""" % locals(), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print >>sys.stderr, """
|
||||
print("""
|
||||
Would carry out the following actions:
|
||||
|
||||
Move downloads from %(old_downloads)s
|
||||
|
@ -111,21 +111,21 @@ print >>sys.stderr, """
|
|||
Convert database from %(old_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':
|
||||
util.make_directory(gpodder.home)
|
||||
schema.convert_gpodder2_db(old_database, 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)
|
||||
|
||||
shutil.move(old_downloads, 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)
|
||||
|
||||
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 ?= /
|
||||
PREFIX ?= /usr
|
||||
|
||||
PYTHON ?= python
|
||||
PYTHON ?= python3
|
||||
HELP2MAN ?= help2man
|
||||
|
||||
##########################################################################
|
||||
|
@ -84,12 +84,6 @@ $(GPODDER_SERVICE_FILE): $(GPODDER_SERVICE_FILE_IN)
|
|||
install: messages $(GPODDER_SERVICE_FILE) $(DESKTOP_FILES)
|
||||
$(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)
|
||||
|
@ -137,16 +131,13 @@ clean:
|
|||
rm -f $(GPODDER_SERVICE_FILE)
|
||||
rm -f $(DESKTOP_FILES) $(DESKTOP_FILES_IN_H)
|
||||
rm -rf build $(LOCALEDIR)
|
||||
rm -f gpodder-*-win32.zip gpodder-*-setup.exe
|
||||
|
||||
distclean: clean
|
||||
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
|
||||
|
@ -37,7 +37,7 @@ author, email = re.match(r'^(.*) <(.*)>$', metadata['author']).groups()
|
|||
class MissingFile(BaseException): pass
|
||||
|
||||
def info(message, item=None):
|
||||
print '=>', message, item if item is not None else ''
|
||||
print('=>', message, item if item is not None else '')
|
||||
|
||||
|
||||
def find_data_files(uis, scripts):
|
||||
|
@ -94,7 +94,7 @@ def find_data_files(uis, scripts):
|
|||
if not result:
|
||||
info('Skipping manpage without script:', filename)
|
||||
return result
|
||||
filenames = filter(have_script, filenames)
|
||||
filenames = list(filter(have_script, filenames))
|
||||
|
||||
def convert_filename(filename):
|
||||
filename = os.path.join(dirpath, filename)
|
||||
|
@ -112,7 +112,7 @@ def find_data_files(uis, scripts):
|
|||
|
||||
return filename
|
||||
|
||||
filenames = filter(None, map(convert_filename, filenames))
|
||||
filenames = [_f for _f in map(convert_filename, filenames) if _f]
|
||||
if filenames:
|
||||
# Some distros/ports install manpages into $PREFIX/man instead
|
||||
# of $PREFIX/share/man (e.g. FreeBSD). To allow this, we strip
|
||||
|
@ -137,10 +137,10 @@ def find_packages(uis):
|
|||
package = '.'.join(dirparts)
|
||||
|
||||
# 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:
|
||||
# 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:
|
||||
if folder_ui not in uis:
|
||||
info('Skipping package:', package)
|
||||
|
@ -181,13 +181,13 @@ try:
|
|||
packages = list(sorted(find_packages(uis)))
|
||||
scripts = list(sorted(find_scripts(uis)))
|
||||
data_files = list(sorted(find_data_files(uis, scripts)))
|
||||
except MissingFile, mf:
|
||||
print >>sys.stderr, """
|
||||
except MissingFile as mf:
|
||||
print("""
|
||||
Missing file: %s
|
||||
|
||||
If you want to install, use "make install" instead of using
|
||||
setup.py directly. See the README file for more information.
|
||||
""" % mf.message
|
||||
""" % mf.message, file=sys.stderr)
|
||||
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
|
||||
#
|
||||
# Set the configuration options "audio_played_dbus" and "video_played_dbus"
|
||||
|
@ -16,9 +16,9 @@ import sys
|
|||
import os
|
||||
|
||||
if len(sys.argv) != 2:
|
||||
print >>sys.stderr, """
|
||||
print("""
|
||||
Usage: %s /path/to/episode.mp3
|
||||
""" % (sys.argv[0],)
|
||||
""" % (sys.argv[0],), file=sys.stderr)
|
||||
sys.exit(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)
|
||||
|
||||
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)
|
||||
|
||||
|
|
|
@ -21,15 +21,15 @@ class gPodderExtension:
|
|||
# into various parts of gPodder.
|
||||
def on_load(self):
|
||||
logger.info('Extension is being loaded.')
|
||||
print '='*40
|
||||
print 'container:', self.container
|
||||
print 'container.manager:', self.container.manager
|
||||
print 'container.config:', self.container.config
|
||||
print 'container.manager.core:', self.container.manager.core
|
||||
print 'container.manager.core.db:', self.container.manager.core.db
|
||||
print 'container.manager.core.config:', self.container.manager.core.config
|
||||
print 'container.manager.core.model:', self.container.manager.core.model
|
||||
print '='*40
|
||||
print('='*40)
|
||||
print('container:', self.container)
|
||||
print('container.manager:', self.container.manager)
|
||||
print('container.config:', self.container.config)
|
||||
print('container.manager.core:', self.container.manager.core)
|
||||
print('container.manager.core.db:', self.container.manager.core.db)
|
||||
print('container.manager.core.config:', self.container.manager.core.config)
|
||||
print('container.manager.core.model:', self.container.manager.core.model)
|
||||
print('='*40)
|
||||
|
||||
# This function will be called when the extension is disabled or
|
||||
# when gPodder shuts down. You can use this to destroy/delete any
|
||||
|
@ -49,5 +49,4 @@ class gPodderExtension:
|
|||
return [("Say Hello", self.say_hello_cb)]
|
||||
|
||||
def say_hello_cb(self):
|
||||
print("HELLO")
|
||||
self.gpodder.notification("Hello Extension", "Message", widget=self.gpodder.main_window)
|
||||
|
|
|
@ -8,7 +8,7 @@ import subprocess
|
|||
import gpodder
|
||||
from gpodder import util
|
||||
|
||||
import gtk
|
||||
from gi.repository import Gtk
|
||||
from gpodder.gtkui.interface.progress import ProgressIndicator
|
||||
import os
|
||||
|
||||
|
@ -34,13 +34,13 @@ class gPodderExtension:
|
|||
self.gpodder = ui_object
|
||||
|
||||
def _get_save_filename(self):
|
||||
dlg = gtk.FileChooserDialog(title=_('Save video'),
|
||||
dlg = Gtk.FileChooserDialog(title=_('Save video'),
|
||||
parent=self.gpodder.get_dialog_parent(),
|
||||
action=gtk.FILE_CHOOSER_ACTION_SAVE)
|
||||
dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
|
||||
dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
|
||||
action=Gtk.FileChooserAction.SAVE)
|
||||
dlg.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
|
||||
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()
|
||||
dlg.destroy()
|
||||
return filename
|
||||
|
|
|
@ -262,7 +262,7 @@ class gPodderExtension:
|
|||
self.config = container.config
|
||||
|
||||
# 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()]
|
||||
|
||||
def on_ui_object_available(self, name, ui_object):
|
||||
|
|
|
@ -14,10 +14,10 @@ _ = gpodder.gettext
|
|||
__title__ = _('Gtk Status Icon')
|
||||
__description__ = _('Show a status icon for Gtk-based Desktops.')
|
||||
__category__ = 'desktop-integration'
|
||||
__only_for__ = 'gtk,python2'
|
||||
__disable_in__ = 'unity,win32'
|
||||
__only_for__ = 'gtk'
|
||||
__disable_in__ = 'unity,win32,python3'
|
||||
|
||||
import gtk
|
||||
from gi.repository import Gtk
|
||||
import os.path
|
||||
|
||||
from gpodder.gtkui import draw
|
||||
|
@ -39,7 +39,7 @@ class gPodderExtension:
|
|||
path = os.path.join(os.path.dirname(__file__), '..', '..', 'icons')
|
||||
icon_path = os.path.abspath(path)
|
||||
|
||||
theme = gtk.icon_theme_get_default()
|
||||
theme = Gtk.IconTheme.get_default()
|
||||
theme.append_search_path(icon_path)
|
||||
|
||||
if self.icon_name is None:
|
||||
|
@ -49,11 +49,11 @@ class gPodderExtension:
|
|||
self.icon_name = 'stock_mic'
|
||||
|
||||
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
|
||||
|
||||
# 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:
|
||||
return
|
||||
|
||||
|
@ -63,7 +63,7 @@ class gPodderExtension:
|
|||
# Currently icon is not a pixbuf => was loaded by name, at which
|
||||
# point size was automatically determined.
|
||||
icon_size = self.status_icon.get_size()
|
||||
icon_pixbuf = theme.load_icon(self.icon_name, icon_size, gtk.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)
|
||||
|
||||
def on_load(self):
|
||||
|
@ -91,7 +91,7 @@ class gPodderExtension:
|
|||
|
||||
def get_icon_pixbuf(self):
|
||||
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)
|
||||
return self.status_icon.get_pixbuf()
|
||||
|
||||
|
@ -117,7 +117,7 @@ class gPodderExtension:
|
|||
|
||||
icon = self.get_icon_pixbuf().copy()
|
||||
progressbar = draw.progressbar_pixbuf(icon.get_width(), icon.get_height(), progress)
|
||||
progressbar.composite(icon, 0, 0, icon.get_width(), icon.get_height(), 0, 0, 1, 1, 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.last_progress = progress
|
||||
|
|
|
@ -24,8 +24,8 @@ import dbus.service
|
|||
import gpodder
|
||||
import logging
|
||||
import time
|
||||
import urllib
|
||||
import urlparse
|
||||
import urllib.request, urllib.parse, urllib.error
|
||||
import urllib.parse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_ = gpodder.gettext
|
||||
|
@ -43,7 +43,7 @@ TrackInfo = collections.namedtuple('TrackInfo',
|
|||
['uri', 'length', 'status', 'pos', 'rate'])
|
||||
|
||||
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):
|
||||
'''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
|
||||
# has changed discontinuously, notify a stop for the old position
|
||||
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'])
|
||||
):
|
||||
logger.debug('notify Stopped: playback discontinuity:' +
|
||||
|
@ -125,6 +125,7 @@ class CurrentTrackTracker(object):
|
|||
self.notify_stop()
|
||||
|
||||
if ( (kwargs['pos']) == 0
|
||||
and self.pos is not None
|
||||
and self.pos > (self.length - USECS_IN_SEC)
|
||||
and self.pos < (self.length + 2 * USECS_IN_SEC)
|
||||
):
|
||||
|
@ -176,7 +177,7 @@ class CurrentTrackTracker(object):
|
|||
):
|
||||
return
|
||||
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
|
||||
|
||||
if status == 'Stopped':
|
||||
|
@ -200,8 +201,8 @@ class CurrentTrackTracker(object):
|
|||
return '%s: %s at %d/%d (@%f)' % (
|
||||
self.uri or 'None',
|
||||
self.status or 'None',
|
||||
(self.pos or 0) / USECS_IN_SEC,
|
||||
(self.length or 0) / USECS_IN_SEC,
|
||||
(self.pos or 0) // USECS_IN_SEC,
|
||||
(self.length or 0) // USECS_IN_SEC,
|
||||
self.rate or 0)
|
||||
|
||||
class MPRISDBusReceiver(object):
|
||||
|
@ -246,23 +247,23 @@ class MPRISDBusReceiver(object):
|
|||
invalidated_properties, path=None):
|
||||
if interface_name != self.INTERFACE_MPRIS:
|
||||
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
|
||||
|
||||
collected_info = {}
|
||||
|
||||
if changed_properties.has_key('PlaybackStatus'):
|
||||
if 'PlaybackStatus' in changed_properties:
|
||||
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
|
||||
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['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['pos'] = self.query_position()
|
||||
|
||||
if not collected_info.has_key('status'):
|
||||
if 'status' not in collected_info:
|
||||
collected_info['status'] = str(self.query_status())
|
||||
logger.debug('collected info: %r', collected_info)
|
||||
|
||||
|
|
|
@ -32,9 +32,14 @@ import logging
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import pynotify
|
||||
import gi
|
||||
gi.require_version('Notify', '0.7')
|
||||
from gi.repository import Notify
|
||||
pynotify = True
|
||||
except ImportError:
|
||||
pynotify = None
|
||||
except ValueError:
|
||||
pynotify = None
|
||||
|
||||
|
||||
if pynotify is None:
|
||||
|
@ -47,16 +52,16 @@ else:
|
|||
self.container = container
|
||||
|
||||
def on_load(self):
|
||||
pynotify.init('gPodder')
|
||||
Notify.init('gPodder')
|
||||
|
||||
def on_unload(self):
|
||||
pynotify.uninit()
|
||||
Notify.uninit()
|
||||
|
||||
def on_notification_show(self, title, message):
|
||||
if not message and not title:
|
||||
return
|
||||
|
||||
notify = pynotify.Notification(title or '', message or '',
|
||||
notify = Notify.Notification.new(title or '', message or '',
|
||||
gpodder.icon_file)
|
||||
|
||||
try:
|
||||
|
|
|
@ -48,7 +48,7 @@ class gPodderExtension:
|
|||
basename, ext = os.path.splitext(filename)
|
||||
|
||||
new_basename = []
|
||||
new_basename.append(util.sanitize_encoding(title) + ext)
|
||||
new_basename.append(title + ext)
|
||||
if self.config.add_podcast_title:
|
||||
new_basename.insert(0, podcast_title)
|
||||
if self.config.add_sortdate:
|
||||
|
@ -56,8 +56,7 @@ class gPodderExtension:
|
|||
new_basename = ' - '.join(new_basename)
|
||||
|
||||
# On Windows, force ASCII encoding for filenames (bug 1724)
|
||||
new_basename = util.sanitize_filename(new_basename,
|
||||
use_ascii=gpodder.ui.win32)
|
||||
new_basename = util.sanitize_filename(new_basename)
|
||||
new_filename = os.path.join(dirname, new_basename)
|
||||
|
||||
if new_filename == current_filename:
|
||||
|
|
|
@ -94,6 +94,6 @@ class gPodderExtension:
|
|||
if found:
|
||||
logger.info('Removed cover art from OGG file: %s', filename)
|
||||
ogg.save()
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
logger.warn('Failed to remove OGG cover: %s', e, exc_info=True)
|
||||
|
||||
|
|
|
@ -88,8 +88,8 @@ class gPodderExtension:
|
|||
if video_height is None:
|
||||
return None
|
||||
|
||||
width_ratio = device_width / video_width
|
||||
height_ratio = device_height / video_height
|
||||
width_ratio = device_width // video_width
|
||||
height_ratio = device_height // video_height
|
||||
|
||||
dest_width = device_width
|
||||
dest_height = width_ratio * video_height
|
||||
|
@ -134,9 +134,6 @@ class gPodderExtension:
|
|||
'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),
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
stdout, stderr = process.communicate()
|
||||
|
|
|
@ -162,7 +162,7 @@ class Mp3File(AudioFile):
|
|||
encoding = 3, # 3 is for utf-8
|
||||
mime = mimetypes.guess_type(self.cover)[0],
|
||||
type = 3,
|
||||
desc = u'Cover',
|
||||
desc = 'Cover',
|
||||
data = open(self.cover).read()
|
||||
)
|
||||
)
|
||||
|
@ -257,7 +257,7 @@ class gPodderExtension:
|
|||
if self.container.config.auto_embed_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):
|
||||
downloader = coverart.CoverDownloader()
|
||||
|
|
|
@ -58,7 +58,7 @@ class gPodderExtension(object):
|
|||
def get_data_from_url(self, url):
|
||||
try:
|
||||
response = util.urlopen(url).read()
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
logger.warn("subtitle url returned error %s", e)
|
||||
return ''
|
||||
return response
|
||||
|
@ -93,7 +93,7 @@ class gPodderExtension(object):
|
|||
intro = episode_data.split('introDuration":')[1] \
|
||||
.split(',')[0] or INTRO_DEFAULT
|
||||
intro = int(float(intro)*1000)
|
||||
except (ValueError, IndexError), e:
|
||||
except (ValueError, IndexError) as e:
|
||||
logger.info("Couldn't parse introDuration string: %s", intro)
|
||||
intro = INTRO_DEFAULT * 1000
|
||||
current_filename = episode.local_filename(create=False)
|
||||
|
@ -103,7 +103,7 @@ class gPodderExtension(object):
|
|||
try:
|
||||
with open(srt_filename, 'w+') as srtFile:
|
||||
srtFile.write(sub.encode("utf-8"))
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
logger.warn("Can't write srt file: %s",e)
|
||||
|
||||
def on_episode_delete(self, episode, filename):
|
||||
|
|
|
@ -17,7 +17,7 @@ __disable_in__ = 'win32'
|
|||
|
||||
|
||||
import appindicator
|
||||
import gtk
|
||||
from gi.repository import Gtk
|
||||
|
||||
import logging
|
||||
|
||||
|
@ -43,8 +43,8 @@ class gPodderExtension:
|
|||
self.indicator.set_status(appindicator.STATUS_ACTIVE)
|
||||
|
||||
def _rebuild_menu(self):
|
||||
menu = gtk.Menu()
|
||||
toggle_visible = gtk.CheckMenuItem(_('Show main window'))
|
||||
menu = Gtk.Menu()
|
||||
toggle_visible = Gtk.CheckMenuItem(_('Show main window'))
|
||||
toggle_visible.set_active(True)
|
||||
def on_toggle_visible(menu_item):
|
||||
if menu_item.get_active():
|
||||
|
@ -53,8 +53,8 @@ class gPodderExtension:
|
|||
self.gpodder.main_window.hide()
|
||||
toggle_visible.connect('activate', on_toggle_visible)
|
||||
menu.append(toggle_visible)
|
||||
menu.append(gtk.SeparatorMenuItem())
|
||||
quit_gpodder = gtk.MenuItem(_('Quit'))
|
||||
menu.append(Gtk.SeparatorMenuItem())
|
||||
quit_gpodder = Gtk.MenuItem(_('Quit'))
|
||||
def on_quit(menu_item):
|
||||
self.gpodder.on_gPodder_delete_event(self.gpodder.main_window)
|
||||
quit_gpodder.connect('activate', on_quit)
|
||||
|
|
|
@ -53,7 +53,7 @@ if __name__ != '__main__':
|
|||
try:
|
||||
self.process.stdin.write('progress %f\n' % progress)
|
||||
self.process.stdin.flush()
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
logger.debug('Ubuntu progress update failed.', exc_info=True)
|
||||
else:
|
||||
from gi.repository import Unity, GObject
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--*- mode: xml -*-->
|
||||
<interface>
|
||||
<!-- interface-requires gtk+ 3.10 -->
|
||||
<object class="GtkAdjustment" id="adjustment1">
|
||||
<property name="upper">10240</property>
|
||||
<property name="lower">0.5</property>
|
||||
|
@ -15,389 +16,8 @@
|
|||
<property name="step_increment">1</property>
|
||||
<property name="page_size">0</property>
|
||||
</object>
|
||||
<object class="GtkUIManager" id="uimanager1">
|
||||
<child>
|
||||
<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">
|
||||
<object class="GtkApplicationWindow" id="gPodder">
|
||||
<property name="application">app</property>
|
||||
<property name="visible">False</property>
|
||||
<property name="title">gPodder</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="focus_on_map">True</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>
|
||||
<object class="GtkVBox" id="vMain">
|
||||
<object class="GtkGrid" id="vMain">
|
||||
<property name="visible">True</property>
|
||||
<property name="homogeneous">False</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>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkToolbar" id="toolbar">
|
||||
<property name="visible">True</property>
|
||||
|
@ -496,7 +104,7 @@
|
|||
<property name="visible_horizontal">True</property>
|
||||
<property name="visible_vertical">True</property>
|
||||
<property name="is_important">False</property>
|
||||
<signal handler="on_itemPreferences_activate" name="clicked"/>
|
||||
<property name="action-name">app.preferences</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
|
@ -529,17 +137,14 @@
|
|||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="padding">0</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkHBox" id="hboxContainer">
|
||||
<object class="GtkGrid" id="hboxContainer">
|
||||
<property name="border_width">5</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>
|
||||
<object class="GtkNotebook" id="wNotebook">
|
||||
<property name="visible">True</property>
|
||||
|
@ -551,21 +156,23 @@
|
|||
<property name="enable_popup">False</property>
|
||||
<signal handler="on_wNotebook_switch_page" name="switch_page"/>
|
||||
<child>
|
||||
<object class="GtkHPaned" id="channelPaned">
|
||||
<object class="GtkPaned" id="channelPaned">
|
||||
<property name="border_width">5</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="orientation">horizontal</property>
|
||||
<child>
|
||||
<object class="GtkVBox" id="vboxChannelNavigator">
|
||||
<object class="GtkGrid" id="vboxChannelNavigator">
|
||||
<property name="visible">True</property>
|
||||
<property name="homogeneous">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="row_spacing">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="scrolledwindow6">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="hscrollbar_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="window_placement">GTK_CORNER_TOP_LEFT</property>
|
||||
<child>
|
||||
|
@ -583,21 +190,17 @@
|
|||
<signal handler="on_treeChannels_row_activated" name="row_activated"/>
|
||||
<signal handler="on_treeChannels_cursor_changed" name="cursor_changed"/>
|
||||
<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_podcasts_button_released" name="button-release-event"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="padding">0</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkHBox" id="hbox_search_podcasts">
|
||||
<property name="spacing">6</property>
|
||||
<object class="GtkGrid" id="hbox_search_podcasts">
|
||||
<property name="column_spacing">6</property>
|
||||
<property name="orientation">horizontal</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="entry_search_podcasts">
|
||||
<property name="visible">True</property>
|
||||
|
@ -608,41 +211,34 @@
|
|||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkVBox" id="vbox42">
|
||||
<object class="GtkGrid" id="vbox42">
|
||||
<property name="visible">True</property>
|
||||
<property name="homogeneous">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="btnUpdateFeeds">
|
||||
<property name="label" translatable="yes">Check for new episodes</property>
|
||||
<property name="can_focus">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>
|
||||
<packing>
|
||||
<property name="padding">0</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkHBox" id="hboxUpdateFeeds">
|
||||
<property name="homogeneous">False</property>
|
||||
<property name="spacing">6</property>
|
||||
<object class="GtkGrid" id="hboxUpdateFeeds">
|
||||
<property name="column_spacing">6</property>
|
||||
<property name="orientation">horizontal</property>
|
||||
<child>
|
||||
<object class="GtkProgressBar" id="pbFeedUpdate">
|
||||
<property name="hexpand">True</property>
|
||||
<property name="pulse_step">0.10000000149</property>
|
||||
<property name="show-text">True</property>
|
||||
<property name="ellipsize">PANGO_ELLIPSIZE_MIDDLE</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="padding">0</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -658,25 +254,12 @@
|
|||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="padding">0</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="padding">0</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="padding">0</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
|
@ -685,9 +268,10 @@
|
|||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkVBox" id="vbox_episode_list">
|
||||
<object class="GtkGrid" id="vbox_episode_list">
|
||||
<property name="visible">True</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="row_spacing">6</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="scrollAvailable">
|
||||
<property name="visible">True</property>
|
||||
|
@ -695,6 +279,8 @@
|
|||
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
|
||||
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</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>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="treeAvailable">
|
||||
|
@ -711,29 +297,22 @@
|
|||
<property name="hover_expand">False</property>
|
||||
<signal handler="on_treeAvailable_row_activated" name="row_activated"/>
|
||||
<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_episodes_button_released" name="button-release-event"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkHBox" id="hbox_search_episodes">
|
||||
<property name="spacing">6</property>
|
||||
<object class="GtkGrid" id="hbox_search_episodes">
|
||||
<property name="column_spacing">6</property>
|
||||
<property name="orientation">horizontal</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_search_episodes">
|
||||
<property name="visible">True</property>
|
||||
<property name="label" translatable="yes">Filter:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="entry_search_episodes">
|
||||
|
@ -745,10 +324,6 @@
|
|||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
|
@ -775,15 +350,16 @@
|
|||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkVBox" id="vboxDownloadStatusWidgets">
|
||||
<object class="GtkGrid" id="vboxDownloadStatusWidgets">
|
||||
<property name="border_width">5</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="homogeneous">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="row_spacing">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="scrolledwindow1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
|
||||
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
|
||||
<property name="shadow_type">GTK_SHADOW_IN</property>
|
||||
|
@ -795,32 +371,30 @@
|
|||
<property name="headers_visible">False</property>
|
||||
<property name="rules_hint">False</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="fixed_height_mode">False</property>
|
||||
<property name="hover_selection">False</property>
|
||||
<property name="hover_expand">False</property>
|
||||
<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_downloads_button_released" name="button-release-event"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="padding">0</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
</packing>
|
||||
</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="spacing">10</property>
|
||||
<property name="column_spacing">10</property>
|
||||
<property name="orientation">horizontal</property>
|
||||
<child>
|
||||
<object class="GtkHBox" id="hboxDownloadLimit">
|
||||
<object class="GtkGrid" id="hboxDownloadLimit">
|
||||
<property name="visible">True</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="column_spacing">5</property>
|
||||
<property name="orientation">horizontal</property>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="cbLimitDownloads">
|
||||
<property name="label" translatable="yes">Limit rate to</property>
|
||||
|
@ -830,9 +404,6 @@
|
|||
<property name="draw_indicator">True</property>
|
||||
<signal name="toggled" handler="on_cbLimitDownloads_toggled"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="spinLimitDownloads">
|
||||
|
@ -843,9 +414,6 @@
|
|||
<property name="digits">1</property>
|
||||
<property name="adjustment">adjustment1</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="labelLimitRate">
|
||||
|
@ -853,27 +421,20 @@
|
|||
<property name="xalign">0</property>
|
||||
<property name="label" translatable="yes">KiB/s</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="DownloadSettingsSpacer">
|
||||
<property name="visible">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkHBox" id="hboxDownloadRate">
|
||||
<object class="GtkGrid" id="hboxDownloadRate">
|
||||
<property name="visible">True</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="column_spacing">5</property>
|
||||
<property name="orientation">horizontal</property>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="cbMaxDownloads">
|
||||
<property name="label" translatable="yes">Limit downloads to</property>
|
||||
|
@ -883,9 +444,6 @@
|
|||
<property name="draw_indicator">True</property>
|
||||
<signal name="toggled" handler="on_cbMaxDownloads_toggled"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="spinMaxDownloads">
|
||||
|
@ -895,21 +453,10 @@
|
|||
<property name="climb_rate">1</property>
|
||||
<property name="adjustment">adjustment2</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="padding">0</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
|
@ -930,17 +477,9 @@
|
|||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="padding">0</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="padding">0</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
|
|
|
@ -5,9 +5,11 @@
|
|||
<property name="title" translatable="yes">Add a new podcast</property>
|
||||
<property name="type_hint">dialog</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="transient-for">parent_widget</property>
|
||||
<property name="default_width">400</property>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkVBox" id="vboxmain">
|
||||
<object class="GtkBox" id="vboxmain">
|
||||
<property name="orientation">vertical</property>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkHButtonBox" id="hbuttonbox">
|
||||
<property name="layout_style">GTK_BUTTONBOX_END</property>
|
||||
|
@ -37,11 +39,12 @@
|
|||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkHBox" id="hboxurlentry">
|
||||
<object class="GtkBox" id="hboxurlentry">
|
||||
<property name="border_width">10</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="homogeneous">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="orientation">horizontal</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_add">
|
||||
<property name="visible">True</property>
|
||||
|
|
|
@ -6,16 +6,18 @@
|
|||
<property name="border_width">10</property>
|
||||
<property name="title" translatable="yes">gPodder Podcast Editor</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="transient-for">parent_widget</property>
|
||||
<property name="window_position">center-on-parent</property>
|
||||
<property name="default_width">500</property>
|
||||
<property name="default_height">400</property>
|
||||
<property name="type_hint">dialog</property>
|
||||
<signal name="destroy" handler="on_gPodderChannel_destroy" swapped="no"/>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkVBox" id="vboxChannelEditorMain">
|
||||
<object class="GtkBox" id="vboxChannelEditorMain">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkHButtonBox" id="hboxButtons">
|
||||
<property name="visible">True</property>
|
||||
|
@ -263,44 +265,44 @@
|
|||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkVBox" id="vboxiPodProperties">
|
||||
<object class="GtkBox" id="vboxiPodProperties">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="border_width">10</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label96">
|
||||
<object class="GtkGrid" id="table10">
|
||||
<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="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="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>
|
||||
<object class="GtkLabel" id="label93">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="label" translatable="yes">Username:</property>
|
||||
<property name="expand">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="x_options">GTK_FILL</property>
|
||||
<property name="y_options"/>
|
||||
</packing>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label94">
|
||||
|
@ -308,12 +310,11 @@
|
|||
<property name="can_focus">False</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="label" translatable="yes">Password:</property>
|
||||
<property name="expand">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="top_attach">1</property>
|
||||
<property name="bottom_attach">2</property>
|
||||
<property name="x_options">GTK_FILL</property>
|
||||
<property name="y_options"/>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -325,11 +326,12 @@
|
|||
<property name="secondary_icon_activatable">False</property>
|
||||
<property name="primary_icon_sensitive">True</property>
|
||||
<property name="secondary_icon_sensitive">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="vexpand">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="right_attach">2</property>
|
||||
<property name="y_options"/>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -341,65 +343,50 @@
|
|||
<property name="secondary_icon_activatable">False</property>
|
||||
<property name="primary_icon_sensitive">True</property>
|
||||
<property name="secondary_icon_sensitive">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="vexpand">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="right_attach">2</property>
|
||||
<property name="top_attach">1</property>
|
||||
<property name="bottom_attach">2</property>
|
||||
<property name="y_options"/>
|
||||
<property name="top_attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkHSeparator" id="hseparator13">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label97">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="label" translatable="yes"><b>Locations</b></property>
|
||||
<property name="use_markup">True</property>
|
||||
</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>
|
||||
<object class="GtkHSeparator" id="hseparator13">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">3</property>
|
||||
<property name="width">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label97">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="label" translatable="yes"><b>Locations</b></property>
|
||||
<property name="use_markup">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">4</property>
|
||||
<property name="width">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label29">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="label" translatable="yes">Download to:</property>
|
||||
<property name="expand">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="x_options">GTK_FILL</property>
|
||||
<property name="y_options"/>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -410,11 +397,13 @@
|
|||
<property name="label">download to label</property>
|
||||
<property name="selectable">True</property>
|
||||
<property name="ellipsize">start</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="vexpand">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="right_attach">3</property>
|
||||
<property name="y_options"/>
|
||||
<property name="top_attach">5</property>
|
||||
<property name="width">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -423,12 +412,11 @@
|
|||
<property name="can_focus">False</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="label" translatable="yes">Website:</property>
|
||||
<property name="expand">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="top_attach">1</property>
|
||||
<property name="bottom_attach">2</property>
|
||||
<property name="x_options">GTK_FILL</property>
|
||||
<property name="y_options"/>
|
||||
<property name="top_attach">6</property>
|
||||
<property name="left_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -439,13 +427,12 @@
|
|||
<property name="label" translatable="yes">website label</property>
|
||||
<property name="selectable">True</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="vexpand">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="right_attach">2</property>
|
||||
<property name="top_attach">1</property>
|
||||
<property name="bottom_attach">2</property>
|
||||
<property name="y_options"/>
|
||||
<property name="top_attach">6</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -465,17 +452,14 @@
|
|||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="right_attach">3</property>
|
||||
<property name="top_attach">1</property>
|
||||
<property name="bottom_attach">2</property>
|
||||
<property name="x_options">GTK_FILL</property>
|
||||
<property name="top_attach">6</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
<interface>
|
||||
<object class="GtkDialog" id="gPodderConfigEditor">
|
||||
<property name="visible">True</property>
|
||||
<property name="has_separator">False</property>
|
||||
<property name="title" translatable="yes">gPodder Configuration Editor</property>
|
||||
<property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="transient-for">parent_widget</property>
|
||||
<property name="default_width">750</property>
|
||||
<property name="default_height">450</property>
|
||||
<property name="destroy_with_parent">False</property>
|
||||
|
@ -17,19 +17,22 @@
|
|||
<property name="urgency_hint">False</property>
|
||||
<signal handler="on_gPodderConfigEditor_destroy" name="destroy"/>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkVBox" id="vbox13">
|
||||
<object class="GtkBox" id="vbox13">
|
||||
<property name="visible">True</property>
|
||||
<property name="homogeneous">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<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="visible">True</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkHBox" id="hbox38">
|
||||
<object class="GtkBox" id="hbox38">
|
||||
<property name="visible">True</property>
|
||||
<property name="homogeneous">False</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="orientation">horizontal</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label121">
|
||||
<property name="visible">True</property>
|
||||
|
@ -115,6 +118,11 @@
|
|||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="padding">0</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkHButtonBox" id="hbuttonbox2">
|
||||
|
|
|
@ -3,12 +3,10 @@
|
|||
<interface>
|
||||
<object class="GtkDialog" id="gPodderEpisodeSelector">
|
||||
<property name="visible">False</property>
|
||||
<property name="has_separator">True</property>
|
||||
<property name="title" translatable="yes">Select episodes</property>
|
||||
<property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="default_width">600</property>
|
||||
<property name="default_height">400</property>
|
||||
<property name="transient-for">parent_widget</property>
|
||||
<property name="destroy_with_parent">False</property>
|
||||
<property name="skip_taskbar_hint">False</property>
|
||||
<property name="skip_pager_hint">False</property>
|
||||
|
@ -16,14 +14,16 @@
|
|||
<property name="focus_on_map">True</property>
|
||||
<property name="urgency_hint">False</property>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkVBox" id="vbox10">
|
||||
<object class="GtkBox" id="vbox10">
|
||||
<property name="visible">True</property>
|
||||
<property name="homogeneous">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<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="visible">True</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="labelInstructions">
|
||||
<property name="label">additional text</property>
|
||||
|
@ -71,10 +71,11 @@
|
|||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkHBox" id="hboxButtons">
|
||||
<object class="GtkBox" id="hboxButtons">
|
||||
<property name="visible">True</property>
|
||||
<property name="homogeneous">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="orientation">horizontal</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="btnCheckAll">
|
||||
<property name="visible">True</property>
|
||||
|
@ -91,10 +92,11 @@
|
|||
<property name="left_padding">0</property>
|
||||
<property name="right_padding">0</property>
|
||||
<child>
|
||||
<object class="GtkHBox" id="hbox34">
|
||||
<object class="GtkBox" id="hbox34">
|
||||
<property name="visible">True</property>
|
||||
<property name="homogeneous">False</property>
|
||||
<property name="spacing">2</property>
|
||||
<property name="orientation">horizontal</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="image2636">
|
||||
<property name="visible">True</property>
|
||||
|
@ -151,10 +153,11 @@
|
|||
<property name="left_padding">0</property>
|
||||
<property name="right_padding">0</property>
|
||||
<child>
|
||||
<object class="GtkHBox" id="hbox33">
|
||||
<object class="GtkBox" id="hbox33">
|
||||
<property name="visible">True</property>
|
||||
<property name="homogeneous">False</property>
|
||||
<property name="spacing">2</property>
|
||||
<property name="orientation">horizontal</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="image2635">
|
||||
<property name="visible">True</property>
|
||||
|
@ -221,12 +224,18 @@
|
|||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="padding">0</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkHBox" id="hbox35">
|
||||
<object class="GtkBox" id="hbox35">
|
||||
<property name="visible">True</property>
|
||||
<property name="homogeneous">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="orientation">horizontal</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="btnRemoveAction">
|
||||
<property name="visible">False</property>
|
||||
|
|
|
@ -8,15 +8,17 @@
|
|||
<property name="border_width">6</property>
|
||||
<property name="title" translatable="yes">Find new podcasts</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="transient-for">parent_widget</property>
|
||||
<property name="window_position">center-on-parent</property>
|
||||
<property name="default_width">600</property>
|
||||
<property name="default_height">400</property>
|
||||
<property name="type_hint">dialog</property>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkVBox" id="vb_directory">
|
||||
<object class="GtkBox" id="vb_directory">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkHButtonBox" id="hboxBottomButtons">
|
||||
<property name="visible">True</property>
|
||||
|
@ -94,9 +96,10 @@
|
|||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkHPaned" id="hpaned">
|
||||
<object class="GtkPaned" id="hpaned">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="orientation">horizontal</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="sw_providers">
|
||||
<property name="visible">True</property>
|
||||
|
@ -121,15 +124,17 @@
|
|||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkVBox" id="vb_podcasts">
|
||||
<object class="GtkBox" id="vb_podcasts">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkHBox" id="hb_text_entry">
|
||||
<object class="GtkBox" id="hb_text_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="orientation">horizontal</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="lb_search">
|
||||
<property name="visible">True</property>
|
||||
|
|
|
@ -26,9 +26,9 @@
|
|||
<property name="value">7</property>
|
||||
</object>
|
||||
<object class="GtkDialog" id="gPodderPreferences">
|
||||
<property name="visible">True</property>
|
||||
<property name="visible">False</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="default_height">260</property>
|
||||
<property name="default_width">320</property>
|
||||
|
@ -36,23 +36,23 @@
|
|||
<property name="type_hint">dialog</property>
|
||||
<signal name="destroy" handler="on_dialog_destroy"/>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkVBox" id="vbox">
|
||||
<object class="GtkBox" id="vbox">
|
||||
<property name="border_width">2</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkNotebook" id="notebook">
|
||||
<property name="border_width">6</property>
|
||||
<property name="visible">True</property>
|
||||
<child>
|
||||
<object class="GtkVBox" id="vbox_general">
|
||||
<object class="GtkBox" id="vbox_general">
|
||||
<property name="border_width">12</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkTable" id="table_players">
|
||||
<object class="GtkGrid" id="table_players">
|
||||
<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="visible">True</property>
|
||||
<child>
|
||||
|
@ -61,9 +61,6 @@
|
|||
<property name="visible">True</property>
|
||||
<property name="xalign">0.0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="x_options">fill</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_video_player">
|
||||
|
@ -72,30 +69,28 @@
|
|||
<property name="xalign">0.0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="bottom_attach">2</property>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
<property name="x_options">fill</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="combo_audio_player_app">
|
||||
<property name="visible">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<signal name="changed" handler="on_combo_audio_player_app_changed"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="right_attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="combo_video_player_app">
|
||||
<property name="visible">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<signal name="changed" handler="on_combo_video_player_app_changed"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="bottom_attach">2</property>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="right_attach">2</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
|
@ -112,8 +107,7 @@
|
|||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="right_attach">3</property>
|
||||
<property name="x_options">fill</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -128,11 +122,8 @@
|
|||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="bottom_attach">2</property>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="right_attach">3</property>
|
||||
<property name="top_attach">1</property>
|
||||
<property name="x_options">fill</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
|
@ -175,15 +166,14 @@
|
|||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkVBox" id="vbox_video">
|
||||
<object class="GtkBox" id="vbox_video">
|
||||
<property name="border_width">12</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkTable" id="table_video">
|
||||
<object class="GtkGrid" id="table_video">
|
||||
<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="visible">True</property>
|
||||
<child>
|
||||
|
@ -195,21 +185,18 @@
|
|||
</object>
|
||||
<packing>
|
||||
<property name="top_attach">0</property>
|
||||
<property name="bottom_attach">1</property>
|
||||
<property name="x_options">fill</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="combobox_preferred_youtube_format">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="hexpand">True</property>
|
||||
<signal name="changed" handler="on_combobox_preferred_youtube_format_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="right_attach">2</property>
|
||||
<property name="top_attach">0</property>
|
||||
<property name="bottom_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -221,23 +208,18 @@
|
|||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="right_attach">1</property>
|
||||
<property name="top_attach">1</property>
|
||||
<property name="bottom_attach">2</property>
|
||||
<property name="x_options">fill</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="entry_youtube_api_key">
|
||||
<property name="visible">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<signal handler="on_youtube_api_key_changed" name="changed"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="right_attach">2</property>
|
||||
<property name="top_attach">1</property>
|
||||
<property name="bottom_attach">2</property>
|
||||
<property name="x_options">fill</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -253,10 +235,7 @@
|
|||
</object>
|
||||
<packing>
|
||||
<property name="top_attach">1</property>
|
||||
<property name="bottom_attach">2</property>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="right_attach">3</property>
|
||||
<property name="x_options">fill</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -268,8 +247,7 @@
|
|||
</object>
|
||||
<packing>
|
||||
<property name="top_attach">2</property>
|
||||
<property name="bottom_attach">3</property>
|
||||
<property name="x_options">fill</property>
|
||||
<property name="left_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
|
@ -280,9 +258,7 @@
|
|||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="right_attach">2</property>
|
||||
<property name="top_attach">2</property>
|
||||
<property name="bottom_attach">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
|
@ -296,10 +272,11 @@
|
|||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkVBox" id="vbox_extensions">
|
||||
<object class="GtkBox" id="vbox_extensions">
|
||||
<property name="border_width">12</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="scrolledwindow2">
|
||||
<property name="visible">True</property>
|
||||
|
@ -477,14 +454,16 @@
|
|||
</child>
|
||||
|
||||
<child>
|
||||
<object class="GtkVBox" id="vbox_updating">
|
||||
<object class="GtkBox" id="vbox_updating">
|
||||
<property name="border_width">12</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkHBox" id="hbox_updating_interval">
|
||||
<object class="GtkBox" id="hbox_updating_interval">
|
||||
<property name="spacing">6</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="orientation">horizontal</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_update_interval">
|
||||
<property name="label" translatable="yes">Update interval:</property>
|
||||
|
@ -497,13 +476,15 @@
|
|||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkHScale" id="hscale_update_interval">
|
||||
<object class="GtkScale" id="hscale_update_interval">
|
||||
<property name="digits">0</property>
|
||||
<property name="is_focus">True</property>
|
||||
<property name="restrict_to_fill_level">False</property>
|
||||
<property name="value_pos">bottom</property>
|
||||
<property name="visible">True</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="value-changed" handler="on_update_interval_value_changed"/>
|
||||
</object>
|
||||
|
@ -526,9 +507,10 @@
|
|||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkHBox" id="hbox_episode_limit">
|
||||
<object class="GtkBox" id="hbox_episode_limit">
|
||||
<property name="spacing">6</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="orientation">horizontal</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_episode_limit">
|
||||
<property name="label" translatable="yes">Maximum number of episodes per podcast:</property>
|
||||
|
@ -565,9 +547,10 @@
|
|||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkHBox" id="hbox_auto_download">
|
||||
<object class="GtkBox" id="hbox_auto_download">
|
||||
<property name="spacing">6</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="orientation">horizontal</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_auto_download">
|
||||
<property name="label" translatable="yes">When new episodes are found:</property>
|
||||
|
@ -600,14 +583,16 @@
|
|||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkVBox" id="vbox_downloads">
|
||||
<object class="GtkBox" id="vbox_downloads">
|
||||
<property name="border_width">12</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkHBox" id="hbox_expiration">
|
||||
<object class="GtkBox" id="hbox_expiration">
|
||||
<property name="spacing">6</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="orientation">horizontal</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label_expiration">
|
||||
<property name="label" translatable="yes">Delete played episodes:</property>
|
||||
|
@ -620,12 +605,14 @@
|
|||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkHScale" id="hscale_expiration">
|
||||
<object class="GtkScale" id="hscale_expiration">
|
||||
<property name="digits">0</property>
|
||||
<property name="is_focus">True</property>
|
||||
<property name="value_pos">bottom</property>
|
||||
<property name="visible">True</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="value-changed" handler="on_expiration_value_changed"/>
|
||||
</object>
|
||||
|
@ -665,10 +652,11 @@
|
|||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkVBox" id="vbox_devices">
|
||||
<object class="GtkBox" id="vbox_devices">
|
||||
<property name="visible">True</property>
|
||||
<property name="border_width">12</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkTable" id="table_devices">
|
||||
<property name="visible">True</property>
|
||||
|
|
|
@ -5,17 +5,19 @@
|
|||
<property name="default_height">230</property>
|
||||
<property name="default_width">340</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="transient-for">parent_widget</property>
|
||||
<property name="title" translatable="yes">Getting started</property>
|
||||
<property name="has_separator">False</property>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkVBox" id="dialog1-vbox">
|
||||
<object class="GtkBox" id="dialog1-vbox">
|
||||
<property name="border_width">2</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkVBox" id="vbox1">
|
||||
<object class="GtkBox" id="vbox1">
|
||||
<property name="border_width">12</property>
|
||||
<property name="spacing">12</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkTable" id="table1">
|
||||
<property name="column_spacing">6</property>
|
||||
|
@ -56,9 +58,10 @@
|
|||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkVBox" id="vbox_buttons">
|
||||
<object class="GtkBox" id="vbox_buttons">
|
||||
<property name="spacing">6</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="btnOPML">
|
||||
<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
|
||||
__tagline__ = 'Media aggregator and podcast client'
|
||||
__author__ = 'Thomas Perl <thp@gpodder.org>'
|
||||
__version__ = '3.9.5'
|
||||
__date__ = '2017-12-16'
|
||||
__version__ = '3.9.3'
|
||||
__date__ = '2016-12-22'
|
||||
__copyright__ = '© 2005-2017 Thomas Perl and the gPodder Team'
|
||||
__license__ = 'GNU General Public License, version 3 or later'
|
||||
__url__ = 'http://gpodder.org/'
|
||||
|
@ -38,7 +38,7 @@ import locale
|
|||
try:
|
||||
import podcastparser
|
||||
except ImportError:
|
||||
print """
|
||||
print("""
|
||||
Error: Module "podcastparser" (python-podcastparser) not found.
|
||||
The podcastparser module can be downloaded from
|
||||
http://gpodder.org/podcastparser/
|
||||
|
@ -46,15 +46,15 @@ except ImportError:
|
|||
From a source checkout, you can download local copies of all
|
||||
CLI dependencies for debugging (will be placed into "src/"):
|
||||
|
||||
python tools/localdepends.py
|
||||
"""
|
||||
python3 tools/localdepends.py
|
||||
""")
|
||||
sys.exit(1)
|
||||
del podcastparser
|
||||
|
||||
try:
|
||||
import mygpoclient
|
||||
except ImportError:
|
||||
print """
|
||||
print("""
|
||||
Error: Module "mygpoclient" (python-mygpoclient) not found.
|
||||
The mygpoclient module can be downloaded from
|
||||
http://gpodder.org/mygpoclient/
|
||||
|
@ -62,19 +62,19 @@ except ImportError:
|
|||
From a source checkout, you can download local copies of all
|
||||
CLI dependencies for debugging (will be placed into "src/"):
|
||||
|
||||
python tools/localdepends.py
|
||||
"""
|
||||
python3 tools/localdepends.py
|
||||
""")
|
||||
sys.exit(1)
|
||||
del mygpoclient
|
||||
|
||||
try:
|
||||
import sqlite3
|
||||
except ImportError:
|
||||
print """
|
||||
print("""
|
||||
Error: Module "sqlite3" not found.
|
||||
Build Python with SQLite 3 support or get it from
|
||||
http://code.google.com/p/pysqlite/
|
||||
"""
|
||||
""")
|
||||
sys.exit(1)
|
||||
del sqlite3
|
||||
|
||||
|
@ -121,18 +121,9 @@ except AttributeError:
|
|||
gettext = t.gettext
|
||||
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
|
||||
|
||||
# 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'):
|
||||
locale.bindtextdomain(textdomain, locale_dir)
|
||||
|
||||
|
@ -152,7 +143,7 @@ images_folder = None
|
|||
user_extensions = None
|
||||
|
||||
# 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)
|
||||
home = None
|
||||
|
@ -193,13 +184,13 @@ default_home = fixup_home(default_home)
|
|||
set_home(os.environ.get(ENV_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:
|
||||
# Allow to relocate the downloads folder (pull request 4, bug 466)
|
||||
downloads = os.environ[ENV_DOWNLOADS]
|
||||
print >>sys.stderr, 'Storing downloads in %s (%s is set)' % (downloads,
|
||||
ENV_DOWNLOADS)
|
||||
print('Storing downloads in %s (%s is set)' % (downloads,
|
||||
ENV_DOWNLOADS), file=sys.stderr)
|
||||
|
||||
# Plugins to load by default
|
||||
DEFAULT_PLUGINS = [
|
||||
|
@ -220,5 +211,5 @@ def load_plugins():
|
|||
for plugin in PLUGINS:
|
||||
try:
|
||||
__import__(plugin)
|
||||
except Exception, e:
|
||||
print >>sys.stderr, 'Cannot load plugin: %s (%s)' % (plugin, e)
|
||||
except Exception as 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)
|
||||
if filename in candidates:
|
||||
found += 1
|
||||
progress_callback(episode.title, float(found)/count)
|
||||
progress_callback(episode.title, found/count)
|
||||
candidates.remove(filename)
|
||||
partial_files.remove(filename+'.partial')
|
||||
|
||||
|
|
|
@ -147,6 +147,8 @@ defaults = {
|
|||
'download_list': {
|
||||
'remove_finished': True,
|
||||
},
|
||||
|
||||
'html_shownotes': True, # enable webkit renderer
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -228,7 +230,7 @@ def config_value_to_string(config_value):
|
|||
|
||||
if config_type == list:
|
||||
return ','.join(map(config_value_to_string, config_value))
|
||||
elif config_type in (str, unicode):
|
||||
elif config_type in (str, str):
|
||||
return config_value
|
||||
else:
|
||||
return str(config_value)
|
||||
|
@ -237,7 +239,7 @@ def string_to_config_value(new_value, old_value):
|
|||
config_type = type(old_value)
|
||||
|
||||
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:
|
||||
return (new_value.strip().lower() in ('1', 'true'))
|
||||
else:
|
||||
|
@ -366,7 +368,7 @@ class Config(object):
|
|||
for observer in self.__observers:
|
||||
try:
|
||||
observer(name, old_value, value)
|
||||
except Exception, exception:
|
||||
except Exception as exception:
|
||||
logger.error('Error while calling observer %r: %s',
|
||||
observer, exception, exc_info=True)
|
||||
|
||||
|
|
|
@ -38,12 +38,12 @@ class CoverDownloader(object):
|
|||
# File name extension dict, lists supported cover art extensions
|
||||
# Values: functions that check if some data is of that file type
|
||||
SUPPORTED_EXTENSIONS = {
|
||||
'.png': lambda d: d.startswith('\x89PNG\r\n\x1a\n\x00'),
|
||||
'.jpg': lambda d: d.startswith('\xff\xd8'),
|
||||
'.gif': lambda d: d.startswith('GIF89a') or d.startswith('GIF87a'),
|
||||
'.png': lambda d: d.startswith(b'\x89PNG\r\n\x1a\n\x00'),
|
||||
'.jpg': lambda d: d.startswith(b'\xff\xd8'),
|
||||
'.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:'
|
||||
|
||||
# Low timeout to avoid unnecessary hangs of GUIs
|
||||
|
@ -85,14 +85,14 @@ class CoverDownloader(object):
|
|||
try:
|
||||
logger.info('Downloading cover art: %s', cover_url)
|
||||
data = util.urlopen(cover_url, timeout=self.TIMEOUT).read()
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
logger.warn('Cover art download failed: %s', e)
|
||||
return self._fallback_filename(title)
|
||||
|
||||
try:
|
||||
extension = None
|
||||
|
||||
for filetype, check in self.SUPPORTED_EXTENSIONS.items():
|
||||
for filetype, check in list(self.SUPPORTED_EXTENSIONS.items()):
|
||||
if check(data):
|
||||
extension = filetype
|
||||
break
|
||||
|
@ -107,7 +107,7 @@ class CoverDownloader(object):
|
|||
fp.close()
|
||||
|
||||
return filename + extension
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
logger.warn('Cannot save cover art', exc_info=True)
|
||||
|
||||
# Fallback to cover art based on the podcast title
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
# 2010-04-24 Thomas Perl <thp@gpodder.org>
|
||||
#
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
|
||||
import gpodder
|
||||
_ = gpodder.gettext
|
||||
|
@ -55,9 +55,9 @@ class Database(object):
|
|||
self.commit()
|
||||
|
||||
with self.lock:
|
||||
cur = self.cursor()
|
||||
cur.execute("VACUUM")
|
||||
cur.close()
|
||||
self.db.isolation_level = None
|
||||
self.db.execute('VACUUM')
|
||||
self.db.isolation_level = ''
|
||||
|
||||
self._db.close()
|
||||
self._db = None
|
||||
|
@ -107,7 +107,7 @@ class Database(object):
|
|||
try:
|
||||
logger.debug('Commit.')
|
||||
self.db.commit()
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
logger.error('Cannot commit: %s', e, exc_info=True)
|
||||
|
||||
def get_content_types(self, id):
|
||||
|
@ -160,7 +160,7 @@ class Database(object):
|
|||
cur.execute(sql)
|
||||
|
||||
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()
|
||||
|
||||
return result
|
||||
|
@ -178,7 +178,7 @@ class Database(object):
|
|||
cur.execute(sql, args)
|
||||
|
||||
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()
|
||||
|
||||
return result
|
||||
|
@ -219,7 +219,7 @@ class Database(object):
|
|||
values.append(o.id)
|
||||
sql = 'UPDATE %s SET %s WHERE id = ?' % (table, qmarks)
|
||||
cur.execute(sql, values)
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
logger.error('Cannot save %s: %s', o, e, exc_info=True)
|
||||
|
||||
cur.close()
|
||||
|
|
|
@ -27,7 +27,7 @@ import gpodder
|
|||
|
||||
_ = gpodder.gettext
|
||||
|
||||
import urllib
|
||||
import urllib.request, urllib.parse, urllib.error
|
||||
import json
|
||||
import os
|
||||
|
||||
|
@ -49,7 +49,7 @@ class DirectoryTag(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):
|
||||
self.name = ''
|
||||
|
@ -97,7 +97,7 @@ class GPodderNetSearchProvider(Provider):
|
|||
self.icon = 'directory-gpodder.png'
|
||||
|
||||
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):
|
||||
def __init__(self):
|
||||
|
@ -142,7 +142,7 @@ class GPodderNetTagsProvider(Provider):
|
|||
self.icon = 'directory-tags.png'
|
||||
|
||||
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):
|
||||
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)
|
||||
#
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -38,8 +38,8 @@ import gpodder
|
|||
|
||||
import socket
|
||||
import threading
|
||||
import urllib
|
||||
import urlparse
|
||||
import urllib.request, urllib.parse, urllib.error
|
||||
import urllib.parse
|
||||
import shutil
|
||||
import os.path
|
||||
import os
|
||||
|
@ -66,13 +66,13 @@ def get_header_param(headers, param, header_name):
|
|||
"""
|
||||
value = None
|
||||
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))
|
||||
if header_name in msg:
|
||||
raw_value = msg.get_param(param, header=header_name)
|
||||
if raw_value is not None:
|
||||
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)
|
||||
|
||||
return value
|
||||
|
@ -188,18 +188,18 @@ class gPodderDownloadHTTPError(Exception):
|
|||
self.error_code = error_code
|
||||
self.error_message = error_message
|
||||
|
||||
class DownloadURLOpener(urllib.FancyURLopener):
|
||||
class DownloadURLOpener(urllib.request.FancyURLopener):
|
||||
version = gpodder.user_agent
|
||||
|
||||
# Sometimes URLs are not escaped correctly - try to fix them
|
||||
# (see RFC2396; Section 2.4.3. Excluded US-ASCII Characters)
|
||||
# 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):
|
||||
self.channel = channel
|
||||
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):
|
||||
"""
|
||||
|
@ -230,7 +230,7 @@ class DownloadURLOpener(urllib.FancyURLopener):
|
|||
fp.close()
|
||||
|
||||
# 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)
|
||||
|
||||
# The following is based on Python's urllib.py "URLopener.retrieve"
|
||||
|
@ -266,11 +266,8 @@ class DownloadURLOpener(urllib.FancyURLopener):
|
|||
tfp = open(filename, 'wb')
|
||||
|
||||
# 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.encode('ascii')
|
||||
|
||||
url = urllib.unwrap(urllib.toBytes(url))
|
||||
fp = self.open(url, data)
|
||||
headers = fp.info()
|
||||
|
||||
|
@ -291,10 +288,10 @@ class DownloadURLOpener(urllib.FancyURLopener):
|
|||
bs = 1024*8
|
||||
size = -1
|
||||
read = current_size
|
||||
blocknum = int(current_size/bs)
|
||||
blocknum = current_size//bs
|
||||
if reporthook:
|
||||
if "content-length" in headers:
|
||||
size = int(headers.getrawheader("Content-Length")) + current_size
|
||||
size = int(headers['Content-Length']) + current_size
|
||||
reporthook(blocknum, bs, size)
|
||||
while read < size or size == -1:
|
||||
if size == -1:
|
||||
|
@ -315,7 +312,7 @@ class DownloadURLOpener(urllib.FancyURLopener):
|
|||
|
||||
# raise exception if actual size does not match content-length header
|
||||
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)
|
||||
|
||||
return result
|
||||
|
@ -337,45 +334,48 @@ class DownloadURLOpener(urllib.FancyURLopener):
|
|||
|
||||
|
||||
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.exit_callback = exit_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):
|
||||
return threading.current_thread().getName()
|
||||
|
||||
def run(self):
|
||||
logger.info('Starting new thread: %s', self)
|
||||
while True:
|
||||
# Check if this thread is allowed to continue accepting tasks
|
||||
# (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):
|
||||
if not self.continue_check_callback(self):
|
||||
return
|
||||
|
||||
try:
|
||||
task = self.queue.pop()
|
||||
task = self.queue.get_next()
|
||||
logger.info('%s is processing: %s', self, task)
|
||||
task.run()
|
||||
task.recycle()
|
||||
except IndexError, e:
|
||||
except StopIteration as e:
|
||||
logger.info('No more tasks for %s to carry out.', self)
|
||||
break
|
||||
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):
|
||||
def __init__(self, config):
|
||||
def __init__(self, config, queue):
|
||||
self._config = config
|
||||
self.tasks = collections.deque()
|
||||
self.tasks = queue
|
||||
|
||||
self.worker_threads_access = threading.RLock()
|
||||
self.worker_threads = []
|
||||
|
@ -393,61 +393,37 @@ class DownloadQueueManager(object):
|
|||
else:
|
||||
return True
|
||||
|
||||
def spawn_threads(self, force_start=False):
|
||||
def __spawn_threads(self):
|
||||
"""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:
|
||||
if not len(self.tasks):
|
||||
if not self.tasks.has_work():
|
||||
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 \
|
||||
not self._config.max_downloads_enabled:
|
||||
# We have to create a new thread here, there's work to do
|
||||
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,
|
||||
self.__continue_check_callback, minimum_tasks)
|
||||
self.__continue_check_callback)
|
||||
self.worker_threads.append(worker)
|
||||
util.run_in_background(worker.run)
|
||||
|
||||
def are_queued_or_active_tasks(self):
|
||||
with self.worker_threads_access:
|
||||
return len(self.worker_threads) > 0
|
||||
def update_max_downloads(self):
|
||||
self.__spawn_threads()
|
||||
|
||||
def add_task(self, task, force_start=False):
|
||||
"""Add a new task to the download queue
|
||||
def force_start_task(self, task):
|
||||
if self.tasks.set_downloading(task):
|
||||
worker = ForceDownloadWorker(task)
|
||||
util.run_in_background(worker.run)
|
||||
|
||||
If force_start is True, ignore the download limit
|
||||
and forcefully start the download right away.
|
||||
def queue_task(self, task):
|
||||
"""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
|
||||
if force_start:
|
||||
# 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)
|
||||
self.__spawn_threads()
|
||||
|
||||
|
||||
class DownloadTask(object):
|
||||
|
@ -526,10 +502,10 @@ class DownloadTask(object):
|
|||
# Possible states this download task can be in
|
||||
STATUS_MESSAGE = (_('Added'), _('Queued'), _('Downloading'),
|
||||
_('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
|
||||
ACTIVITY_DOWNLOAD, ACTIVITY_SYNCHRONIZE = range(2)
|
||||
ACTIVITY_DOWNLOAD, ACTIVITY_SYNCHRONIZE = list(range(2))
|
||||
|
||||
# Minimum time between progress updates (in seconds)
|
||||
MIN_TIME_BETWEEN_UPDATES = 1.
|
||||
|
@ -623,8 +599,8 @@ class DownloadTask(object):
|
|||
try:
|
||||
already_downloaded = os.path.getsize(self.tempname)
|
||||
if self.total_size > 0:
|
||||
self.progress = max(0.0, min(1.0, float(already_downloaded)/self.total_size))
|
||||
except OSError, os_error:
|
||||
self.progress = max(0.0, min(1.0, already_downloaded/self.total_size))
|
||||
except OSError as os_error:
|
||||
logger.error('Cannot get size for %s', os_error)
|
||||
else:
|
||||
# "touch self.tempname", so we also get partial
|
||||
|
@ -669,7 +645,7 @@ class DownloadTask(object):
|
|||
self.__episode.save()
|
||||
|
||||
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:
|
||||
diff = time.time() - self._last_progress_updated
|
||||
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:
|
||||
# calculate the time that should have passed to reach
|
||||
# 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:
|
||||
# sleep a maximum of 10 seconds to not cause time-outs
|
||||
delay = min(10.0, float(should_have_passed-passed))
|
||||
|
@ -739,8 +715,8 @@ class DownloadTask(object):
|
|||
self.speed = 0.0
|
||||
return False
|
||||
|
||||
# We only start this download if its status is "queued"
|
||||
if self.status != DownloadTask.QUEUED:
|
||||
# We only start this download if its status is "downloading"
|
||||
if self.status != DownloadTask.DOWNLOADING:
|
||||
return False
|
||||
|
||||
# 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 = vimeo.get_real_download_url(url, self._config.vimeo.fileformat)
|
||||
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()
|
||||
|
||||
# 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)
|
||||
|
||||
# HTTP Status codes for which we retry the download
|
||||
|
@ -786,18 +763,18 @@ class DownloadTask(object):
|
|||
self.tempname, reporthook=self.status_updated)
|
||||
# If we arrive here, the download was successful
|
||||
break
|
||||
except urllib.ContentTooShortError, ctse:
|
||||
except urllib.error.ContentTooShortError as ctse:
|
||||
if retry < max_retries:
|
||||
logger.info('Content too short: %s - will retry.',
|
||||
url)
|
||||
continue
|
||||
raise
|
||||
except socket.timeout, tmout:
|
||||
except socket.timeout as tmout:
|
||||
if retry < max_retries:
|
||||
logger.info('Socket timeout: %s - will retry.', url)
|
||||
continue
|
||||
raise
|
||||
except gPodderDownloadHTTPError, http:
|
||||
except gPodderDownloadHTTPError as http:
|
||||
if retry < max_retries and http.error_code in retry_codes:
|
||||
logger.info('HTTP error %d: %s - will retry.',
|
||||
http.error_code, url)
|
||||
|
@ -871,23 +848,23 @@ class DownloadTask(object):
|
|||
util.delete_file(self.tempname)
|
||||
self.progress = 0.0
|
||||
self.speed = 0.0
|
||||
except urllib.ContentTooShortError, ctse:
|
||||
except urllib.error.ContentTooShortError as ctse:
|
||||
self.status = DownloadTask.FAILED
|
||||
self.error_message = _('Missing content from server')
|
||||
except IOError, ioe:
|
||||
except IOError as ioe:
|
||||
logger.error('%s while downloading "%s": %s', ioe.strerror,
|
||||
self.__episode.title, ioe.filename, exc_info=True)
|
||||
self.status = DownloadTask.FAILED
|
||||
d = {'error': ioe.strerror, 'filename': ioe.filename}
|
||||
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',
|
||||
gdhe.error_code, self.__episode.title, gdhe.error_message,
|
||||
exc_info=True)
|
||||
self.status = DownloadTask.FAILED
|
||||
d = {'code': gdhe.error_code, 'message': gdhe.error_message}
|
||||
self.error_message = _('HTTP Error %(code)s: %(message)s') % d
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
self.status = DownloadTask.FAILED
|
||||
logger.error('Download failed: %s', str(e), exc_info=True)
|
||||
self.error_message = _('Error: %s') % (str(e),)
|
||||
|
|
|
@ -30,15 +30,10 @@ from gpodder import util
|
|||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
# 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 json
|
||||
|
||||
import re
|
||||
import urllib
|
||||
import urllib.request, urllib.parse, urllib.error
|
||||
|
||||
# This matches the more reliable URL
|
||||
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:
|
||||
return None
|
||||
|
||||
# FIXME: can I be sure to decode it as utf-8?
|
||||
rss_data = util.urlopen(rss_url).read()
|
||||
rss_data_frag = DATA_COVERART_RE.search(rss_data)
|
||||
|
||||
|
@ -124,6 +120,7 @@ def get_escapist_web(video_id):
|
|||
if video_id is None:
|
||||
return None
|
||||
|
||||
# FIXME: must check if it's utf-8
|
||||
web_url = 'http://www.escapistmagazine.com/videos/view/%s' % video_id
|
||||
return util.urlopen(web_url).read()
|
||||
|
||||
|
@ -131,7 +128,7 @@ def get_escapist_config_url(data):
|
|||
if data is 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
|
||||
|
||||
|
@ -162,7 +159,7 @@ def get_escapist_real_url(data, config_json):
|
|||
result_num.append(num_hashes[idx]^hash_n[idx % len(hash_n)])
|
||||
|
||||
# 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...
|
||||
# You use "Master Ball"...
|
||||
escapist_cfg = json.loads(result)
|
||||
|
|
|
@ -85,7 +85,7 @@ def call_extensions(func):
|
|||
result.extend(cb_res)
|
||||
elif cb_res is not None:
|
||||
result = cb_res
|
||||
except Exception, exception:
|
||||
except Exception as exception:
|
||||
logger.error('Error in %s in %s: %s', container.filename,
|
||||
method_name, exception, exc_info=True)
|
||||
func(self, *args, **kwargs)
|
||||
|
@ -123,12 +123,12 @@ class ExtensionMetadata(object):
|
|||
def __getattr__(self, name):
|
||||
try:
|
||||
return self.DEFAULTS[name]
|
||||
except KeyError, e:
|
||||
except KeyError as e:
|
||||
raise AttributeError(name, e)
|
||||
|
||||
def get_sorted(self):
|
||||
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):
|
||||
"""Checks metadata information like
|
||||
|
@ -159,7 +159,7 @@ class ExtensionMetadata(object):
|
|||
if not hasattr(self, target):
|
||||
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)
|
||||
|
||||
@property
|
||||
|
@ -253,15 +253,14 @@ class ExtensionContainer(object):
|
|||
self.enabled = True
|
||||
if hasattr(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,
|
||||
self.filename, exception, exc_info=True)
|
||||
if isinstance(exception, ImportError):
|
||||
# Wrap ImportError in MissingCommand for user-friendly
|
||||
# message (might be displayed in the GUI)
|
||||
match = re.match('No module named (.*)', exception.message)
|
||||
if match:
|
||||
module = match.group(1)
|
||||
if exception.name:
|
||||
module = exception.name
|
||||
msg = _('Python module not found: %(module)s') % {
|
||||
'module': module
|
||||
}
|
||||
|
@ -272,7 +271,7 @@ class ExtensionContainer(object):
|
|||
try:
|
||||
if hasattr(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,
|
||||
exception, exc_info=True)
|
||||
self.enabled = False
|
||||
|
|
|
@ -29,9 +29,9 @@ from gpodder import util
|
|||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from urllib2 import HTTPError
|
||||
from HTMLParser import HTMLParser
|
||||
import urlparse
|
||||
from urllib.error import HTTPError
|
||||
from html.parser import HTMLParser
|
||||
import urllib.parse
|
||||
|
||||
try:
|
||||
# Python 2
|
||||
|
@ -67,7 +67,7 @@ class UnknownStatusCode(ExceptionWithData): pass
|
|||
class AuthenticationRequired(Exception): pass
|
||||
|
||||
# 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:
|
||||
def __init__(self, status, feed=None):
|
||||
|
@ -88,7 +88,7 @@ class FeedAutodiscovery(HTMLParser):
|
|||
is_feed = attrs.get('type', '') in Fetcher.FEED_TYPES
|
||||
is_alternate = attrs.get('rel', '') == 'alternate'
|
||||
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:
|
||||
logger.info('Feed autodiscovery: %s', url)
|
||||
|
@ -175,10 +175,16 @@ class Fetcher(object):
|
|||
|
||||
data = stream
|
||||
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
|
||||
data = StringIO(stream.read())
|
||||
data = StringIO(stream.read().decode(charset))
|
||||
ad = FeedAutodiscovery(url)
|
||||
ad.feed(data.read())
|
||||
|
||||
ad.feed(data.getvalue())
|
||||
if ad._resolved_url:
|
||||
try:
|
||||
self._parse_feed(ad._resolved_url, None, None, False)
|
||||
|
@ -190,15 +196,16 @@ class Fetcher(object):
|
|||
url = self._resolve_url(url)
|
||||
if url:
|
||||
return Result(NEW_LOCATION, url)
|
||||
|
||||
|
||||
# Reset the stream so podcastparser can give it a go
|
||||
data.seek(0)
|
||||
|
||||
|
||||
try:
|
||||
feed = podcastparser.parse(url, data)
|
||||
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:
|
||||
feed['headers'] = {}
|
||||
return Result(UPDATED_FEED, feed)
|
||||
|
|
|
@ -26,10 +26,10 @@ import re
|
|||
|
||||
import tokenize
|
||||
|
||||
import gtk
|
||||
from gi.repository import Gtk
|
||||
|
||||
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
|
||||
from the textdomain) and initializes attributes.
|
||||
|
@ -43,11 +43,16 @@ class GtkBuilderWidget(object):
|
|||
**kwargs:
|
||||
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)
|
||||
|
||||
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)
|
||||
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__
|
||||
|
||||
|
@ -74,11 +79,11 @@ class GtkBuilderWidget(object):
|
|||
"""
|
||||
for widget in self.builder.get_objects():
|
||||
# Just to be safe - every widget from the builder is buildable
|
||||
if not isinstance(widget, gtk.Buildable):
|
||||
if not isinstance(widget, Gtk.Buildable):
|
||||
continue
|
||||
|
||||
# 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))
|
||||
if hasattr(self, widget_api_name):
|
||||
|
@ -101,27 +106,27 @@ class GtkBuilderWidget(object):
|
|||
def main(self):
|
||||
"""
|
||||
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.
|
||||
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.
|
||||
Use the method run() instead.
|
||||
"""
|
||||
gtk.main()
|
||||
Gtk.main()
|
||||
|
||||
def quit(self):
|
||||
"""
|
||||
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.
|
||||
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):
|
||||
"""
|
||||
|
|
|
@ -23,8 +23,9 @@
|
|||
#
|
||||
|
||||
|
||||
import gtk
|
||||
import pango
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import Pango
|
||||
|
||||
import gpodder
|
||||
from gpodder import util
|
||||
|
@ -32,13 +33,12 @@ from gpodder import config
|
|||
|
||||
_ = 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_IS_BOOLEAN, C_BOOLEAN_VALUE = range(8)
|
||||
C_IS_BOOLEAN, C_BOOLEAN_VALUE = list(range(8))
|
||||
|
||||
def __init__(self, config):
|
||||
gtk.ListStore.__init__(self, str, str, str, object, \
|
||||
bool, int, bool, bool)
|
||||
Gtk.ListStore.__init__(self, str, str, str, object, bool, int, bool, bool)
|
||||
|
||||
self._config = config
|
||||
self._fill_model()
|
||||
|
@ -65,11 +65,11 @@ class ConfigModel(gtk.ListStore):
|
|||
value = self._config._lookup(key)
|
||||
fieldtype = type(value)
|
||||
|
||||
style = pango.STYLE_NORMAL
|
||||
style = Pango.Style.NORMAL
|
||||
#if value == default:
|
||||
# style = pango.STYLE_NORMAL
|
||||
# style = Pango.Style.NORMAL
|
||||
#else:
|
||||
# style = pango.STYLE_ITALIC
|
||||
# style = Pango.Style.ITALIC
|
||||
|
||||
self.append((key, self._type_as_string(fieldtype),
|
||||
config.config_value_to_string(value),
|
||||
|
@ -79,11 +79,11 @@ class ConfigModel(gtk.ListStore):
|
|||
def _on_update(self, name, old_value, new_value):
|
||||
for row in self:
|
||||
if row[self.C_NAME] == name:
|
||||
style = pango.STYLE_NORMAL
|
||||
style = Pango.Style.NORMAL
|
||||
#if new_value == self._config.Settings[name]:
|
||||
# style = pango.STYLE_NORMAL
|
||||
# style = Pango.Style.NORMAL
|
||||
#else:
|
||||
# style = pango.STYLE_ITALIC
|
||||
# style = Pango.Style.ITALIC
|
||||
new_value_text = config.config_value_to_string(new_value)
|
||||
self.set(row.iter, \
|
||||
self.C_VALUE_TEXT, new_value_text,
|
||||
|
@ -139,11 +139,11 @@ class UIConfig(config.Config):
|
|||
cfg = getattr(self.ui.gtk.state, config_prefix)
|
||||
|
||||
if gpodder.ui.win32:
|
||||
window.set_gravity(gtk.gdk.GRAVITY_STATIC)
|
||||
window.set_gravity(Gdk.GRAVITY_STATIC)
|
||||
|
||||
window.resize(cfg.width, cfg.height)
|
||||
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:
|
||||
window.move(cfg.x, cfg.y)
|
||||
|
||||
|
@ -153,8 +153,7 @@ class UIConfig(config.Config):
|
|||
def _receive_configure_event(widget, event):
|
||||
x_pos, y_pos = event.x, event.y
|
||||
width_size, height_size = event.width, event.height
|
||||
maximized = bool(event.window.get_state() &
|
||||
gtk.gdk.WINDOW_STATE_MAXIMIZED)
|
||||
maximized = bool(event.window.get_state() & Gdk.WindowState.MAXIMIZED)
|
||||
if not self.__ignore_window_events and not maximized:
|
||||
cfg.x = x_pos
|
||||
cfg.y = y_pos
|
||||
|
@ -164,9 +163,10 @@ class UIConfig(config.Config):
|
|||
window.connect('configure-event', _receive_configure_event)
|
||||
|
||||
def _receive_window_state(widget, event):
|
||||
new_value = bool(event.new_window_state &
|
||||
gtk.gdk.WINDOW_STATE_MAXIMIZED)
|
||||
cfg.maximized = new_value
|
||||
# ELL: why is it commented out?
|
||||
#new_value = bool(event.new_window_state & Gdk.WindowState.MAXIMIZED)
|
||||
#cfg.maximized = new_value
|
||||
pass
|
||||
|
||||
window.connect('window-state-event', _receive_window_state)
|
||||
|
||||
|
|
|
@ -17,8 +17,9 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
import gtk
|
||||
import gtk.gdk
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import GdkPixbuf
|
||||
|
||||
import gpodder
|
||||
|
||||
|
@ -41,19 +42,19 @@ class gPodderChannel(BuilderWidget):
|
|||
self.cbSkipFeedUpdate.set_active(self.channel.pause_subscription)
|
||||
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
|
||||
for index, section in enumerate(sorted(self.sections)):
|
||||
self.section_list.append([section])
|
||||
if section == self.channel.section:
|
||||
active_index = index
|
||||
self.combo_section.set_model(self.section_list)
|
||||
cell_renderer = gtk.CellRendererText()
|
||||
self.combo_section.pack_start(cell_renderer)
|
||||
cell_renderer = Gtk.CellRendererText()
|
||||
self.combo_section.pack_start(cell_renderer, True)
|
||||
self.combo_section.add_attribute(cell_renderer, 'text', 0)
|
||||
self.combo_section.set_active(active_index)
|
||||
|
||||
self.strategy_list = gtk.ListStore(str, int)
|
||||
self.strategy_list = Gtk.ListStore(str, int)
|
||||
active_index = 0
|
||||
for index, (checked, strategy_id, strategy) in \
|
||||
enumerate(self.channel.get_download_strategies()):
|
||||
|
@ -61,8 +62,8 @@ class gPodderChannel(BuilderWidget):
|
|||
if checked:
|
||||
active_index = index
|
||||
self.combo_strategy.set_model(self.strategy_list)
|
||||
cell_renderer = gtk.CellRendererText()
|
||||
self.combo_strategy.pack_start(cell_renderer)
|
||||
cell_renderer = Gtk.CellRendererText()
|
||||
self.combo_strategy.pack_start(cell_renderer, True)
|
||||
self.combo_strategy.add_attribute(cell_renderer, 'text', 0)
|
||||
self.combo_strategy.set_active(active_index)
|
||||
|
||||
|
@ -81,14 +82,14 @@ class gPodderChannel(BuilderWidget):
|
|||
if not self.channel.link:
|
||||
self.btn_website.hide_all()
|
||||
|
||||
b = gtk.TextBuffer()
|
||||
b = Gtk.TextBuffer()
|
||||
b.set_text( self.channel.description)
|
||||
self.channel_description.set_buffer( b)
|
||||
|
||||
#Add Drag and Drop Support
|
||||
flags = gtk.DEST_DEFAULT_ALL
|
||||
targets = [('text/uri-list', 0, 2), ('text/plain', 0, 4)]
|
||||
actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY
|
||||
flags = Gtk.DestDefaults.ALL
|
||||
targets = [Gtk.TargetEntry.new('text/uri-list', 0, 2), Gtk.TargetEntry.new('text/plain', 0, 4)]
|
||||
actions = Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY
|
||||
self.imgCover.drag_dest_set(flags, targets, actions)
|
||||
self.imgCover.connect('drag_data_received', self.drag_data_received)
|
||||
border = 6
|
||||
|
@ -98,7 +99,7 @@ class gPodderChannel(BuilderWidget):
|
|||
|
||||
def on_button_add_section_clicked(self, widget):
|
||||
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:
|
||||
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)
|
||||
|
||||
def on_cover_popup_menu(self, widget, event):
|
||||
if event.button != 3:
|
||||
if not event.triggers_context_menu():
|
||||
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)
|
||||
menu.append(item)
|
||||
|
||||
item = gtk.ImageMenuItem(gtk.STOCK_REFRESH)
|
||||
item = Gtk.MenuItem.new_with_mnemonic(_('_Refresh'))
|
||||
item.connect('activate', self.on_btnClearCover_clicked)
|
||||
menu.append(item)
|
||||
|
||||
menu.attach_to_widget(widget)
|
||||
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):
|
||||
util.open_website(self.channel.link)
|
||||
|
||||
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.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
|
||||
dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
|
||||
dlg = Gtk.FileChooserDialog(title=_('Select new podcast cover artwork'), parent=self.gPodderChannel, action=Gtk.FileChooserAction.OPEN)
|
||||
dlg.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
|
||||
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()
|
||||
self.clear_cover_cache(self.channel.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:
|
||||
f = float(self.MAX_SIZE)/pixbuf.get_width()
|
||||
(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
|
||||
if pixbuf.get_height() > self.MAX_SIZE:
|
||||
f = float(self.MAX_SIZE)/pixbuf.get_height()
|
||||
(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
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ class gPodderDevicePlaylist(object):
|
|||
self.linebreak = '\r\n'
|
||||
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.mountpoint = util.find_mount_point(util.sanitize_encoding(self.playlist_folder))
|
||||
self.mountpoint = util.find_mount_point(self.playlist_folder)
|
||||
if self.mountpoint == '/':
|
||||
self.mountpoint = self.playlist_folder
|
||||
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/>.
|
||||
#
|
||||
|
||||
import gtk
|
||||
import pango
|
||||
import cgi
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Pango
|
||||
|
||||
import gpodder
|
||||
|
||||
|
@ -66,7 +65,7 @@ class gPodderEpisodeSelector(BuilderWidget):
|
|||
- stock_ok_button: (optional) Will replace the "OK" button with
|
||||
another GTK+ stock item to be used for the
|
||||
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
|
||||
dialog)
|
||||
- selection_buttons: (optional) A dictionary with labels as
|
||||
|
@ -140,25 +139,25 @@ class gPodderEpisodeSelector(BuilderWidget):
|
|||
|
||||
if hasattr(self, 'stock_ok_button'):
|
||||
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'))
|
||||
else:
|
||||
self.btnOK.set_label(self.stock_ok_button)
|
||||
self.btnOK.set_use_stock(True)
|
||||
|
||||
# check/uncheck column
|
||||
toggle_cell = gtk.CellRendererToggle()
|
||||
toggle_cell = Gtk.CellRendererToggle()
|
||||
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)
|
||||
self.treeviewEpisodes.append_column(toggle_column)
|
||||
|
||||
next_column = self.COLUMN_ADDITIONAL
|
||||
for name, sort_name, sort_type, caption in self.columns:
|
||||
renderer = gtk.CellRendererText()
|
||||
renderer = Gtk.CellRendererText()
|
||||
if next_column < self.COLUMN_ADDITIONAL + 1:
|
||||
renderer.set_property('ellipsize', pango.ELLIPSIZE_END)
|
||||
column = gtk.TreeViewColumn(caption, renderer, markup=next_column)
|
||||
renderer.set_property('ellipsize', Pango.EllipsizeMode.END)
|
||||
column = Gtk.TreeViewColumn(caption, renderer, markup=next_column)
|
||||
column.set_clickable(False)
|
||||
column.set_resizable( True)
|
||||
# Only set "expand" on the first column
|
||||
|
@ -173,7 +172,7 @@ class gPodderEpisodeSelector(BuilderWidget):
|
|||
|
||||
if sort_name is not None:
|
||||
# add the sort column
|
||||
column = gtk.TreeViewColumn()
|
||||
column = Gtk.TreeViewColumn()
|
||||
column.set_clickable(False)
|
||||
column.set_visible(False)
|
||||
self.treeviewEpisodes.append_column( column)
|
||||
|
@ -185,7 +184,7 @@ class gPodderEpisodeSelector(BuilderWidget):
|
|||
column_types.append(str)
|
||||
if sort_name is not None:
|
||||
column_types.append(sort_type)
|
||||
self.model = gtk.ListStore( *column_types)
|
||||
self.model = Gtk.ListStore( *column_types)
|
||||
|
||||
tooltip = None
|
||||
for index, episode in enumerate( self.episodes):
|
||||
|
@ -275,21 +274,21 @@ class gPodderEpisodeSelector(BuilderWidget):
|
|||
return False
|
||||
|
||||
def treeview_episodes_button_pressed(self, treeview, event=None):
|
||||
if event is None or event.button == 3:
|
||||
menu = gtk.Menu()
|
||||
if event is None or event.triggers_context_menu():
|
||||
menu = Gtk.Menu()
|
||||
|
||||
if len(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)
|
||||
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)
|
||||
menu.append(item)
|
||||
|
||||
item = gtk.MenuItem(_('Select none'))
|
||||
item = Gtk.MenuItem(_('Select none'))
|
||||
item.connect('activate', self.on_btnCheckNone_clicked)
|
||||
menu.append(item)
|
||||
|
||||
|
@ -300,9 +299,9 @@ class gPodderEpisodeSelector(BuilderWidget):
|
|||
menu.connect('deactivate', lambda menushell: self.episode_list_allow_tooltips())
|
||||
if event is None:
|
||||
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:
|
||||
menu.popup(None, None, None, event.button, event.time)
|
||||
menu.popup(None, None, None, None, event.button, event.time)
|
||||
|
||||
return True
|
||||
|
||||
|
@ -329,9 +328,9 @@ class gPodderEpisodeSelector(BuilderWidget):
|
|||
self.btnOK.set_sensitive(count>0)
|
||||
self.btnRemoveAction.set_sensitive(count>0)
|
||||
if count > 0:
|
||||
self.btnCancel.set_label(gtk.STOCK_CANCEL)
|
||||
self.btnCancel.set_label(Gtk.STOCK_CANCEL)
|
||||
else:
|
||||
self.btnCancel.set_label(gtk.STOCK_CLOSE)
|
||||
self.btnCancel.set_label(Gtk.STOCK_CLOSE)
|
||||
else:
|
||||
self.btnOK.set_sensitive(False)
|
||||
self.btnRemoveAction.set_sensitive(False)
|
||||
|
|
|
@ -24,8 +24,9 @@
|
|||
#
|
||||
|
||||
|
||||
import gtk
|
||||
import pango
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GdkPixbuf
|
||||
from gi.repository import Pango
|
||||
import cgi
|
||||
import os
|
||||
|
||||
|
@ -43,11 +44,11 @@ from gpodder.gtkui.interface.common import BuilderWidget
|
|||
from gpodder.gtkui.interface.progress import ProgressIndicator
|
||||
from gpodder.gtkui.interface.tagcloud import TagCloud
|
||||
|
||||
class DirectoryPodcastsModel(gtk.ListStore):
|
||||
C_SELECTED, C_MARKUP, C_TITLE, C_URL = range(4)
|
||||
class DirectoryPodcastsModel(Gtk.ListStore):
|
||||
C_SELECTED, C_MARKUP, C_TITLE, C_URL = list(range(4))
|
||||
|
||||
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
|
||||
|
||||
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]]
|
||||
|
||||
|
||||
class DirectoryProvidersModel(gtk.ListStore):
|
||||
C_WEIGHT, C_TEXT, C_ICON, C_PROVIDER = range(4)
|
||||
class DirectoryProvidersModel(Gtk.ListStore):
|
||||
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):
|
||||
gtk.ListStore.__init__(self, int, str, gtk.gdk.Pixbuf, object)
|
||||
Gtk.ListStore.__init__(self, int, str, GdkPixbuf.Pixbuf, object)
|
||||
for provider in providers:
|
||||
self.add_provider(provider() if provider else None)
|
||||
|
||||
|
@ -89,11 +90,11 @@ class DirectoryProvidersModel(gtk.ListStore):
|
|||
self.append(self.SEPARATOR)
|
||||
else:
|
||||
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:
|
||||
logger.warn('Could not load icon: %s (%s)', provider.icon or '-', e)
|
||||
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):
|
||||
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)
|
||||
|
||||
def setup_podcasts_treeview(self):
|
||||
column = gtk.TreeViewColumn('')
|
||||
cell = gtk.CellRendererToggle()
|
||||
column = Gtk.TreeViewColumn('')
|
||||
cell = Gtk.CellRendererToggle()
|
||||
column.pack_start(cell, False)
|
||||
column.add_attribute(cell, 'active', DirectoryPodcastsModel.C_SELECTED)
|
||||
cell.connect('toggled', lambda cell, path: self.podcasts_model.toggle(path))
|
||||
self.tv_podcasts.append_column(column)
|
||||
|
||||
column = gtk.TreeViewColumn('')
|
||||
cell = gtk.CellRendererText()
|
||||
cell.set_property('ellipsize', pango.ELLIPSIZE_END)
|
||||
column.pack_start(cell)
|
||||
column = Gtk.TreeViewColumn('')
|
||||
cell = Gtk.CellRendererText()
|
||||
cell.set_property('ellipsize', Pango.EllipsizeMode.END)
|
||||
column.pack_start(cell, True)
|
||||
column.add_attribute(cell, 'markup', DirectoryPodcastsModel.C_MARKUP)
|
||||
self.tv_podcasts.append_column(column)
|
||||
|
||||
|
@ -145,13 +146,13 @@ class gPodderPodcastDirectory(BuilderWidget):
|
|||
self.podcasts_model.append((False, 'a', 'b', 'c'))
|
||||
|
||||
def setup_providers_treeview(self):
|
||||
column = gtk.TreeViewColumn('')
|
||||
cell = gtk.CellRendererPixbuf()
|
||||
column = Gtk.TreeViewColumn('')
|
||||
cell = Gtk.CellRendererPixbuf()
|
||||
column.pack_start(cell, False)
|
||||
column.add_attribute(cell, 'pixbuf', DirectoryProvidersModel.C_ICON)
|
||||
cell = gtk.CellRendererText()
|
||||
#cell.set_property('ellipsize', pango.ELLIPSIZE_END)
|
||||
column.pack_start(cell)
|
||||
cell = Gtk.CellRendererText()
|
||||
#cell.set_property('ellipsize', Pango.EllipsizeMode.END)
|
||||
column.pack_start(cell, True)
|
||||
column.add_attribute(cell, 'text', DirectoryProvidersModel.C_TEXT)
|
||||
column.add_attribute(cell, 'weight', DirectoryProvidersModel.C_WEIGHT)
|
||||
self.tv_providers.append_column(column)
|
||||
|
@ -175,10 +176,10 @@ class gPodderPodcastDirectory(BuilderWidget):
|
|||
it = self.providers_model.get_iter(path)
|
||||
|
||||
for row in self.providers_model:
|
||||
row[DirectoryProvidersModel.C_WEIGHT] = pango.WEIGHT_NORMAL
|
||||
row[DirectoryProvidersModel.C_WEIGHT] = Pango.Weight.NORMAL
|
||||
|
||||
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)
|
||||
self.use_provider(provider)
|
||||
|
||||
|
@ -229,7 +230,8 @@ class gPodderPodcastDirectory(BuilderWidget):
|
|||
|
||||
def on_tv_providers_cursor_changed(self, treeview):
|
||||
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):
|
||||
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 = 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')
|
||||
return
|
||||
|
||||
self.podcasts_model.load(podcasts or [])
|
||||
self.en_query.set_sensitive(True)
|
||||
self.bt_search.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):
|
||||
if self.current_provider is None:
|
||||
|
|
|
@ -17,10 +17,11 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
import gtk
|
||||
import pango
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import Pango
|
||||
import cgi
|
||||
import urlparse
|
||||
import urllib.parse
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -40,13 +41,13 @@ from gpodder.gtkui.interface.configeditor import gPodderConfigEditor
|
|||
|
||||
from gpodder.gtkui.desktopfile import PlayerListModel
|
||||
|
||||
class NewEpisodeActionList(gtk.ListStore):
|
||||
C_CAPTION, C_AUTO_DOWNLOAD = range(2)
|
||||
class NewEpisodeActionList(Gtk.ListStore):
|
||||
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):
|
||||
gtk.ListStore.__init__(self, str, str)
|
||||
Gtk.ListStore.__init__(self, str, str)
|
||||
self._config = config
|
||||
self.append((_('Do nothing'), 'ignore'))
|
||||
self.append((_('Show episode list'), 'show'))
|
||||
|
@ -63,11 +64,11 @@ class NewEpisodeActionList(gtk.ListStore):
|
|||
def set_index(self, index):
|
||||
self._config.auto_download = self[index][self.C_AUTO_DOWNLOAD]
|
||||
|
||||
class DeviceTypeActionList(gtk.ListStore):
|
||||
C_CAPTION, C_DEVICE_TYPE = range(2)
|
||||
class DeviceTypeActionList(Gtk.ListStore):
|
||||
C_CAPTION, C_DEVICE_TYPE = list(range(2))
|
||||
|
||||
def __init__(self, config):
|
||||
gtk.ListStore.__init__(self, str, str)
|
||||
Gtk.ListStore.__init__(self, str, str)
|
||||
self._config = config
|
||||
self.append((_('None'), 'none'))
|
||||
self.append((_('iPod'), 'ipod'))
|
||||
|
@ -83,12 +84,12 @@ class DeviceTypeActionList(gtk.ListStore):
|
|||
self._config.device_sync.device_type = self[index][self.C_DEVICE_TYPE]
|
||||
|
||||
|
||||
class OnSyncActionList(gtk.ListStore):
|
||||
C_CAPTION, C_ON_SYNC_DELETE, C_ON_SYNC_MARK_PLAYED = range(3)
|
||||
ACTION_NONE, ACTION_ASK, ACTION_MINIMIZED, ACTION_ALWAYS = range(4)
|
||||
class OnSyncActionList(Gtk.ListStore):
|
||||
C_CAPTION, C_ON_SYNC_DELETE, C_ON_SYNC_MARK_PLAYED = list(range(3))
|
||||
ACTION_NONE, ACTION_ASK, ACTION_MINIMIZED, ACTION_ALWAYS = list(range(4))
|
||||
|
||||
def __init__(self, config):
|
||||
gtk.ListStore.__init__(self, str, bool, bool)
|
||||
Gtk.ListStore.__init__(self, str, bool, bool)
|
||||
self._config = config
|
||||
self.append((_('Do nothing'), False, False))
|
||||
self.append((_('Mark as played'), False, True))
|
||||
|
@ -111,11 +112,11 @@ class OnSyncActionList(gtk.ListStore):
|
|||
|
||||
|
||||
|
||||
class YouTubeVideoFormatListModel(gtk.ListStore):
|
||||
C_CAPTION, C_ID = range(2)
|
||||
class YouTubeVideoFormatListModel(Gtk.ListStore):
|
||||
C_CAPTION, C_ID = list(range(2))
|
||||
|
||||
def __init__(self, config):
|
||||
gtk.ListStore.__init__(self, str, int)
|
||||
Gtk.ListStore.__init__(self, str, int)
|
||||
self._config = config
|
||||
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
|
||||
|
||||
|
||||
class VimeoVideoFormatListModel(gtk.ListStore):
|
||||
C_CAPTION, C_ID = range(2)
|
||||
class VimeoVideoFormatListModel(Gtk.ListStore):
|
||||
C_CAPTION, C_ID = list(range(2))
|
||||
|
||||
def __init__(self, config):
|
||||
gtk.ListStore.__init__(self, str, str)
|
||||
Gtk.ListStore.__init__(self, str, str)
|
||||
self._config = config
|
||||
|
||||
for fileformat, description in vimeo.FORMATS:
|
||||
|
@ -168,20 +169,20 @@ class VimeoVideoFormatListModel(gtk.ListStore):
|
|||
|
||||
def set_index(self, index):
|
||||
value = self[index][self.C_ID]
|
||||
if value > 0:
|
||||
if value is not None:
|
||||
self._config.vimeo.fileformat = value
|
||||
|
||||
|
||||
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):
|
||||
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.add_attribute(cellrenderer, 'pixbuf', PlayerListModel.C_ICON)
|
||||
cellrenderer = gtk.CellRendererText()
|
||||
cellrenderer.set_property('ellipsize', pango.ELLIPSIZE_END)
|
||||
cellrenderer = Gtk.CellRendererText()
|
||||
cellrenderer.set_property('ellipsize', Pango.EllipsizeMode.END)
|
||||
cb.pack_start(cellrenderer, True)
|
||||
cb.add_attribute(cellrenderer, 'markup', PlayerListModel.C_NAME)
|
||||
cb.set_row_separator_func(PlayerListModel.is_separator)
|
||||
|
@ -198,14 +199,14 @@ class gPodderPreferences(BuilderWidget):
|
|||
|
||||
self.preferred_youtube_format_model = YouTubeVideoFormatListModel(self._config)
|
||||
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.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.preferred_vimeo_format_model = VimeoVideoFormatListModel(self._config)
|
||||
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.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())
|
||||
|
@ -217,7 +218,7 @@ class gPodderPreferences(BuilderWidget):
|
|||
|
||||
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.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:
|
||||
index = self.update_interval_presets.index(self._config.auto_update_frequency)
|
||||
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.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)
|
||||
self.hscale_update_interval.set_value(index)
|
||||
|
||||
|
@ -234,7 +235,7 @@ class gPodderPreferences(BuilderWidget):
|
|||
|
||||
self.auto_download_model = NewEpisodeActionList(self._config)
|
||||
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.add_attribute(cellrenderer, 'text', NewEpisodeActionList.C_CAPTION)
|
||||
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()
|
||||
if self._config.episode_old_age > adjustment_expiration.get_upper():
|
||||
# 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)
|
||||
else:
|
||||
|
@ -256,7 +257,7 @@ class gPodderPreferences(BuilderWidget):
|
|||
|
||||
self.device_type_model = DeviceTypeActionList(self._config)
|
||||
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.add_attribute(cellrenderer, 'text',
|
||||
DeviceTypeActionList.C_CAPTION)
|
||||
|
@ -264,7 +265,7 @@ class gPodderPreferences(BuilderWidget):
|
|||
|
||||
self.on_sync_model = OnSyncActionList(self._config)
|
||||
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.add_attribute(cellrenderer, 'text', OnSyncActionList.C_CAPTION)
|
||||
self.combobox_on_sync.set_active(self.on_sync_model.get_index())
|
||||
|
@ -292,12 +293,16 @@ class gPodderPreferences(BuilderWidget):
|
|||
|
||||
# Configure the extensions manager GUI
|
||||
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 search_equal_func(model, column, key, it):
|
||||
label = model.get_value(it, self.C_LABEL)
|
||||
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
|
||||
# the search criteria."
|
||||
return False
|
||||
|
@ -305,24 +310,27 @@ class gPodderPreferences(BuilderWidget):
|
|||
return True
|
||||
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_column = gtk.TreeViewColumn('')
|
||||
toggle_column = Gtk.TreeViewColumn('')
|
||||
toggle_column.pack_start(toggle_cell, True)
|
||||
toggle_column.add_attribute(toggle_cell, 'active', self.C_TOGGLE)
|
||||
toggle_column.add_attribute(toggle_cell, 'visible', self.C_SHOW_TOGGLE)
|
||||
toggle_column.set_property('min-width', 32)
|
||||
self.treeviewExtensions.append_column(toggle_column)
|
||||
|
||||
name_cell = gtk.CellRendererText()
|
||||
name_cell.set_property('ellipsize', pango.ELLIPSIZE_END)
|
||||
extension_column = gtk.TreeViewColumn(_('Name'))
|
||||
name_cell = Gtk.CellRendererText()
|
||||
name_cell.set_property('ellipsize', Pango.EllipsizeMode.END)
|
||||
extension_column = Gtk.TreeViewColumn(_('Name'))
|
||||
extension_column.pack_start(name_cell, True)
|
||||
extension_column.add_attribute(name_cell, 'markup', self.C_LABEL)
|
||||
extension_column.set_expand(True)
|
||||
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):
|
||||
category, container = pair
|
||||
|
@ -351,7 +359,7 @@ class gPodderPreferences(BuilderWidget):
|
|||
if event.window != treeview.get_bin_window():
|
||||
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 False
|
||||
|
@ -364,29 +372,30 @@ class gPodderPreferences(BuilderWidget):
|
|||
if not container:
|
||||
return
|
||||
|
||||
menu = gtk.Menu()
|
||||
menu = Gtk.Menu()
|
||||
|
||||
if container.metadata.doc:
|
||||
menu_item = gtk.MenuItem(_('Documentation'))
|
||||
menu_item = Gtk.MenuItem(_('Documentation'))
|
||||
menu_item.connect('activate', self.open_weblink,
|
||||
container.metadata.doc)
|
||||
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.append(menu_item)
|
||||
|
||||
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.append(menu_item)
|
||||
|
||||
menu.show_all()
|
||||
if event is None:
|
||||
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:
|
||||
menu.popup(None, None, None, 3, 0)
|
||||
menu.popup(None, None, None, None, 3, Gtk.get_current_event_time())
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
@ -414,7 +423,11 @@ class gPodderPreferences(BuilderWidget):
|
|||
else:
|
||||
self.on_extension_disabled(container.module)
|
||||
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)
|
||||
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
|
||||
# the metadata object of the container..
|
||||
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())
|
||||
|
||||
self.show_message(info, _('Extension module info'), important=True)
|
||||
|
@ -486,12 +499,19 @@ class gPodderPreferences(BuilderWidget):
|
|||
|
||||
def format_update_interval_value(self, scale, value):
|
||||
value = int(value)
|
||||
ret = None
|
||||
if value == 0:
|
||||
return _('manually')
|
||||
ret = _('manually')
|
||||
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:
|
||||
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):
|
||||
value = int(range.get_value())
|
||||
|
@ -626,12 +646,12 @@ class gPodderPreferences(BuilderWidget):
|
|||
pass
|
||||
|
||||
def on_btn_device_mountpoint_clicked(self, widget):
|
||||
fs = gtk.FileChooserDialog(title=_('Select folder for mount point'),
|
||||
action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
|
||||
fs.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
|
||||
fs.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
|
||||
fs = Gtk.FileChooserDialog(title=_('Select folder for mount point'),
|
||||
action=Gtk.FileChooserAction.SELECT_FOLDER)
|
||||
fs.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
|
||||
fs.add_button(Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
|
||||
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()
|
||||
if self._config.device_sync.device_type == 'filesystem':
|
||||
self._config.device_sync.device_folder = filename
|
||||
|
@ -643,12 +663,12 @@ class gPodderPreferences(BuilderWidget):
|
|||
fs.destroy()
|
||||
|
||||
def on_btn_playlist_folder_clicked(self, widget):
|
||||
fs = gtk.FileChooserDialog(title=_('Select folder for playlists'),
|
||||
action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
|
||||
fs.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
|
||||
fs.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK)
|
||||
fs = Gtk.FileChooserDialog(title=_('Select folder for playlists'),
|
||||
action=Gtk.FileChooserAction.SELECT_FOLDER)
|
||||
fs.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
|
||||
fs.add_button(Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
|
||||
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,
|
||||
fs.get_filename())
|
||||
if self._config.device_sync.device_type == 'filesystem':
|
||||
|
|
|
@ -37,7 +37,7 @@ logger = logging.getLogger(__name__)
|
|||
class gPodderSyncUI(object):
|
||||
def __init__(self, config, notification, parent_window,
|
||||
show_confirmation,
|
||||
preferences_widget,
|
||||
show_preferences,
|
||||
channels,
|
||||
download_status_model,
|
||||
download_queue_manager,
|
||||
|
@ -51,7 +51,7 @@ class gPodderSyncUI(object):
|
|||
self.parent_window = parent_window
|
||||
self.show_confirmation = show_confirmation
|
||||
|
||||
self.preferences_widget = preferences_widget
|
||||
self.show_preferences = show_preferences
|
||||
self.channels=channels
|
||||
self.download_status_model = download_status_model
|
||||
self.download_queue_manager = download_queue_manager
|
||||
|
@ -81,12 +81,13 @@ class gPodderSyncUI(object):
|
|||
def _show_message_unconfigured(self):
|
||||
title = _('No device configured')
|
||||
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):
|
||||
title = _('Cannot open device')
|
||||
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):
|
||||
device = sync.open_device(self)
|
||||
|
@ -185,7 +186,7 @@ class gPodderSyncUI(object):
|
|||
key=lambda ep: ep.published)
|
||||
#don't add played episodes to playlist if skip_played_episodes is True
|
||||
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)
|
||||
|
||||
#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):
|
||||
title = _('Update successful')
|
||||
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
|
||||
@util.run_in_background
|
||||
|
@ -216,10 +217,10 @@ class gPodderSyncUI(object):
|
|||
#get episodes to be written to playlist
|
||||
episodes_for_playlist=sorted(current_channel.get_episodes(gpodder.STATE_DOWNLOADED),
|
||||
key=lambda ep: ep.published)
|
||||
episode_keys=map(playlist.get_absolute_filename_for_playlist,
|
||||
episodes_for_playlist)
|
||||
episode_keys=list(map(playlist.get_absolute_filename_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
|
||||
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
|
||||
try:
|
||||
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',
|
||||
episode_filename)
|
||||
|
||||
|
@ -277,10 +278,10 @@ class gPodderSyncUI(object):
|
|||
logger.warning("Starting sync - no episodes to delete")
|
||||
resume_sync([],[],None)
|
||||
|
||||
except IOError, ioe:
|
||||
except IOError as ioe:
|
||||
title = _('Error writing playlist files')
|
||||
message = _(str(ioe))
|
||||
self.notification(message, title, widget=self.preferences_widget)
|
||||
self.notification(message, title)
|
||||
else:
|
||||
logger.info ('Not creating playlists - starting sync')
|
||||
resume_sync([],[],None)
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
import gtk
|
||||
from gi.repository import Gtk
|
||||
|
||||
import gpodder
|
||||
|
||||
|
@ -31,12 +31,12 @@ class gPodderWelcome(BuilderWidget):
|
|||
def new(self):
|
||||
for widget in self.vbox_buttons.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,
|
||||
self.PADDING, self.PADDING)
|
||||
else:
|
||||
child.set_padding(self.PADDING, self.PADDING)
|
||||
|
||||
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 threading
|
||||
|
||||
from ConfigParser import RawConfigParser
|
||||
from configparser import RawConfigParser
|
||||
|
||||
import gobject
|
||||
import gtk
|
||||
import gtk.gdk
|
||||
from gi.repository import GObject
|
||||
from gi.repository import GdkPixbuf
|
||||
from gi.repository import Gtk
|
||||
|
||||
import gpodder
|
||||
|
||||
|
@ -49,11 +49,11 @@ userappsdirs = [ '/usr/share/applications/', '/usr/local/share/applications/', '
|
|||
# the name of the section in the .desktop files
|
||||
sect = 'Desktop Entry'
|
||||
|
||||
class PlayerListModel(gtk.ListStore):
|
||||
C_ICON, C_NAME, C_COMMAND, C_CUSTOM = range(4)
|
||||
class PlayerListModel(Gtk.ListStore):
|
||||
C_ICON, C_NAME, C_COMMAND, C_CUSTOM = list(range(4))
|
||||
|
||||
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):
|
||||
self.append((pixbuf, name, command, False))
|
||||
|
@ -92,13 +92,13 @@ class UserApplication(object):
|
|||
# Load it from an absolute filename
|
||||
if os.path.exists(self.icon):
|
||||
try:
|
||||
return gtk.gdk.pixbuf_new_from_file_at_size(self.icon, 24, 24)
|
||||
except gobject.GError, ge:
|
||||
return GdkPixbuf.Pixbuf.new_from_file_at_size(self.icon, 24, 24)
|
||||
except GObject.GError as ge:
|
||||
pass
|
||||
|
||||
# Load it from the current icon theme
|
||||
(icon_name, extension) = os.path.splitext(os.path.basename(self.icon))
|
||||
theme = gtk.IconTheme()
|
||||
theme = Gtk.IconTheme()
|
||||
if theme.has_icon(icon_name):
|
||||
return theme.load_icon(icon_name, 24, 0)
|
||||
|
||||
|
@ -116,23 +116,23 @@ WIN32_APP_REG_KEYS = [
|
|||
|
||||
|
||||
def win32_read_registry_key(path):
|
||||
import _winreg
|
||||
import winreg
|
||||
|
||||
rootmap = {
|
||||
'HKEY_CLASSES_ROOT': _winreg.HKEY_CLASSES_ROOT,
|
||||
'HKEY_CLASSES_ROOT': winreg.HKEY_CLASSES_ROOT,
|
||||
}
|
||||
|
||||
parts = path.split('\\')
|
||||
root = parts.pop(0)
|
||||
key = _winreg.OpenKey(rootmap[root], parts.pop(0))
|
||||
key = winreg.OpenKey(rootmap[root], parts.pop(0))
|
||||
|
||||
while parts:
|
||||
key = _winreg.OpenKey(key, parts.pop(0))
|
||||
key = winreg.OpenKey(key, parts.pop(0))
|
||||
|
||||
value, type_ = _winreg.QueryValueEx(key, '')
|
||||
if type_ == _winreg.REG_EXPAND_SZ:
|
||||
value, type_ = winreg.QueryValueEx(key, '')
|
||||
if type_ == winreg.REG_EXPAND_SZ:
|
||||
cmdline = re.sub(r'%([^%]+)%', lambda m: os.environ[m.group(1)], value)
|
||||
elif type_ == _winreg.REG_SZ:
|
||||
elif type_ == winreg.REG_SZ:
|
||||
cmdline = value
|
||||
else:
|
||||
raise ValueError('Not a string: ' + path)
|
||||
|
@ -151,7 +151,7 @@ class UserAppsReader(object):
|
|||
self.__has_read = False
|
||||
self.__finished = threading.Event()
|
||||
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):
|
||||
self.apps.append(UserApplication('', '', ';'.join((mime+'/*' for mime in self.mimetypes)), ''))
|
||||
|
@ -163,7 +163,7 @@ class UserAppsReader(object):
|
|||
|
||||
self.__has_read = True
|
||||
if gpodder.ui.win32:
|
||||
import _winreg
|
||||
import winreg
|
||||
for caption, types, hkey in WIN32_APP_REG_KEYS:
|
||||
try:
|
||||
cmdline = win32_read_registry_key(hkey)
|
||||
|
|
|
@ -23,35 +23,40 @@
|
|||
# Based on code from gpodder.services (thp, 2007-08-24)
|
||||
#
|
||||
|
||||
|
||||
|
||||
import gpodder
|
||||
|
||||
from gpodder import util
|
||||
from gpodder import download
|
||||
|
||||
import gtk
|
||||
from gi.repository import Gtk
|
||||
import cgi
|
||||
|
||||
import collections
|
||||
import threading
|
||||
|
||||
_ = gpodder.gettext
|
||||
|
||||
|
||||
class DownloadStatusModel(gtk.ListStore):
|
||||
class DownloadStatusModel(Gtk.ListStore):
|
||||
# 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)
|
||||
|
||||
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
|
||||
self._status_ids = collections.defaultdict(lambda: None)
|
||||
self._status_ids[download.DownloadTask.DOWNLOADING] = gtk.STOCK_GO_DOWN
|
||||
self._status_ids[download.DownloadTask.DONE] = gtk.STOCK_APPLY
|
||||
self._status_ids[download.DownloadTask.FAILED] = gtk.STOCK_STOP
|
||||
self._status_ids[download.DownloadTask.CANCELLED] = gtk.STOCK_CANCEL
|
||||
self._status_ids[download.DownloadTask.PAUSED] = gtk.STOCK_MEDIA_PAUSE
|
||||
self._status_ids[download.DownloadTask.DOWNLOADING] = 'go-down'
|
||||
self._status_ids[download.DownloadTask.DONE] = Gtk.STOCK_APPLY
|
||||
self._status_ids[download.DownloadTask.FAILED] = 'dialog-error'
|
||||
self._status_ids[download.DownloadTask.CANCELLED] = 'media-playback-stop'
|
||||
self._status_ids[download.DownloadTask.PAUSED] = 'media-playback-pause'
|
||||
|
||||
def _format_message(self, episode, message, podcast):
|
||||
episode = cgi.escape(episode)
|
||||
|
@ -138,6 +143,27 @@ class DownloadStatusModel(gtk.ListStore):
|
|||
|
||||
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):
|
||||
"""A helper class that abstracts download events"""
|
||||
|
|
|
@ -25,11 +25,18 @@
|
|||
|
||||
import gpodder
|
||||
|
||||
import gtk
|
||||
import pango
|
||||
import pangocairo
|
||||
import gi
|
||||
gi.require_version('PangoCairo', '1.0')
|
||||
|
||||
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 StringIO
|
||||
|
||||
import io
|
||||
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):
|
||||
style = widget.rc_get_style()
|
||||
text_color = style.text[gtk.STATE_PRELIGHT]
|
||||
style_context = widget.get_style_context()
|
||||
text_color = style_context.get_color(Gtk.StateFlags.PRELIGHT)
|
||||
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)
|
||||
|
||||
if font_desc is None:
|
||||
font_desc = style.font_desc
|
||||
font_desc.set_size(14*pango.SCALE)
|
||||
font_desc = style_context.get_font(Gtk.StateFlags.NORMAL)
|
||||
font_desc.set_size(14*Pango.SCALE)
|
||||
|
||||
pango_context = widget.create_pango_context()
|
||||
layout = pango.Layout(pango_context)
|
||||
layout = Pango.Layout(pango_context)
|
||||
layout.set_font_description(font_desc)
|
||||
layout.set_text(text)
|
||||
layout.set_text(text, -1)
|
||||
width, height = layout.get_pixel_size()
|
||||
|
||||
ctx.move_to(w_width/2-width/2, w_height/2-height/2)
|
||||
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)
|
||||
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
|
||||
|
||||
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size, size)
|
||||
ctx = pangocairo.CairoContext(cairo.Context(surface))
|
||||
ctx = cairo.Context(surface)
|
||||
|
||||
widget = gtk.ProgressBar()
|
||||
style = widget.rc_get_style()
|
||||
bgc = style.bg[gtk.STATE_NORMAL]
|
||||
fgc = style.bg[gtk.STATE_SELECTED]
|
||||
txc = style.text[gtk.STATE_NORMAL]
|
||||
# ELL: get all black
|
||||
#widget = Gtk.ProgressBar()
|
||||
#style_context = widget.get_style_context()
|
||||
bgc = Gdk.RGBA() #style_context.get_background_color(Gtk.StateFlags.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
|
||||
height = int(size*.4)
|
||||
|
@ -142,19 +153,19 @@ def draw_cake(percentage, text=None, emblem=None, size=None):
|
|||
|
||||
# Background
|
||||
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()
|
||||
|
||||
# Filling
|
||||
if percentage > 0:
|
||||
fill_width = max(1, min(width-2, (width-2)*percentage+.5))
|
||||
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()
|
||||
|
||||
# Border
|
||||
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.stroke()
|
||||
|
||||
|
@ -162,12 +173,10 @@ def draw_cake(percentage, text=None, emblem=None, size=None):
|
|||
return surface
|
||||
|
||||
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
|
||||
widget = gtk.Label()
|
||||
style = widget.rc_get_style()
|
||||
widget = Gtk.Label()
|
||||
style_context = widget.get_style_context()
|
||||
|
||||
# Padding (in px) at the right edge of the image (for Ubuntu; bug 1533)
|
||||
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
|
||||
|
||||
if font_desc is None:
|
||||
font_desc = style.font_desc
|
||||
font_desc.set_weight(pango.WEIGHT_BOLD)
|
||||
font_desc = style_context.get_font(Gtk.StateFlags.NORMAL)
|
||||
font_desc.set_weight(Pango.Weight.BOLD)
|
||||
|
||||
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_text(left_text)
|
||||
layout_right = pango.Layout(pango_context)
|
||||
layout_left.set_text(left_text, -1)
|
||||
layout_right = Pango.Layout(pango_context)
|
||||
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_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_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)
|
||||
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.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.set_source_rgba( 1, 1, 1, 1)
|
||||
ctx.show_layout(layout_left)
|
||||
PangoCairo.show_layout(ctx, layout_left)
|
||||
|
||||
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)
|
||||
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(.4, .2, .2, .2, .8)
|
||||
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(1, .2, .2, .2, .5)
|
||||
ctx.set_source(linear)
|
||||
ctx.fill()
|
||||
xpos, ypos, width, height = x, y+1, rect_width-1, rect_height-2
|
||||
if left_text is None:
|
||||
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)
|
||||
ctx.set_source_rgba(1., 1., 1., .3)
|
||||
ctx.set_line_width(1)
|
||||
ctx.stroke()
|
||||
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_line_width(1)
|
||||
ctx.stroke()
|
||||
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.add_color_stop_rgba(0, .2, .2, .2, .9)
|
||||
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(.9, .2, .2, .2, .7)
|
||||
linear.add_color_stop_rgba(1, .2, .2, .2, .5)
|
||||
ctx.set_source(linear)
|
||||
ctx.fill()
|
||||
xpos, ypos, width, height = x, y+1, rect_width-1, rect_height-2
|
||||
if left_text is None:
|
||||
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)
|
||||
ctx.set_source_rgba(1., 1., 1., .3)
|
||||
ctx.set_line_width(1)
|
||||
ctx.stroke()
|
||||
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_line_width(1)
|
||||
ctx.stroke()
|
||||
|
||||
ctx.move_to(x+left_side_width+x_border, y+1+border)
|
||||
ctx.set_source_rgba( 0, 0, 0, 1)
|
||||
ctx.show_layout(layout_right)
|
||||
ctx.move_to(x-1+left_side_width+x_border, y+border)
|
||||
ctx.set_source_rgba( 1, 1, 1, 1)
|
||||
ctx.show_layout(layout_right)
|
||||
ctx.move_to(x+left_side_width+x_border, y+1+border)
|
||||
ctx.set_source_rgba( 0, 0, 0, 1)
|
||||
PangoCairo.show_layout(ctx, layout_right)
|
||||
ctx.move_to(x-1+left_side_width+x_border, y+border)
|
||||
ctx.set_source_rgba( 1, 1, 1, 1)
|
||||
PangoCairo.show_layout(ctx, layout_right)
|
||||
|
||||
return surface
|
||||
|
||||
|
@ -284,19 +293,19 @@ def cairo_surface_to_pixbuf(s):
|
|||
Converts a Cairo surface to a Gtk Pixbuf by
|
||||
encoding it as PNG and using the PixbufLoader.
|
||||
"""
|
||||
sio = StringIO.StringIO()
|
||||
bio = io.BytesIO()
|
||||
try:
|
||||
s.write_to_png(sio)
|
||||
s.write_to_png(bio)
|
||||
except:
|
||||
# Write an empty PNG file to the StringIO, so
|
||||
# in case of an error we have "something" to
|
||||
# load. This happens in PyCairo < 1.1.6, see:
|
||||
# http://webcvs.cairographics.org/pycairo/NEWS?view=markup
|
||||
# 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.write(sio.getvalue())
|
||||
pbl = GdkPixbuf.PixbufLoader()
|
||||
pbl.write(bio.getvalue())
|
||||
pbl.close()
|
||||
|
||||
pixbuf = pbl.get_pixbuf()
|
||||
|
@ -312,7 +321,7 @@ def progressbar_pixbuf(width, height, percentage):
|
|||
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
|
||||
ctx = cairo.Context(surface)
|
||||
|
||||
padding = int(float(width)/8.0)
|
||||
padding = int(width/8.0)
|
||||
bar_width = 2*padding
|
||||
bar_height = height - 2*padding
|
||||
bar_height_fill = bar_height*percentage
|
||||
|
@ -337,4 +346,3 @@ def progressbar_pixbuf(width, height, percentage):
|
|||
ctx.stroke()
|
||||
|
||||
return cairo_surface_to_pixbuf(surface)
|
||||
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
# 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
|
||||
|
||||
|
@ -43,8 +44,11 @@ class gPodderAddPodcast(BuilderWidget):
|
|||
|
||||
if not hasattr(self, 'preset_url'):
|
||||
# Fill the entry if a valid URL is in the clipboard, but
|
||||
# only if there's no preset_url available (see bug 1132)
|
||||
clipboard = gtk.Clipboard(selection='PRIMARY')
|
||||
# only if there's no preset_url available (see bug 1132).
|
||||
# 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):
|
||||
# Heuristic: If there is a space in the clipboard
|
||||
# text, assume it's some arbitrary text, and no URL
|
||||
|
@ -56,7 +60,7 @@ class gPodderAddPodcast(BuilderWidget):
|
|||
return
|
||||
|
||||
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, False)
|
||||
|
||||
|
@ -64,7 +68,7 @@ class gPodderAddPodcast(BuilderWidget):
|
|||
self.gPodderAddPodcast.destroy()
|
||||
|
||||
def on_btn_paste_clicked(self, widget):
|
||||
clipboard = gtk.Clipboard()
|
||||
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||
clipboard.request_text(self.receive_clipboard_text)
|
||||
|
||||
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/>.
|
||||
#
|
||||
|
||||
import gtk
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
|
||||
import os
|
||||
import shutil
|
||||
|
||||
|
@ -33,40 +35,17 @@ from gpodder.gtkui.base import GtkBuilderWidget
|
|||
class BuilderWidget(GtkBuilderWidget):
|
||||
def __init__(self, parent, **kwargs):
|
||||
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
|
||||
if hasattr(self, 'on_iconify') and hasattr(self, 'on_uniconify'):
|
||||
self.main_window.connect('window-state-event', \
|
||||
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):
|
||||
if event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED:
|
||||
if event.new_window_state & Gdk.WindowState.ICONIFIED:
|
||||
if not self._window_iconified:
|
||||
self._window_iconified = True
|
||||
self.on_iconify()
|
||||
|
@ -84,12 +63,12 @@ class BuilderWidget(GtkBuilderWidget):
|
|||
util.idle_add(self.show_message, message, title, important, widget)
|
||||
|
||||
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
|
||||
|
||||
def show_message(self, message, title=None, important=False, widget=None):
|
||||
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:
|
||||
dlg.set_title(str(title))
|
||||
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)
|
||||
|
||||
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:
|
||||
dlg.set_title(str(title))
|
||||
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))
|
||||
response = dlg.run()
|
||||
dlg.destroy()
|
||||
return response == gtk.RESPONSE_YES
|
||||
return response == Gtk.ResponseType.YES
|
||||
|
||||
def show_text_edit_dialog(self, title, prompt, text=None, empty=False, \
|
||||
is_url=False, affirmative_text=gtk.STOCK_OK):
|
||||
dialog = gtk.Dialog(title, self.get_dialog_parent(), \
|
||||
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT)
|
||||
is_url=False, affirmative_text=Gtk.STOCK_OK):
|
||||
dialog = Gtk.Dialog(title, self.get_dialog_parent(), \
|
||||
Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT)
|
||||
|
||||
dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
|
||||
dialog.add_button(affirmative_text, gtk.RESPONSE_OK)
|
||||
dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
|
||||
dialog.add_button(affirmative_text, Gtk.ResponseType.OK)
|
||||
|
||||
dialog.set_has_separator(False)
|
||||
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)
|
||||
if text is not None:
|
||||
text_entry.set_text(text)
|
||||
|
@ -132,24 +110,24 @@ class BuilderWidget(GtkBuilderWidget):
|
|||
if not empty:
|
||||
def on_text_changed(editable):
|
||||
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)
|
||||
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_spacing(10)
|
||||
hbox.pack_start(gtk.Label(prompt), False, False)
|
||||
hbox.pack_start(text_entry, True, True)
|
||||
dialog.vbox.pack_start(hbox, True, True)
|
||||
hbox.pack_start(Gtk.Label(prompt, True, True, 0), False, False, 0)
|
||||
hbox.pack_start(text_entry, True, True, 0)
|
||||
dialog.vbox.pack_start(hbox, True, True, 0)
|
||||
|
||||
dialog.show_all()
|
||||
response = dialog.run()
|
||||
result = text_entry.get_text()
|
||||
dialog.destroy()
|
||||
|
||||
if response == gtk.RESPONSE_OK:
|
||||
if response == Gtk.ResponseType.OK:
|
||||
return result
|
||||
else:
|
||||
return None
|
||||
|
@ -162,25 +140,25 @@ class BuilderWidget(GtkBuilderWidget):
|
|||
if register_text is None:
|
||||
register_text = _('New user')
|
||||
|
||||
dialog = gtk.MessageDialog(
|
||||
dialog = Gtk.MessageDialog(
|
||||
self.main_window,
|
||||
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
|
||||
gtk.MESSAGE_QUESTION,
|
||||
gtk.BUTTONS_CANCEL)
|
||||
dialog.add_button(_('Login'), gtk.RESPONSE_OK)
|
||||
dialog.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG))
|
||||
Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
|
||||
Gtk.MessageType.QUESTION,
|
||||
Gtk.ButtonsType.CANCEL)
|
||||
dialog.add_button(_('Login'), Gtk.ResponseType.OK)
|
||||
dialog.set_image(Gtk.Image.new_from_icon_name('dialog-password', Gtk.IconSize.DIALOG))
|
||||
dialog.set_title(_('Authentication required'))
|
||||
dialog.set_markup('<span weight="bold" size="larger">' + title + '</span>')
|
||||
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:
|
||||
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)'))
|
||||
username_entry = gtk.Entry()
|
||||
password_entry = gtk.Entry()
|
||||
username_entry = Gtk.Entry()
|
||||
password_entry = Gtk.Entry()
|
||||
|
||||
server_entry.connect('activate', lambda w: username_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:
|
||||
password_entry.set_text(password)
|
||||
|
||||
table = gtk.Table(3, 2)
|
||||
table = Gtk.Table(3, 2)
|
||||
table.set_row_spacings(6)
|
||||
table.set_col_spacings(6)
|
||||
|
||||
server_label = gtk.Label()
|
||||
server_label = Gtk.Label()
|
||||
server_label.set_markup('<b>' + _('Server') + ':</b>')
|
||||
|
||||
username_label = gtk.Label()
|
||||
username_label = Gtk.Label()
|
||||
username_label.set_markup('<b>' + username_prompt + ':</b>')
|
||||
|
||||
password_label = gtk.Label()
|
||||
password_label = Gtk.Label()
|
||||
password_label.set_markup('<b>' + _('Password') + ':</b>')
|
||||
|
||||
label_entries = [(username_label, username_entry),
|
||||
|
@ -215,7 +193,7 @@ class BuilderWidget(GtkBuilderWidget):
|
|||
|
||||
for i, (label, entry) in enumerate(label_entries):
|
||||
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)
|
||||
|
||||
dialog.vbox.pack_end(table, True, True, 0)
|
||||
|
@ -223,7 +201,7 @@ class BuilderWidget(GtkBuilderWidget):
|
|||
username_entry.grab_focus()
|
||||
response = dialog.run()
|
||||
|
||||
while response == gtk.RESPONSE_HELP:
|
||||
while response == Gtk.ResponseType.HELP:
|
||||
register_callback()
|
||||
response = dialog.run()
|
||||
|
||||
|
@ -231,7 +209,7 @@ class BuilderWidget(GtkBuilderWidget):
|
|||
root_url = server_entry.get_text()
|
||||
username = username_entry.get_text()
|
||||
password = password_entry.get_text()
|
||||
success = (response == gtk.RESPONSE_OK)
|
||||
success = (response == Gtk.ResponseType.OK)
|
||||
|
||||
dialog.destroy()
|
||||
|
||||
|
@ -240,36 +218,22 @@ class BuilderWidget(GtkBuilderWidget):
|
|||
else:
|
||||
return (success, (username, password))
|
||||
|
||||
def show_copy_dialog(self, src_filename, dst_filename=None, dst_directory=None, title=_('Select destination')):
|
||||
if dst_filename is None:
|
||||
dst_filename = src_filename
|
||||
def show_folder_select_dialog(self, initial_directory=None, title=_('Select destination')):
|
||||
if initial_directory is None:
|
||||
initial_directory = os.path.expanduser('~')
|
||||
|
||||
if dst_directory is None:
|
||||
dst_directory = os.path.expanduser('~')
|
||||
|
||||
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 = Gtk.FileChooserDialog(title=title, parent=self.main_window, action=Gtk.FileChooserAction.SELECT_FOLDER)
|
||||
dlg.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
|
||||
dlg.add_button(Gtk.STOCK_SAVE, Gtk.ResponseType.OK)
|
||||
|
||||
dlg.set_do_overwrite_confirmation(True)
|
||||
dlg.set_current_name(os.path.basename(dst_filename))
|
||||
dlg.set_current_folder(dst_directory)
|
||||
dlg.set_current_folder(initial_directory)
|
||||
|
||||
result = False
|
||||
folder = dst_directory
|
||||
if dlg.run() == gtk.RESPONSE_OK:
|
||||
folder = initial_directory
|
||||
if dlg.run() == Gtk.ResponseType.OK:
|
||||
result = True
|
||||
dst_filename = dlg.get_filename()
|
||||
folder = dlg.get_current_folder()
|
||||
if not dst_filename.endswith(extension):
|
||||
dst_filename += extension
|
||||
|
||||
shutil.copyfile(src_filename, dst_filename)
|
||||
|
||||
dlg.destroy()
|
||||
return (result, folder)
|
||||
|
@ -282,7 +246,7 @@ class TreeViewHelper(object):
|
|||
COLUMNS = '_gpodder_columns'
|
||||
|
||||
# Enum for the role attribute
|
||||
ROLE_PODCASTS, ROLE_EPISODES, ROLE_DOWNLOADS = range(3)
|
||||
ROLE_PODCASTS, ROLE_EPISODES, ROLE_DOWNLOADS = list(range(3))
|
||||
|
||||
@classmethod
|
||||
def set(cls, treeview, role):
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
import gtk
|
||||
from gi.repository import Gtk
|
||||
import cgi
|
||||
|
||||
import gpodder
|
||||
|
@ -31,28 +31,30 @@ from gpodder.gtkui.interface.common import BuilderWidget
|
|||
|
||||
class gPodderConfigEditor(BuilderWidget):
|
||||
def new(self):
|
||||
name_column = gtk.TreeViewColumn(_('Setting'))
|
||||
name_renderer = gtk.CellRendererText()
|
||||
name_column.pack_start(name_renderer)
|
||||
name_column = Gtk.TreeViewColumn(_('Setting'))
|
||||
name_renderer = Gtk.CellRendererText()
|
||||
name_column.pack_start(name_renderer, True)
|
||||
name_column.add_attribute(name_renderer, 'text', 0)
|
||||
name_column.add_attribute(name_renderer, 'style', 5)
|
||||
name_column.set_expand(True)
|
||||
self.configeditor.append_column(name_column)
|
||||
|
||||
value_column = gtk.TreeViewColumn(_('Set to'))
|
||||
value_check_renderer = gtk.CellRendererToggle()
|
||||
value_column.pack_start(value_check_renderer, expand=False)
|
||||
value_column = Gtk.TreeViewColumn(_('Set to'))
|
||||
value_check_renderer = Gtk.CellRendererToggle()
|
||||
value_column.pack_start(value_check_renderer, False)
|
||||
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, 'activatable', 6)
|
||||
value_check_renderer.connect('toggled', self.value_toggled)
|
||||
|
||||
value_renderer = gtk.CellRendererText()
|
||||
value_column.pack_start(value_renderer)
|
||||
value_renderer = Gtk.CellRendererText()
|
||||
value_column.pack_start(value_renderer, True)
|
||||
value_column.add_attribute(value_renderer, 'text', 2)
|
||||
value_column.add_attribute(value_renderer, 'visible', 4)
|
||||
value_column.add_attribute(value_renderer, 'editable', 4)
|
||||
value_column.add_attribute(value_renderer, 'style', 5)
|
||||
value_renderer.connect('edited', self.value_edited)
|
||||
value_column.set_expand(False)
|
||||
self.configeditor.append_column(value_column)
|
||||
|
||||
self.model = ConfigModel(self._config)
|
||||
|
|
|
@ -17,9 +17,9 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
import gtk
|
||||
import gobject
|
||||
import pango
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Pango
|
||||
|
||||
import gpodder
|
||||
|
||||
|
@ -45,16 +45,16 @@ class ProgressIndicator(object):
|
|||
self._initial_message = None
|
||||
self._initial_progress = None
|
||||
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):
|
||||
if self.cancellable:
|
||||
self.dialog.response(gtk.RESPONSE_CANCEL)
|
||||
self.dialog.response(Gtk.ResponseType.CANCEL)
|
||||
return True
|
||||
|
||||
def _create_progress(self):
|
||||
self.dialog = gtk.MessageDialog(self.parent, \
|
||||
0, 0, gtk.BUTTONS_CANCEL, self.subtitle or self.title)
|
||||
self.dialog = Gtk.MessageDialog(self.parent, \
|
||||
0, 0, Gtk.ButtonsType.CANCEL, self.subtitle or self.title)
|
||||
self.dialog.set_modal(True)
|
||||
self.dialog.connect('delete-event', self._on_delete_event)
|
||||
self.dialog.set_title(self.title)
|
||||
|
@ -63,14 +63,15 @@ class ProgressIndicator(object):
|
|||
# Avoid selectable text (requires PyGTK >= 2.22)
|
||||
if hasattr(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)
|
||||
|
||||
self.dialog.set_response_sensitive(gtk.RESPONSE_CANCEL, \
|
||||
self.dialog.set_response_sensitive(Gtk.ResponseType.CANCEL, \
|
||||
self.cancellable)
|
||||
|
||||
self.progressbar = gtk.ProgressBar()
|
||||
self.progressbar.set_ellipsize(pango.ELLIPSIZE_END)
|
||||
self.progressbar = Gtk.ProgressBar()
|
||||
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
|
||||
# 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.show_all()
|
||||
|
||||
gobject.source_remove(self.source_id)
|
||||
self.source_id = gobject.timeout_add(self.INTERVAL, self._update_gui)
|
||||
GObject.source_remove(self.source_id)
|
||||
self.source_id = GObject.timeout_add(self.INTERVAL, self._update_gui)
|
||||
return False
|
||||
|
||||
def _update_gui(self):
|
||||
|
@ -111,5 +112,5 @@ class ProgressIndicator(object):
|
|||
def on_finished(self):
|
||||
if self.dialog is not None:
|
||||
self.dialog.destroy()
|
||||
gobject.source_remove(self.source_id)
|
||||
GObject.source_remove(self.source_id)
|
||||
|
||||
|
|
|
@ -18,19 +18,18 @@
|
|||
#
|
||||
|
||||
|
||||
import gtk
|
||||
import gobject
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GObject
|
||||
import cgi
|
||||
|
||||
class TagCloud(gtk.Layout):
|
||||
class TagCloud(Gtk.Layout):
|
||||
__gsignals__ = {
|
||||
'selected': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
|
||||
(gobject.TYPE_STRING,))
|
||||
'selected': (GObject.SignalFlags.RUN_LAST, None,
|
||||
(GObject.TYPE_STRING,))
|
||||
}
|
||||
|
||||
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._max_weight = 0
|
||||
self._min_size = min_size
|
||||
|
@ -49,10 +48,10 @@ class TagCloud(gtk.Layout):
|
|||
self._max_weight = max(weight 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))
|
||||
label.set_markup(markup)
|
||||
button = gtk.ToolButton(label)
|
||||
button = Gtk.ToolButton(label)
|
||||
button.connect('clicked', lambda b, t: self.emit('selected', t), tag)
|
||||
self.put(button, 1, 1)
|
||||
button.show_all()
|
||||
|
@ -65,8 +64,8 @@ class TagCloud(gtk.Layout):
|
|||
self.relayout()
|
||||
|
||||
def _scale(self, weight):
|
||||
weight_range = float(self._max_weight-self._min_weight)
|
||||
ratio = float(weight-self._min_weight)/weight_range
|
||||
weight_range = self._max_weight-self._min_weight
|
||||
ratio = (weight-self._min_weight)/weight_range
|
||||
return int(self._min_size + (self._max_size-self._min_size)*ratio)
|
||||
|
||||
def relayout(self):
|
||||
|
@ -76,10 +75,10 @@ class TagCloud(gtk.Layout):
|
|||
pw, ph = self._size
|
||||
def fixup_row(widgets, x, y, max_h):
|
||||
residue = (pw - x)
|
||||
x = int(residue/2)
|
||||
x = int(residue//2)
|
||||
for widget in widgets:
|
||||
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
|
||||
for child in self.get_children():
|
||||
w, h = child.size_request()
|
||||
|
@ -98,6 +97,6 @@ class TagCloud(gtk.Layout):
|
|||
def unrelayout():
|
||||
self._in_relayout = 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
|
||||
# comes with macpython
|
||||
from Carbon.AppleEvents import *
|
||||
try:
|
||||
from Carbon.AppleEvents import *
|
||||
except ImportError:
|
||||
...
|
||||
|
||||
# all this depends on pyObjc (http://pyobjc.sourceforge.net/).
|
||||
# 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)
|
||||
urls.append(str(url))
|
||||
|
||||
print >>sys.stderr,("open Files :",urls)
|
||||
print(("open Files :",urls), file=sys.stderr)
|
||||
result = NSAppleEventDescriptor.descriptorWithInt32_(42)
|
||||
reply.setParamDescriptor_forKeyword_(result, aeKeyword('----'))
|
||||
|
||||
|
@ -88,7 +91,7 @@ try:
|
|||
fileURLData = filelist.data()
|
||||
url = buffer(fileURLData.bytes(),0,fileURLData.length())
|
||||
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)
|
||||
|
||||
result = NSAppleEventDescriptor.descriptorWithInt32_(42)
|
||||
|
@ -97,9 +100,9 @@ try:
|
|||
# global reference to the handler (mustn't be destroyed)
|
||||
handler = gPodderEventHandler.alloc().init()
|
||||
except ImportError:
|
||||
print >> sys.stderr, """
|
||||
print("""
|
||||
Warning: pyobjc not found. Disabling "Subscribe with" events handling
|
||||
"""
|
||||
""", file=sys.stderr)
|
||||
handler = None
|
||||
|
||||
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
|
||||
|
||||
import os
|
||||
import gtk
|
||||
import gobject
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GObject
|
||||
from gi.repository import GdkPixbuf
|
||||
import cgi
|
||||
import re
|
||||
import time
|
||||
|
||||
try:
|
||||
import gio
|
||||
from gi.repository import Gio
|
||||
have_gio = True
|
||||
except ImportError:
|
||||
have_gio = False
|
||||
|
@ -140,15 +141,17 @@ class BackgroundUpdate(object):
|
|||
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_PUBLISHED_TEXT, C_DESCRIPTION, C_TOOLTIP, \
|
||||
C_VIEW_SHOW_UNDELETED, C_VIEW_SHOW_DOWNLOADED, \
|
||||
C_VIEW_SHOW_UNPLAYED, C_FILESIZE, C_PUBLISHED, \
|
||||
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
|
||||
_UI_UPDATE_STEP = .03
|
||||
|
@ -157,9 +160,9 @@ class EpisodeListModel(gtk.ListStore):
|
|||
PROGRESS_STEPS = 20
|
||||
|
||||
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, \
|
||||
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
|
||||
|
||||
|
@ -169,7 +172,7 @@ class EpisodeListModel(gtk.ListStore):
|
|||
|
||||
# Filter to allow hiding some episodes
|
||||
self._filter = self.filter_new()
|
||||
self._sorter = gtk.TreeModelSort(self._filter)
|
||||
self._sorter = Gtk.TreeModelSort(self._filter)
|
||||
self._view_mode = self.VIEW_ALL
|
||||
self._search_term = None
|
||||
self._search_term_eql = None
|
||||
|
@ -182,8 +185,8 @@ class EpisodeListModel(gtk.ListStore):
|
|||
self.ICON_VIDEO_FILE = 'video-x-generic'
|
||||
self.ICON_IMAGE_FILE = 'image-x-generic'
|
||||
self.ICON_GENERIC_FILE = 'text-x-generic'
|
||||
self.ICON_DOWNLOADING = gtk.STOCK_GO_DOWN
|
||||
self.ICON_DELETED = gtk.STOCK_DELETE
|
||||
self.ICON_DOWNLOADING = Gtk.STOCK_GO_DOWN
|
||||
self.ICON_DELETED = Gtk.STOCK_DELETE
|
||||
|
||||
self.background_update = None
|
||||
self.background_update_tag = None
|
||||
|
@ -201,7 +204,7 @@ class EpisodeListModel(gtk.ListStore):
|
|||
else:
|
||||
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 self._search_term is not None:
|
||||
episode = model.get_value(iter, self.C_EPISODE)
|
||||
|
@ -210,7 +213,7 @@ class EpisodeListModel(gtk.ListStore):
|
|||
|
||||
try:
|
||||
return self._search_term_eql.match(episode)
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
return True
|
||||
|
||||
if self._view_mode == self.VIEW_ALL:
|
||||
|
@ -314,10 +317,10 @@ class EpisodeListModel(gtk.ListStore):
|
|||
|
||||
def _update_from_episodes(self, episodes, include_description):
|
||||
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_tag = gobject.idle_add(self._update_background)
|
||||
self.background_update_tag = GObject.idle_add(self._update_background)
|
||||
|
||||
def _update_background(self):
|
||||
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):
|
||||
# Convenience function for use by "outside" methods that use iters
|
||||
# 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),
|
||||
include_description)
|
||||
|
||||
|
@ -362,7 +365,7 @@ class EpisodeListModel(gtk.ListStore):
|
|||
view_show_undeleted = True
|
||||
view_show_downloaded = False
|
||||
view_show_unplayed = False
|
||||
icon_theme = gtk.icon_theme_get_default()
|
||||
icon_theme = Gtk.IconTheme.get_default()
|
||||
|
||||
if episode.downloading:
|
||||
tooltip.append('%s %d%%' % (_('Downloading'),
|
||||
|
@ -408,9 +411,9 @@ class EpisodeListModel(gtk.ListStore):
|
|||
|
||||
# Try to find a themed icon for this file
|
||||
if filename is not None and have_gio:
|
||||
file = gio.File(filename)
|
||||
file = Gio.File.new_for_path(filename)
|
||||
if file.query_exists():
|
||||
file_info = file.query_info('*')
|
||||
file_info = file.query_info('*', Gio.FileQueryInfoFlags.NONE, None)
|
||||
icon = file_info.get_icon()
|
||||
for icon_name in icon.get_names():
|
||||
if icon_theme.has_icon(icon_name):
|
||||
|
@ -504,12 +507,12 @@ class PodcastChannelProxy(object):
|
|||
pass
|
||||
|
||||
|
||||
class PodcastListModel(gtk.ListStore):
|
||||
class PodcastListModel(Gtk.ListStore):
|
||||
C_URL, C_TITLE, C_DESCRIPTION, C_PILL, C_CHANNEL, \
|
||||
C_COVER, C_ERROR, C_PILL_VISIBLE, \
|
||||
C_VIEW_SHOW_UNDELETED, C_VIEW_SHOW_DOWNLOADED, \
|
||||
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)
|
||||
|
||||
|
@ -518,8 +521,8 @@ class PodcastListModel(gtk.ListStore):
|
|||
return model.get_value(iter, cls.C_SEPARATOR)
|
||||
|
||||
def __init__(self, cover_downloader):
|
||||
gtk.ListStore.__init__(self, str, str, str, gtk.gdk.Pixbuf, \
|
||||
object, gtk.gdk.Pixbuf, str, bool, bool, bool, bool, \
|
||||
Gtk.ListStore.__init__(self, str, str, str, GdkPixbuf.Pixbuf, \
|
||||
object, GdkPixbuf.Pixbuf, str, bool, bool, bool, bool, \
|
||||
bool, bool, int, bool, str)
|
||||
|
||||
# Filter to allow hiding some episodes
|
||||
|
@ -534,7 +537,7 @@ class PodcastListModel(gtk.ListStore):
|
|||
|
||||
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 self._search_term is not None:
|
||||
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:
|
||||
f = float(self._max_image_side)/pixbuf.get_width()
|
||||
(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
|
||||
|
||||
# Resize if too high
|
||||
if pixbuf.get_height() > self._max_image_side:
|
||||
f = float(self._max_image_side)/pixbuf.get_height()
|
||||
(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
|
||||
|
||||
if changed:
|
||||
|
@ -632,7 +635,7 @@ class PodcastListModel(gtk.ListStore):
|
|||
|
||||
def _overlay_pixbuf(self, pixbuf, icon):
|
||||
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)
|
||||
(width, height) = (emblem.get_width(), emblem.get_height())
|
||||
xpos = pixbuf.get_width() - width
|
||||
|
@ -643,7 +646,7 @@ class PodcastListModel(gtk.ListStore):
|
|||
(width, height) = (emblem.get_width(), emblem.get_height())
|
||||
xpos = pixbuf.get_width() - width
|
||||
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:
|
||||
pass
|
||||
|
||||
|
@ -654,11 +657,11 @@ class PodcastListModel(gtk.ListStore):
|
|||
return None
|
||||
|
||||
try:
|
||||
loader = gtk.gdk.PixbufLoader('png')
|
||||
loader = GdkPixbuf.PixbufLoader()
|
||||
loader.write(channel.cover_thumb)
|
||||
loader.close()
|
||||
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)
|
||||
channel.cover_thumb = None
|
||||
channel.save()
|
||||
|
@ -666,8 +669,11 @@ class PodcastListModel(gtk.ListStore):
|
|||
|
||||
def _save_cached_thumb(self, channel, pixbuf):
|
||||
bufs = []
|
||||
pixbuf.save_to_callback(lambda buf, data: data.append(buf), 'png', {}, bufs)
|
||||
channel.cover_thumb = buffer(''.join(bufs))
|
||||
def save_callback(buf, length, user_data):
|
||||
user_data.append(buf)
|
||||
return True
|
||||
pixbuf.save_to_callbackv(save_callback, bufs, 'png', [None], [])
|
||||
channel.cover_thumb = bytes(b''.join(bufs))
|
||||
channel.save()
|
||||
|
||||
def _get_cover_image(self, channel, add_overlay=False):
|
||||
|
@ -799,7 +805,7 @@ class PodcastListModel(gtk.ListStore):
|
|||
def iter_is_first_row(self, iter):
|
||||
iter = self._filter.convert_iter_to_child_iter(iter)
|
||||
path = self.get_path(iter)
|
||||
return (path == (0,))
|
||||
return (path == Gtk.TreePath.new_first())
|
||||
|
||||
def update_by_filter_iter(self, 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]
|
||||
|
||||
# Calculate the stats over all podcasts of this section
|
||||
total, deleted, new, downloaded, unplayed = map(sum,
|
||||
zip(*[c.get_statistics() for c in channels]))
|
||||
if len(channels) is 0:
|
||||
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
|
||||
# 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 coverart
|
||||
|
||||
import gtk
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GdkPixbuf
|
||||
|
||||
|
||||
class CoverDownloader(ObservableService):
|
||||
|
@ -117,15 +118,15 @@ class CoverDownloader(ObservableService):
|
|||
pixbuf = None
|
||||
|
||||
try:
|
||||
pixbuf = gtk.gdk.pixbuf_new_from_file(filename)
|
||||
except Exception, e:
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename)
|
||||
except Exception as e:
|
||||
logger.warn('Cannot load cover art', exc_info=True)
|
||||
if pixbuf is None and filename.startswith(channel.cover_file):
|
||||
logger.info('Deleting broken cover: %s', filename)
|
||||
util.delete_file(filename)
|
||||
filename = get_filename()
|
||||
try:
|
||||
pixbuf = gtk.gdk.pixbuf_new_from_file(filename)
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename)
|
||||
except Exception as e:
|
||||
logger.warn('Corrupt cover art on server, deleting', exc_info=True)
|
||||
util.delete_file(filename)
|
||||
|
|
|
@ -16,39 +16,54 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import gtk
|
||||
import gtk.gdk
|
||||
import gobject
|
||||
import pango
|
||||
import os
|
||||
import cgi
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import Pango
|
||||
|
||||
import html
|
||||
import logging
|
||||
|
||||
import gpodder
|
||||
|
||||
_ = gpodder.gettext
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from gpodder import util
|
||||
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:
|
||||
def __init__(self, shownotes_pane):
|
||||
self.shownotes_pane = shownotes_pane
|
||||
|
||||
self.scrolled_window = gtk.ScrolledWindow()
|
||||
self.scrolled_window.set_shadow_type(gtk.SHADOW_IN)
|
||||
self.scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
|
||||
self.scrolled_window = Gtk.ScrolledWindow()
|
||||
self.scrolled_window.set_shadow_type(Gtk.ShadowType.IN)
|
||||
self.scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||
self.scrolled_window.add(self.init())
|
||||
self.scrolled_window.show_all()
|
||||
|
||||
self.da_message = gtk.DrawingArea()
|
||||
self.da_message.connect('expose-event', \
|
||||
self.on_shownotes_message_expose_event)
|
||||
self.da_message = Gtk.DrawingArea()
|
||||
self.da_message.set_property('expand', True)
|
||||
self.da_message.connect('draw', self.on_shownotes_message_expose_event)
|
||||
self.shownotes_pane.add(self.da_message)
|
||||
self.shownotes_pane.add(self.scrolled_window)
|
||||
|
||||
|
@ -90,19 +105,14 @@ class gPodderShownotes:
|
|||
else:
|
||||
self.show_pane(selected_episodes)
|
||||
|
||||
def on_shownotes_message_expose_event(self, drawingarea, event):
|
||||
ctx = event.window.cairo_create()
|
||||
ctx.rectangle(event.area.x, event.area.y, \
|
||||
event.area.width, event.area.height)
|
||||
ctx.clip()
|
||||
|
||||
def on_shownotes_message_expose_event(self, drawingarea, ctx):
|
||||
# paint the background white
|
||||
colormap = event.window.get_colormap()
|
||||
gc = event.window.new_gc(foreground=colormap.alloc_color('white'))
|
||||
event.window.draw_rectangle(gc, True, event.area.x, event.area.y, \
|
||||
event.area.width, event.area.height)
|
||||
ctx.set_source_rgba(1, 1, 1)
|
||||
x1, y1, x2, y2 = ctx.clip_extents()
|
||||
ctx.rectangle(x1, y1, x2 - x1, y2 - y1)
|
||||
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')
|
||||
draw_text_box_centered(ctx, drawingarea, width, height, text, None, None)
|
||||
return False
|
||||
|
@ -110,19 +120,18 @@ class gPodderShownotes:
|
|||
|
||||
class gPodderShownotesText(gPodderShownotes):
|
||||
def init(self):
|
||||
self.text_view = gtk.TextView()
|
||||
self.text_view.set_wrap_mode(gtk.WRAP_WORD_CHAR)
|
||||
self.text_view = Gtk.TextView()
|
||||
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_editable(False)
|
||||
self.text_view.connect('button-release-event', self.on_button_release)
|
||||
self.text_view.connect('key-press-event', self.on_key_press)
|
||||
self.text_buffer = gtk.TextBuffer()
|
||||
self.text_buffer.create_tag('heading', scale=pango.SCALE_LARGE, weight=pango.WEIGHT_BOLD)
|
||||
self.text_buffer.create_tag('subheading', scale=pango.SCALE_SMALL)
|
||||
self.text_buffer.create_tag('hyperlink', foreground="#0000FF", underline=pango.UNDERLINE_SINGLE)
|
||||
self.text_buffer = Gtk.TextBuffer()
|
||||
self.text_buffer.create_tag('heading', scale=2, weight=Pango.Weight.BOLD)
|
||||
self.text_buffer.create_tag('subheading', scale=1.5)
|
||||
self.text_buffer.create_tag('hyperlink', foreground="#0000FF", underline=Pango.Underline.SINGLE)
|
||||
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
|
||||
|
||||
def update(self, heading, subheading, episode):
|
||||
|
@ -149,7 +158,7 @@ class gPodderShownotesText(gPodderShownotes):
|
|||
self.activate_links()
|
||||
|
||||
def on_key_press(self, widget, event):
|
||||
if gtk.gdk.keyval_name(event.keyval) == 'Return':
|
||||
if event.keyval == Gdk.KEY_Return:
|
||||
self.activate_links()
|
||||
return True
|
||||
|
||||
|
@ -161,3 +170,123 @@ class gPodderShownotesText(gPodderShownotes):
|
|||
target = next((url for start, end, url in self.hyperlinks if start < pos < end), None)
|
||||
if target is not None:
|
||||
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
|
||||
#
|
||||
|
||||
import gtk
|
||||
import gobject
|
||||
import pango
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GObject
|
||||
from gi.repository import Pango
|
||||
|
||||
import cgi
|
||||
|
||||
class SimpleMessageArea(gtk.HBox):
|
||||
class SimpleMessageArea(Gtk.HBox):
|
||||
"""A simple, yellow message area. Inspired by gedit.
|
||||
|
||||
Original C source code:
|
||||
http://svn.gnome.org/viewvc/gedit/trunk/gedit/gedit-message-area.c
|
||||
"""
|
||||
def __init__(self, message, buttons=()):
|
||||
gtk.HBox.__init__(self, spacing=6)
|
||||
Gtk.HBox.__init__(self, spacing=6)
|
||||
self.set_border_width(6)
|
||||
self.__in_style_set = False
|
||||
self.connect('style-set', self.__style_set)
|
||||
self.connect('expose-event', self.__expose_event)
|
||||
self.__in_style_updated = False
|
||||
self.connect('style-updated', self.__style_updated)
|
||||
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_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.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:
|
||||
hbox.pack_start(button, expand=True, fill=False)
|
||||
self.pack_start(hbox, expand=False, fill=False)
|
||||
hbox.pack_start(button, True, False, 0)
|
||||
self.pack_start(hbox, False, False, 0)
|
||||
|
||||
def set_markup(self, markup, line_wrap=True, min_width=3, max_width=100):
|
||||
# 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_line_wrap(line_wrap)
|
||||
|
||||
def __style_set(self, widget, previous_style):
|
||||
if self.__in_style_set:
|
||||
def __style_updated(self, widget):
|
||||
if self.__in_style_updated:
|
||||
return
|
||||
|
||||
w = gtk.Window(gtk.WINDOW_POPUP)
|
||||
w = Gtk.Window(Gtk.WindowType.POPUP)
|
||||
w.set_name('gtk-tooltip')
|
||||
w.ensure_style()
|
||||
style = w.get_style()
|
||||
|
@ -84,33 +85,33 @@ class SimpleMessageArea(gtk.HBox):
|
|||
|
||||
self.queue_draw()
|
||||
|
||||
def __expose_event(self, widget, event):
|
||||
def __on_draw(self, widget, cr):
|
||||
style = widget.get_style()
|
||||
rect = widget.get_allocation()
|
||||
style.paint_flat_box(widget.window, gtk.STATE_NORMAL,
|
||||
gtk.SHADOW_OUT, None, widget, "tooltip",
|
||||
x, rect = Gdk.cairo_get_clip_rectangle(cr)
|
||||
Gtk.paint_flat_box(style, cr, Gtk.StateType.NORMAL,
|
||||
Gtk.ShadowType.OUT, widget, "tooltip",
|
||||
rect.x, rect.y, rect.width, rect.height)
|
||||
return False
|
||||
|
||||
|
||||
class SpinningProgressIndicator(gtk.Image):
|
||||
class SpinningProgressIndicator(Gtk.Image):
|
||||
# Progress indicator loading inspired by glchess from gnome-games-clutter
|
||||
def __init__(self, size=32):
|
||||
gtk.Image.__init__(self)
|
||||
Gtk.Image.__init__(self)
|
||||
|
||||
self._frames = []
|
||||
self._frame_id = 0
|
||||
|
||||
# Load the progress indicator
|
||||
icon_theme = gtk.icon_theme_get_default()
|
||||
icon_theme = Gtk.IconTheme.get_default()
|
||||
|
||||
try:
|
||||
icon = icon_theme.load_icon('process-working', size, 0)
|
||||
width, height = icon.get_width(), icon.get_height()
|
||||
if width < size or height < size:
|
||||
size = min(width, height)
|
||||
for row in range(height/size):
|
||||
for column in range(width/size):
|
||||
for row in range(height//size):
|
||||
for column in range(width//size):
|
||||
frame = icon.subpixbuf(column*size, row*size, size, size)
|
||||
self._frames.append(frame)
|
||||
# Remove the first frame (the "idle" icon)
|
||||
|
@ -119,7 +120,7 @@ class SpinningProgressIndicator(gtk.Image):
|
|||
self.step_animation()
|
||||
except:
|
||||
# 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):
|
||||
if len(self._frames) > 1:
|
||||
|
|
|
@ -24,13 +24,9 @@
|
|||
#
|
||||
|
||||
import copy
|
||||
from functools import reduce
|
||||
|
||||
try:
|
||||
# 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 json
|
||||
|
||||
|
||||
class JsonConfigSubtree(object):
|
||||
|
@ -89,7 +85,7 @@ class JsonConfig(object):
|
|||
For newly-set keys, on_key_changed is also called. In this case,
|
||||
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.a.b = 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:
|
||||
|
||||
>>> def callback(*args): print 'callback:', args
|
||||
>>> def callback(*args): print('callback:', args)
|
||||
>>> c = JsonConfig(on_key_changed=callback)
|
||||
>>> c.a.b = 1 # This works as expected
|
||||
callback: ('a.b', None, 1)
|
||||
|
@ -129,14 +125,14 @@ class JsonConfig(object):
|
|||
>>> c = JsonConfig()
|
||||
>>> c.a.b = 10
|
||||
>>> backup = repr(c)
|
||||
>>> print c.a.b
|
||||
>>> print(c.a.b)
|
||||
10
|
||||
>>> c.a.b = 11
|
||||
>>> print c.a.b
|
||||
>>> print(c.a.b)
|
||||
11
|
||||
>>> c._restore(backup)
|
||||
False
|
||||
>>> print c.a.b
|
||||
>>> print(c.a.b)
|
||||
10
|
||||
"""
|
||||
self._data = json.loads(backup)
|
||||
|
@ -156,7 +152,7 @@ class JsonConfig(object):
|
|||
work_queue = [(self._data, merge_source)]
|
||||
while work_queue:
|
||||
data, default = work_queue.pop()
|
||||
for key, value in default.iteritems():
|
||||
for key, value in default.items():
|
||||
if key not in data:
|
||||
# Copy defaults for missing key
|
||||
data[key] = copy.deepcopy(value)
|
||||
|
@ -175,7 +171,7 @@ class JsonConfig(object):
|
|||
def __repr__(self):
|
||||
"""
|
||||
>>> c = JsonConfig('{"a": 1}')
|
||||
>>> print c
|
||||
>>> print(c)
|
||||
{
|
||||
"a": 1
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
|
||||
# For Python 2.5, we need to request the "with" statement
|
||||
from __future__ import with_statement
|
||||
|
||||
|
||||
try:
|
||||
import sqlite3.dbapi2 as sqlite
|
||||
|
@ -55,7 +55,7 @@ class Store(object):
|
|||
# necessary. The value None is special-cased and never cast.
|
||||
cls = o.__class__.__slots__[slot]
|
||||
if value is not None:
|
||||
if isinstance(value, unicode):
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode('utf-8')
|
||||
value = cls(value)
|
||||
setattr(o, slot, value)
|
||||
|
@ -66,7 +66,9 @@ class Store(object):
|
|||
|
||||
def close(self):
|
||||
with self.lock:
|
||||
self.db.isolation_level = None
|
||||
self.db.execute('VACUUM')
|
||||
self.db.isolation_level = ''
|
||||
self.db.close()
|
||||
|
||||
def _register(self, class_):
|
||||
|
@ -86,7 +88,7 @@ class Store(object):
|
|||
', '.join('%s TEXT'%s for s in slots)))
|
||||
|
||||
def convert(self, v):
|
||||
if isinstance(v, unicode):
|
||||
if isinstance(v, str):
|
||||
return v
|
||||
elif isinstance(v, str):
|
||||
# XXX: Rewrite ^^^ as "isinstance(v, bytes)" in Python 3
|
||||
|
@ -96,7 +98,7 @@ class Store(object):
|
|||
|
||||
def update(self, o, **kwargs):
|
||||
self.remove(o)
|
||||
for k, v in kwargs.items():
|
||||
for k, v in list(kwargs.items()):
|
||||
setattr(o, k, v)
|
||||
self.save(o)
|
||||
|
||||
|
@ -134,9 +136,9 @@ class Store(object):
|
|||
if kwargs:
|
||||
sql += ' WHERE %s' % (' AND '.join('%s=?' % k for k in kwargs))
|
||||
try:
|
||||
self.db.execute(sql, kwargs.values())
|
||||
self.db.execute(sql, list(kwargs.values()))
|
||||
return True
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
def remove(self, o):
|
||||
|
@ -164,18 +166,18 @@ class Store(object):
|
|||
if kwargs:
|
||||
sql += ' WHERE %s' % (' AND '.join('%s=?' % k for k in kwargs))
|
||||
try:
|
||||
cur = self.db.execute(sql, kwargs.values())
|
||||
except Exception, e:
|
||||
cur = self.db.execute(sql, list(kwargs.values()))
|
||||
except Exception as e:
|
||||
raise
|
||||
def apply(row):
|
||||
o = class_.__new__(class_)
|
||||
for attr, value in zip(slots, row):
|
||||
try:
|
||||
self._set(o, attr, value)
|
||||
except ValueError, ve:
|
||||
except ValueError as ve:
|
||||
return None
|
||||
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):
|
||||
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))
|
||||
|
||||
p = m.get(Person, id=200)
|
||||
print p
|
||||
print(p)
|
||||
m.remove(p)
|
||||
p = m.get(Person, id=200)
|
||||
|
||||
|
@ -219,5 +221,5 @@ if __name__ == '__main__':
|
|||
|
||||
# A schema update takes place here
|
||||
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)
|
||||
|
||||
# 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)
|
||||
|
||||
return o
|
||||
|
@ -433,7 +433,7 @@ class PodcastEpisode(PodcastModelObject):
|
|||
if self.download_filename is None and (check_only or not create):
|
||||
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):
|
||||
# Avoid and catch gPodder bug 1440 and similar situations
|
||||
|
@ -500,8 +500,7 @@ class PodcastEpisode(PodcastModelObject):
|
|||
self.download_filename = wanted_filename
|
||||
self.save()
|
||||
|
||||
return os.path.join(util.sanitize_encoding(self.channel.save_dir),
|
||||
util.sanitize_encoding(self.download_filename))
|
||||
return os.path.join(self.channel.save_dir, self.download_filename)
|
||||
|
||||
def extension(self, may_call_local_filename=True):
|
||||
filename, ext = util.filename_from_url(self.url)
|
||||
|
@ -640,10 +639,10 @@ class PodcastEpisode(PodcastModelObject):
|
|||
class PodcastChannel(PodcastModelObject):
|
||||
__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
|
||||
STRATEGY_DEFAULT, STRATEGY_LATEST = range(2)
|
||||
STRATEGY_DEFAULT, STRATEGY_LATEST = list(range(2))
|
||||
|
||||
# Description and ordering of strategies
|
||||
STRATEGIES = [
|
||||
|
@ -812,12 +811,8 @@ class PodcastChannel(PodcastModelObject):
|
|||
return re.sub('^the ', '', key).translate(cls.UNICODE_TRANSLATE)
|
||||
|
||||
@classmethod
|
||||
def load(cls, model, url, create=True, authentication_tokens=None,\
|
||||
max_episodes=0):
|
||||
if isinstance(url, unicode):
|
||||
url = url.encode('utf-8')
|
||||
|
||||
existing = filter(lambda p: p.url == url, model.get_podcasts())
|
||||
def load(cls, model, url, create=True, authentication_tokens=None, max_episodes=0):
|
||||
existing = [p for p in model.get_podcasts() if p.url == url]
|
||||
|
||||
if existing:
|
||||
return existing[0]
|
||||
|
@ -835,7 +830,7 @@ class PodcastChannel(PodcastModelObject):
|
|||
|
||||
try:
|
||||
tmp.update(max_episodes)
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
logger.debug('Fetch failed. Removing buggy feed.')
|
||||
tmp.remove_downloaded()
|
||||
tmp.delete()
|
||||
|
@ -1034,7 +1029,7 @@ class PodcastChannel(PodcastModelObject):
|
|||
pass
|
||||
|
||||
self.save()
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
# "Not really" errors
|
||||
#feedcore.AuthenticationRequired
|
||||
# Temporary errors
|
||||
|
@ -1153,7 +1148,7 @@ class PodcastChannel(PodcastModelObject):
|
|||
return self.children
|
||||
|
||||
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):
|
||||
# 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)
|
||||
|
||||
# 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
|
||||
if not util.make_directory(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 \
|
||||
not mygpoclient.require_version(MYGPOCLIENT_REQUIRED):
|
||||
print >>sys.stderr, """
|
||||
print("""
|
||||
Please upgrade your mygpoclient library.
|
||||
See http://thp.io/2010/mygpoclient/
|
||||
|
||||
Required version: %s
|
||||
Installed version: %s
|
||||
""" % (MYGPOCLIENT_REQUIRED, mygpoclient.__version__)
|
||||
""" % (MYGPOCLIENT_REQUIRED, mygpoclient.__version__), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
|
@ -79,7 +79,7 @@ class SinceValue(object):
|
|||
__slots__ = {'host': str, 'device_id': str, 'category': int, 'since': int}
|
||||
|
||||
# Possible values for the "category" field
|
||||
PODCASTS, EPISODES = range(2)
|
||||
PODCASTS, EPISODES = list(range(2))
|
||||
|
||||
def __init__(self, host, device_id, category, since=0):
|
||||
self.host = host
|
||||
|
@ -91,7 +91,7 @@ class SubscribeAction(object):
|
|||
__slots__ = {'action_type': int, 'url': str}
|
||||
|
||||
# Possible values for the "action_type" field
|
||||
ADD, REMOVE = range(2)
|
||||
ADD, REMOVE = list(range(2))
|
||||
|
||||
def __init__(self, action_type, url):
|
||||
self.action_type = action_type
|
||||
|
@ -506,7 +506,7 @@ class MygPoClient(object):
|
|||
# handle outside
|
||||
raise
|
||||
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
logger.warn('Exception while polling for episodes.', exc_info=True)
|
||||
|
||||
# Step 2: Upload Episode actions
|
||||
|
@ -533,7 +533,7 @@ class MygPoClient(object):
|
|||
self._config.mygpo.enabled = False
|
||||
return False
|
||||
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
logger.error('Cannot upload episode actions: %s', str(e), exc_info=True)
|
||||
return False
|
||||
|
||||
|
@ -598,7 +598,7 @@ class MygPoClient(object):
|
|||
self._config.mygpo.enabled = False
|
||||
return False
|
||||
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
logger.error('Cannot upload subscriptions: %s', str(e), exc_info=True)
|
||||
return False
|
||||
|
||||
|
@ -615,7 +615,7 @@ class MygPoClient(object):
|
|||
self._config.mygpo.enabled = False
|
||||
return False
|
||||
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
logger.error('Cannot update device %s: %s', self.device_id,
|
||||
str(e), exc_info=True)
|
||||
return False
|
||||
|
|
|
@ -71,6 +71,7 @@ class Importer(object):
|
|||
if os.path.exists(url):
|
||||
doc = xml.dom.minidom.parse(url)
|
||||
else:
|
||||
# FIXME: is it ok to pass bytes to parseString?
|
||||
doc = xml.dom.minidom.parseString(util.urlopen(url).read())
|
||||
|
||||
for outline in doc.getElementsByTagName('outline'):
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
|
||||
|
||||
import gpodder
|
||||
import urllib
|
||||
import urllib.request, urllib.parse, urllib.error
|
||||
|
||||
class MediaPlayerDBusReceiver(object):
|
||||
INTERFACE = 'org.gpodder.player'
|
||||
|
@ -77,12 +77,7 @@ class MediaPlayerDBusReceiver(object):
|
|||
pass
|
||||
|
||||
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('/'):
|
||||
file_uri = 'file://' + urllib.quote(file_uri)
|
||||
file_uri = 'file://' + urllib.parse.quote(file_uri)
|
||||
self.on_play_event(start, end, total, file_uri)
|
||||
|
||||
|
|
|
@ -28,12 +28,7 @@ _ = gpodder.gettext
|
|||
from gpodder import model
|
||||
from gpodder import util
|
||||
|
||||
try:
|
||||
# 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 json
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
@ -41,7 +36,7 @@ import time
|
|||
|
||||
import re
|
||||
import email
|
||||
import urllib
|
||||
import urllib.request, urllib.parse, urllib.error
|
||||
|
||||
|
||||
# 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).
|
||||
"""
|
||||
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'):
|
||||
"""Get a parameter from a string of headers
|
||||
|
@ -76,8 +71,8 @@ def get_param(s, param='filename', header='content-disposition'):
|
|||
if encoding:
|
||||
value.append(part.decode(encoding))
|
||||
else:
|
||||
value.append(unicode(part))
|
||||
return u''.join(value)
|
||||
value.append(str(part))
|
||||
return ''.join(value)
|
||||
|
||||
return None
|
||||
|
||||
|
@ -92,7 +87,7 @@ def get_metadata(url):
|
|||
headers = track_fp.info()
|
||||
filesize = headers['content-length'] or '0'
|
||||
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))
|
||||
track_fp.close()
|
||||
return filesize, filetype, filename
|
||||
|
@ -121,7 +116,7 @@ class SoundcloudUser(object):
|
|||
|
||||
try:
|
||||
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
|
||||
finally:
|
||||
self.commit_cache()
|
||||
|
@ -252,5 +247,5 @@ model.register_custom_handler(SoundcloudFeed)
|
|||
model.register_custom_handler(SoundcloudFavFeed)
|
||||
|
||||
def search_for_user(query):
|
||||
json_url = 'https://api.soundcloud.com/users.json?q=%s&consumer_key=%s' % (urllib.quote(query), CONSUMER_KEY)
|
||||
return json.load(util.urlopen(json_url))
|
||||
json_url = 'https://api.soundcloud.com/users.json?q=%s&consumer_key=%s' % (urllib.parse.quote(query), CONSUMER_KEY)
|
||||
return json.loads(util.urlopen(json_url).read().decode('utf-8'))
|
||||
|
|
|
@ -40,8 +40,8 @@ class Matcher(object):
|
|||
def match(self, term):
|
||||
try:
|
||||
return bool(eval(term, {'__builtins__': None}, self))
|
||||
except Exception, e:
|
||||
print e
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return False
|
||||
|
||||
def __getitem__(self, k):
|
||||
|
@ -69,7 +69,7 @@ class Matcher(object):
|
|||
|
||||
# Nouns (for comparisons)
|
||||
if k in ('megabytes', 'mb'):
|
||||
return float(episode.file_size) / (1024*1024)
|
||||
return episode.file_size / (1024*1024)
|
||||
elif k == 'title':
|
||||
return episode.title
|
||||
elif k == 'description':
|
||||
|
@ -79,9 +79,9 @@ class Matcher(object):
|
|||
elif k == 'age':
|
||||
return episode.age_in_days()
|
||||
elif k in ('minutes', 'min'):
|
||||
return float(episode.total_time) / 60
|
||||
return episode.total_time / 60
|
||||
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)
|
||||
|
||||
|
@ -140,8 +140,8 @@ class EQL(object):
|
|||
if not self._regex and not self._string:
|
||||
try:
|
||||
self._query = compile(query, '<eql-string>', 'eval')
|
||||
except Exception, e:
|
||||
print e
|
||||
except Exception as e:
|
||||
print(e)
|
||||
self._query = None
|
||||
|
||||
|
||||
|
@ -157,7 +157,7 @@ class EQL(object):
|
|||
return Matcher(episode).match(self._query)
|
||||
|
||||
def filter(self, episodes):
|
||||
return filter(self.match, episodes)
|
||||
return list(filter(self.match, episodes))
|
||||
|
||||
|
||||
def UserEQL(query):
|
||||
|
|
|
@ -210,7 +210,7 @@ def upgrade(db, filename):
|
|||
backup = '%s_upgraded-v%d_%d' % (filename, int(version), int(time.time()))
|
||||
try:
|
||||
shutil.copy(filename, backup)
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
raise Exception('Cannot create DB backup before upgrade: ' + e)
|
||||
|
||||
db.execute("DELETE FROM version")
|
||||
|
@ -246,7 +246,7 @@ def convert_gpodder2_db(old_db, new_db):
|
|||
old_cur = old_db.cursor()
|
||||
columns = [x[1] for x in old_cur.execute('PRAGMA table_info(channels)')]
|
||||
for row in old_cur.execute('SELECT * FROM channels'):
|
||||
row = dict(zip(columns, row))
|
||||
row = dict(list(zip(columns, row)))
|
||||
values = (
|
||||
row['id'],
|
||||
row['override_title'] or row['title'],
|
||||
|
@ -276,7 +276,7 @@ def convert_gpodder2_db(old_db, new_db):
|
|||
old_cur = old_db.cursor()
|
||||
columns = [x[1] for x in old_cur.execute('PRAGMA table_info(episodes)')]
|
||||
for row in old_cur.execute('SELECT * FROM episodes'):
|
||||
row = dict(zip(columns, row))
|
||||
row = dict(list(zip(columns, row)))
|
||||
values = (
|
||||
row['id'],
|
||||
row['channel_id'],
|
||||
|
|
|
@ -85,8 +85,8 @@ if pymtp_available:
|
|||
folder = folder.contents
|
||||
name = self.sep.join([path, folder.name]).lstrip(self.sep)
|
||||
result[name] = folder.folder_id
|
||||
if folder.child:
|
||||
result.update(self.unfold(folder.child, name))
|
||||
if folder.get_child():
|
||||
result.update(self.unfold(folder.get_child(), name))
|
||||
folder = folder.sibling
|
||||
return result
|
||||
|
||||
|
@ -97,7 +97,7 @@ if pymtp_available:
|
|||
while parts:
|
||||
prefix.append(parts[0])
|
||||
tmpath = self.sep.join(prefix)
|
||||
if self.folders.has_key(tmpath):
|
||||
if tmpath in self.folders:
|
||||
folder_id = self.folders[tmpath]
|
||||
else:
|
||||
folder_id = self.create_folder(parts[0], parent=folder_id)
|
||||
|
@ -136,7 +136,7 @@ def get_track_length(filename):
|
|||
try:
|
||||
mp3file = eyed3.mp3.Mp3AudioFile(filename)
|
||||
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)
|
||||
|
||||
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.device=self
|
||||
# New Task, we must wait on the GTK Loop
|
||||
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:
|
||||
logger.warning("No episodes to sync")
|
||||
|
||||
if done_callback:
|
||||
done_callback()
|
||||
|
||||
return True
|
||||
|
||||
def remove_tracks(self, tracklist):
|
||||
for idx, track in enumerate(tracklist):
|
||||
if self.cancelled:
|
||||
|
@ -379,7 +379,7 @@ class iPodDevice(Device):
|
|||
try:
|
||||
released = gpod.itdb_time_mac_to_host(track.time_released)
|
||||
released = util.format_date(released)
|
||||
except ValueError, ve:
|
||||
except ValueError as ve:
|
||||
# timestamp out of range for platform time_t (bug 418)
|
||||
logger.info('Cannot convert track time: %s', ve)
|
||||
released = 0
|
||||
|
@ -502,7 +502,7 @@ class MP3PlayerDevice(Device):
|
|||
download_status_model,
|
||||
download_queue_manager):
|
||||
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.download_status_model = download_status_model
|
||||
self.download_queue_manager = download_queue_manager
|
||||
|
@ -532,11 +532,11 @@ class MP3PlayerDevice(Device):
|
|||
else:
|
||||
folder = self.destination
|
||||
|
||||
return util.sanitize_encoding(folder)
|
||||
return folder
|
||||
|
||||
def get_episode_file_on_device(self, episode):
|
||||
# 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
|
||||
filename_base = util.sanitize_filename(episode.sync_filename(
|
||||
self._config.device_sync.custom_sync_name_enabled,
|
||||
|
@ -554,7 +554,7 @@ class MP3PlayerDevice(Device):
|
|||
return to_file
|
||||
|
||||
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
|
||||
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
|
||||
assert filename is not None
|
||||
|
||||
from_file = util.sanitize_encoding(filename)
|
||||
from_file = filename
|
||||
|
||||
# verify free space
|
||||
needed = util.calculate_size(from_file)
|
||||
|
@ -578,7 +578,7 @@ class MP3PlayerDevice(Device):
|
|||
|
||||
# get the filename that will be used on the device
|
||||
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):
|
||||
try:
|
||||
|
@ -590,7 +590,7 @@ class MP3PlayerDevice(Device):
|
|||
if not os.path.exists(to_file):
|
||||
logger.info('Copying %s => %s',
|
||||
os.path.basename(from_file),
|
||||
to_file.decode(util.encoding))
|
||||
to_file)
|
||||
self.copy_file_progress(from_file, to_file, reporthook)
|
||||
|
||||
return True
|
||||
|
@ -598,7 +598,7 @@ class MP3PlayerDevice(Device):
|
|||
def copy_file_progress(self, from_file, to_file, reporthook=None):
|
||||
try:
|
||||
out_file = open(to_file, 'wb')
|
||||
except IOError, ioerror:
|
||||
except IOError as ioerror:
|
||||
d = {'filename': ioerror.filename, 'message': ioerror.strerror}
|
||||
self.errors.append(_('Error opening %(filename)s: %(message)s') % d)
|
||||
self.cancel()
|
||||
|
@ -606,7 +606,7 @@ class MP3PlayerDevice(Device):
|
|||
|
||||
try:
|
||||
in_file = open(from_file, 'rb')
|
||||
except IOError, ioerror:
|
||||
except IOError as ioerror:
|
||||
d = {'filename': ioerror.filename, 'message': ioerror.strerror}
|
||||
self.errors.append(_('Error opening %(filename)s: %(message)s') % d)
|
||||
self.cancel()
|
||||
|
@ -622,7 +622,7 @@ class MP3PlayerDevice(Device):
|
|||
bytes_read += len(s)
|
||||
try:
|
||||
out_file.write(s)
|
||||
except IOError, ioerror:
|
||||
except IOError as ioerror:
|
||||
self.errors.append(ioerror.strerror)
|
||||
try:
|
||||
out_file.close()
|
||||
|
@ -697,7 +697,7 @@ class MTPDevice(Device):
|
|||
self.__model_name = None
|
||||
try:
|
||||
self.__MTPDevice = MTP()
|
||||
except NameError, e:
|
||||
except NameError as e:
|
||||
# pymtp not available / not installed (see bug 924)
|
||||
logger.error('pymtp not found: %s', str(e))
|
||||
self.__MTPDevice = None
|
||||
|
@ -705,7 +705,7 @@ class MTPDevice(Device):
|
|||
def __callback(self, sent, total):
|
||||
if self.cancelled:
|
||||
return -1
|
||||
percentage = round(float(sent)/float(total)*100)
|
||||
percentage = round(sent/total*100)
|
||||
text = ('%i%%' % percentage)
|
||||
self.notify('progress', sent, total, text)
|
||||
|
||||
|
@ -722,7 +722,7 @@ class MTPDevice(Device):
|
|||
try:
|
||||
d = time.gmtime(date)
|
||||
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')
|
||||
return None
|
||||
|
||||
|
@ -752,10 +752,10 @@ class MTPDevice(Device):
|
|||
_date -= shift_in_sec
|
||||
else:
|
||||
raise ValueError("Expected + or -")
|
||||
except Exception, exc:
|
||||
except Exception as exc:
|
||||
logger.warning('WARNING: ignoring invalid time zone information for %s (%s)')
|
||||
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)')
|
||||
return None
|
||||
|
||||
|
@ -796,7 +796,7 @@ class MTPDevice(Device):
|
|||
self.__MTPDevice.connect()
|
||||
# build the initial tracks_list
|
||||
self.tracks_list = self.get_all_tracks()
|
||||
except Exception, exc:
|
||||
except Exception as exc:
|
||||
logger.error('unable to find an MTP device (%s)')
|
||||
return False
|
||||
|
||||
|
@ -809,7 +809,7 @@ class MTPDevice(Device):
|
|||
|
||||
try:
|
||||
self.__MTPDevice.disconnect()
|
||||
except Exception, exc:
|
||||
except Exception as exc:
|
||||
logger.error('unable to close %s (%s)', self.get_name())
|
||||
return False
|
||||
|
||||
|
@ -876,7 +876,7 @@ class MTPDevice(Device):
|
|||
|
||||
try:
|
||||
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.info('%s removed', sync_track.mtptrack.title)
|
||||
|
@ -884,7 +884,7 @@ class MTPDevice(Device):
|
|||
def get_all_tracks(self):
|
||||
try:
|
||||
listing = self.__MTPDevice.get_tracklisting(callback=self.__callback)
|
||||
except Exception, exc:
|
||||
except Exception as exc:
|
||||
logger.error('unable to get file listing %s (%s)')
|
||||
|
||||
tracks = []
|
||||
|
@ -923,7 +923,7 @@ class SyncTask(download.DownloadTask):
|
|||
# Possible states this sync task can be in
|
||||
STATUS_MESSAGE = (_('Added'), _('Queued'), _('Synchronizing'),
|
||||
_('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):
|
||||
|
@ -1040,7 +1040,7 @@ class SyncTask(download.DownloadTask):
|
|||
self.total_size = float(totalSize)
|
||||
|
||||
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)
|
||||
|
||||
if self.status == SyncTask.CANCELLED:
|
||||
|
@ -1064,8 +1064,8 @@ class SyncTask(download.DownloadTask):
|
|||
self.speed = 0.0
|
||||
return False
|
||||
|
||||
# We only start this download if its status is "queued"
|
||||
if self.status != SyncTask.QUEUED:
|
||||
# We only start this download if its status is "downloading"
|
||||
if self.status != SyncTask.DOWNLOADING:
|
||||
return False
|
||||
|
||||
# We are synching this file right now
|
||||
|
@ -1075,7 +1075,7 @@ class SyncTask(download.DownloadTask):
|
|||
try:
|
||||
logger.info('Starting SyncTask')
|
||||
self.device.add_track(self.episode, reporthook=self.status_updated)
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
self.status = SyncTask.FAILED
|
||||
logger.error('Sync failed: %s', str(e), exc_info=True)
|
||||
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
|
||||
# warning about this missing dependency in order to avoid bogus errors.
|
||||
import minimock
|
||||
except ImportError, e:
|
||||
print >>sys.stderr, """
|
||||
except ImportError as e:
|
||||
print("""
|
||||
Error: Unit tests require the "minimock" module (python-minimock).
|
||||
Please install it before running the unit tests.
|
||||
"""
|
||||
""", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
# Main package and test package (for modules in main package)
|
||||
|
@ -73,9 +73,9 @@ try:
|
|||
import HTMLTestRunner
|
||||
REPORT_FILENAME = 'test_report.html'
|
||||
runner = HTMLTestRunner.HTMLTestRunner(stream=open(REPORT_FILENAME, 'w'))
|
||||
print """
|
||||
print("""
|
||||
HTML Test Report will be written to %s
|
||||
""" % REPORT_FILENAME
|
||||
""" % REPORT_FILENAME)
|
||||
except ImportError:
|
||||
runner = unittest.TextTestRunner(verbosity=2)
|
||||
|
||||
|
@ -100,7 +100,7 @@ if __name__ == '__main__':
|
|||
cov.report(coverage_modules)
|
||||
cov.erase()
|
||||
else:
|
||||
print >>sys.stderr, """
|
||||
print("""
|
||||
No coverage reporting done (Python module "coverage" is missing)
|
||||
Please install the python-coverage package to get coverage reporting.
|
||||
"""
|
||||
""", file=sys.stderr)
|
||||
|
|
|
@ -49,28 +49,28 @@ import string
|
|||
|
||||
import re
|
||||
import subprocess
|
||||
from htmlentitydefs import entitydefs
|
||||
from html.entities import entitydefs
|
||||
import time
|
||||
import gzip
|
||||
import datetime
|
||||
import threading
|
||||
|
||||
import urlparse
|
||||
import urllib
|
||||
import urllib2
|
||||
import httplib
|
||||
import urllib.parse
|
||||
import urllib.request, urllib.parse, urllib.error
|
||||
import urllib.request, urllib.error, urllib.parse
|
||||
import http.client
|
||||
import webbrowser
|
||||
import mimetypes
|
||||
import itertools
|
||||
|
||||
import StringIO
|
||||
import io
|
||||
import xml.dom.minidom
|
||||
|
||||
import collections
|
||||
|
||||
if sys.hexversion < 0x03000000:
|
||||
from HTMLParser import HTMLParser
|
||||
from htmlentitydefs import name2codepoint
|
||||
from html.parser import HTMLParser
|
||||
from html.entities import name2codepoint
|
||||
else:
|
||||
from html.parser import HTMLParser
|
||||
from html.entities import name2codepoint
|
||||
|
@ -95,7 +95,7 @@ N_ = gpodder.ngettext
|
|||
import locale
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, '')
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
logger.warn('Cannot set locale (%s)', e, exc_info=True)
|
||||
|
||||
# Native filesystem encoding detection
|
||||
|
@ -119,15 +119,15 @@ if encoding is None:
|
|||
# Filename / folder name sanitization
|
||||
def _sanitize_char(c):
|
||||
if c in string.whitespace:
|
||||
return ' '
|
||||
return b' '
|
||||
elif c in ',-.()':
|
||||
return c
|
||||
elif c in string.punctuation or ord(c) <= 31:
|
||||
return '_'
|
||||
return c.encode('utf-8')
|
||||
elif c in string.punctuation or ord(c) <= 31 or ord(c) >= 127:
|
||||
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
|
||||
|
||||
_MIME_TYPE_LIST = [
|
||||
|
@ -232,7 +232,7 @@ def normalize_feed_url(url):
|
|||
'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):
|
||||
url = expansion % (url[len(prefix):],)
|
||||
break
|
||||
|
@ -241,7 +241,7 @@ def normalize_feed_url(url):
|
|||
if not '://' in 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)
|
||||
if '@' in netloc:
|
||||
|
@ -265,7 +265,7 @@ def normalize_feed_url(url):
|
|||
return None
|
||||
|
||||
# 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):
|
||||
|
@ -287,11 +287,11 @@ def username_password_from_url(url):
|
|||
>>> username_password_from_url(1)
|
||||
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)
|
||||
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/')
|
||||
('a@b', 'c')
|
||||
>>> 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/')
|
||||
('i/o', 'P@ss:')
|
||||
>>> 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/')
|
||||
('w x', 'y z')
|
||||
>>> username_password_from_url('http://example.com/x@y:z@test.com/')
|
||||
(None, None)
|
||||
"""
|
||||
if type(url) not in (str, unicode):
|
||||
raise ValueError('URL has to be a string or unicode object.')
|
||||
if not isinstance(url, str):
|
||||
raise ValueError('URL has to be a string.')
|
||||
|
||||
(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:
|
||||
(authentication, netloc) = netloc.rsplit('@', 1)
|
||||
|
@ -330,10 +330,10 @@ def username_password_from_url(url):
|
|||
# is handled by the authentication.split(':', 1) above, and
|
||||
# will cause any extraneous ':'s to be part of the password.
|
||||
|
||||
username = urllib.unquote(username)
|
||||
password = urllib.unquote(password)
|
||||
username = urllib.parse.unquote(username)
|
||||
password = urllib.parse.unquote(password)
|
||||
else:
|
||||
username = urllib.unquote(authentication)
|
||||
username = urllib.parse.unquote(authentication)
|
||||
|
||||
return (username, password)
|
||||
|
||||
|
@ -353,10 +353,10 @@ def calculate_size( path):
|
|||
to list all subdirectories of the given path.
|
||||
"""
|
||||
if path is None:
|
||||
return 0L
|
||||
return 0
|
||||
|
||||
if os.path.dirname( path) == '/':
|
||||
return 0L
|
||||
return 0
|
||||
|
||||
if os.path.isfile( path):
|
||||
return os.path.getsize( path)
|
||||
|
@ -375,7 +375,7 @@ def calculate_size( path):
|
|||
|
||||
return sum
|
||||
|
||||
return 0L
|
||||
return 0
|
||||
|
||||
|
||||
def file_modification_datetime(filename):
|
||||
|
@ -433,9 +433,9 @@ def file_age_to_string(days):
|
|||
>>> file_age_to_string(0)
|
||||
''
|
||||
>>> file_age_to_string(1)
|
||||
u'1 day ago'
|
||||
'1 day ago'
|
||||
>>> file_age_to_string(2)
|
||||
u'2 days ago'
|
||||
'2 days ago'
|
||||
"""
|
||||
if days < 1:
|
||||
return ''
|
||||
|
@ -511,10 +511,10 @@ def format_date(timestamp):
|
|||
yesterday = time.localtime(time.time() - seconds_in_a_day)[:3]
|
||||
try:
|
||||
timestamp_date = time.localtime(timestamp)[:3]
|
||||
except ValueError, ve:
|
||||
except ValueError as ve:
|
||||
logger.warn('Cannot convert timestamp', exc_info=True)
|
||||
return None
|
||||
except TypeError, te:
|
||||
except TypeError as te:
|
||||
logger.warn('Cannot convert timestamp', exc_info=True)
|
||||
return None
|
||||
|
||||
|
@ -536,10 +536,10 @@ def format_date(timestamp):
|
|||
|
||||
if diff < 7:
|
||||
# Weekday name
|
||||
return str(timestamp.strftime('%A').decode(encoding))
|
||||
return timestamp.strftime('%A')
|
||||
else:
|
||||
# Locale's appropriate date representation
|
||||
return str(timestamp.strftime('%x'))
|
||||
return timestamp.strftime('%x')
|
||||
|
||||
|
||||
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)
|
||||
|
||||
# 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
|
||||
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
|
||||
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])
|
||||
result = []
|
||||
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
|
||||
t = re.sub(' +\n', '\n', t)
|
||||
# Convert more than two newlines to two newlines
|
||||
|
@ -705,14 +705,14 @@ class HyperlinkExtracter(object):
|
|||
self.output(self.htmlws(data))
|
||||
|
||||
def handle_entityref(self, name):
|
||||
c = unichr(name2codepoint[name])
|
||||
c = chr(name2codepoint[name])
|
||||
self.output(c)
|
||||
|
||||
def handle_charref(self, name):
|
||||
if name.startswith('x'):
|
||||
c = unichr(int(name[1:], 16))
|
||||
c = chr(int(name[1:], 16))
|
||||
else:
|
||||
c = unichr(int(name))
|
||||
c = chr(int(name))
|
||||
self.output(c)
|
||||
|
||||
def output_newline(self, attrs=None):
|
||||
|
@ -740,7 +740,7 @@ class ExtractHyperlinkedText(object):
|
|||
def visit(self, element):
|
||||
NS = '{http://www.w3.org/1999/xhtml}'
|
||||
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:
|
||||
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://s/redirect.mp4?http://serv2/test.mp4 => ("test", ".mp4")
|
||||
"""
|
||||
(scheme, netloc, path, para, query, fragid) = urlparse.urlparse(url)
|
||||
(filename, extension) = os.path.splitext(os.path.basename( urllib.unquote(path)))
|
||||
(scheme, netloc, path, para, query, fragid) = urllib.parse.urlparse(url)
|
||||
(filename, extension) = os.path.splitext(os.path.basename( urllib.parse.unquote(path)))
|
||||
|
||||
if file_type_by_extension(extension) is not None and not \
|
||||
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 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)
|
||||
|
||||
if file_type_by_extension(query_extension) is not None:
|
||||
|
@ -1033,7 +1033,7 @@ def object_string_formatter(s, **kwargs):
|
|||
'Hi 123 456'
|
||||
"""
|
||||
result = s
|
||||
for key, o in kwargs.iteritems():
|
||||
for key, o in kwargs.items():
|
||||
matches = re.findall(r'\{%s\.([^\}]+)\}' % key, s)
|
||||
for attr in matches:
|
||||
if hasattr(o, attr):
|
||||
|
@ -1116,14 +1116,14 @@ def url_strip_authentication(url):
|
|||
>>> url_strip_authentication('http://x@x.com:s3cret@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
|
||||
|
||||
# Remove existing authentication data
|
||||
if '@' in url_parts[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):
|
||||
|
@ -1158,21 +1158,21 @@ def url_add_authentication(url, username, password):
|
|||
# Relaxations of the strict quoting rules (bug 1521):
|
||||
# 1. Accept '@' in username and password
|
||||
# 2. Acecpt ':' in password only
|
||||
username = urllib.quote(username, safe='@')
|
||||
username = urllib.parse.quote(username, safe='@')
|
||||
|
||||
if password is not None:
|
||||
password = urllib.quote(password, safe='@:')
|
||||
password = urllib.parse.quote(password, safe='@:')
|
||||
auth_string = ':'.join((username, password))
|
||||
else:
|
||||
auth_string = username
|
||||
|
||||
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] = '@'.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):
|
||||
|
@ -1182,12 +1182,12 @@ def urlopen(url, headers=None, data=None, timeout=None):
|
|||
username, password = username_password_from_url(url)
|
||||
if username is not None or password is not None:
|
||||
url = url_strip_authentication(url)
|
||||
password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
|
||||
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
|
||||
password_mgr.add_password(None, url, username, password)
|
||||
handler = urllib2.HTTPBasicAuthHandler(password_mgr)
|
||||
opener = urllib2.build_opener(handler)
|
||||
handler = urllib.request.HTTPBasicAuthHandler(password_mgr)
|
||||
opener = urllib.request.build_opener(handler)
|
||||
else:
|
||||
opener = urllib2.build_opener()
|
||||
opener = urllib.request.build_opener()
|
||||
|
||||
if headers is None:
|
||||
headers = {}
|
||||
|
@ -1195,7 +1195,7 @@ def urlopen(url, headers=None, data=None, timeout=None):
|
|||
headers = dict(headers)
|
||||
|
||||
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:
|
||||
return opener.open(request)
|
||||
else:
|
||||
|
@ -1251,8 +1251,8 @@ def idle_add(func, *args):
|
|||
as possible from the main UI thread.
|
||||
"""
|
||||
if gpodder.ui.gtk:
|
||||
import gobject
|
||||
gobject.idle_add(func, *args)
|
||||
from gi.repository import GObject
|
||||
GObject.idle_add(func, *args)
|
||||
else:
|
||||
func(*args)
|
||||
|
||||
|
@ -1358,11 +1358,11 @@ def format_seconds_to_hour_min_sec(seconds):
|
|||
human-readable string (duration).
|
||||
|
||||
>>> 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)
|
||||
u'1 hour'
|
||||
'1 hour'
|
||||
>>> format_seconds_to_hour_min_sec(62)
|
||||
u'1 minute and 2 seconds'
|
||||
'1 minute and 2 seconds'
|
||||
"""
|
||||
|
||||
if seconds < 1:
|
||||
|
@ -1372,10 +1372,10 @@ def format_seconds_to_hour_min_sec(seconds):
|
|||
|
||||
seconds = int(seconds)
|
||||
|
||||
hours = seconds/3600
|
||||
hours = seconds//3600
|
||||
seconds = seconds%3600
|
||||
|
||||
minutes = seconds/60
|
||||
minutes = seconds//60
|
||||
seconds = seconds%60
|
||||
|
||||
if hours:
|
||||
|
@ -1393,8 +1393,8 @@ def format_seconds_to_hour_min_sec(seconds):
|
|||
return result[0]
|
||||
|
||||
def http_request(url, method='HEAD'):
|
||||
(scheme, netloc, path, parms, qry, fragid) = urlparse.urlparse(url)
|
||||
conn = httplib.HTTPConnection(netloc)
|
||||
(scheme, netloc, path, parms, qry, fragid) = urllib.parse.urlparse(url)
|
||||
conn = http.client.HTTPConnection(netloc)
|
||||
start = len(scheme) + len('://') + len(netloc)
|
||||
conn.request(method, url[start:])
|
||||
return conn.getresponse()
|
||||
|
@ -1437,75 +1437,38 @@ def convert_bytes(d):
|
|||
strings. Any other data types will be left alone.
|
||||
|
||||
>>> convert_bytes(None)
|
||||
>>> convert_bytes(1)
|
||||
1
|
||||
>>> convert_bytes(4711L)
|
||||
4711L
|
||||
>>> convert_bytes(4711)
|
||||
4711
|
||||
>>> convert_bytes(True)
|
||||
True
|
||||
>>> convert_bytes(3.1415)
|
||||
3.1415
|
||||
>>> convert_bytes('Hello')
|
||||
u'Hello'
|
||||
>>> convert_bytes(u'Hey')
|
||||
u'Hey'
|
||||
>>> type(convert_bytes(buffer('hoho')))
|
||||
<type 'buffer'>
|
||||
'Hello'
|
||||
>>> type(convert_bytes(b'hoho'))
|
||||
<class 'bytes'>
|
||||
"""
|
||||
if d is None:
|
||||
return d
|
||||
if isinstance(d, buffer):
|
||||
elif isinstance(d, bytes):
|
||||
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
|
||||
elif not isinstance(d, unicode):
|
||||
elif not isinstance(d, str):
|
||||
return d.decode('utf-8', 'ignore')
|
||||
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')
|
||||
''
|
||||
>>> sanitize_encoding(u'unicode')
|
||||
'unicode'
|
||||
def sanitize_filename(filename, max_length=0):
|
||||
"""
|
||||
# The encoding problem goes away in Python 3.. hopefully!
|
||||
if sys.version_info >= (3, 0):
|
||||
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; trim filename
|
||||
if greater than max_length (0 = no limit).
|
||||
"""
|
||||
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:
|
||||
logger.info('Limiting file/folder name "%s" to %d characters.',
|
||||
filename, max_length)
|
||||
logger.info('Limiting file/folder name "%s" to %d characters.', filename, max_length)
|
||||
filename = filename[:max_length]
|
||||
|
||||
filename = filename.encode('ascii' if use_ascii else encoding, 'ignore')
|
||||
filename = filename.translate(SANITIZATION_TABLE)
|
||||
filename = filename.strip('.' + string.whitespace)
|
||||
|
||||
return filename
|
||||
return filename.strip('.' + string.whitespace)
|
||||
|
||||
|
||||
def find_mount_point(directory):
|
||||
|
@ -1519,10 +1482,10 @@ def find_mount_point(directory):
|
|||
>>> find_mount_point('/')
|
||||
'/'
|
||||
|
||||
>>> find_mount_point(u'/something')
|
||||
>>> find_mount_point(b'/something')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: Convert unicode objects to str first.
|
||||
ValueError: Convert bytes objects to str first.
|
||||
|
||||
>>> find_mount_point(None)
|
||||
Traceback (most recent call last):
|
||||
|
@ -1573,15 +1536,14 @@ def find_mount_point(directory):
|
|||
'/media/usbdisk'
|
||||
>>> restore()
|
||||
"""
|
||||
if isinstance(directory, unicode):
|
||||
# XXX: This is only valid for Python 2 - misleading error in Python 3?
|
||||
# We do not accept unicode strings, because they could fail when
|
||||
if isinstance(directory, bytes):
|
||||
# We do not accept byte strings, because they could fail when
|
||||
# trying to be converted to some native encoding, so fail loudly
|
||||
# and leave it up to the callee to encode into the proper encoding.
|
||||
raise ValueError('Convert unicode objects to str first.')
|
||||
# and leave it up to the callee to decode from the proper encoding.
|
||||
raise ValueError('Convert bytes objects to str first.')
|
||||
|
||||
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
|
||||
# os.path work with unicode str in Python 3, but not in Python 2.
|
||||
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):
|
||||
"""Check if a command line command/program exists"""
|
||||
# Prior to Python 2.7.3, this module (shlex) did not support Unicode input.
|
||||
cmd = sanitize_encoding(cmd)
|
||||
program = shlex.split(cmd)[0]
|
||||
return (find_command(program) is not None)
|
||||
|
||||
|
@ -1818,7 +1779,7 @@ def linux_get_active_interfaces():
|
|||
"""
|
||||
process = subprocess.Popen(['ip', 'link'], stdout=subprocess.PIPE)
|
||||
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':
|
||||
yield interface
|
||||
|
||||
|
@ -1832,7 +1793,7 @@ def osx_get_active_interfaces():
|
|||
"""
|
||||
process = subprocess.Popen(['ifconfig'], stdout=subprocess.PIPE)
|
||||
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)
|
||||
if b:
|
||||
yield b.group(1)
|
||||
|
@ -1846,7 +1807,7 @@ def unix_get_active_interfaces():
|
|||
"""
|
||||
process = subprocess.Popen(['ifconfig'], stdout=subprocess.PIPE)
|
||||
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)
|
||||
if b:
|
||||
yield b.group(1)
|
||||
|
@ -1885,7 +1846,7 @@ def connection_available():
|
|||
return not offline
|
||||
|
||||
return False
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
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)
|
||||
return True
|
||||
|
@ -1900,9 +1861,9 @@ def website_reachable(url):
|
|||
return (False, None)
|
||||
|
||||
try:
|
||||
response = urllib2.urlopen(url, timeout=1)
|
||||
response = urllib.request.urlopen(url, timeout=1)
|
||||
return (True, response)
|
||||
except urllib2.URLError as err:
|
||||
except urllib.error.URLError as err:
|
||||
pass
|
||||
|
||||
return (False, None)
|
||||
|
|
|
@ -32,12 +32,7 @@ from gpodder import util
|
|||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
# 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 json
|
||||
|
||||
import re
|
||||
|
||||
|
@ -64,7 +59,7 @@ def get_real_download_url(url, preferred_fileformat=None):
|
|||
def get_urls(data_config_url):
|
||||
data_config_data = util.urlopen(data_config_url).read().decode('utf-8')
|
||||
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):
|
||||
continue
|
||||
|
||||
|
|
|
@ -30,20 +30,12 @@ import os.path
|
|||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
import json
|
||||
import json
|
||||
|
||||
import re
|
||||
import urllib
|
||||
import urllib.request, urllib.parse, urllib.error
|
||||
|
||||
try:
|
||||
# Python >= 2.6
|
||||
from urlparse import parse_qs
|
||||
except ImportError:
|
||||
# Python < 2.6
|
||||
from cgi import parse_qs
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
# http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs
|
||||
# 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):
|
||||
r4 = re.search('url_encoded_fmt_stream_map=([^&]+)', page)
|
||||
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(','):
|
||||
video_info = parse_qs(fmt_url_encoded)
|
||||
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):
|
||||
try:
|
||||
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.
|
||||
m = re.search('<link rel="image_src"[^>]* href=[\'"]([^\'"]+)[\'"][^>]*>', data)
|
||||
if m is None:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
# create-desktop-icon.py: Create a Desktop icon
|
||||
# 2016-12-22 Thomas Perl <m@thp.io>
|
||||
|
||||
|
@ -19,11 +19,11 @@ Type=Application
|
|||
DESTINATION = os.path.expanduser('~/Desktop/gpodder-git.desktop')
|
||||
|
||||
if os.path.exists(DESTINATION):
|
||||
print '%(DESTINATION)s already exists, not overwriting'
|
||||
print('%(DESTINATION)s already exists, not overwriting')
|
||||
sys.exit(1)
|
||||
|
||||
with open(DESTINATION, 'w') as fp:
|
||||
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 '.'
|
||||
main_module = open(os.path.join(here, '../src/gpodder/__init__.py')).read()
|
||||
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
|
||||
# Thomas Perl <thp@gpodder.org>, 2009-01-03
|
||||
#
|
||||
|
@ -22,13 +22,13 @@ class Language(object):
|
|||
self.untranslated = int(untranslated)
|
||||
|
||||
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):
|
||||
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):
|
||||
return float(self.untranslated)/float(self.translated+self.fuzzy+self.untranslated)
|
||||
return self.untranslated/(self.translated+self.fuzzy+self.untranslated)
|
||||
|
||||
def __cmp__(self, other):
|
||||
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()
|
||||
languages.append(Language(language, match[1] or '0', match[3] or '0', match[5] or '0'))
|
||||
|
||||
print ''
|
||||
print('')
|
||||
for language in sorted(languages):
|
||||
tc = '#'*(int(math.floor(width*language.get_translated_ratio())))
|
||||
fc = '~'*(int(math.floor(width*language.get_fuzzy_ratio())))
|
||||
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
|
||||
""" % (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
|
||||
#
|
||||
|
@ -8,10 +8,10 @@
|
|||
# Thomas Perl <thp.io/about>; 2012-02-11
|
||||
#
|
||||
|
||||
import urllib2
|
||||
import urllib.request, urllib.error, urllib.parse
|
||||
import re
|
||||
import sys
|
||||
import StringIO
|
||||
import io
|
||||
import tarfile
|
||||
import os
|
||||
import shutil
|
||||
|
@ -30,33 +30,33 @@ MODULES = [
|
|||
|
||||
def get_tarball_url(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)
|
||||
return match.group(0) if match is not None else None
|
||||
|
||||
for module, required_files in MODULES:
|
||||
print 'Fetching', module, '...',
|
||||
print('Fetching', module, '...', end=' ')
|
||||
tarball_url = get_tarball_url(module)
|
||||
if tarball_url is None:
|
||||
print 'Cannot determine download URL for', module, '- aborting!'
|
||||
print('Cannot determine download URL for', module, '- aborting!')
|
||||
break
|
||||
data = urllib2.urlopen(tarball_url).read()
|
||||
print '%d KiB' % (len(data)/1024)
|
||||
tar = tarfile.open(fileobj=StringIO.StringIO(data))
|
||||
data = urllib.request.urlopen(tarball_url).read()
|
||||
print('%d KiB' % (len(data)//1024))
|
||||
tar = tarfile.open(fileobj=io.BytesIO(data))
|
||||
for name in tar.getnames():
|
||||
match = re.match(required_files, name)
|
||||
if match is not None:
|
||||
target_name = match.group(1)
|
||||
target_file = os.path.join(src_dir, target_name)
|
||||
if os.path.exists(target_file):
|
||||
print 'Skipping:', target_file
|
||||
print('Skipping:', target_file)
|
||||
continue
|
||||
|
||||
target_dir = os.path.dirname(target_file)
|
||||
if not os.path.isdir(target_dir):
|
||||
os.mkdir(target_dir)
|
||||
|
||||
print 'Extracting:', target_name
|
||||
print('Extracting:', target_name)
|
||||
tar.extract(name, tmp_dir)
|
||||
shutil.move(os.path.join(tmp_dir, name), target_file)
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/python
|
||||
#!/usr/bin/python3
|
||||
# Progressbar icon tester
|
||||
# Thomas Perl <thp.io/about>; 2012-02-05
|
||||
#
|
||||
|
@ -8,26 +8,26 @@
|
|||
import sys
|
||||
sys.path.insert(0, 'src')
|
||||
|
||||
import gtk
|
||||
from gi.repository import Gtk
|
||||
|
||||
from gpodder.gtkui.draw import draw_cake_pixbuf
|
||||
|
||||
def gen(percentage):
|
||||
pixbuf = draw_cake_pixbuf(percentage)
|
||||
return gtk.image_new_from_pixbuf(pixbuf)
|
||||
return Gtk.Image.new_from_pixbuf(pixbuf)
|
||||
|
||||
w = gtk.Window()
|
||||
w.connect('destroy', gtk.main_quit)
|
||||
v = gtk.VBox()
|
||||
w = Gtk.Window()
|
||||
w.connect('destroy', Gtk.main_quit)
|
||||
v = Gtk.VBox()
|
||||
w.add(v)
|
||||
for y in xrange(1):
|
||||
h = gtk.HBox()
|
||||
for y in range(1):
|
||||
h = Gtk.HBox()
|
||||
h.set_homogeneous(True)
|
||||
v.add(h)
|
||||
PARTS = 20
|
||||
for x in xrange(PARTS + 1):
|
||||
h.add(gen(float(x)/float(PARTS)))
|
||||
for x in range(PARTS + 1):
|
||||
h.add(gen(x/PARTS))
|
||||
w.set_default_size(400, 100)
|
||||
w.show_all()
|
||||
gtk.main()
|
||||
Gtk.main()
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
#!/usr/bin/python
|
||||
#!/usr/bin/python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Simple HTTP web server for testing HTTP Authentication (see bug 1539)
|
||||
# from our crappy-but-does-the-job department
|
||||
# Thomas Perl <thp.io/about>; 2012-01-20
|
||||
|
||||
import BaseHTTPServer
|
||||
import http.server
|
||||
import sys
|
||||
import re
|
||||
import hashlib
|
||||
|
@ -47,7 +47,7 @@ def mkrss(items=EP_COUNT):
|
|||
type="%(EPISODES_MIME)s"
|
||||
length="%(SIZE)s"/>
|
||||
</item>
|
||||
""" % dict(locals().items()+globals().items())
|
||||
""" % dict(list(locals().items())+list(globals().items()))
|
||||
for INDEX, PUBDATE in enumerate(mkpubdates(items)))
|
||||
|
||||
return """
|
||||
|
@ -56,13 +56,13 @@ def mkrss(items=EP_COUNT):
|
|||
%(ITEMS)s
|
||||
</channel>
|
||||
</rss>
|
||||
""" % dict(locals().items()+globals().items())
|
||||
""" % dict(list(locals().items())+list(globals().items()))
|
||||
|
||||
def mkdata(size=SIZE):
|
||||
"""Generate dummy data of a given size (in bytes)"""
|
||||
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
|
||||
EPISODES_PATH = '/%s' % EPISODES
|
||||
|
||||
|
@ -77,21 +77,21 @@ class AuthRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
|||
auth_data = m.group(1).decode('base64').split(':', 1)
|
||||
if len(auth_data) == 2:
|
||||
username, password = auth_data
|
||||
print 'Got username:', username
|
||||
print 'Got password:', password
|
||||
print('Got username:', username)
|
||||
print('Got password:', password)
|
||||
if (username, password) == (USERNAME, PASSWORD):
|
||||
print 'Valid credentials provided.'
|
||||
print('Valid credentials provided.')
|
||||
authorized = True
|
||||
|
||||
if self.path == self.FEEDFILE_PATH:
|
||||
print 'Feed request.'
|
||||
print('Feed request.')
|
||||
is_feed = True
|
||||
elif self.path.startswith(self.EPISODES_PATH):
|
||||
print 'Episode request.'
|
||||
print('Episode request.')
|
||||
is_episode = True
|
||||
|
||||
if not authorized:
|
||||
print 'Not authorized - sending WWW-Authenticate header.'
|
||||
print('Not authorized - sending WWW-Authenticate header.')
|
||||
self.send_response(401)
|
||||
self.send_header('WWW-Authenticate',
|
||||
'Basic realm="%s"' % sys.argv[0])
|
||||
|
@ -108,12 +108,12 @@ class AuthRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
|||
|
||||
|
||||
if __name__ == '__main__':
|
||||
httpd = BaseHTTPServer.HTTPServer((HOST, PORT), AuthRequestHandler)
|
||||
print """
|
||||
httpd = http.server.HTTPServer((HOST, PORT), AuthRequestHandler)
|
||||
print("""
|
||||
Feed URL: %(URL)s/%(FEEDFILE)s
|
||||
Username: %(USERNAME)s
|
||||
Password: %(PASSWORD)s
|
||||
""" % locals()
|
||||
""" % locals())
|
||||
while True:
|
||||
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