Merge branch 'gtk3'

This commit is contained in:
Thomas Perl 2017-12-29 16:05:41 +01:00
commit 66a354d9d1
101 changed files with 2677 additions and 3490 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
*.pyc
__pycache__
src/podcastparser.py
src/mygpoclient
src/dbus

View file

@ -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

View file

@ -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
View file

@ -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
View 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
View file

@ -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='')

View file

@ -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', '') == '':

View file

@ -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!')

View file

@ -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
##########################################################################

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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):

View file

@ -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

View file

@ -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)

View file

@ -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:

View file

@ -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:

View file

@ -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)

View file

@ -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()

View file

@ -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()

View file

@ -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):

View file

@ -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)

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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">&lt;b&gt;HTTP/FTP Authentication&lt;/b&gt;</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">&lt;b&gt;HTTP/FTP Authentication&lt;/b&gt;</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">&lt;b&gt;Locations&lt;/b&gt;</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">&lt;b&gt;Locations&lt;/b&gt;</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>

View file

@ -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">

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View 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">&lt;Primary&gt;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">&lt;Primary&gt;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">&lt;Primary&gt;r</attribute>
</item>
<item>
<attribute name="action">win.downloadAllNew</attribute>
<attribute name="label" translatable="yes">Download new episodes</attribute>
<attribute name="accel">&lt;Primary&gt;n</attribute>
</item>
<item>
<attribute name="action">win.removeOldEpisodes</attribute>
<attribute name="label" translatable="yes">Delete episodes</attribute>
<attribute name="accel">&lt;Primary&gt;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">&lt;Primary&gt;f</attribute>
</item>
<item>
<attribute name="action">win.addChannel</attribute>
<attribute name="label" translatable="yes">Add podcast via URL</attribute>
<attribute name="accel">&lt;Primary&gt;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">&lt;Shift&gt;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">&lt;Primary&gt;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">&lt;Primary&gt;t</attribute>
</item>
<item>
<attribute name="action">win.showEpisodeDescription</attribute>
<attribute name="label" translatable="yes">Episode descriptions</attribute>
<attribute name="accel">&lt;Primary&gt;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">&lt;Primary&gt;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">&lt;Primary&gt;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">&lt;Primary&gt;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">&lt;Primary&gt;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: -->

View file

@ -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)

View file

@ -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')

View file

@ -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)

View file

@ -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

View file

@ -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()

View file

@ -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'))]

View file

@ -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),)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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):
"""

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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:

View file

@ -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':

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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"""

View file

@ -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)

View file

@ -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):

View file

@ -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):

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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 " ")

View file

@ -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:

View file

@ -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
}

View file

@ -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))

View file

@ -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)

View file

@ -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

View file

@ -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'):

View file

@ -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)

View file

@ -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'))

View file

@ -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):

View file

@ -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'],

View file

@ -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),)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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:

View file

@ -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())

View file

@ -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'])

View file

@ -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)))

View file

@ -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)

View 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()

View file

@ -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()

View file

@ -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.

View file

@ -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,
&regKey) != 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, &regKey) != 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);
}

View file

@ -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.

View file

@ -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;
}

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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