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 dist: trusty
sudo: required sudo: required
python: python:
- "2.7" - "3.5"
install: install:
- sudo apt-get update -q - sudo apt-get update -q
- sudo apt-get install intltool desktop-file-utils wine mingw-w64-i686-dev binutils-mingw-w64-i686 gcc-mingw-w64 xvfb - sudo apt-get install intltool desktop-file-utils
- pip install coverage minimock - pip3 install coverage minimock
- python tools/localdepends.py - python3 tools/localdepends.py
script: script:
- make releasetest - make releasetest
- make -C tools/win32-setup
- make -C tools/win32-portable
deploy:
provider: releases
api_key:
secure: "huPoTQRwhXZVD45JSBnCgtrzofpcotXShBWk9FYH2MOFwXQHRbp2ueaD0rxQxHNBTBXQDnOX+OQMnh99peYlxB1bPAx6LUMBgtesxvsUc3T5m7yZvqXyDBhjIBycYwxG0fBrnxEokaJKQDnZ4S/cKmk766iwhyGr66s+l9UBD/Y="
file:
- tools/win32-setup/gpodder-*-setup.exe
- tools/win32-portable/gpodder-*-win32.zip
file_glob: true
on:
tags: true
skip_cleanup: true

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 share *
recursive-include po * recursive-include po *
recursive-include tools * 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 -*- # -*- coding: utf-8 -*-
# #
@ -63,7 +63,7 @@
""" """
from __future__ import print_function
import sys import sys
import collections import collections
@ -142,38 +142,6 @@ def incolor(color_id, s):
return '\033[9%dm%s\033[0m' % (color_id, s) return '\033[9%dm%s\033[0m' % (color_id, s)
return s return s
def safe_print(*args, **kwargs):
def convert(arg):
return unicode(util.convert_bytes(arg))
ofile = kwargs.get('file', sys.stdout)
output = u' '.join(map(convert, args))
if ofile.encoding is None:
output = util.sanitize_encoding(output)
else:
output = output.encode(ofile.encoding, 'replace')
try:
ofile.write(output)
except Exception, e:
print("""
*** ENCODING FAIL ***
Please report this to https://github.com/gpodder/gpodder/issues:
args = %s
map(convert, args) = %s
Exception = %s
""" % (repr(args), repr(map(convert, args)), e))
ofile.write(kwargs.get('end', os.linesep))
ofile.flush()
# On Python 3 the encoding insanity is gone, so our safe_print()
# function simply becomes the normal print() function. Good stuff!
if sys.version_info >= (3,):
safe_print = print
# ANSI Colors: red = 1, green = 2, yellow = 3, blue = 4 # ANSI Colors: red = 1, green = 2, yellow = 3, blue = 4
inred, ingreen, inyellow, inblue = (functools.partial(incolor, x) inred, ingreen, inyellow, inblue = (functools.partial(incolor, x)
@ -241,7 +209,7 @@ class gPodderCli(object):
# Generator for all prefixes of a given string (longest first) # Generator for all prefixes of a given string (longest first)
# e.g. ['gpodder', 'gpodde', 'gpodd', 'gpod', 'gpo', 'gp', 'g'] # e.g. ['gpodder', 'gpodde', 'gpodd', 'gpod', 'gpo', 'gp', 'g']
mkprefixes = lambda n: (n[:x] for x in xrange(len(n), 0, -1)) mkprefixes = lambda n: (n[:x] for x in range(len(n), 0, -1))
# Return True if the given prefix is unique in "names" # Return True if the given prefix is unique in "names"
is_unique = lambda p: len([n for n in names if n.startswith(p)]) == 1 is_unique = lambda p: len([n for n in names if n.startswith(p)]) == 1
@ -276,13 +244,13 @@ class gPodderCli(object):
else: else:
line = line + (' '*(self.COLUMNS-7-len(line))) line = line + (' '*(self.COLUMNS-7-len(line)))
self._current_action = line self._current_action = line
safe_print(self._current_action, end='') print(self._current_action, end='')
def _update_action(self, progress): def _update_action(self, progress):
if have_ansi: if have_ansi:
progress = '%3.0f%%' % (progress*100.,) progress = '%3.0f%%' % (progress*100.,)
result = '['+inblue(progress)+']' result = '['+inblue(progress)+']'
safe_print('\r' + self._current_action + result, end='') print('\r' + self._current_action + result, end='')
def _finish_action(self, success=True, skip=False): def _finish_action(self, success=True, skip=False):
if skip: if skip:
@ -293,9 +261,9 @@ class gPodderCli(object):
result = '['+inred('FAIL')+']' result = '['+inred('FAIL')+']'
if have_ansi: if have_ansi:
safe_print('\r' + self._current_action + result) print('\r' + self._current_action + result)
else: else:
safe_print(result) print(result)
self._current_action = '' self._current_action = ''
def _atexit(self): def _atexit(self):
@ -354,7 +322,7 @@ class gPodderCli(object):
if title is not None: if title is not None:
podcast.rename(title) podcast.rename(title)
podcast.save() podcast.save()
except Exception, e: except Exception as e:
logger.warn('Cannot subscribe: %s', e, exc_info=True) logger.warn('Cannot subscribe: %s', e, exc_info=True)
if hasattr(e, 'strerror'): if hasattr(e, 'strerror'):
self._error(e.strerror) self._error(e.strerror)
@ -371,7 +339,7 @@ class gPodderCli(object):
for key in self._config.all_keys(): for key in self._config.all_keys():
if search_for is None or search_for.lower() in key.lower(): if search_for is None or search_for.lower() in key.lower():
value = config_value_to_string(self._config._lookup(key)) value = config_value_to_string(self._config._lookup(key))
safe_print(key, '=', value) print(key, '=', value)
def set(self, key=None, value=None): def set(self, key=None, value=None):
if value is None: if value is None:
@ -427,17 +395,17 @@ class gPodderCli(object):
def status_str(episode): def status_str(episode):
# is new # is new
if self.is_episode_new(episode): if self.is_episode_new(episode):
return u' * ' return ' * '
# is downloaded # is downloaded
if (episode.state == gpodder.STATE_DOWNLOADED): if (episode.state == gpodder.STATE_DOWNLOADED):
return u' ▉ ' return ' ▉ '
# is deleted # is deleted
if (episode.state == gpodder.STATE_DELETED): if (episode.state == gpodder.STATE_DELETED):
return u' ░ ' return ' ░ '
return u' ' return ' '
episodes = (u'%3d. %s %s' % (i+1, status_str(e), e.title) episodes = ('%3d. %s %s' % (i+1, status_str(e), e.title)
for i, e in enumerate(podcast.get_all_episodes())) for i, e in enumerate(podcast.get_all_episodes()))
return episodes return episodes
@ -456,8 +424,8 @@ class gPodderCli(object):
title, url, status = podcast.title, podcast.url, \ title, url, status = podcast.title, podcast.url, \
feed_update_status_msg(podcast) feed_update_status_msg(podcast)
episodes = self._episodesList(podcast) episodes = self._episodesList(podcast)
episodes = u'\n '.join(episodes) episodes = '\n '.join(episodes)
self._pager(u""" self._pager("""
Title: %(title)s Title: %(title)s
URL: %(url)s URL: %(url)s
Feed update is %(status)s Feed update is %(status)s
@ -475,24 +443,24 @@ class gPodderCli(object):
podcast_printed = False podcast_printed = False
if url is None or podcast.url == url: if url is None or podcast.url == url:
episodes = self._episodesList(podcast) episodes = self._episodesList(podcast)
episodes = u'\n '.join(episodes) episodes = '\n '.join(episodes)
output.append(u""" output.append("""
Episodes from %s: Episodes from %s:
%s %s
""" % (podcast.url, episodes)) """ % (podcast.url, episodes))
self._pager(u'\n'.join(output)) self._pager('\n'.join(output))
return True return True
def list(self): def list(self):
for podcast in self._model.get_podcasts(): for podcast in self._model.get_podcasts():
if not podcast.pause_subscription: if not podcast.pause_subscription:
safe_print('#', ingreen(podcast.title)) print('#', ingreen(podcast.title))
else: else:
safe_print('#', inred(podcast.title), print('#', inred(podcast.title),
'-', _('Updates disabled')) '-', _('Updates disabled'))
safe_print(podcast.url) print(podcast.url)
return True return True
@ -507,7 +475,7 @@ class gPodderCli(object):
@FirstArgumentIsPodcastURL @FirstArgumentIsPodcastURL
def update(self, url=None): def update(self, url=None):
count = 0 count = 0
safe_print(_('Checking for new episodes')) print(_('Checking for new episodes'))
for podcast in self._model.get_podcasts(): for podcast in self._model.get_podcasts():
if url is not None and podcast.url != url: if url is not None and podcast.url != url:
continue continue
@ -521,7 +489,7 @@ class gPodderCli(object):
self._finish_action(skip=True) self._finish_action(skip=True)
util.delete_empty_folders(gpodder.downloads) util.delete_empty_folders(gpodder.downloads)
safe_print(inblue(self._pending_message(count))) print(inblue(self._pending_message(count)))
return True return True
@FirstArgumentIsPodcastURL @FirstArgumentIsPodcastURL
@ -533,13 +501,13 @@ class gPodderCli(object):
for episode in podcast.get_all_episodes(): for episode in podcast.get_all_episodes():
if self.is_episode_new(episode): if self.is_episode_new(episode):
if not podcast_printed: if not podcast_printed:
safe_print('#', ingreen(podcast.title)) print('#', ingreen(podcast.title))
podcast_printed = True podcast_printed = True
safe_print(' ', episode.title) print(' ', episode.title)
count += 1 count += 1
util.delete_empty_folders(gpodder.downloads) util.delete_empty_folders(gpodder.downloads)
safe_print(inblue(self._pending_message(count))) print(inblue(self._pending_message(count)))
return True return True
def _download_episode(self, episode): def _download_episode(self, episode):
@ -565,12 +533,12 @@ class gPodderCli(object):
last_podcast = None last_podcast = None
for episode in episodes: for episode in episodes:
if episode.channel != last_podcast: if episode.channel != last_podcast:
safe_print(inblue(episode.channel.title)) print(inblue(episode.channel.title))
last_podcast = episode.channel last_podcast = episode.channel
self._download_episode(episode) self._download_episode(episode)
util.delete_empty_folders(gpodder.downloads) util.delete_empty_folders(gpodder.downloads)
safe_print(len(episodes), 'episodes downloaded.') print(len(episodes), 'episodes downloaded.')
return True return True
@FirstArgumentIsPodcastURL @FirstArgumentIsPodcastURL
@ -606,7 +574,7 @@ class gPodderCli(object):
def youtube(self, url): def youtube(self, url):
fmt_ids = youtube.get_fmt_ids(self._config.youtube) fmt_ids = youtube.get_fmt_ids(self._config.youtube)
yurl = youtube.get_real_download_url(url, fmt_ids) yurl = youtube.get_real_download_url(url, fmt_ids)
safe_print(yurl) print(yurl)
return True return True
@ -672,11 +640,11 @@ class gPodderCli(object):
return return
if not interactive_console or is_single_command: if not interactive_console or is_single_command:
safe_print('\n'.join(url for title, url in results)) print('\n'.join(url for title, url in results))
return return
def show_list(): def show_list():
self._pager('\n'.join(u'%3d: %s\n %s' % self._pager('\n'.join('%3d: %s\n %s' %
(index+1, title, url if title != url else '') (index+1, title, url if title != url else '')
for index, (title, url) in enumerate(results))) for index, (title, url) in enumerate(results)))
@ -684,7 +652,7 @@ class gPodderCli(object):
msg = _('Enter index to subscribe, ? for list') msg = _('Enter index to subscribe, ? for list')
while True: while True:
index = raw_input(msg + ': ') index = input(msg + ': ')
if not index: if not index:
return return
@ -728,7 +696,7 @@ class gPodderCli(object):
return True return True
def help(self): def help(self):
safe_print(stylize(__doc__), file=sys.stderr, end='') print(stylize(__doc__), file=sys.stderr, end='')
return True return True
# ------------------------------------------------------------------- # -------------------------------------------------------------------
@ -739,14 +707,14 @@ class gPodderCli(object):
rows_needed = len(output.splitlines()) + 2 rows_needed = len(output.splitlines()) + 2
rows, cols = get_terminal_size() rows, cols = get_terminal_size()
if rows_needed < rows: if rows_needed < rows:
safe_print(output) print(output)
else: else:
pydoc.pager(util.sanitize_encoding(output)) pydoc.pager(output)
else: else:
safe_print(output) print(output)
def _shell(self): def _shell(self):
safe_print(os.linesep.join(x.strip() for x in (""" print(os.linesep.join(x.strip() for x in ("""
gPodder %(__version__)s (%(__date__)s) - %(__url__)s gPodder %(__version__)s (%(__date__)s) - %(__url__)s
%(__copyright__)s %(__copyright__)s
License: %(__license__)s License: %(__license__)s
@ -764,12 +732,12 @@ class gPodderCli(object):
while True: while True:
try: try:
line = raw_input('gpo> ') line = input('gpo> ')
except EOFError: except EOFError:
safe_print('') print('')
break break
except KeyboardInterrupt: except KeyboardInterrupt:
safe_print('') print('')
continue continue
if self._prefixes.get(line, line) in self.EXIT_COMMANDS: if self._prefixes.get(line, line) in self.EXIT_COMMANDS:
@ -777,7 +745,7 @@ class gPodderCli(object):
try: try:
args = shlex.split(line) args = shlex.split(line)
except ValueError, value_error: except ValueError as value_error:
self._error(_('Syntax error: %(error)s') % self._error(_('Syntax error: %(error)s') %
{'error': value_error}) {'error': value_error})
continue continue
@ -792,13 +760,13 @@ class gPodderCli(object):
self._atexit() self._atexit()
def _error(self, *args): def _error(self, *args):
safe_print(inred(' '.join(args)), file=sys.stderr) print(inred(' '.join(args)), file=sys.stderr)
# Warnings look like error messages for now # Warnings look like error messages for now
_warn = _error _warn = _error
def _info(self, *args): def _info(self, *args):
safe_print(*args) print(*args)
def _checkargs(self, func, command_line): def _checkargs(self, func, command_line):
args, varargs, keywords, defaults = inspect.getargspec(func) args, varargs, keywords, defaults = inspect.getargspec(func)
@ -872,9 +840,9 @@ class gPodderCli(object):
return self._checkargs(func, command_line) return self._checkargs(func, command_line)
if command in self._expansions: if command in self._expansions:
safe_print(_('Ambiguous command. Did you mean..')) print(_('Ambiguous command. Did you mean..'))
for cmd in self._expansions[command]: for cmd in self._expansions[command]:
safe_print(' ', inblue(cmd)) print(' ', inblue(cmd))
else: else:
self._error(_('The requested function is not available.')) self._error(_('The requested function is not available.'))
@ -897,5 +865,5 @@ if __name__ == '__main__':
elif interactive_console: elif interactive_console:
cli._shell() cli._shell()
else: else:
safe_print(__doc__, end='') print(__doc__, end='')

View file

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
@ -43,9 +43,9 @@ try:
import dbus.glib import dbus.glib
have_dbus = True have_dbus = True
except ImportError: except ImportError:
print >>sys.stderr, """ print("""
Warning: python-dbus not found. Disabling D-Bus support. Warning: python-dbus not found. Disabling D-Bus support.
""" """, file=sys.stderr)
have_dbus = False have_dbus = False
from optparse import OptionParser from optparse import OptionParser
@ -73,9 +73,9 @@ def main():
process = subprocess.Popen(locale_cmd, stdout=subprocess.PIPE) process = subprocess.Popen(locale_cmd, stdout=subprocess.PIPE)
output, error_output = process.communicate() output, error_output = process.communicate()
# the output is a string like 'fr_FR', and we need 'fr_FR.utf-8' # the output is a string like 'fr_FR', and we need 'fr_FR.utf-8'
user_locale = output.strip() + '.UTF-8' user_locale = output.decode('utf-8').strip() + '.UTF-8'
os.environ['LANG'] = user_locale os.environ['LANG'] = user_locale
print >>sys.stderr, 'Setting locale to', user_locale print('Setting locale to', user_locale, file=sys.stderr)
# Set up the path to translation files # Set up the path to translation files
gettext.bindtextdomain('gpodder', locale_dir) gettext.bindtextdomain('gpodder', locale_dir)
@ -112,7 +112,7 @@ def main():
options, args = parser.parse_args(sys.argv) options, args = parser.parse_args(sys.argv)
gpodder.ui.gtk = True gpodder.ui.gtk = True
gpodder.ui.python2 = True gpodder.ui.python3 = True
gpodder.ui.unity = (os.environ.get('DESKTOP_SESSION', 'unknown').lower() in gpodder.ui.unity = (os.environ.get('DESKTOP_SESSION', 'unknown').lower() in
('ubuntu', 'ubuntu-2d')) ('ubuntu', 'ubuntu-2d'))
@ -140,7 +140,7 @@ def main():
remote_object.subscribe_to_url(options.subscribe) remote_object.subscribe_to_url(options.subscribe)
return return
except dbus.exceptions.DBusException, dbus_exception: except dbus.exceptions.DBusException as dbus_exception:
logger.info('Cannot connect to remote object.', exc_info=True) logger.info('Cannot connect to remote object.', exc_info=True)
if not gpodder.ui.win32 and os.environ.get('DISPLAY', '') == '': if not gpodder.ui.win32 and os.environ.get('DISPLAY', '') == '':

View file

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
@ -27,7 +27,7 @@
import sys import sys
import os import os
import re import re
import ConfigParser import configparser
import shutil import shutil
gpodder_script = sys.argv[0] gpodder_script = sys.argv[0]
@ -56,25 +56,25 @@ old_config = os.path.expanduser('~/.config/gpodder/gpodder.conf')
new_config = gpodder.config_file new_config = gpodder.config_file
if not os.path.exists(old_database): if not os.path.exists(old_database):
print >>sys.stderr, """ print("""
Turns out that you never ran gPodder 2. Turns out that you never ran gPodder 2.
Can't find this required file: Can't find this required file:
%(old_database)s %(old_database)s
""" % locals() """ % locals(), file=sys.stderr)
sys.exit(1) sys.exit(1)
old_downloads = None old_downloads = None
if os.path.exists(old_config): if os.path.exists(old_config):
parser = ConfigParser.RawConfigParser() parser = configparser.RawConfigParser()
parser.read(old_config) parser.read(old_config)
try: try:
old_downloads = parser.get('gpodder-conf-1', 'download_dir') old_downloads = parser.get('gpodder-conf-1', 'download_dir')
except ConfigParser.NoSectionError: except configparser.NoSectionError:
# The file is empty / section (gpodder-conf-1) not found # The file is empty / section (gpodder-conf-1) not found
pass pass
except ConfigParser.NoOptionError: except configparser.NoOptionError:
# The section is available, but the key (download_dir) is not # The section is available, but the key (download_dir) is not
pass pass
@ -87,22 +87,22 @@ if old_downloads is None:
new_downloads = gpodder.downloads new_downloads = gpodder.downloads
if not os.path.exists(old_downloads): if not os.path.exists(old_downloads):
print >>sys.stderr, """ print("""
Old download directory does not exist. Creating empty one. Old download directory does not exist. Creating empty one.
""" """, file=sys.stderr)
os.makedirs(old_downloads) os.makedirs(old_downloads)
if any(os.path.exists(x) for x in (new_database, new_downloads)): if any(os.path.exists(x) for x in (new_database, new_downloads)):
print >>sys.stderr, """ print("""
Existing gPodder 3 user data found. Existing gPodder 3 user data found.
To continue, please remove: To continue, please remove:
%(new_database)s %(new_database)s
%(new_downloads)s %(new_downloads)s
""" % locals() """ % locals(), file=sys.stderr)
sys.exit(1) sys.exit(1)
print >>sys.stderr, """ print("""
Would carry out the following actions: Would carry out the following actions:
Move downloads from %(old_downloads)s Move downloads from %(old_downloads)s
@ -111,21 +111,21 @@ print >>sys.stderr, """
Convert database from %(old_database)s Convert database from %(old_database)s
to %(new_database)s to %(new_database)s
""" % locals() """ % locals(), file=sys.stderr)
result = raw_input('Continue? (Y/n) ') result = input('Continue? (Y/n) ')
if result in 'Yy': if result in 'Yy':
util.make_directory(gpodder.home) util.make_directory(gpodder.home)
schema.convert_gpodder2_db(old_database, new_database) schema.convert_gpodder2_db(old_database, new_database)
if not os.path.exists(new_database): if not os.path.exists(new_database):
print >>sys.stderr, 'Could not convert database.' print('Could not convert database.', file=sys.stderr)
sys.exit(1) sys.exit(1)
shutil.move(old_downloads, new_downloads) shutil.move(old_downloads, new_downloads)
if not os.path.exists(new_downloads): if not os.path.exists(new_downloads):
print >>sys.stderr, 'Could not move downloads.' print('Could not move downloads.', file=sys.stderr)
sys.exit(1) sys.exit(1)
print 'Done. Have fun with gPodder 3!' print('Done. Have fun with gPodder 3!')

View file

@ -50,7 +50,7 @@ GETTEXT_SOURCE += $(DESKTOP_FILES_IN_H)
DESTDIR ?= / DESTDIR ?= /
PREFIX ?= /usr PREFIX ?= /usr
PYTHON ?= python PYTHON ?= python3
HELP2MAN ?= help2man HELP2MAN ?= help2man
########################################################################## ##########################################################################
@ -84,12 +84,6 @@ $(GPODDER_SERVICE_FILE): $(GPODDER_SERVICE_FILE_IN)
install: messages $(GPODDER_SERVICE_FILE) $(DESKTOP_FILES) install: messages $(GPODDER_SERVICE_FILE) $(DESKTOP_FILES)
$(PYTHON) setup.py install --root=$(DESTDIR) --prefix=$(PREFIX) --optimize=1 $(PYTHON) setup.py install --root=$(DESTDIR) --prefix=$(PREFIX) --optimize=1
release-win32:
$(MAKE) -C tools/win32-setup
cp tools/win32-setup/gpodder-*-setup.exe .
$(MAKE) -C tools/win32-portable
cp tools/win32-portable/gpodder-*-win32.zip .
########################################################################## ##########################################################################
manpage: $(MANPAGE) manpage: $(MANPAGE)
@ -137,16 +131,13 @@ clean:
rm -f $(GPODDER_SERVICE_FILE) rm -f $(GPODDER_SERVICE_FILE)
rm -f $(DESKTOP_FILES) $(DESKTOP_FILES_IN_H) rm -f $(DESKTOP_FILES) $(DESKTOP_FILES_IN_H)
rm -rf build $(LOCALEDIR) rm -rf build $(LOCALEDIR)
rm -f gpodder-*-win32.zip gpodder-*-setup.exe
distclean: clean distclean: clean
rm -rf dist rm -rf dist
-$(MAKE) -C tools/win32-portable distclean
-$(MAKE) -C tools/win32-setup distclean
########################################################################## ##########################################################################
.PHONY: help unittest release releasetest install manpage clean distclean messages headlink release-win32 .PHONY: help unittest release releasetest install manpage clean distclean messages headlink
########################################################################## ##########################################################################

View file

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# #
# gPodder - A media aggregator and podcast client # gPodder - A media aggregator and podcast client
@ -37,7 +37,7 @@ author, email = re.match(r'^(.*) <(.*)>$', metadata['author']).groups()
class MissingFile(BaseException): pass class MissingFile(BaseException): pass
def info(message, item=None): def info(message, item=None):
print '=>', message, item if item is not None else '' print('=>', message, item if item is not None else '')
def find_data_files(uis, scripts): def find_data_files(uis, scripts):
@ -94,7 +94,7 @@ def find_data_files(uis, scripts):
if not result: if not result:
info('Skipping manpage without script:', filename) info('Skipping manpage without script:', filename)
return result return result
filenames = filter(have_script, filenames) filenames = list(filter(have_script, filenames))
def convert_filename(filename): def convert_filename(filename):
filename = os.path.join(dirpath, filename) filename = os.path.join(dirpath, filename)
@ -112,7 +112,7 @@ def find_data_files(uis, scripts):
return filename return filename
filenames = filter(None, map(convert_filename, filenames)) filenames = [_f for _f in map(convert_filename, filenames) if _f]
if filenames: if filenames:
# Some distros/ports install manpages into $PREFIX/man instead # Some distros/ports install manpages into $PREFIX/man instead
# of $PREFIX/share/man (e.g. FreeBSD). To allow this, we strip # of $PREFIX/share/man (e.g. FreeBSD). To allow this, we strip
@ -137,10 +137,10 @@ def find_packages(uis):
package = '.'.join(dirparts) package = '.'.join(dirparts)
# Extract all parts of the package name ending in "ui" # Extract all parts of the package name ending in "ui"
ui_parts = filter(lambda p: p.endswith('ui'), dirparts) ui_parts = [p for p in dirparts if p.endswith('ui')]
if uis is not None and ui_parts: if uis is not None and ui_parts:
# Strip the trailing "ui", e.g. "gtkui" -> "gtk" # Strip the trailing "ui", e.g. "gtkui" -> "gtk"
folder_uis = map(lambda p: p[:-2], ui_parts) folder_uis = [p[:-2] for p in ui_parts]
for folder_ui in folder_uis: for folder_ui in folder_uis:
if folder_ui not in uis: if folder_ui not in uis:
info('Skipping package:', package) info('Skipping package:', package)
@ -181,13 +181,13 @@ try:
packages = list(sorted(find_packages(uis))) packages = list(sorted(find_packages(uis)))
scripts = list(sorted(find_scripts(uis))) scripts = list(sorted(find_scripts(uis)))
data_files = list(sorted(find_data_files(uis, scripts))) data_files = list(sorted(find_data_files(uis, scripts)))
except MissingFile, mf: except MissingFile as mf:
print >>sys.stderr, """ print("""
Missing file: %s Missing file: %s
If you want to install, use "make install" instead of using If you want to install, use "make install" instead of using
setup.py directly. See the README file for more information. setup.py directly. See the README file for more information.
""" % mf.message """ % mf.message, file=sys.stderr)
sys.exit(1) sys.exit(1)

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 # Example script that can be used as post-play extension in media players
# #
# Set the configuration options "audio_played_dbus" and "video_played_dbus" # Set the configuration options "audio_played_dbus" and "video_played_dbus"
@ -16,9 +16,9 @@ import sys
import os import os
if len(sys.argv) != 2: if len(sys.argv) != 2:
print >>sys.stderr, """ print("""
Usage: %s /path/to/episode.mp3 Usage: %s /path/to/episode.mp3
""" % (sys.argv[0],) """ % (sys.argv[0],), file=sys.stderr)
sys.exit(1) sys.exit(1)
filename = os.path.abspath(sys.argv[1]) filename = os.path.abspath(sys.argv[1])
@ -32,6 +32,6 @@ proxy = session_bus.get_object(gpodder.dbus_bus_name, \
interface = dbus.Interface(proxy, gpodder.dbus_interface) interface = dbus.Interface(proxy, gpodder.dbus_interface)
if not interface.mark_episode_played(filename): if not interface.mark_episode_played(filename):
print >>sys.stderr, 'Warning: Could not mark episode as played.' print('Warning: Could not mark episode as played.', file=sys.stderr)
sys.exit(2) sys.exit(2)

View file

@ -21,15 +21,15 @@ class gPodderExtension:
# into various parts of gPodder. # into various parts of gPodder.
def on_load(self): def on_load(self):
logger.info('Extension is being loaded.') logger.info('Extension is being loaded.')
print '='*40 print('='*40)
print 'container:', self.container print('container:', self.container)
print 'container.manager:', self.container.manager print('container.manager:', self.container.manager)
print 'container.config:', self.container.config print('container.config:', self.container.config)
print 'container.manager.core:', self.container.manager.core print('container.manager.core:', self.container.manager.core)
print 'container.manager.core.db:', self.container.manager.core.db print('container.manager.core.db:', self.container.manager.core.db)
print 'container.manager.core.config:', self.container.manager.core.config print('container.manager.core.config:', self.container.manager.core.config)
print 'container.manager.core.model:', self.container.manager.core.model print('container.manager.core.model:', self.container.manager.core.model)
print '='*40 print('='*40)
# This function will be called when the extension is disabled or # This function will be called when the extension is disabled or
# when gPodder shuts down. You can use this to destroy/delete any # when gPodder shuts down. You can use this to destroy/delete any
@ -49,5 +49,4 @@ class gPodderExtension:
return [("Say Hello", self.say_hello_cb)] return [("Say Hello", self.say_hello_cb)]
def say_hello_cb(self): def say_hello_cb(self):
print("HELLO")
self.gpodder.notification("Hello Extension", "Message", widget=self.gpodder.main_window) self.gpodder.notification("Hello Extension", "Message", widget=self.gpodder.main_window)

View file

@ -8,7 +8,7 @@ import subprocess
import gpodder import gpodder
from gpodder import util from gpodder import util
import gtk from gi.repository import Gtk
from gpodder.gtkui.interface.progress import ProgressIndicator from gpodder.gtkui.interface.progress import ProgressIndicator
import os import os
@ -34,13 +34,13 @@ class gPodderExtension:
self.gpodder = ui_object self.gpodder = ui_object
def _get_save_filename(self): def _get_save_filename(self):
dlg = gtk.FileChooserDialog(title=_('Save video'), dlg = Gtk.FileChooserDialog(title=_('Save video'),
parent=self.gpodder.get_dialog_parent(), parent=self.gpodder.get_dialog_parent(),
action=gtk.FILE_CHOOSER_ACTION_SAVE) action=Gtk.FileChooserAction.SAVE)
dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) dlg.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK) dlg.add_button(Gtk.STOCK_SAVE, Gtk.ResponseType.OK)
if dlg.run() == gtk.RESPONSE_OK: if dlg.run() == Gtk.ResponseType.OK:
filename = dlg.get_filename() filename = dlg.get_filename()
dlg.destroy() dlg.destroy()
return filename return filename

View file

@ -262,7 +262,7 @@ class gPodderExtension:
self.config = container.config self.config = container.config
# Only display media players that can be found at extension load time # Only display media players that can be found at extension load time
self.players = [p for p in PLAYERS if p.is_installed()] self.players = [player for player in PLAYERS if player.is_installed()]
self.resumers = [r for r in RESUMERS if r.is_installed()] self.resumers = [r for r in RESUMERS if r.is_installed()]
def on_ui_object_available(self, name, ui_object): def on_ui_object_available(self, name, ui_object):

View file

@ -14,10 +14,10 @@ _ = gpodder.gettext
__title__ = _('Gtk Status Icon') __title__ = _('Gtk Status Icon')
__description__ = _('Show a status icon for Gtk-based Desktops.') __description__ = _('Show a status icon for Gtk-based Desktops.')
__category__ = 'desktop-integration' __category__ = 'desktop-integration'
__only_for__ = 'gtk,python2' __only_for__ = 'gtk'
__disable_in__ = 'unity,win32' __disable_in__ = 'unity,win32,python3'
import gtk from gi.repository import Gtk
import os.path import os.path
from gpodder.gtkui import draw from gpodder.gtkui import draw
@ -39,7 +39,7 @@ class gPodderExtension:
path = os.path.join(os.path.dirname(__file__), '..', '..', 'icons') path = os.path.join(os.path.dirname(__file__), '..', '..', 'icons')
icon_path = os.path.abspath(path) icon_path = os.path.abspath(path)
theme = gtk.icon_theme_get_default() theme = Gtk.IconTheme.get_default()
theme.append_search_path(icon_path) theme.append_search_path(icon_path)
if self.icon_name is None: if self.icon_name is None:
@ -49,11 +49,11 @@ class gPodderExtension:
self.icon_name = 'stock_mic' self.icon_name = 'stock_mic'
if self.status_icon is None: if self.status_icon is None:
self.status_icon = gtk.status_icon_new_from_icon_name(self.icon_name) self.status_icon = Gtk.status_icon_new_from_icon_name(self.icon_name)
return return
# If current mode matches desired mode, nothing to do. # If current mode matches desired mode, nothing to do.
is_pixbuf = (self.status_icon.get_storage_type() == gtk.IMAGE_PIXBUF) is_pixbuf = (self.status_icon.get_storage_type() == Gtk.ImageType.PIXBUF)
if is_pixbuf == use_pixbuf: if is_pixbuf == use_pixbuf:
return return
@ -63,7 +63,7 @@ class gPodderExtension:
# Currently icon is not a pixbuf => was loaded by name, at which # Currently icon is not a pixbuf => was loaded by name, at which
# point size was automatically determined. # point size was automatically determined.
icon_size = self.status_icon.get_size() icon_size = self.status_icon.get_size()
icon_pixbuf = theme.load_icon(self.icon_name, icon_size, gtk.ICON_LOOKUP_USE_BUILTIN) icon_pixbuf = theme.load_icon(self.icon_name, icon_size, Gtk.IconLookupFlags.USE_BUILTIN)
self.status_icon.set_from_pixbuf(icon_pixbuf) self.status_icon.set_from_pixbuf(icon_pixbuf)
def on_load(self): def on_load(self):
@ -91,7 +91,7 @@ class gPodderExtension:
def get_icon_pixbuf(self): def get_icon_pixbuf(self):
assert self.status_icon is not None assert self.status_icon is not None
if self.status_icon.get_storage_type() != gtk.IMAGE_PIXBUF: if self.status_icon.get_storage_type() != Gtk.ImageType.PIXBUF:
self.set_icon(use_pixbuf=True) self.set_icon(use_pixbuf=True)
return self.status_icon.get_pixbuf() return self.status_icon.get_pixbuf()
@ -117,7 +117,7 @@ class gPodderExtension:
icon = self.get_icon_pixbuf().copy() icon = self.get_icon_pixbuf().copy()
progressbar = draw.progressbar_pixbuf(icon.get_width(), icon.get_height(), progress) progressbar = draw.progressbar_pixbuf(icon.get_width(), icon.get_height(), progress)
progressbar.composite(icon, 0, 0, icon.get_width(), icon.get_height(), 0, 0, 1, 1, gtk.gdk.INTERP_NEAREST, 255) progressbar.composite(icon, 0, 0, icon.get_width(), icon.get_height(), 0, 0, 1, 1, GdkPixbuf.InterpType.NEAREST, 255)
self.status_icon.set_from_pixbuf(icon) self.status_icon.set_from_pixbuf(icon)
self.last_progress = progress self.last_progress = progress

View file

@ -24,8 +24,8 @@ import dbus.service
import gpodder import gpodder
import logging import logging
import time import time
import urllib import urllib.request, urllib.parse, urllib.error
import urlparse import urllib.parse
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_ = gpodder.gettext _ = gpodder.gettext
@ -43,7 +43,7 @@ TrackInfo = collections.namedtuple('TrackInfo',
['uri', 'length', 'status', 'pos', 'rate']) ['uri', 'length', 'status', 'pos', 'rate'])
def subsecond_difference(usec1, usec2): def subsecond_difference(usec1, usec2):
return abs(usec1 - usec2) < USECS_IN_SEC return usec1 is not None and usec2 is not None and abs(usec1 - usec2) < USECS_IN_SEC
class CurrentTrackTracker(object): class CurrentTrackTracker(object):
'''An instance of this class is responsible for tracking the state of the '''An instance of this class is responsible for tracking the state of the
@ -117,7 +117,7 @@ class CurrentTrackTracker(object):
# If the status *is* playing, and *was* playing, but the position # If the status *is* playing, and *was* playing, but the position
# has changed discontinuously, notify a stop for the old position # has changed discontinuously, notify a stop for the old position
if ( cur['status'] == 'Playing' if ( cur['status'] == 'Playing'
and (not kwargs.has_key('status') or kwargs['status'] == 'Playing') and ('status' not in kwargs or kwargs['status'] == 'Playing')
and not subsecond_difference(cur['pos'], kwargs['pos']) and not subsecond_difference(cur['pos'], kwargs['pos'])
): ):
logger.debug('notify Stopped: playback discontinuity:' + logger.debug('notify Stopped: playback discontinuity:' +
@ -125,6 +125,7 @@ class CurrentTrackTracker(object):
self.notify_stop() self.notify_stop()
if ( (kwargs['pos']) == 0 if ( (kwargs['pos']) == 0
and self.pos is not None
and self.pos > (self.length - USECS_IN_SEC) and self.pos > (self.length - USECS_IN_SEC)
and self.pos < (self.length + 2 * USECS_IN_SEC) and self.pos < (self.length + 2 * USECS_IN_SEC)
): ):
@ -176,7 +177,7 @@ class CurrentTrackTracker(object):
): ):
return return
pos = self.pos // USECS_IN_SEC pos = self.pos // USECS_IN_SEC
file_uri = urllib.url2pathname(urlparse.urlparse(self.uri).path).encode('utf-8') file_uri = urllib.request.url2pathname(urllib.parse.urlparse(self.uri).path).encode('utf-8')
total_time = self.length // USECS_IN_SEC total_time = self.length // USECS_IN_SEC
if status == 'Stopped': if status == 'Stopped':
@ -200,8 +201,8 @@ class CurrentTrackTracker(object):
return '%s: %s at %d/%d (@%f)' % ( return '%s: %s at %d/%d (@%f)' % (
self.uri or 'None', self.uri or 'None',
self.status or 'None', self.status or 'None',
(self.pos or 0) / USECS_IN_SEC, (self.pos or 0) // USECS_IN_SEC,
(self.length or 0) / USECS_IN_SEC, (self.length or 0) // USECS_IN_SEC,
self.rate or 0) self.rate or 0)
class MPRISDBusReceiver(object): class MPRISDBusReceiver(object):
@ -246,23 +247,23 @@ class MPRISDBusReceiver(object):
invalidated_properties, path=None): invalidated_properties, path=None):
if interface_name != self.INTERFACE_MPRIS: if interface_name != self.INTERFACE_MPRIS:
if interface_name not in self.OTHER_MPRIS_INTERFACES: if interface_name not in self.OTHER_MPRIS_INTERFACES:
logger.warn('unexpected interface: %s, props=%r', interface_name, changed_properties.keys()) logger.warn('unexpected interface: %s, props=%r', interface_name, list(changed_properties.keys()))
return return
collected_info = {} collected_info = {}
if changed_properties.has_key('PlaybackStatus'): if 'PlaybackStatus' in changed_properties:
collected_info['status'] = str(changed_properties['PlaybackStatus']) collected_info['status'] = str(changed_properties['PlaybackStatus'])
if changed_properties.has_key('Metadata'): if 'Metadata' in changed_properties:
# on stop there is no xesam:url # on stop there is no xesam:url
if changed_properties['Metadata'].has_key('xesam:url'): if 'xesam:url' in changed_properties['Metadata']:
collected_info['uri'] = changed_properties['Metadata']['xesam:url'] collected_info['uri'] = changed_properties['Metadata']['xesam:url']
collected_info['length'] = changed_properties['Metadata']['mpris:length'] collected_info['length'] = changed_properties['Metadata']['mpris:length']
if changed_properties.has_key('Rate'): if 'Rate' in changed_properties:
collected_info['rate'] = changed_properties['Rate'] collected_info['rate'] = changed_properties['Rate']
collected_info['pos'] = self.query_position() collected_info['pos'] = self.query_position()
if not collected_info.has_key('status'): if 'status' not in collected_info:
collected_info['status'] = str(self.query_status()) collected_info['status'] = str(self.query_status())
logger.debug('collected info: %r', collected_info) logger.debug('collected info: %r', collected_info)

View file

@ -32,9 +32,14 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
try: try:
import pynotify import gi
gi.require_version('Notify', '0.7')
from gi.repository import Notify
pynotify = True
except ImportError: except ImportError:
pynotify = None pynotify = None
except ValueError:
pynotify = None
if pynotify is None: if pynotify is None:
@ -47,16 +52,16 @@ else:
self.container = container self.container = container
def on_load(self): def on_load(self):
pynotify.init('gPodder') Notify.init('gPodder')
def on_unload(self): def on_unload(self):
pynotify.uninit() Notify.uninit()
def on_notification_show(self, title, message): def on_notification_show(self, title, message):
if not message and not title: if not message and not title:
return return
notify = pynotify.Notification(title or '', message or '', notify = Notify.Notification.new(title or '', message or '',
gpodder.icon_file) gpodder.icon_file)
try: try:

View file

@ -48,7 +48,7 @@ class gPodderExtension:
basename, ext = os.path.splitext(filename) basename, ext = os.path.splitext(filename)
new_basename = [] new_basename = []
new_basename.append(util.sanitize_encoding(title) + ext) new_basename.append(title + ext)
if self.config.add_podcast_title: if self.config.add_podcast_title:
new_basename.insert(0, podcast_title) new_basename.insert(0, podcast_title)
if self.config.add_sortdate: if self.config.add_sortdate:
@ -56,8 +56,7 @@ class gPodderExtension:
new_basename = ' - '.join(new_basename) new_basename = ' - '.join(new_basename)
# On Windows, force ASCII encoding for filenames (bug 1724) # On Windows, force ASCII encoding for filenames (bug 1724)
new_basename = util.sanitize_filename(new_basename, new_basename = util.sanitize_filename(new_basename)
use_ascii=gpodder.ui.win32)
new_filename = os.path.join(dirname, new_basename) new_filename = os.path.join(dirname, new_basename)
if new_filename == current_filename: if new_filename == current_filename:

View file

@ -94,6 +94,6 @@ class gPodderExtension:
if found: if found:
logger.info('Removed cover art from OGG file: %s', filename) logger.info('Removed cover art from OGG file: %s', filename)
ogg.save() ogg.save()
except Exception, e: except Exception as e:
logger.warn('Failed to remove OGG cover: %s', e, exc_info=True) logger.warn('Failed to remove OGG cover: %s', e, exc_info=True)

View file

@ -88,8 +88,8 @@ class gPodderExtension:
if video_height is None: if video_height is None:
return None return None
width_ratio = device_width / video_width width_ratio = device_width // video_width
height_ratio = device_height / video_height height_ratio = device_height // video_height
dest_width = device_width dest_width = device_width
dest_height = width_ratio * video_height dest_height = width_ratio * video_height
@ -134,9 +134,6 @@ class gPodderExtension:
'options': self.container.config.ffmpeg_options 'options': self.container.config.ffmpeg_options
} }
# Prior to Python 2.7.3, this module (shlex) did not support Unicode input.
convert_command = util.sanitize_encoding(convert_command)
process = subprocess.Popen(shlex.split(convert_command), process = subprocess.Popen(shlex.split(convert_command),
stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = process.communicate() stdout, stderr = process.communicate()

View file

@ -162,7 +162,7 @@ class Mp3File(AudioFile):
encoding = 3, # 3 is for utf-8 encoding = 3, # 3 is for utf-8
mime = mimetypes.guess_type(self.cover)[0], mime = mimetypes.guess_type(self.cover)[0],
type = 3, type = 3,
desc = u'Cover', desc = 'Cover',
data = open(self.cover).read() data = open(self.cover).read()
) )
) )
@ -257,7 +257,7 @@ class gPodderExtension:
if self.container.config.auto_embed_coverart: if self.container.config.auto_embed_coverart:
audio.insert_coverart() audio.insert_coverart()
logger.info(u'tagging.on_episode_downloaded(%s/%s)', episode.channel.title, episode.title) logger.info('tagging.on_episode_downloaded(%s/%s)', episode.channel.title, episode.title)
def get_cover(self, podcast): def get_cover(self, podcast):
downloader = coverart.CoverDownloader() downloader = coverart.CoverDownloader()

View file

@ -58,7 +58,7 @@ class gPodderExtension(object):
def get_data_from_url(self, url): def get_data_from_url(self, url):
try: try:
response = util.urlopen(url).read() response = util.urlopen(url).read()
except Exception, e: except Exception as e:
logger.warn("subtitle url returned error %s", e) logger.warn("subtitle url returned error %s", e)
return '' return ''
return response return response
@ -93,7 +93,7 @@ class gPodderExtension(object):
intro = episode_data.split('introDuration":')[1] \ intro = episode_data.split('introDuration":')[1] \
.split(',')[0] or INTRO_DEFAULT .split(',')[0] or INTRO_DEFAULT
intro = int(float(intro)*1000) intro = int(float(intro)*1000)
except (ValueError, IndexError), e: except (ValueError, IndexError) as e:
logger.info("Couldn't parse introDuration string: %s", intro) logger.info("Couldn't parse introDuration string: %s", intro)
intro = INTRO_DEFAULT * 1000 intro = INTRO_DEFAULT * 1000
current_filename = episode.local_filename(create=False) current_filename = episode.local_filename(create=False)
@ -103,7 +103,7 @@ class gPodderExtension(object):
try: try:
with open(srt_filename, 'w+') as srtFile: with open(srt_filename, 'w+') as srtFile:
srtFile.write(sub.encode("utf-8")) srtFile.write(sub.encode("utf-8"))
except Exception, e: except Exception as e:
logger.warn("Can't write srt file: %s",e) logger.warn("Can't write srt file: %s",e)
def on_episode_delete(self, episode, filename): def on_episode_delete(self, episode, filename):

View file

@ -17,7 +17,7 @@ __disable_in__ = 'win32'
import appindicator import appindicator
import gtk from gi.repository import Gtk
import logging import logging
@ -43,8 +43,8 @@ class gPodderExtension:
self.indicator.set_status(appindicator.STATUS_ACTIVE) self.indicator.set_status(appindicator.STATUS_ACTIVE)
def _rebuild_menu(self): def _rebuild_menu(self):
menu = gtk.Menu() menu = Gtk.Menu()
toggle_visible = gtk.CheckMenuItem(_('Show main window')) toggle_visible = Gtk.CheckMenuItem(_('Show main window'))
toggle_visible.set_active(True) toggle_visible.set_active(True)
def on_toggle_visible(menu_item): def on_toggle_visible(menu_item):
if menu_item.get_active(): if menu_item.get_active():
@ -53,8 +53,8 @@ class gPodderExtension:
self.gpodder.main_window.hide() self.gpodder.main_window.hide()
toggle_visible.connect('activate', on_toggle_visible) toggle_visible.connect('activate', on_toggle_visible)
menu.append(toggle_visible) menu.append(toggle_visible)
menu.append(gtk.SeparatorMenuItem()) menu.append(Gtk.SeparatorMenuItem())
quit_gpodder = gtk.MenuItem(_('Quit')) quit_gpodder = Gtk.MenuItem(_('Quit'))
def on_quit(menu_item): def on_quit(menu_item):
self.gpodder.on_gPodder_delete_event(self.gpodder.main_window) self.gpodder.on_gPodder_delete_event(self.gpodder.main_window)
quit_gpodder.connect('activate', on_quit) quit_gpodder.connect('activate', on_quit)

View file

@ -53,7 +53,7 @@ if __name__ != '__main__':
try: try:
self.process.stdin.write('progress %f\n' % progress) self.process.stdin.write('progress %f\n' % progress)
self.process.stdin.flush() self.process.stdin.flush()
except Exception, e: except Exception as e:
logger.debug('Ubuntu progress update failed.', exc_info=True) logger.debug('Ubuntu progress update failed.', exc_info=True)
else: else:
from gi.repository import Unity, GObject from gi.repository import Unity, GObject

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--*- mode: xml -*--> <!--*- mode: xml -*-->
<interface> <interface>
<!-- interface-requires gtk+ 3.10 -->
<object class="GtkAdjustment" id="adjustment1"> <object class="GtkAdjustment" id="adjustment1">
<property name="upper">10240</property> <property name="upper">10240</property>
<property name="lower">0.5</property> <property name="lower">0.5</property>
@ -15,389 +16,8 @@
<property name="step_increment">1</property> <property name="step_increment">1</property>
<property name="page_size">0</property> <property name="page_size">0</property>
</object> </object>
<object class="GtkUIManager" id="uimanager1"> <object class="GtkApplicationWindow" id="gPodder">
<child> <property name="application">app</property>
<object class="GtkActionGroup" id="actiongroup1">
<child>
<object class="GtkAction" id="menuPodcasts">
<property name="name">menuPodcasts</property>
<property name="label" translatable="yes">_Podcasts</property>
</object>
</child>
<child>
<object class="GtkAction" id="itemUpdate">
<property name="stock_id">gtk-refresh</property>
<property name="name">itemUpdate</property>
<property name="label" translatable="yes">Check for new episodes</property>
<signal handler="on_itemUpdate_activate" name="activate"/>
</object>
<accelerator key="R" modifiers="GDK_CONTROL_MASK"/>
</child>
<child>
<object class="GtkAction" id="itemDownloadAllNew">
<property name="stock_id">gtk-goto-bottom</property>
<property name="name">itemDownloadAllNew</property>
<property name="label" translatable="yes">Download new episodes</property>
<signal handler="on_itemDownloadAllNew_activate" name="activate"/>
</object>
<accelerator key="N" modifiers="GDK_CONTROL_MASK"/>
</child>
<child>
<object class="GtkAction" id="itemRemoveOldEpisodes">
<property name="stock_id">gtk-delete</property>
<property name="name">itemRemoveOldEpisodes</property>
<property name="label" translatable="yes">Delete episodes</property>
<signal handler="on_itemRemoveOldEpisodes_activate" name="activate"/>
</object>
<accelerator key="K" modifiers="GDK_CONTROL_MASK"/>
</child>
<child>
<object class="GtkAction" id="itemPreferences">
<property name="stock_id">gtk-preferences</property>
<property name="name">itemPreferences</property>
<property name="label" translatable="yes">Preferences</property>
<signal handler="on_itemPreferences_activate" name="activate"/>
</object>
<accelerator key="P" modifiers="GDK_CONTROL_MASK"/>
</child>
<child>
<object class="GtkAction" id="itemQuit">
<property name="stock_id">gtk-quit</property>
<property name="name">itemQuit</property>
<property name="label" translatable="yes">Quit</property>
<signal handler="on_gPodder_delete_event" name="activate"/>
</object>
<accelerator key="Q" modifiers="GDK_CONTROL_MASK"/>
</child>
<child>
<object class="GtkAction" id="menuSubscriptions">
<property name="name">menuSubscriptions</property>
<property name="label" translatable="yes">_Subscriptions</property>
</object>
</child>
<child>
<object class="GtkAction" id="itemFind">
<property name="stock_id">gtk-find</property>
<property name="name">itemFind</property>
<property name="label" translatable="yes">Discover new podcasts</property>
<signal handler="on_itemImportChannels_activate" name="activate"/>
</object>
<accelerator key="F" modifiers="GDK_CONTROL_MASK"/>
</child>
<child>
<object class="GtkAction" id="itemAddChannel">
<property name="stock_id">gtk-add</property>
<property name="name">itemAddChannel</property>
<property name="label" translatable="yes">Add podcast via URL</property>
<signal handler="on_itemAddChannel_activate" name="activate"/>
</object>
<accelerator key="L" modifiers="GDK_CONTROL_MASK"/>
</child>
<child>
<object class="GtkAction" id="itemEditChannel">
<property name="stock_id">gtk-edit</property>
<property name="name">itemEditChannel</property>
<property name="label" translatable="yes">Podcast settings</property>
<signal handler="on_itemEditChannel_activate" name="activate"/>
</object>
</child>
<child>
<object class="GtkAction" id="itemRemoveChannel">
<property name="stock_id">gtk-remove</property>
<property name="name">itemRemoveChannel</property>
<property name="label" translatable="yes">Unsubscribe</property>
<signal handler="on_itemRemoveChannel_activate" name="activate"/>
</object>
</child>
<child>
<object class="GtkAction" id="itemMassUnsubscribe">
<property name="stock_id">gtk-remove</property>
<property name="label" translatable="yes">Remove podcasts</property>
<signal handler="on_itemMassUnsubscribe_activate" name="activate"/>
</object>
</child>
<child>
<object class="GtkAction" id="itemUpdateChannel">
<property name="stock_id">gtk-refresh</property>
<property name="name">itemUpdateChannel</property>
<property name="label" translatable="yes">Update podcast</property>
<signal handler="on_itemUpdateChannel_activate" name="activate"/>
</object>
</child>
<child>
<object class="GtkAction" id="item_import_from_file">
<property name="stock_id">gtk-open</property>
<property name="name">item_import_from_file</property>
<property name="label" translatable="yes">Import from OPML file</property>
<signal handler="on_item_import_from_file_activate" name="activate"/>
</object>
</child>
<child>
<object class="GtkAction" id="itemExportChannels">
<property name="stock_id">gtk-save-as</property>
<property name="name">itemExportChannels</property>
<property name="label" translatable="yes">Export to OPML file</property>
<signal handler="on_itemExportChannels_activate" name="activate"/>
</object>
</child>
<child>
<object class="GtkAction" id="item_goto_mygpo">
<property name="label" translatable="yes">Go to gpodder.net</property>
<signal handler="on_goto_mygpo" name="activate"/>
</object>
</child>
<child>
<object class="GtkAction" id="menuChannels">
<property name="name">menuChannels</property>
<property name="label" translatable="yes">_Episodes</property>
</object>
</child>
<child>
<object class="GtkAction" id="itemPlaySelected">
<property name="stock_id">gtk-media-play</property>
<property name="name">itemPlaySelected</property>
<property name="label" translatable="yes">Play</property>
<signal handler="on_playback_selected_episodes" name="activate"/>
</object>
<accelerator key="Return" modifiers="GDK_SHIFT_MASK"/>
</child>
<child>
<object class="GtkAction" id="itemOpenSelected">
<property name="stock_id">gtk-open</property>
<property name="name">itemOpenSelected</property>
<property name="label" translatable="yes">Open</property>
<signal handler="on_playback_selected_episodes" name="activate"/>
</object>
</child>
<child>
<object class="GtkAction" id="itemDownloadSelected">
<property name="stock_id">gtk-goto-bottom</property>
<property name="name">itemDownloadSelected</property>
<property name="label" translatable="yes">Download</property>
<signal handler="on_download_selected_episodes" name="activate"/>
</object>
</child>
<child>
<object class="GtkAction" id="item_cancel_download">
<property name="stock_id">gtk-stop</property>
<property name="name">item_cancel_download</property>
<property name="label" translatable="yes">Cancel</property>
<signal handler="on_item_cancel_download_activate" name="activate"/>
</object>
</child>
<child>
<object class="GtkAction" id="itemDeleteSelected">
<property name="stock_id">gtk-delete</property>
<property name="name">itemDeleteSelected</property>
<property name="label" translatable="yes">Delete</property>
<signal handler="on_btnDownloadedDelete_clicked" name="activate"/>
</object>
<accelerator key="Delete" modifiers="0"/>
</child>
<child>
<object class="GtkAction" id="item_toggle_played">
<property name="stock_id">gtk-apply</property>
<property name="name">item_toggle_played</property>
<property name="label" translatable="yes">Toggle new status</property>
<signal handler="on_item_toggle_played_activate" name="activate"/>
</object>
</child>
<child>
<object class="GtkAction" id="item_toggle_lock">
<property name="stock_id">gtk-dialog-authentication</property>
<property name="name">item_toggle_lock</property>
<property name="label" translatable="yes">Change delete lock</property>
<signal handler="on_item_toggle_lock_activate" name="activate"/>
</object>
</child>
<child>
<object class="GtkAction" id="item_episode_details">
<property name="stock_id">gtk-info</property>
<property name="name">item_episode_details</property>
<property name="label" translatable="yes">Episode details</property>
<signal handler="on_shownotes_selected_episodes" name="activate"/>
</object>
</child>
<child>
<object class="GtkAction" id="menuExtras">
<property name="name">menuExtras</property>
<property name="label" translatable="yes">E_xtras</property>
</object>
</child>
<child>
<object class="GtkAction" id="item_sync">
<property name="stock_id">gtk-refresh</property>
<property name="name">item_sync</property>
<property name="label" translatable="yes">Sync to device</property>
<signal handler="on_sync_to_device_activate" name="activate"/>
</object>
<accelerator key="S" modifiers="GDK_CONTROL_MASK"/>
</child>
<child>
<object class="GtkAction" id="item_update_youtube_subscriptions">
<property name="name">item_update_youtube_subscriptions</property>
<property name="label" translatable="yes">Update YouTube subscriptions</property>
<signal handler="on_update_youtube_subscriptions_activate" name="activate"/>
</object>
</child>
<child>
<object class="GtkAction" id="menuView">
<property name="name">menuView</property>
<property name="label" translatable="yes">_View</property>
</object>
</child>
<child>
<object class="GtkToggleAction" id="itemShowToolbar">
<property name="active">True</property>
<property name="name">itemShowToolbar</property>
<property name="label" translatable="yes">Toolbar</property>
<signal handler="on_itemShowToolbar_activate" name="activate"/>
</object>
<accelerator key="T" modifiers="GDK_CONTROL_MASK"/>
</child>
<child>
<object class="GtkToggleAction" id="itemShowDescription">
<property name="active">True</property>
<property name="name">itemShowDescription</property>
<property name="label" translatable="yes">Episode descriptions</property>
<signal handler="on_itemShowDescription_activate" name="activate"/>
</object>
<accelerator key="D" modifiers="GDK_CONTROL_MASK"/>
</child>
<child>
<object class="GtkRadioAction" id="item_view_episodes_all">
<property name="name">item_view_episodes_all</property>
<property name="label" translatable="yes">All episodes</property>
<signal handler="on_item_view_episodes_changed" name="changed"/>
</object>
<accelerator key="0" modifiers="GDK_CONTROL_MASK"/>
</child>
<child>
<object class="GtkRadioAction" id="item_view_episodes_undeleted">
<property name="active">True</property>
<property name="group">item_view_episodes_all</property>
<property name="name">item_view_episodes_undeleted</property>
<property name="label" translatable="yes">Hide deleted episodes</property>
<signal handler="on_item_view_episodes_changed" name="changed"/>
</object>
<accelerator key="1" modifiers="GDK_CONTROL_MASK"/>
</child>
<child>
<object class="GtkRadioAction" id="item_view_episodes_downloaded">
<property name="group">item_view_episodes_all</property>
<property name="name">item_view_episodes_downloaded</property>
<property name="label" translatable="yes">Downloaded episodes</property>
<signal handler="on_item_view_episodes_changed" name="changed"/>
</object>
<accelerator key="2" modifiers="GDK_CONTROL_MASK"/>
</child>
<child>
<object class="GtkRadioAction" id="item_view_episodes_unplayed">
<property name="group">item_view_episodes_all</property>
<property name="name">item_view_episodes_unplayed</property>
<property name="label" translatable="yes">Unplayed episodes</property>
<signal handler="on_item_view_episodes_changed" name="changed"/>
</object>
<accelerator key="3" modifiers="GDK_CONTROL_MASK"/>
</child>
<child>
<object class="GtkToggleAction" id="item_view_hide_boring_podcasts">
<property name="active">False</property>
<property name="name">item_view_hide_boring_podcasts</property>
<property name="label" translatable="yes">Hide podcasts without episodes</property>
<signal handler="on_item_view_hide_boring_podcasts_toggled" name="toggled"/>
</object>
</child>
<child>
<object class="GtkAction" id="menuHelp">
<property name="name">menuHelp</property>
<property name="label" translatable="yes">_Help</property>
</object>
</child>
<child>
<object class="GtkAction" id="help">
<property name="stock_id">gtk-help</property>
<property name="name">help</property>
<property name="label" translatable="yes">User manual</property>
<signal handler="on_help_activate" name="activate"/>
</object>
</child>
<child>
<object class="GtkAction" id="item_check_for_updates">
<property name="name">item_check_for_updates</property>
<property name="label" translatable="yes">Software updates</property>
<signal handler="on_check_for_updates_activate" name="activate"/>
</object>
</child>
<child>
<object class="GtkAction" id="itemAbout">
<property name="stock_id">gtk-about</property>
<property name="name">itemAbout</property>
<signal handler="on_itemAbout_activate" name="activate"/>
</object>
</child>
</object>
</child>
<ui>
<menubar name="mainMenu">
<menu action="menuPodcasts">
<menuitem action="itemUpdate"/>
<menuitem action="itemDownloadAllNew"/>
<menuitem action="itemRemoveOldEpisodes"/>
<separator/>
<menuitem action="itemPreferences"/>
<separator/>
<menuitem action="itemQuit"/>
</menu>
<menu action="menuSubscriptions">
<menuitem action="itemFind"/>
<menuitem action="itemAddChannel"/>
<menuitem action="itemMassUnsubscribe"/>
<separator/>
<menuitem action="itemUpdateChannel"/>
<menuitem action="itemEditChannel"/>
<separator/>
<menuitem action="item_import_from_file"/>
<menuitem action="itemExportChannels"/>
</menu>
<menu action="menuChannels">
<menuitem action="itemPlaySelected"/>
<menuitem action="itemOpenSelected"/>
<menuitem action="itemDownloadSelected"/>
<menuitem action="item_cancel_download"/>
<menuitem action="itemDeleteSelected"/>
<separator/>
<menuitem action="item_toggle_played"/>
<menuitem action="item_toggle_lock"/>
<separator/>
<menuitem action="item_episode_details"/>
</menu>
<menu action="menuExtras">
<menuitem action="item_sync"/>
<menuitem action="item_update_youtube_subscriptions"/>
</menu>
<menu action="menuView">
<menuitem action="itemShowToolbar"/>
<menuitem action="itemShowDescription"/>
<separator/>
<menuitem action="item_view_episodes_all"/>
<menuitem action="item_view_episodes_undeleted"/>
<menuitem action="item_view_episodes_downloaded"/>
<menuitem action="item_view_episodes_unplayed"/>
<separator/>
<menuitem action="item_view_hide_boring_podcasts"/>
</menu>
<menu action="menuHelp">
<menuitem action="help"/>
<menuitem action="item_goto_mygpo"/>
<menuitem action="item_check_for_updates"/>
<separator/>
<menuitem action="itemAbout"/>
</menu>
</menubar>
</ui>
</object>
<object class="GtkWindow" id="gPodder">
<property name="visible">False</property> <property name="visible">False</property>
<property name="title">gPodder</property> <property name="title">gPodder</property>
<property name="window_position">GTK_WIN_POS_CENTER</property> <property name="window_position">GTK_WIN_POS_CENTER</property>
@ -408,23 +28,11 @@
<property name="type_hint">GDK_WINDOW_TYPE_HINT_NORMAL</property> <property name="type_hint">GDK_WINDOW_TYPE_HINT_NORMAL</property>
<property name="focus_on_map">True</property> <property name="focus_on_map">True</property>
<property name="urgency_hint">False</property> <property name="urgency_hint">False</property>
<signal handler="on_gPodder_delete_event" name="delete_event"/> <signal handler="on_gPodder_delete_event" name="delete-event"/>
<child> <child>
<object class="GtkVBox" id="vMain"> <object class="GtkGrid" id="vMain">
<property name="visible">True</property> <property name="visible">True</property>
<property name="homogeneous">False</property> <property name="orientation">vertical</property>
<child>
<object class="GtkMenuBar" constructor="uimanager1" id="mainMenu">
<property name="visible">True</property>
<property name="pack_direction">GTK_PACK_DIRECTION_LTR</property>
<property name="child_pack_direction">GTK_PACK_DIRECTION_LTR</property>
</object>
<packing>
<property name="padding">0</property>
<property name="expand">False</property>
<property name="fill">False</property>
</packing>
</child>
<child> <child>
<object class="GtkToolbar" id="toolbar"> <object class="GtkToolbar" id="toolbar">
<property name="visible">True</property> <property name="visible">True</property>
@ -496,7 +104,7 @@
<property name="visible_horizontal">True</property> <property name="visible_horizontal">True</property>
<property name="visible_vertical">True</property> <property name="visible_vertical">True</property>
<property name="is_important">False</property> <property name="is_important">False</property>
<signal handler="on_itemPreferences_activate" name="clicked"/> <property name="action-name">app.preferences</property>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@ -529,17 +137,14 @@
</packing> </packing>
</child> </child>
</object> </object>
<packing>
<property name="padding">0</property>
<property name="expand">False</property>
<property name="fill">True</property>
</packing>
</child> </child>
<child> <child>
<object class="GtkHBox" id="hboxContainer"> <object class="GtkGrid" id="hboxContainer">
<property name="border_width">5</property> <property name="border_width">5</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="homogeneous">False</property> <property name="orientation">horizontal</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<child> <child>
<object class="GtkNotebook" id="wNotebook"> <object class="GtkNotebook" id="wNotebook">
<property name="visible">True</property> <property name="visible">True</property>
@ -551,21 +156,23 @@
<property name="enable_popup">False</property> <property name="enable_popup">False</property>
<signal handler="on_wNotebook_switch_page" name="switch_page"/> <signal handler="on_wNotebook_switch_page" name="switch_page"/>
<child> <child>
<object class="GtkHPaned" id="channelPaned"> <object class="GtkPaned" id="channelPaned">
<property name="border_width">5</property> <property name="border_width">5</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="orientation">horizontal</property>
<child> <child>
<object class="GtkVBox" id="vboxChannelNavigator"> <object class="GtkGrid" id="vboxChannelNavigator">
<property name="visible">True</property> <property name="visible">True</property>
<property name="homogeneous">False</property> <property name="row_spacing">5</property>
<property name="spacing">5</property> <property name="orientation">vertical</property>
<child> <child>
<object class="GtkScrolledWindow" id="scrolledwindow6"> <object class="GtkScrolledWindow" id="scrolledwindow6">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property> <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property> <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vexpand">True</property>
<property name="shadow_type">GTK_SHADOW_IN</property> <property name="shadow_type">GTK_SHADOW_IN</property>
<property name="window_placement">GTK_CORNER_TOP_LEFT</property> <property name="window_placement">GTK_CORNER_TOP_LEFT</property>
<child> <child>
@ -583,21 +190,17 @@
<signal handler="on_treeChannels_row_activated" name="row_activated"/> <signal handler="on_treeChannels_row_activated" name="row_activated"/>
<signal handler="on_treeChannels_cursor_changed" name="cursor_changed"/> <signal handler="on_treeChannels_cursor_changed" name="cursor_changed"/>
<signal handler="on_treeview_query_tooltip" name="query-tooltip"/> <signal handler="on_treeview_query_tooltip" name="query-tooltip"/>
<signal handler="on_treeview_expose_event" name="expose-event"/> <signal handler="on_treeview_expose_event" name="draw"/>
<signal handler="on_treeview_button_pressed" name="button-press-event"/> <signal handler="on_treeview_button_pressed" name="button-press-event"/>
<signal handler="on_treeview_podcasts_button_released" name="button-release-event"/> <signal handler="on_treeview_podcasts_button_released" name="button-release-event"/>
</object> </object>
</child> </child>
</object> </object>
<packing>
<property name="padding">0</property>
<property name="expand">True</property>
<property name="fill">True</property>
</packing>
</child> </child>
<child> <child>
<object class="GtkHBox" id="hbox_search_podcasts"> <object class="GtkGrid" id="hbox_search_podcasts">
<property name="spacing">6</property> <property name="column_spacing">6</property>
<property name="orientation">horizontal</property>
<child> <child>
<object class="GtkEntry" id="entry_search_podcasts"> <object class="GtkEntry" id="entry_search_podcasts">
<property name="visible">True</property> <property name="visible">True</property>
@ -608,41 +211,34 @@
</object> </object>
</child> </child>
</object> </object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
</packing>
</child> </child>
<child> <child>
<object class="GtkVBox" id="vbox42"> <object class="GtkGrid" id="vbox42">
<property name="visible">True</property> <property name="visible">True</property>
<property name="homogeneous">False</property> <property name="orientation">vertical</property>
<child> <child>
<object class="GtkButton" id="btnUpdateFeeds"> <object class="GtkButton" id="btnUpdateFeeds">
<property name="label" translatable="yes">Check for new episodes</property> <property name="label" translatable="yes">Check for new episodes</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="focus_on_click">True</property> <property name="focus_on_click">True</property>
<signal handler="on_itemUpdate_activate" name="clicked"/> <property name="action-name">win.update</property>
<property name="hexpand">True</property>
</object> </object>
<packing> <packing>
<property name="padding">0</property>
<property name="expand">True</property>
<property name="fill">True</property>
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkHBox" id="hboxUpdateFeeds"> <object class="GtkGrid" id="hboxUpdateFeeds">
<property name="homogeneous">False</property> <property name="column_spacing">6</property>
<property name="spacing">6</property> <property name="orientation">horizontal</property>
<child> <child>
<object class="GtkProgressBar" id="pbFeedUpdate"> <object class="GtkProgressBar" id="pbFeedUpdate">
<property name="hexpand">True</property>
<property name="pulse_step">0.10000000149</property> <property name="pulse_step">0.10000000149</property>
<property name="show-text">True</property>
<property name="ellipsize">PANGO_ELLIPSIZE_MIDDLE</property> <property name="ellipsize">PANGO_ELLIPSIZE_MIDDLE</property>
</object> </object>
<packing> <packing>
<property name="padding">0</property>
<property name="expand">True</property>
<property name="fill">True</property>
</packing> </packing>
</child> </child>
<child> <child>
@ -658,25 +254,12 @@
</object> </object>
</child> </child>
</object> </object>
<packing>
<property name="padding">0</property>
<property name="expand">False</property>
<property name="fill">False</property>
</packing>
</child> </child>
</object> </object>
<packing> <packing>
<property name="padding">0</property>
<property name="expand">True</property>
<property name="fill">True</property>
</packing> </packing>
</child> </child>
</object> </object>
<packing>
<property name="padding">0</property>
<property name="expand">False</property>
<property name="fill">False</property>
</packing>
</child> </child>
</object> </object>
<packing> <packing>
@ -685,9 +268,10 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkVBox" id="vbox_episode_list"> <object class="GtkGrid" id="vbox_episode_list">
<property name="visible">True</property> <property name="visible">True</property>
<property name="spacing">6</property> <property name="row_spacing">6</property>
<property name="orientation">vertical</property>
<child> <child>
<object class="GtkScrolledWindow" id="scrollAvailable"> <object class="GtkScrolledWindow" id="scrollAvailable">
<property name="visible">True</property> <property name="visible">True</property>
@ -695,6 +279,8 @@
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property> <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property> <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="shadow_type">GTK_SHADOW_IN</property> <property name="shadow_type">GTK_SHADOW_IN</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="window_placement">GTK_CORNER_TOP_LEFT</property> <property name="window_placement">GTK_CORNER_TOP_LEFT</property>
<child> <child>
<object class="GtkTreeView" id="treeAvailable"> <object class="GtkTreeView" id="treeAvailable">
@ -711,29 +297,22 @@
<property name="hover_expand">False</property> <property name="hover_expand">False</property>
<signal handler="on_treeAvailable_row_activated" name="row_activated"/> <signal handler="on_treeAvailable_row_activated" name="row_activated"/>
<signal handler="on_treeview_query_tooltip" name="query-tooltip"/> <signal handler="on_treeview_query_tooltip" name="query-tooltip"/>
<signal handler="on_treeview_expose_event" name="expose-event"/> <signal handler="on_treeview_expose_event" name="draw"/>
<signal handler="on_treeview_button_pressed" name="button-press-event"/> <signal handler="on_treeview_button_pressed" name="button-press-event"/>
<signal handler="on_treeview_episodes_button_released" name="button-release-event"/> <signal handler="on_treeview_episodes_button_released" name="button-release-event"/>
</object> </object>
</child> </child>
</object> </object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
</packing>
</child> </child>
<child> <child>
<object class="GtkHBox" id="hbox_search_episodes"> <object class="GtkGrid" id="hbox_search_episodes">
<property name="spacing">6</property> <property name="column_spacing">6</property>
<property name="orientation">horizontal</property>
<child> <child>
<object class="GtkLabel" id="label_search_episodes"> <object class="GtkLabel" id="label_search_episodes">
<property name="visible">True</property> <property name="visible">True</property>
<property name="label" translatable="yes">Filter:</property> <property name="label" translatable="yes">Filter:</property>
</object> </object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
</packing>
</child> </child>
<child> <child>
<object class="GtkEntry" id="entry_search_episodes"> <object class="GtkEntry" id="entry_search_episodes">
@ -745,10 +324,6 @@
</object> </object>
</child> </child>
</object> </object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
</packing>
</child> </child>
</object> </object>
<packing> <packing>
@ -775,15 +350,16 @@
</object> </object>
</child> </child>
<child> <child>
<object class="GtkVBox" id="vboxDownloadStatusWidgets"> <object class="GtkGrid" id="vboxDownloadStatusWidgets">
<property name="border_width">5</property> <property name="border_width">5</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="homogeneous">False</property> <property name="row_spacing">5</property>
<property name="spacing">5</property> <property name="orientation">vertical</property>
<child> <child>
<object class="GtkScrolledWindow" id="scrolledwindow1"> <object class="GtkScrolledWindow" id="scrolledwindow1">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="vexpand">True</property>
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property> <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property> <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="shadow_type">GTK_SHADOW_IN</property> <property name="shadow_type">GTK_SHADOW_IN</property>
@ -795,32 +371,30 @@
<property name="headers_visible">False</property> <property name="headers_visible">False</property>
<property name="rules_hint">False</property> <property name="rules_hint">False</property>
<property name="rubber-banding">True</property> <property name="rubber-banding">True</property>
<property name="reorderable">False</property> <property name="reorderable">True</property>
<property name="enable_search">True</property> <property name="enable_search">True</property>
<property name="fixed_height_mode">False</property> <property name="fixed_height_mode">False</property>
<property name="hover_selection">False</property> <property name="hover_selection">False</property>
<property name="hover_expand">False</property> <property name="hover_expand">False</property>
<signal handler="on_treeDownloads_row_activated" name="row_activated"/> <signal handler="on_treeDownloads_row_activated" name="row_activated"/>
<signal handler="on_treeview_expose_event" name="expose-event"/> <signal handler="on_treeview_expose_event" name="draw"/>
<signal handler="on_treeview_button_pressed" name="button-press-event"/> <signal handler="on_treeview_button_pressed" name="button-press-event"/>
<signal handler="on_treeview_downloads_button_released" name="button-release-event"/> <signal handler="on_treeview_downloads_button_released" name="button-release-event"/>
</object> </object>
</child> </child>
</object> </object>
<packing>
<property name="padding">0</property>
<property name="expand">True</property>
<property name="fill">True</property>
</packing>
</child> </child>
<child> <child>
<object class="GtkHBox" id="hboxDownloadSettings"> <object class="GtkGrid" id="hboxDownloadSettings">
<property name="border_width">5</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="spacing">10</property> <property name="column_spacing">10</property>
<property name="orientation">horizontal</property>
<child> <child>
<object class="GtkHBox" id="hboxDownloadLimit"> <object class="GtkGrid" id="hboxDownloadLimit">
<property name="visible">True</property> <property name="visible">True</property>
<property name="spacing">5</property> <property name="column_spacing">5</property>
<property name="orientation">horizontal</property>
<child> <child>
<object class="GtkCheckButton" id="cbLimitDownloads"> <object class="GtkCheckButton" id="cbLimitDownloads">
<property name="label" translatable="yes">Limit rate to</property> <property name="label" translatable="yes">Limit rate to</property>
@ -830,9 +404,6 @@
<property name="draw_indicator">True</property> <property name="draw_indicator">True</property>
<signal name="toggled" handler="on_cbLimitDownloads_toggled"/> <signal name="toggled" handler="on_cbLimitDownloads_toggled"/>
</object> </object>
<packing>
<property name="expand">False</property>
</packing>
</child> </child>
<child> <child>
<object class="GtkSpinButton" id="spinLimitDownloads"> <object class="GtkSpinButton" id="spinLimitDownloads">
@ -843,9 +414,6 @@
<property name="digits">1</property> <property name="digits">1</property>
<property name="adjustment">adjustment1</property> <property name="adjustment">adjustment1</property>
</object> </object>
<packing>
<property name="expand">False</property>
</packing>
</child> </child>
<child> <child>
<object class="GtkLabel" id="labelLimitRate"> <object class="GtkLabel" id="labelLimitRate">
@ -853,27 +421,20 @@
<property name="xalign">0</property> <property name="xalign">0</property>
<property name="label" translatable="yes">KiB/s</property> <property name="label" translatable="yes">KiB/s</property>
</object> </object>
<packing>
<property name="expand">False</property>
</packing>
</child> </child>
</object> </object>
<packing>
<property name="expand">False</property>
</packing>
</child> </child>
<child> <child>
<object class="GtkLabel" id="DownloadSettingsSpacer"> <object class="GtkLabel" id="DownloadSettingsSpacer">
<property name="visible">True</property> <property name="visible">True</property>
<property name="hexpand">True</property>
</object> </object>
<packing>
<property name="expand">True</property>
</packing>
</child> </child>
<child> <child>
<object class="GtkHBox" id="hboxDownloadRate"> <object class="GtkGrid" id="hboxDownloadRate">
<property name="visible">True</property> <property name="visible">True</property>
<property name="spacing">5</property> <property name="column_spacing">5</property>
<property name="orientation">horizontal</property>
<child> <child>
<object class="GtkCheckButton" id="cbMaxDownloads"> <object class="GtkCheckButton" id="cbMaxDownloads">
<property name="label" translatable="yes">Limit downloads to</property> <property name="label" translatable="yes">Limit downloads to</property>
@ -883,9 +444,6 @@
<property name="draw_indicator">True</property> <property name="draw_indicator">True</property>
<signal name="toggled" handler="on_cbMaxDownloads_toggled"/> <signal name="toggled" handler="on_cbMaxDownloads_toggled"/>
</object> </object>
<packing>
<property name="expand">False</property>
</packing>
</child> </child>
<child> <child>
<object class="GtkSpinButton" id="spinMaxDownloads"> <object class="GtkSpinButton" id="spinMaxDownloads">
@ -895,21 +453,10 @@
<property name="climb_rate">1</property> <property name="climb_rate">1</property>
<property name="adjustment">adjustment2</property> <property name="adjustment">adjustment2</property>
</object> </object>
<packing>
<property name="expand">False</property>
</packing>
</child> </child>
</object> </object>
<packing>
<property name="expand">False</property>
</packing>
</child> </child>
</object> </object>
<packing>
<property name="padding">0</property>
<property name="expand">False</property>
<property name="fill">False</property>
</packing>
</child> </child>
</object> </object>
<packing> <packing>
@ -930,17 +477,9 @@
</object> </object>
</child> </child>
</object> </object>
<packing>
<property name="padding">0</property>
<property name="expand">True</property>
<property name="fill">True</property>
</packing>
</child> </child>
</object> </object>
<packing> <packing>
<property name="padding">0</property>
<property name="expand">True</property>
<property name="fill">True</property>
</packing> </packing>
</child> </child>
</object> </object>

View file

@ -5,9 +5,11 @@
<property name="title" translatable="yes">Add a new podcast</property> <property name="title" translatable="yes">Add a new podcast</property>
<property name="type_hint">dialog</property> <property name="type_hint">dialog</property>
<property name="modal">True</property> <property name="modal">True</property>
<property name="transient-for">parent_widget</property>
<property name="default_width">400</property> <property name="default_width">400</property>
<child internal-child="vbox"> <child internal-child="vbox">
<object class="GtkVBox" id="vboxmain"> <object class="GtkBox" id="vboxmain">
<property name="orientation">vertical</property>
<child internal-child="action_area"> <child internal-child="action_area">
<object class="GtkHButtonBox" id="hbuttonbox"> <object class="GtkHButtonBox" id="hbuttonbox">
<property name="layout_style">GTK_BUTTONBOX_END</property> <property name="layout_style">GTK_BUTTONBOX_END</property>
@ -37,11 +39,12 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkHBox" id="hboxurlentry"> <object class="GtkBox" id="hboxurlentry">
<property name="border_width">10</property> <property name="border_width">10</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="homogeneous">False</property> <property name="homogeneous">False</property>
<property name="spacing">5</property> <property name="spacing">5</property>
<property name="orientation">horizontal</property>
<child> <child>
<object class="GtkLabel" id="label_add"> <object class="GtkLabel" id="label_add">
<property name="visible">True</property> <property name="visible">True</property>

View file

@ -6,16 +6,18 @@
<property name="border_width">10</property> <property name="border_width">10</property>
<property name="title" translatable="yes">gPodder Podcast Editor</property> <property name="title" translatable="yes">gPodder Podcast Editor</property>
<property name="modal">True</property> <property name="modal">True</property>
<property name="transient-for">parent_widget</property>
<property name="window_position">center-on-parent</property> <property name="window_position">center-on-parent</property>
<property name="default_width">500</property> <property name="default_width">500</property>
<property name="default_height">400</property> <property name="default_height">400</property>
<property name="type_hint">dialog</property> <property name="type_hint">dialog</property>
<signal name="destroy" handler="on_gPodderChannel_destroy" swapped="no"/> <signal name="destroy" handler="on_gPodderChannel_destroy" swapped="no"/>
<child internal-child="vbox"> <child internal-child="vbox">
<object class="GtkVBox" id="vboxChannelEditorMain"> <object class="GtkBox" id="vboxChannelEditorMain">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="spacing">5</property> <property name="spacing">5</property>
<property name="orientation">vertical</property>
<child internal-child="action_area"> <child internal-child="action_area">
<object class="GtkHButtonBox" id="hboxButtons"> <object class="GtkHButtonBox" id="hboxButtons">
<property name="visible">True</property> <property name="visible">True</property>
@ -263,44 +265,44 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkVBox" id="vboxiPodProperties"> <object class="GtkBox" id="vboxiPodProperties">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="border_width">10</property> <property name="border_width">10</property>
<property name="spacing">5</property> <property name="spacing">5</property>
<property name="orientation">vertical</property>
<child> <child>
<object class="GtkLabel" id="label96"> <object class="GtkGrid" id="table10">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">&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="column_spacing">6</property>
<property name="row_spacing">6</property> <property name="row_spacing">6</property>
<child>
<object class="GtkLabel" id="label96">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">&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> <child>
<object class="GtkLabel" id="label93"> <object class="GtkLabel" id="label93">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="xalign">0</property> <property name="xalign">0</property>
<property name="label" translatable="yes">Username:</property> <property name="label" translatable="yes">Username:</property>
<property name="expand">False</property>
</object> </object>
<packing> <packing>
<property name="x_options">GTK_FILL</property> <property name="left_attach">0</property>
<property name="y_options"/> <property name="top_attach">1</property>
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkLabel" id="label94"> <object class="GtkLabel" id="label94">
@ -308,12 +310,11 @@
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="xalign">0</property> <property name="xalign">0</property>
<property name="label" translatable="yes">Password:</property> <property name="label" translatable="yes">Password:</property>
<property name="expand">False</property>
</object> </object>
<packing> <packing>
<property name="top_attach">1</property> <property name="left_attach">0</property>
<property name="bottom_attach">2</property> <property name="top_attach">2</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"/>
</packing> </packing>
</child> </child>
<child> <child>
@ -325,11 +326,12 @@
<property name="secondary_icon_activatable">False</property> <property name="secondary_icon_activatable">False</property>
<property name="primary_icon_sensitive">True</property> <property name="primary_icon_sensitive">True</property>
<property name="secondary_icon_sensitive">True</property> <property name="secondary_icon_sensitive">True</property>
<property name="hexpand">True</property>
<property name="vexpand">False</property>
</object> </object>
<packing> <packing>
<property name="left_attach">1</property> <property name="left_attach">1</property>
<property name="right_attach">2</property> <property name="top_attach">1</property>
<property name="y_options"/>
</packing> </packing>
</child> </child>
<child> <child>
@ -341,65 +343,50 @@
<property name="secondary_icon_activatable">False</property> <property name="secondary_icon_activatable">False</property>
<property name="primary_icon_sensitive">True</property> <property name="primary_icon_sensitive">True</property>
<property name="secondary_icon_sensitive">True</property> <property name="secondary_icon_sensitive">True</property>
<property name="hexpand">True</property>
<property name="vexpand">False</property>
</object> </object>
<packing> <packing>
<property name="left_attach">1</property> <property name="left_attach">1</property>
<property name="right_attach">2</property> <property name="top_attach">2</property>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
<property name="y_options"/>
</packing> </packing>
</child> </child>
</object> <child>
<packing> <object class="GtkHSeparator" id="hseparator13">
<property name="expand">False</property> <property name="visible">True</property>
<property name="fill">True</property> <property name="can_focus">False</property>
<property name="position">1</property> </object>
</packing> <packing>
</child> <property name="left_attach">0</property>
<child> <property name="top_attach">3</property>
<object class="GtkHSeparator" id="hseparator13"> <property name="width">3</property>
<property name="visible">True</property> </packing>
<property name="can_focus">False</property> </child>
</object> <child>
<packing> <object class="GtkLabel" id="label97">
<property name="expand">False</property> <property name="visible">True</property>
<property name="fill">True</property> <property name="can_focus">False</property>
<property name="position">2</property> <property name="xalign">0</property>
</packing> <property name="label" translatable="yes">&lt;b&gt;Locations&lt;/b&gt;</property>
</child> <property name="use_markup">True</property>
<child> </object>
<object class="GtkLabel" id="label97"> <packing>
<property name="visible">True</property> <property name="left_attach">0</property>
<property name="can_focus">False</property> <property name="top_attach">4</property>
<property name="xalign">0</property> <property name="width">3</property>
<property name="label" translatable="yes">&lt;b&gt;Locations&lt;/b&gt;</property> </packing>
<property name="use_markup">True</property> </child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkTable" id="table3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="n_rows">2</property>
<property name="n_columns">3</property>
<property name="column_spacing">6</property>
<property name="row_spacing">6</property>
<child> <child>
<object class="GtkLabel" id="label29"> <object class="GtkLabel" id="label29">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="xalign">0</property> <property name="xalign">0</property>
<property name="label" translatable="yes">Download to:</property> <property name="label" translatable="yes">Download to:</property>
<property name="expand">False</property>
</object> </object>
<packing> <packing>
<property name="x_options">GTK_FILL</property> <property name="left_attach">0</property>
<property name="y_options"/> <property name="top_attach">5</property>
</packing> </packing>
</child> </child>
<child> <child>
@ -410,11 +397,13 @@
<property name="label">download to label</property> <property name="label">download to label</property>
<property name="selectable">True</property> <property name="selectable">True</property>
<property name="ellipsize">start</property> <property name="ellipsize">start</property>
<property name="hexpand">True</property>
<property name="vexpand">False</property>
</object> </object>
<packing> <packing>
<property name="left_attach">1</property> <property name="left_attach">1</property>
<property name="right_attach">3</property> <property name="top_attach">5</property>
<property name="y_options"/> <property name="width">2</property>
</packing> </packing>
</child> </child>
<child> <child>
@ -423,12 +412,11 @@
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="xalign">0</property> <property name="xalign">0</property>
<property name="label" translatable="yes">Website:</property> <property name="label" translatable="yes">Website:</property>
<property name="expand">False</property>
</object> </object>
<packing> <packing>
<property name="top_attach">1</property> <property name="top_attach">6</property>
<property name="bottom_attach">2</property> <property name="left_attach">0</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"/>
</packing> </packing>
</child> </child>
<child> <child>
@ -439,13 +427,12 @@
<property name="label" translatable="yes">website label</property> <property name="label" translatable="yes">website label</property>
<property name="selectable">True</property> <property name="selectable">True</property>
<property name="ellipsize">end</property> <property name="ellipsize">end</property>
<property name="hexpand">True</property>
<property name="vexpand">False</property>
</object> </object>
<packing> <packing>
<property name="left_attach">1</property> <property name="left_attach">1</property>
<property name="right_attach">2</property> <property name="top_attach">6</property>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
<property name="y_options"/>
</packing> </packing>
</child> </child>
<child> <child>
@ -465,17 +452,14 @@
</object> </object>
<packing> <packing>
<property name="left_attach">2</property> <property name="left_attach">2</property>
<property name="right_attach">3</property> <property name="top_attach">6</property>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
<property name="x_options">GTK_FILL</property>
</packing> </packing>
</child> </child>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="position">4</property> <property name="position">1</property>
</packing> </packing>
</child> </child>
</object> </object>

View file

@ -3,10 +3,10 @@
<interface> <interface>
<object class="GtkDialog" id="gPodderConfigEditor"> <object class="GtkDialog" id="gPodderConfigEditor">
<property name="visible">True</property> <property name="visible">True</property>
<property name="has_separator">False</property>
<property name="title" translatable="yes">gPodder Configuration Editor</property> <property name="title" translatable="yes">gPodder Configuration Editor</property>
<property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property> <property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
<property name="modal">True</property> <property name="modal">True</property>
<property name="transient-for">parent_widget</property>
<property name="default_width">750</property> <property name="default_width">750</property>
<property name="default_height">450</property> <property name="default_height">450</property>
<property name="destroy_with_parent">False</property> <property name="destroy_with_parent">False</property>
@ -17,19 +17,22 @@
<property name="urgency_hint">False</property> <property name="urgency_hint">False</property>
<signal handler="on_gPodderConfigEditor_destroy" name="destroy"/> <signal handler="on_gPodderConfigEditor_destroy" name="destroy"/>
<child internal-child="vbox"> <child internal-child="vbox">
<object class="GtkVBox" id="vbox13"> <object class="GtkBox" id="vbox13">
<property name="visible">True</property> <property name="visible">True</property>
<property name="homogeneous">False</property> <property name="homogeneous">False</property>
<property name="orientation">vertical</property>
<child> <child>
<object class="GtkVBox" id="vbox_for_episode_selector"> <object class="GtkBox" id="vbox_for_episode_selector">
<property name="border_width">5</property> <property name="border_width">5</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="spacing">5</property> <property name="spacing">5</property>
<property name="orientation">vertical</property>
<child> <child>
<object class="GtkHBox" id="hbox38"> <object class="GtkBox" id="hbox38">
<property name="visible">True</property> <property name="visible">True</property>
<property name="homogeneous">False</property> <property name="homogeneous">False</property>
<property name="spacing">6</property> <property name="spacing">6</property>
<property name="orientation">horizontal</property>
<child> <child>
<object class="GtkLabel" id="label121"> <object class="GtkLabel" id="label121">
<property name="visible">True</property> <property name="visible">True</property>
@ -115,6 +118,11 @@
</packing> </packing>
</child> </child>
</object> </object>
<packing>
<property name="padding">0</property>
<property name="expand">True</property>
<property name="fill">True</property>
</packing>
</child> </child>
<child> <child>
<object class="GtkHButtonBox" id="hbuttonbox2"> <object class="GtkHButtonBox" id="hbuttonbox2">

View file

@ -3,12 +3,10 @@
<interface> <interface>
<object class="GtkDialog" id="gPodderEpisodeSelector"> <object class="GtkDialog" id="gPodderEpisodeSelector">
<property name="visible">False</property> <property name="visible">False</property>
<property name="has_separator">True</property>
<property name="title" translatable="yes">Select episodes</property> <property name="title" translatable="yes">Select episodes</property>
<property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property> <property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
<property name="modal">True</property> <property name="modal">True</property>
<property name="default_width">600</property> <property name="transient-for">parent_widget</property>
<property name="default_height">400</property>
<property name="destroy_with_parent">False</property> <property name="destroy_with_parent">False</property>
<property name="skip_taskbar_hint">False</property> <property name="skip_taskbar_hint">False</property>
<property name="skip_pager_hint">False</property> <property name="skip_pager_hint">False</property>
@ -16,14 +14,16 @@
<property name="focus_on_map">True</property> <property name="focus_on_map">True</property>
<property name="urgency_hint">False</property> <property name="urgency_hint">False</property>
<child internal-child="vbox"> <child internal-child="vbox">
<object class="GtkVBox" id="vbox10"> <object class="GtkBox" id="vbox10">
<property name="visible">True</property> <property name="visible">True</property>
<property name="homogeneous">False</property> <property name="homogeneous">False</property>
<property name="orientation">vertical</property>
<child> <child>
<object class="GtkVBox" id="vbox_for_episode_selector"> <object class="GtkBox" id="vbox_for_episode_selector">
<property name="border_width">5</property> <property name="border_width">5</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="spacing">5</property> <property name="spacing">5</property>
<property name="orientation">vertical</property>
<child> <child>
<object class="GtkLabel" id="labelInstructions"> <object class="GtkLabel" id="labelInstructions">
<property name="label">additional text</property> <property name="label">additional text</property>
@ -71,10 +71,11 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkHBox" id="hboxButtons"> <object class="GtkBox" id="hboxButtons">
<property name="visible">True</property> <property name="visible">True</property>
<property name="homogeneous">False</property> <property name="homogeneous">False</property>
<property name="spacing">5</property> <property name="spacing">5</property>
<property name="orientation">horizontal</property>
<child> <child>
<object class="GtkButton" id="btnCheckAll"> <object class="GtkButton" id="btnCheckAll">
<property name="visible">True</property> <property name="visible">True</property>
@ -91,10 +92,11 @@
<property name="left_padding">0</property> <property name="left_padding">0</property>
<property name="right_padding">0</property> <property name="right_padding">0</property>
<child> <child>
<object class="GtkHBox" id="hbox34"> <object class="GtkBox" id="hbox34">
<property name="visible">True</property> <property name="visible">True</property>
<property name="homogeneous">False</property> <property name="homogeneous">False</property>
<property name="spacing">2</property> <property name="spacing">2</property>
<property name="orientation">horizontal</property>
<child> <child>
<object class="GtkImage" id="image2636"> <object class="GtkImage" id="image2636">
<property name="visible">True</property> <property name="visible">True</property>
@ -151,10 +153,11 @@
<property name="left_padding">0</property> <property name="left_padding">0</property>
<property name="right_padding">0</property> <property name="right_padding">0</property>
<child> <child>
<object class="GtkHBox" id="hbox33"> <object class="GtkBox" id="hbox33">
<property name="visible">True</property> <property name="visible">True</property>
<property name="homogeneous">False</property> <property name="homogeneous">False</property>
<property name="spacing">2</property> <property name="spacing">2</property>
<property name="orientation">horizontal</property>
<child> <child>
<object class="GtkImage" id="image2635"> <object class="GtkImage" id="image2635">
<property name="visible">True</property> <property name="visible">True</property>
@ -221,12 +224,18 @@
</packing> </packing>
</child> </child>
</object> </object>
<packing>
<property name="padding">0</property>
<property name="expand">True</property>
<property name="fill">True</property>
</packing>
</child> </child>
<child internal-child="action_area"> <child internal-child="action_area">
<object class="GtkHBox" id="hbox35"> <object class="GtkBox" id="hbox35">
<property name="visible">True</property> <property name="visible">True</property>
<property name="homogeneous">False</property> <property name="homogeneous">False</property>
<property name="spacing">5</property> <property name="spacing">5</property>
<property name="orientation">horizontal</property>
<child> <child>
<object class="GtkButton" id="btnRemoveAction"> <object class="GtkButton" id="btnRemoveAction">
<property name="visible">False</property> <property name="visible">False</property>

View file

@ -8,15 +8,17 @@
<property name="border_width">6</property> <property name="border_width">6</property>
<property name="title" translatable="yes">Find new podcasts</property> <property name="title" translatable="yes">Find new podcasts</property>
<property name="modal">True</property> <property name="modal">True</property>
<property name="transient-for">parent_widget</property>
<property name="window_position">center-on-parent</property> <property name="window_position">center-on-parent</property>
<property name="default_width">600</property> <property name="default_width">600</property>
<property name="default_height">400</property> <property name="default_height">400</property>
<property name="type_hint">dialog</property> <property name="type_hint">dialog</property>
<child internal-child="vbox"> <child internal-child="vbox">
<object class="GtkVBox" id="vb_directory"> <object class="GtkBox" id="vb_directory">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="spacing">6</property> <property name="spacing">6</property>
<property name="orientation">vertical</property>
<child internal-child="action_area"> <child internal-child="action_area">
<object class="GtkHButtonBox" id="hboxBottomButtons"> <object class="GtkHButtonBox" id="hboxBottomButtons">
<property name="visible">True</property> <property name="visible">True</property>
@ -94,9 +96,10 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkHPaned" id="hpaned"> <object class="GtkPaned" id="hpaned">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="orientation">horizontal</property>
<child> <child>
<object class="GtkScrolledWindow" id="sw_providers"> <object class="GtkScrolledWindow" id="sw_providers">
<property name="visible">True</property> <property name="visible">True</property>
@ -121,15 +124,17 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkVBox" id="vb_podcasts"> <object class="GtkBox" id="vb_podcasts">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="spacing">5</property> <property name="spacing">5</property>
<property name="orientation">vertical</property>
<child> <child>
<object class="GtkHBox" id="hb_text_entry"> <object class="GtkBox" id="hb_text_entry">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="spacing">5</property> <property name="spacing">5</property>
<property name="orientation">horizontal</property>
<child> <child>
<object class="GtkLabel" id="lb_search"> <object class="GtkLabel" id="lb_search">
<property name="visible">True</property> <property name="visible">True</property>

View file

@ -26,9 +26,9 @@
<property name="value">7</property> <property name="value">7</property>
</object> </object>
<object class="GtkDialog" id="gPodderPreferences"> <object class="GtkDialog" id="gPodderPreferences">
<property name="visible">True</property> <property name="visible">False</property>
<property name="modal">True</property> <property name="modal">True</property>
<property name="has-separator">False</property> <property name="transient-for">parent_widget</property>
<property name="window-position">GTK_WIN_POS_CENTER_ON_PARENT</property> <property name="window-position">GTK_WIN_POS_CENTER_ON_PARENT</property>
<property name="default_height">260</property> <property name="default_height">260</property>
<property name="default_width">320</property> <property name="default_width">320</property>
@ -36,23 +36,23 @@
<property name="type_hint">dialog</property> <property name="type_hint">dialog</property>
<signal name="destroy" handler="on_dialog_destroy"/> <signal name="destroy" handler="on_dialog_destroy"/>
<child internal-child="vbox"> <child internal-child="vbox">
<object class="GtkVBox" id="vbox"> <object class="GtkBox" id="vbox">
<property name="border_width">2</property> <property name="border_width">2</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="orientation">vertical</property>
<child> <child>
<object class="GtkNotebook" id="notebook"> <object class="GtkNotebook" id="notebook">
<property name="border_width">6</property> <property name="border_width">6</property>
<property name="visible">True</property> <property name="visible">True</property>
<child> <child>
<object class="GtkVBox" id="vbox_general"> <object class="GtkBox" id="vbox_general">
<property name="border_width">12</property> <property name="border_width">12</property>
<property name="spacing">6</property> <property name="spacing">6</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="orientation">vertical</property>
<child> <child>
<object class="GtkTable" id="table_players"> <object class="GtkGrid" id="table_players">
<property name="column_spacing">6</property> <property name="column_spacing">6</property>
<property name="n_columns">3</property>
<property name="n_rows">2</property>
<property name="row_spacing">6</property> <property name="row_spacing">6</property>
<property name="visible">True</property> <property name="visible">True</property>
<child> <child>
@ -61,9 +61,6 @@
<property name="visible">True</property> <property name="visible">True</property>
<property name="xalign">0.0</property> <property name="xalign">0.0</property>
</object> </object>
<packing>
<property name="x_options">fill</property>
</packing>
</child> </child>
<child> <child>
<object class="GtkLabel" id="label_video_player"> <object class="GtkLabel" id="label_video_player">
@ -72,30 +69,28 @@
<property name="xalign">0.0</property> <property name="xalign">0.0</property>
</object> </object>
<packing> <packing>
<property name="bottom_attach">2</property> <property name="left_attach">0</property>
<property name="top_attach">1</property> <property name="top_attach">1</property>
<property name="x_options">fill</property>
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkComboBox" id="combo_audio_player_app"> <object class="GtkComboBox" id="combo_audio_player_app">
<property name="visible">True</property> <property name="visible">True</property>
<property name="hexpand">True</property>
<signal name="changed" handler="on_combo_audio_player_app_changed"/> <signal name="changed" handler="on_combo_audio_player_app_changed"/>
</object> </object>
<packing> <packing>
<property name="left_attach">1</property> <property name="left_attach">1</property>
<property name="right_attach">2</property>
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkComboBox" id="combo_video_player_app"> <object class="GtkComboBox" id="combo_video_player_app">
<property name="visible">True</property> <property name="visible">True</property>
<property name="hexpand">True</property>
<signal name="changed" handler="on_combo_video_player_app_changed"/> <signal name="changed" handler="on_combo_video_player_app_changed"/>
</object> </object>
<packing> <packing>
<property name="bottom_attach">2</property>
<property name="left_attach">1</property> <property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">1</property> <property name="top_attach">1</property>
</packing> </packing>
</child> </child>
@ -112,8 +107,7 @@
</object> </object>
<packing> <packing>
<property name="left_attach">2</property> <property name="left_attach">2</property>
<property name="right_attach">3</property> <property name="top_attach">0</property>
<property name="x_options">fill</property>
</packing> </packing>
</child> </child>
<child> <child>
@ -128,11 +122,8 @@
</child> </child>
</object> </object>
<packing> <packing>
<property name="bottom_attach">2</property>
<property name="left_attach">2</property> <property name="left_attach">2</property>
<property name="right_attach">3</property>
<property name="top_attach">1</property> <property name="top_attach">1</property>
<property name="x_options">fill</property>
</packing> </packing>
</child> </child>
</object> </object>
@ -175,15 +166,14 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkVBox" id="vbox_video"> <object class="GtkBox" id="vbox_video">
<property name="border_width">12</property> <property name="border_width">12</property>
<property name="spacing">6</property> <property name="spacing">6</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="orientation">vertical</property>
<child> <child>
<object class="GtkTable" id="table_video"> <object class="GtkGrid" id="table_video">
<property name="column_spacing">6</property> <property name="column_spacing">6</property>
<property name="n_columns">3</property>
<property name="n_rows">3</property>
<property name="row_spacing">6</property> <property name="row_spacing">6</property>
<property name="visible">True</property> <property name="visible">True</property>
<child> <child>
@ -195,21 +185,18 @@
</object> </object>
<packing> <packing>
<property name="top_attach">0</property> <property name="top_attach">0</property>
<property name="bottom_attach">1</property>
<property name="x_options">fill</property>
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkComboBox" id="combobox_preferred_youtube_format"> <object class="GtkComboBox" id="combobox_preferred_youtube_format">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="hexpand">True</property>
<signal name="changed" handler="on_combobox_preferred_youtube_format_changed" swapped="no"/> <signal name="changed" handler="on_combobox_preferred_youtube_format_changed" swapped="no"/>
</object> </object>
<packing> <packing>
<property name="left_attach">1</property> <property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">0</property> <property name="top_attach">0</property>
<property name="bottom_attach">1</property>
</packing> </packing>
</child> </child>
<child> <child>
@ -221,23 +208,18 @@
</object> </object>
<packing> <packing>
<property name="left_attach">0</property> <property name="left_attach">0</property>
<property name="right_attach">1</property>
<property name="top_attach">1</property> <property name="top_attach">1</property>
<property name="bottom_attach">2</property>
<property name="x_options">fill</property>
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkEntry" id="entry_youtube_api_key"> <object class="GtkEntry" id="entry_youtube_api_key">
<property name="visible">True</property> <property name="visible">True</property>
<property name="hexpand">True</property>
<signal handler="on_youtube_api_key_changed" name="changed"/> <signal handler="on_youtube_api_key_changed" name="changed"/>
</object> </object>
<packing> <packing>
<property name="left_attach">1</property> <property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">1</property> <property name="top_attach">1</property>
<property name="bottom_attach">2</property>
<property name="x_options">fill</property>
</packing> </packing>
</child> </child>
<child> <child>
@ -253,10 +235,7 @@
</object> </object>
<packing> <packing>
<property name="top_attach">1</property> <property name="top_attach">1</property>
<property name="bottom_attach">2</property>
<property name="left_attach">2</property> <property name="left_attach">2</property>
<property name="right_attach">3</property>
<property name="x_options">fill</property>
</packing> </packing>
</child> </child>
<child> <child>
@ -268,8 +247,7 @@
</object> </object>
<packing> <packing>
<property name="top_attach">2</property> <property name="top_attach">2</property>
<property name="bottom_attach">3</property> <property name="left_attach">0</property>
<property name="x_options">fill</property>
</packing> </packing>
</child> </child>
<child> <child>
@ -280,9 +258,7 @@
</object> </object>
<packing> <packing>
<property name="left_attach">1</property> <property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">2</property> <property name="top_attach">2</property>
<property name="bottom_attach">3</property>
</packing> </packing>
</child> </child>
</object> </object>
@ -296,10 +272,11 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkVBox" id="vbox_extensions"> <object class="GtkBox" id="vbox_extensions">
<property name="border_width">12</property> <property name="border_width">12</property>
<property name="spacing">6</property> <property name="spacing">6</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="orientation">vertical</property>
<child> <child>
<object class="GtkScrolledWindow" id="scrolledwindow2"> <object class="GtkScrolledWindow" id="scrolledwindow2">
<property name="visible">True</property> <property name="visible">True</property>
@ -477,14 +454,16 @@
</child> </child>
<child> <child>
<object class="GtkVBox" id="vbox_updating"> <object class="GtkBox" id="vbox_updating">
<property name="border_width">12</property> <property name="border_width">12</property>
<property name="spacing">6</property> <property name="spacing">6</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="orientation">vertical</property>
<child> <child>
<object class="GtkHBox" id="hbox_updating_interval"> <object class="GtkBox" id="hbox_updating_interval">
<property name="spacing">6</property> <property name="spacing">6</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="orientation">horizontal</property>
<child> <child>
<object class="GtkLabel" id="label_update_interval"> <object class="GtkLabel" id="label_update_interval">
<property name="label" translatable="yes">Update interval:</property> <property name="label" translatable="yes">Update interval:</property>
@ -497,13 +476,15 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkHScale" id="hscale_update_interval"> <object class="GtkScale" id="hscale_update_interval">
<property name="digits">0</property> <property name="digits">0</property>
<property name="is_focus">True</property> <property name="is_focus">True</property>
<property name="restrict_to_fill_level">False</property> <property name="restrict_to_fill_level">False</property>
<property name="value_pos">bottom</property> <property name="value_pos">bottom</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="adjustment">adjustment_update_interval</property> <property name="adjustment">adjustment_update_interval</property>
<property name="orientation">horizontal</property>
<property name="hexpand">True</property>
<signal name="format-value" handler="format_update_interval_value"/> <signal name="format-value" handler="format_update_interval_value"/>
<signal name="value-changed" handler="on_update_interval_value_changed"/> <signal name="value-changed" handler="on_update_interval_value_changed"/>
</object> </object>
@ -526,9 +507,10 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkHBox" id="hbox_episode_limit"> <object class="GtkBox" id="hbox_episode_limit">
<property name="spacing">6</property> <property name="spacing">6</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="orientation">horizontal</property>
<child> <child>
<object class="GtkLabel" id="label_episode_limit"> <object class="GtkLabel" id="label_episode_limit">
<property name="label" translatable="yes">Maximum number of episodes per podcast:</property> <property name="label" translatable="yes">Maximum number of episodes per podcast:</property>
@ -565,9 +547,10 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkHBox" id="hbox_auto_download"> <object class="GtkBox" id="hbox_auto_download">
<property name="spacing">6</property> <property name="spacing">6</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="orientation">horizontal</property>
<child> <child>
<object class="GtkLabel" id="label_auto_download"> <object class="GtkLabel" id="label_auto_download">
<property name="label" translatable="yes">When new episodes are found:</property> <property name="label" translatable="yes">When new episodes are found:</property>
@ -600,14 +583,16 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkVBox" id="vbox_downloads"> <object class="GtkBox" id="vbox_downloads">
<property name="border_width">12</property> <property name="border_width">12</property>
<property name="spacing">6</property> <property name="spacing">6</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="orientation">vertical</property>
<child> <child>
<object class="GtkHBox" id="hbox_expiration"> <object class="GtkBox" id="hbox_expiration">
<property name="spacing">6</property> <property name="spacing">6</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="orientation">horizontal</property>
<child> <child>
<object class="GtkLabel" id="label_expiration"> <object class="GtkLabel" id="label_expiration">
<property name="label" translatable="yes">Delete played episodes:</property> <property name="label" translatable="yes">Delete played episodes:</property>
@ -620,12 +605,14 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkHScale" id="hscale_expiration"> <object class="GtkScale" id="hscale_expiration">
<property name="digits">0</property> <property name="digits">0</property>
<property name="is_focus">True</property> <property name="is_focus">True</property>
<property name="value_pos">bottom</property> <property name="value_pos">bottom</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="adjustment">adjustment_expiration</property> <property name="adjustment">adjustment_expiration</property>
<property name="orientation">horizontal</property>
<property name="hexpand">True</property>
<signal name="format-value" handler="format_expiration_value"/> <signal name="format-value" handler="format_expiration_value"/>
<signal name="value-changed" handler="on_expiration_value_changed"/> <signal name="value-changed" handler="on_expiration_value_changed"/>
</object> </object>
@ -665,10 +652,11 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkVBox" id="vbox_devices"> <object class="GtkBox" id="vbox_devices">
<property name="visible">True</property> <property name="visible">True</property>
<property name="border_width">12</property> <property name="border_width">12</property>
<property name="spacing">6</property> <property name="spacing">6</property>
<property name="orientation">vertical</property>
<child> <child>
<object class="GtkTable" id="table_devices"> <object class="GtkTable" id="table_devices">
<property name="visible">True</property> <property name="visible">True</property>

View file

@ -5,17 +5,19 @@
<property name="default_height">230</property> <property name="default_height">230</property>
<property name="default_width">340</property> <property name="default_width">340</property>
<property name="modal">True</property> <property name="modal">True</property>
<property name="transient-for">parent_widget</property>
<property name="title" translatable="yes">Getting started</property> <property name="title" translatable="yes">Getting started</property>
<property name="has_separator">False</property>
<child internal-child="vbox"> <child internal-child="vbox">
<object class="GtkVBox" id="dialog1-vbox"> <object class="GtkBox" id="dialog1-vbox">
<property name="border_width">2</property> <property name="border_width">2</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="orientation">vertical</property>
<child> <child>
<object class="GtkVBox" id="vbox1"> <object class="GtkBox" id="vbox1">
<property name="border_width">12</property> <property name="border_width">12</property>
<property name="spacing">12</property> <property name="spacing">12</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="orientation">vertical</property>
<child> <child>
<object class="GtkTable" id="table1"> <object class="GtkTable" id="table1">
<property name="column_spacing">6</property> <property name="column_spacing">6</property>
@ -56,9 +58,10 @@
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkVBox" id="vbox_buttons"> <object class="GtkBox" id="vbox_buttons">
<property name="spacing">6</property> <property name="spacing">6</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="orientation">vertical</property>
<child> <child>
<object class="GtkButton" id="btnOPML"> <object class="GtkButton" id="btnOPML">
<property name="is_focus">True</property> <property name="is_focus">True</property>

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 # This metadata block gets parsed by setup.py - use single quotes only
__tagline__ = 'Media aggregator and podcast client' __tagline__ = 'Media aggregator and podcast client'
__author__ = 'Thomas Perl <thp@gpodder.org>' __author__ = 'Thomas Perl <thp@gpodder.org>'
__version__ = '3.9.5' __version__ = '3.9.3'
__date__ = '2017-12-16' __date__ = '2016-12-22'
__copyright__ = '© 2005-2017 Thomas Perl and the gPodder Team' __copyright__ = '© 2005-2017 Thomas Perl and the gPodder Team'
__license__ = 'GNU General Public License, version 3 or later' __license__ = 'GNU General Public License, version 3 or later'
__url__ = 'http://gpodder.org/' __url__ = 'http://gpodder.org/'
@ -38,7 +38,7 @@ import locale
try: try:
import podcastparser import podcastparser
except ImportError: except ImportError:
print """ print("""
Error: Module "podcastparser" (python-podcastparser) not found. Error: Module "podcastparser" (python-podcastparser) not found.
The podcastparser module can be downloaded from The podcastparser module can be downloaded from
http://gpodder.org/podcastparser/ http://gpodder.org/podcastparser/
@ -46,15 +46,15 @@ except ImportError:
From a source checkout, you can download local copies of all From a source checkout, you can download local copies of all
CLI dependencies for debugging (will be placed into "src/"): CLI dependencies for debugging (will be placed into "src/"):
python tools/localdepends.py python3 tools/localdepends.py
""" """)
sys.exit(1) sys.exit(1)
del podcastparser del podcastparser
try: try:
import mygpoclient import mygpoclient
except ImportError: except ImportError:
print """ print("""
Error: Module "mygpoclient" (python-mygpoclient) not found. Error: Module "mygpoclient" (python-mygpoclient) not found.
The mygpoclient module can be downloaded from The mygpoclient module can be downloaded from
http://gpodder.org/mygpoclient/ http://gpodder.org/mygpoclient/
@ -62,19 +62,19 @@ except ImportError:
From a source checkout, you can download local copies of all From a source checkout, you can download local copies of all
CLI dependencies for debugging (will be placed into "src/"): CLI dependencies for debugging (will be placed into "src/"):
python tools/localdepends.py python3 tools/localdepends.py
""" """)
sys.exit(1) sys.exit(1)
del mygpoclient del mygpoclient
try: try:
import sqlite3 import sqlite3
except ImportError: except ImportError:
print """ print("""
Error: Module "sqlite3" not found. Error: Module "sqlite3" not found.
Build Python with SQLite 3 support or get it from Build Python with SQLite 3 support or get it from
http://code.google.com/p/pysqlite/ http://code.google.com/p/pysqlite/
""" """)
sys.exit(1) sys.exit(1)
del sqlite3 del sqlite3
@ -121,18 +121,9 @@ except AttributeError:
gettext = t.gettext gettext = t.gettext
ngettext = t.ngettext ngettext = t.ngettext
if ui.win32:
try:
# Workaround for bug 650
from gtk.glade import bindtextdomain
bindtextdomain(textdomain, locale_dir)
del bindtextdomain
except:
# Ignore for missing glade module
pass
del t del t
# Set up textdomain for gtk.Builder (this accesses the C library functions) # Set up textdomain for Gtk.Builder (this accesses the C library functions)
if hasattr(locale, 'bindtextdomain'): if hasattr(locale, 'bindtextdomain'):
locale.bindtextdomain(textdomain, locale_dir) locale.bindtextdomain(textdomain, locale_dir)
@ -152,7 +143,7 @@ images_folder = None
user_extensions = None user_extensions = None
# Episode states used in the database # Episode states used in the database
STATE_NORMAL, STATE_DOWNLOADED, STATE_DELETED = range(3) STATE_NORMAL, STATE_DOWNLOADED, STATE_DELETED = list(range(3))
# Paths (gPodder's home folder, config, db, download and data prefix) # Paths (gPodder's home folder, config, db, download and data prefix)
home = None home = None
@ -193,13 +184,13 @@ default_home = fixup_home(default_home)
set_home(os.environ.get(ENV_HOME, default_home)) set_home(os.environ.get(ENV_HOME, default_home))
if home != default_home: if home != default_home:
print >>sys.stderr, 'Storing data in', home, '(GPODDER_HOME is set)' print('Storing data in', home, '(GPODDER_HOME is set)', file=sys.stderr)
if ENV_DOWNLOADS in os.environ: if ENV_DOWNLOADS in os.environ:
# Allow to relocate the downloads folder (pull request 4, bug 466) # Allow to relocate the downloads folder (pull request 4, bug 466)
downloads = os.environ[ENV_DOWNLOADS] downloads = os.environ[ENV_DOWNLOADS]
print >>sys.stderr, 'Storing downloads in %s (%s is set)' % (downloads, print('Storing downloads in %s (%s is set)' % (downloads,
ENV_DOWNLOADS) ENV_DOWNLOADS), file=sys.stderr)
# Plugins to load by default # Plugins to load by default
DEFAULT_PLUGINS = [ DEFAULT_PLUGINS = [
@ -220,5 +211,5 @@ def load_plugins():
for plugin in PLUGINS: for plugin in PLUGINS:
try: try:
__import__(plugin) __import__(plugin)
except Exception, e: except Exception as e:
print >>sys.stderr, 'Cannot load plugin: %s (%s)' % (plugin, e) print('Cannot load plugin: %s (%s)' % (plugin, e), file=sys.stderr)

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) filename = episode.local_filename(create=False, check_only=True)
if filename in candidates: if filename in candidates:
found += 1 found += 1
progress_callback(episode.title, float(found)/count) progress_callback(episode.title, found/count)
candidates.remove(filename) candidates.remove(filename)
partial_files.remove(filename+'.partial') partial_files.remove(filename+'.partial')

View file

@ -147,6 +147,8 @@ defaults = {
'download_list': { 'download_list': {
'remove_finished': True, 'remove_finished': True,
}, },
'html_shownotes': True, # enable webkit renderer
}, },
}, },
@ -228,7 +230,7 @@ def config_value_to_string(config_value):
if config_type == list: if config_type == list:
return ','.join(map(config_value_to_string, config_value)) return ','.join(map(config_value_to_string, config_value))
elif config_type in (str, unicode): elif config_type in (str, str):
return config_value return config_value
else: else:
return str(config_value) return str(config_value)
@ -237,7 +239,7 @@ def string_to_config_value(new_value, old_value):
config_type = type(old_value) config_type = type(old_value)
if config_type == list: if config_type == list:
return filter(None, [x.strip() for x in new_value.split(',')]) return [_f for _f in [x.strip() for x in new_value.split(',')] if _f]
elif config_type == bool: elif config_type == bool:
return (new_value.strip().lower() in ('1', 'true')) return (new_value.strip().lower() in ('1', 'true'))
else: else:
@ -366,7 +368,7 @@ class Config(object):
for observer in self.__observers: for observer in self.__observers:
try: try:
observer(name, old_value, value) observer(name, old_value, value)
except Exception, exception: except Exception as exception:
logger.error('Error while calling observer %r: %s', logger.error('Error while calling observer %r: %s',
observer, exception, exc_info=True) observer, exception, exc_info=True)

View file

@ -38,12 +38,12 @@ class CoverDownloader(object):
# File name extension dict, lists supported cover art extensions # File name extension dict, lists supported cover art extensions
# Values: functions that check if some data is of that file type # Values: functions that check if some data is of that file type
SUPPORTED_EXTENSIONS = { SUPPORTED_EXTENSIONS = {
'.png': lambda d: d.startswith('\x89PNG\r\n\x1a\n\x00'), '.png': lambda d: d.startswith(b'\x89PNG\r\n\x1a\n\x00'),
'.jpg': lambda d: d.startswith('\xff\xd8'), '.jpg': lambda d: d.startswith(b'\xff\xd8'),
'.gif': lambda d: d.startswith('GIF89a') or d.startswith('GIF87a'), '.gif': lambda d: d.startswith(b'GIF89a') or d.startswith(b'GIF87a'),
} }
EXTENSIONS = SUPPORTED_EXTENSIONS.keys() EXTENSIONS = list(SUPPORTED_EXTENSIONS.keys())
ALL_EPISODES_ID = ':gpodder:all-episodes:' ALL_EPISODES_ID = ':gpodder:all-episodes:'
# Low timeout to avoid unnecessary hangs of GUIs # Low timeout to avoid unnecessary hangs of GUIs
@ -85,14 +85,14 @@ class CoverDownloader(object):
try: try:
logger.info('Downloading cover art: %s', cover_url) logger.info('Downloading cover art: %s', cover_url)
data = util.urlopen(cover_url, timeout=self.TIMEOUT).read() data = util.urlopen(cover_url, timeout=self.TIMEOUT).read()
except Exception, e: except Exception as e:
logger.warn('Cover art download failed: %s', e) logger.warn('Cover art download failed: %s', e)
return self._fallback_filename(title) return self._fallback_filename(title)
try: try:
extension = None extension = None
for filetype, check in self.SUPPORTED_EXTENSIONS.items(): for filetype, check in list(self.SUPPORTED_EXTENSIONS.items()):
if check(data): if check(data):
extension = filetype extension = filetype
break break
@ -107,7 +107,7 @@ class CoverDownloader(object):
fp.close() fp.close()
return filename + extension return filename + extension
except Exception, e: except Exception as e:
logger.warn('Cannot save cover art', exc_info=True) logger.warn('Cannot save cover art', exc_info=True)
# Fallback to cover art based on the podcast title # Fallback to cover art based on the podcast title

View file

@ -24,7 +24,7 @@
# 2010-04-24 Thomas Perl <thp@gpodder.org> # 2010-04-24 Thomas Perl <thp@gpodder.org>
# #
from __future__ import with_statement
import gpodder import gpodder
_ = gpodder.gettext _ = gpodder.gettext
@ -55,9 +55,9 @@ class Database(object):
self.commit() self.commit()
with self.lock: with self.lock:
cur = self.cursor() self.db.isolation_level = None
cur.execute("VACUUM") self.db.execute('VACUUM')
cur.close() self.db.isolation_level = ''
self._db.close() self._db.close()
self._db = None self._db = None
@ -107,7 +107,7 @@ class Database(object):
try: try:
logger.debug('Commit.') logger.debug('Commit.')
self.db.commit() self.db.commit()
except Exception, e: except Exception as e:
logger.error('Cannot commit: %s', e, exc_info=True) logger.error('Cannot commit: %s', e, exc_info=True)
def get_content_types(self, id): def get_content_types(self, id):
@ -160,7 +160,7 @@ class Database(object):
cur.execute(sql) cur.execute(sql)
keys = [desc[0] for desc in cur.description] keys = [desc[0] for desc in cur.description]
result = map(lambda row: factory(dict(zip(keys, row)), self), cur) result = [factory(dict(list(zip(keys, row))), self) for row in cur]
cur.close() cur.close()
return result return result
@ -178,7 +178,7 @@ class Database(object):
cur.execute(sql, args) cur.execute(sql, args)
keys = [desc[0] for desc in cur.description] keys = [desc[0] for desc in cur.description]
result = map(lambda row: factory(dict(zip(keys, row))), cur) result = [factory(dict(list(zip(keys, row)))) for row in cur]
cur.close() cur.close()
return result return result
@ -219,7 +219,7 @@ class Database(object):
values.append(o.id) values.append(o.id)
sql = 'UPDATE %s SET %s WHERE id = ?' % (table, qmarks) sql = 'UPDATE %s SET %s WHERE id = ?' % (table, qmarks)
cur.execute(sql, values) cur.execute(sql, values)
except Exception, e: except Exception as e:
logger.error('Cannot save %s: %s', o, e, exc_info=True) logger.error('Cannot save %s: %s', o, e, exc_info=True)
cur.close() cur.close()

View file

@ -27,7 +27,7 @@ import gpodder
_ = gpodder.gettext _ = gpodder.gettext
import urllib import urllib.request, urllib.parse, urllib.error
import json import json
import os import os
@ -49,7 +49,7 @@ class DirectoryTag(object):
class Provider(object): class Provider(object):
PROVIDER_SEARCH, PROVIDER_URL, PROVIDER_FILE, PROVIDER_TAGCLOUD, PROVIDER_STATIC = range(5) PROVIDER_SEARCH, PROVIDER_URL, PROVIDER_FILE, PROVIDER_TAGCLOUD, PROVIDER_STATIC = list(range(5))
def __init__(self): def __init__(self):
self.name = '' self.name = ''
@ -97,7 +97,7 @@ class GPodderNetSearchProvider(Provider):
self.icon = 'directory-gpodder.png' self.icon = 'directory-gpodder.png'
def on_search(self, query): def on_search(self, query):
return directory_entry_from_mygpo_json('http://gpodder.net/search.json?q=' + urllib.quote(query)) return directory_entry_from_mygpo_json('http://gpodder.net/search.json?q=' + urllib.parse.quote(query))
class OpmlWebImportProvider(Provider): class OpmlWebImportProvider(Provider):
def __init__(self): def __init__(self):
@ -142,7 +142,7 @@ class GPodderNetTagsProvider(Provider):
self.icon = 'directory-tags.png' self.icon = 'directory-tags.png'
def on_tag(self, tag): def on_tag(self, tag):
return directory_entry_from_mygpo_json('http://gpodder.net/api/2/tag/%s/50.json' % urllib.quote(tag)) return directory_entry_from_mygpo_json('http://gpodder.net/api/2/tag/%s/50.json' % urllib.parse.quote(tag))
def get_tags(self): def get_tags(self):
return [DirectoryTag(d['tag'], d['usage']) for d in json.load(util.urlopen('http://gpodder.net/api/2/tags/40.json'))] return [DirectoryTag(d['tag'], d['usage']) for d in json.load(util.urlopen('http://gpodder.net/api/2/tags/40.json'))]

View file

@ -25,7 +25,7 @@
# Based on libwget.py (2005-10-29) # Based on libwget.py (2005-10-29)
# #
from __future__ import with_statement
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -38,8 +38,8 @@ import gpodder
import socket import socket
import threading import threading
import urllib import urllib.request, urllib.parse, urllib.error
import urlparse import urllib.parse
import shutil import shutil
import os.path import os.path
import os import os
@ -66,13 +66,13 @@ def get_header_param(headers, param, header_name):
""" """
value = None value = None
try: try:
headers_string = ['%s:%s'%(k,v) for k,v in headers.items()] headers_string = ['%s:%s'%(k,v) for k,v in list(headers.items())]
msg = email.message_from_string('\n'.join(headers_string)) msg = email.message_from_string('\n'.join(headers_string))
if header_name in msg: if header_name in msg:
raw_value = msg.get_param(param, header=header_name) raw_value = msg.get_param(param, header=header_name)
if raw_value is not None: if raw_value is not None:
value = email.utils.collapse_rfc2231_value(raw_value) value = email.utils.collapse_rfc2231_value(raw_value)
except Exception, e: except Exception as e:
logger.error('Cannot get %s from %s', param, header_name, exc_info=True) logger.error('Cannot get %s from %s', param, header_name, exc_info=True)
return value return value
@ -188,18 +188,18 @@ class gPodderDownloadHTTPError(Exception):
self.error_code = error_code self.error_code = error_code
self.error_message = error_message self.error_message = error_message
class DownloadURLOpener(urllib.FancyURLopener): class DownloadURLOpener(urllib.request.FancyURLopener):
version = gpodder.user_agent version = gpodder.user_agent
# Sometimes URLs are not escaped correctly - try to fix them # Sometimes URLs are not escaped correctly - try to fix them
# (see RFC2396; Section 2.4.3. Excluded US-ASCII Characters) # (see RFC2396; Section 2.4.3. Excluded US-ASCII Characters)
# FYI: The omission of "%" in the list is to avoid double escaping! # FYI: The omission of "%" in the list is to avoid double escaping!
ESCAPE_CHARS = dict((ord(c), u'%%%x'%ord(c)) for c in u' <>#"{}|\\^[]`') ESCAPE_CHARS = dict((ord(c), '%%%x'%ord(c)) for c in ' <>#"{}|\\^[]`')
def __init__( self, channel): def __init__( self, channel):
self.channel = channel self.channel = channel
self._auth_retry_counter = 0 self._auth_retry_counter = 0
urllib.FancyURLopener.__init__(self, None) urllib.request.FancyURLopener.__init__(self, None)
def http_error_default(self, url, fp, errcode, errmsg, headers): def http_error_default(self, url, fp, errcode, errmsg, headers):
""" """
@ -230,7 +230,7 @@ class DownloadURLOpener(urllib.FancyURLopener):
fp.close() fp.close()
# In case the server sent a relative URL, join with original: # In case the server sent a relative URL, join with original:
newurl = urlparse.urljoin(self.type + ":" + url, newurl) newurl = urllib.parse.urljoin(self.type + ":" + url, newurl)
return self.open(newurl) return self.open(newurl)
# The following is based on Python's urllib.py "URLopener.retrieve" # The following is based on Python's urllib.py "URLopener.retrieve"
@ -266,11 +266,8 @@ class DownloadURLOpener(urllib.FancyURLopener):
tfp = open(filename, 'wb') tfp = open(filename, 'wb')
# Fix a problem with bad URLs that are not encoded correctly (bug 549) # Fix a problem with bad URLs that are not encoded correctly (bug 549)
url = url.decode('ascii', 'ignore')
url = url.translate(self.ESCAPE_CHARS) url = url.translate(self.ESCAPE_CHARS)
url = url.encode('ascii')
url = urllib.unwrap(urllib.toBytes(url))
fp = self.open(url, data) fp = self.open(url, data)
headers = fp.info() headers = fp.info()
@ -291,10 +288,10 @@ class DownloadURLOpener(urllib.FancyURLopener):
bs = 1024*8 bs = 1024*8
size = -1 size = -1
read = current_size read = current_size
blocknum = int(current_size/bs) blocknum = current_size//bs
if reporthook: if reporthook:
if "content-length" in headers: if "content-length" in headers:
size = int(headers.getrawheader("Content-Length")) + current_size size = int(headers['Content-Length']) + current_size
reporthook(blocknum, bs, size) reporthook(blocknum, bs, size)
while read < size or size == -1: while read < size or size == -1:
if size == -1: if size == -1:
@ -315,7 +312,7 @@ class DownloadURLOpener(urllib.FancyURLopener):
# raise exception if actual size does not match content-length header # raise exception if actual size does not match content-length header
if size >= 0 and read < size: if size >= 0 and read < size:
raise urllib.ContentTooShortError("retrieval incomplete: got only %i out " raise urllib.error.ContentTooShortError("retrieval incomplete: got only %i out "
"of %i bytes" % (read, size), result) "of %i bytes" % (read, size), result)
return result return result
@ -337,45 +334,48 @@ class DownloadURLOpener(urllib.FancyURLopener):
class DownloadQueueWorker(object): class DownloadQueueWorker(object):
def __init__(self, queue, exit_callback, continue_check_callback, minimum_tasks): def __init__(self, queue, exit_callback, continue_check_callback):
self.queue = queue self.queue = queue
self.exit_callback = exit_callback self.exit_callback = exit_callback
self.continue_check_callback = continue_check_callback self.continue_check_callback = continue_check_callback
# The minimum amount of tasks that should be downloaded by this worker
# before using the continue_check_callback to determine if it might
# continue accepting tasks. This can be used to forcefully start a
# download, even if a download limit is in effect.
self.minimum_tasks = minimum_tasks
def __repr__(self): def __repr__(self):
return threading.current_thread().getName() return threading.current_thread().getName()
def run(self): def run(self):
logger.info('Starting new thread: %s', self) logger.info('Starting new thread: %s', self)
while True: while True:
# Check if this thread is allowed to continue accepting tasks if not self.continue_check_callback(self):
# (But only after reducing minimum_tasks to zero - see above)
if self.minimum_tasks > 0:
self.minimum_tasks -= 1
elif not self.continue_check_callback(self):
return return
try: try:
task = self.queue.pop() task = self.queue.get_next()
logger.info('%s is processing: %s', self, task) logger.info('%s is processing: %s', self, task)
task.run() task.run()
task.recycle() task.recycle()
except IndexError, e: except StopIteration as e:
logger.info('No more tasks for %s to carry out.', self) logger.info('No more tasks for %s to carry out.', self)
break break
self.exit_callback(self) self.exit_callback(self)
class ForceDownloadWorker(object):
def __init__(self, task):
self.task = task
def __repr__(self):
return threading.current_thread().getName()
def run(self):
logger.info('Starting new thread: %s', self)
logger.info('%s is processing: %s', self, self.task)
self.task.run()
class DownloadQueueManager(object): class DownloadQueueManager(object):
def __init__(self, config): def __init__(self, config, queue):
self._config = config self._config = config
self.tasks = collections.deque() self.tasks = queue
self.worker_threads_access = threading.RLock() self.worker_threads_access = threading.RLock()
self.worker_threads = [] self.worker_threads = []
@ -393,61 +393,37 @@ class DownloadQueueManager(object):
else: else:
return True return True
def spawn_threads(self, force_start=False): def __spawn_threads(self):
"""Spawn new worker threads if necessary """Spawn new worker threads if necessary
If force_start is True, forcefully spawn a thread and
let it process at least one episodes, even if a download
limit is in effect at the moment.
""" """
with self.worker_threads_access: with self.worker_threads_access:
if not len(self.tasks): if not self.tasks.has_work():
return return
if force_start or len(self.worker_threads) == 0 or \ if len(self.worker_threads) == 0 or \
len(self.worker_threads) < self._config.max_downloads or \ len(self.worker_threads) < self._config.max_downloads or \
not self._config.max_downloads_enabled: not self._config.max_downloads_enabled:
# We have to create a new thread here, there's work to do # We have to create a new thread here, there's work to do
logger.info('Starting new worker thread.') logger.info('Starting new worker thread.')
# The new worker should process at least one task (the one
# that we want to forcefully start) if force_start is True.
if force_start:
minimum_tasks = 1
else:
minimum_tasks = 0
worker = DownloadQueueWorker(self.tasks, self.__exit_callback, worker = DownloadQueueWorker(self.tasks, self.__exit_callback,
self.__continue_check_callback, minimum_tasks) self.__continue_check_callback)
self.worker_threads.append(worker) self.worker_threads.append(worker)
util.run_in_background(worker.run) util.run_in_background(worker.run)
def are_queued_or_active_tasks(self): def update_max_downloads(self):
with self.worker_threads_access: self.__spawn_threads()
return len(self.worker_threads) > 0
def add_task(self, task, force_start=False): def force_start_task(self, task):
"""Add a new task to the download queue if self.tasks.set_downloading(task):
worker = ForceDownloadWorker(task)
util.run_in_background(worker.run)
If force_start is True, ignore the download limit def queue_task(self, task):
and forcefully start the download right away. """Marks a task as queued
""" """
if task.status != DownloadTask.INIT:
# Remove the task from its current position in the
# download queue (if any) to avoid race conditions
# where two worker threads download the same file
try:
self.tasks.remove(task)
except ValueError, e:
pass
task.status = DownloadTask.QUEUED task.status = DownloadTask.QUEUED
if force_start: self.__spawn_threads()
# Add the task to be taken on next pop
self.tasks.append(task)
else:
# Add the task to the end of the queue
self.tasks.appendleft(task)
self.spawn_threads(force_start)
class DownloadTask(object): class DownloadTask(object):
@ -526,10 +502,10 @@ class DownloadTask(object):
# Possible states this download task can be in # Possible states this download task can be in
STATUS_MESSAGE = (_('Added'), _('Queued'), _('Downloading'), STATUS_MESSAGE = (_('Added'), _('Queued'), _('Downloading'),
_('Finished'), _('Failed'), _('Cancelled'), _('Paused')) _('Finished'), _('Failed'), _('Cancelled'), _('Paused'))
(INIT, QUEUED, DOWNLOADING, DONE, FAILED, CANCELLED, PAUSED) = range(7) (INIT, QUEUED, DOWNLOADING, DONE, FAILED, CANCELLED, PAUSED) = list(range(7))
# Wheter this task represents a file download or a device sync operation # Wheter this task represents a file download or a device sync operation
ACTIVITY_DOWNLOAD, ACTIVITY_SYNCHRONIZE = range(2) ACTIVITY_DOWNLOAD, ACTIVITY_SYNCHRONIZE = list(range(2))
# Minimum time between progress updates (in seconds) # Minimum time between progress updates (in seconds)
MIN_TIME_BETWEEN_UPDATES = 1. MIN_TIME_BETWEEN_UPDATES = 1.
@ -623,8 +599,8 @@ class DownloadTask(object):
try: try:
already_downloaded = os.path.getsize(self.tempname) already_downloaded = os.path.getsize(self.tempname)
if self.total_size > 0: if self.total_size > 0:
self.progress = max(0.0, min(1.0, float(already_downloaded)/self.total_size)) self.progress = max(0.0, min(1.0, already_downloaded/self.total_size))
except OSError, os_error: except OSError as os_error:
logger.error('Cannot get size for %s', os_error) logger.error('Cannot get size for %s', os_error)
else: else:
# "touch self.tempname", so we also get partial # "touch self.tempname", so we also get partial
@ -669,7 +645,7 @@ class DownloadTask(object):
self.__episode.save() self.__episode.save()
if self.total_size > 0: if self.total_size > 0:
self.progress = max(0.0, min(1.0, float(count*blockSize)/self.total_size)) self.progress = max(0.0, min(1.0, count*blockSize/self.total_size))
if self._progress_updated is not None: if self._progress_updated is not None:
diff = time.time() - self._last_progress_updated diff = time.time() - self._last_progress_updated
if diff > self.MIN_TIME_BETWEEN_UPDATES or self.progress == 1.: if diff > self.MIN_TIME_BETWEEN_UPDATES or self.progress == 1.:
@ -718,7 +694,7 @@ class DownloadTask(object):
if self._config.limit_rate and speed > self._config.limit_rate_value: if self._config.limit_rate and speed > self._config.limit_rate_value:
# calculate the time that should have passed to reach # calculate the time that should have passed to reach
# the desired download rate and wait if necessary # the desired download rate and wait if necessary
should_have_passed = float((count-self.__start_blocks)*blockSize)/(self._config.limit_rate_value*1024.0) should_have_passed = (count-self.__start_blocks)*blockSize/(self._config.limit_rate_value*1024.0)
if should_have_passed > passed: if should_have_passed > passed:
# sleep a maximum of 10 seconds to not cause time-outs # sleep a maximum of 10 seconds to not cause time-outs
delay = min(10.0, float(should_have_passed-passed)) delay = min(10.0, float(should_have_passed-passed))
@ -739,8 +715,8 @@ class DownloadTask(object):
self.speed = 0.0 self.speed = 0.0
return False return False
# We only start this download if its status is "queued" # We only start this download if its status is "downloading"
if self.status != DownloadTask.QUEUED: if self.status != DownloadTask.DOWNLOADING:
return False return False
# We are downloading this file right now # We are downloading this file right now
@ -753,22 +729,23 @@ class DownloadTask(object):
url = youtube.get_real_download_url(self.__episode.url, fmt_ids) url = youtube.get_real_download_url(self.__episode.url, fmt_ids)
url = vimeo.get_real_download_url(url, self._config.vimeo.fileformat) url = vimeo.get_real_download_url(url, self._config.vimeo.fileformat)
url = escapist_videos.get_real_download_url(url) url = escapist_videos.get_real_download_url(url)
# We should have properly-escaped characters in the URL, but sometimes
# this is not true -- take any characters that are not in ASCII and
# convert them to UTF-8 and then percent-encode the UTF-8 string data
# Example: https://github.com/gpodder/gpodder/issues/232
url_chars = []
for char in url:
if ord(char) <= 31 or ord(char) >= 127:
for char in urllib.quote(char.encode('utf-8')):
url_chars.append(char.decode('utf-8'))
else:
url_chars.append(char)
url = u''.join(url_chars)
url = url.strip() url = url.strip()
# Properly escapes Unicode characters in the URL path section
# TODO: Explore if this should also handle the domain
# Based on: http://stackoverflow.com/a/18269491/1072626
# In response to issue: https://github.com/gpodder/gpodder/issues/232
def iri_to_url(url):
url = urllib.parse.urlsplit(url)
url = list(url)
# First unquote to avoid escaping quoted content
url[2] = urllib.parse.unquote(url[2])
url[2] = urllib.parse.quote(url[2])
url = urllib.parse.urlunsplit(url)
return url
url = iri_to_url(url)
downloader = DownloadURLOpener(self.__episode.channel) downloader = DownloadURLOpener(self.__episode.channel)
# HTTP Status codes for which we retry the download # HTTP Status codes for which we retry the download
@ -786,18 +763,18 @@ class DownloadTask(object):
self.tempname, reporthook=self.status_updated) self.tempname, reporthook=self.status_updated)
# If we arrive here, the download was successful # If we arrive here, the download was successful
break break
except urllib.ContentTooShortError, ctse: except urllib.error.ContentTooShortError as ctse:
if retry < max_retries: if retry < max_retries:
logger.info('Content too short: %s - will retry.', logger.info('Content too short: %s - will retry.',
url) url)
continue continue
raise raise
except socket.timeout, tmout: except socket.timeout as tmout:
if retry < max_retries: if retry < max_retries:
logger.info('Socket timeout: %s - will retry.', url) logger.info('Socket timeout: %s - will retry.', url)
continue continue
raise raise
except gPodderDownloadHTTPError, http: except gPodderDownloadHTTPError as http:
if retry < max_retries and http.error_code in retry_codes: if retry < max_retries and http.error_code in retry_codes:
logger.info('HTTP error %d: %s - will retry.', logger.info('HTTP error %d: %s - will retry.',
http.error_code, url) http.error_code, url)
@ -871,23 +848,23 @@ class DownloadTask(object):
util.delete_file(self.tempname) util.delete_file(self.tempname)
self.progress = 0.0 self.progress = 0.0
self.speed = 0.0 self.speed = 0.0
except urllib.ContentTooShortError, ctse: except urllib.error.ContentTooShortError as ctse:
self.status = DownloadTask.FAILED self.status = DownloadTask.FAILED
self.error_message = _('Missing content from server') self.error_message = _('Missing content from server')
except IOError, ioe: except IOError as ioe:
logger.error('%s while downloading "%s": %s', ioe.strerror, logger.error('%s while downloading "%s": %s', ioe.strerror,
self.__episode.title, ioe.filename, exc_info=True) self.__episode.title, ioe.filename, exc_info=True)
self.status = DownloadTask.FAILED self.status = DownloadTask.FAILED
d = {'error': ioe.strerror, 'filename': ioe.filename} d = {'error': ioe.strerror, 'filename': ioe.filename}
self.error_message = _('I/O Error: %(error)s: %(filename)s') % d self.error_message = _('I/O Error: %(error)s: %(filename)s') % d
except gPodderDownloadHTTPError, gdhe: except gPodderDownloadHTTPError as gdhe:
logger.error('HTTP %s while downloading "%s": %s', logger.error('HTTP %s while downloading "%s": %s',
gdhe.error_code, self.__episode.title, gdhe.error_message, gdhe.error_code, self.__episode.title, gdhe.error_message,
exc_info=True) exc_info=True)
self.status = DownloadTask.FAILED self.status = DownloadTask.FAILED
d = {'code': gdhe.error_code, 'message': gdhe.error_message} d = {'code': gdhe.error_code, 'message': gdhe.error_message}
self.error_message = _('HTTP Error %(code)s: %(message)s') % d self.error_message = _('HTTP Error %(code)s: %(message)s') % d
except Exception, e: except Exception as e:
self.status = DownloadTask.FAILED self.status = DownloadTask.FAILED
logger.error('Download failed: %s', str(e), exc_info=True) logger.error('Download failed: %s', str(e), exc_info=True)
self.error_message = _('Error: %s') % (str(e),) self.error_message = _('Error: %s') % (str(e),)

View file

@ -30,15 +30,10 @@ from gpodder import util
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
try: import json
# For Python < 2.6, we use the "simplejson" add-on module
import simplejson as json
except ImportError:
# Python 2.6 already ships with a nice "json" module
import json
import re import re
import urllib import urllib.request, urllib.parse, urllib.error
# This matches the more reliable URL # This matches the more reliable URL
ESCAPIST_NUMBER_RE = re.compile(r'http://www.escapistmagazine.com/videos/view/(\d+)', re.IGNORECASE) ESCAPIST_NUMBER_RE = re.compile(r'http://www.escapistmagazine.com/videos/view/(\d+)', re.IGNORECASE)
@ -112,6 +107,7 @@ def get_real_cover(url):
if rss_url is None: if rss_url is None:
return None return None
# FIXME: can I be sure to decode it as utf-8?
rss_data = util.urlopen(rss_url).read() rss_data = util.urlopen(rss_url).read()
rss_data_frag = DATA_COVERART_RE.search(rss_data) rss_data_frag = DATA_COVERART_RE.search(rss_data)
@ -124,6 +120,7 @@ def get_escapist_web(video_id):
if video_id is None: if video_id is None:
return None return None
# FIXME: must check if it's utf-8
web_url = 'http://www.escapistmagazine.com/videos/view/%s' % video_id web_url = 'http://www.escapistmagazine.com/videos/view/%s' % video_id
return util.urlopen(web_url).read() return util.urlopen(web_url).read()
@ -131,7 +128,7 @@ def get_escapist_config_url(data):
if data is None: if data is None:
return None return None
query_string = urllib.urlencode(json.loads(data)) query_string = urllib.parse.urlencode(json.loads(data))
return 'http://www.escapistmagazine.com/videos/vidconfig.php?%s' % query_string return 'http://www.escapistmagazine.com/videos/vidconfig.php?%s' % query_string
@ -162,7 +159,7 @@ def get_escapist_real_url(data, config_json):
result_num.append(num_hashes[idx]^hash_n[idx % len(hash_n)]) result_num.append(num_hashes[idx]^hash_n[idx % len(hash_n)])
# At last, Numbers back into characters # At last, Numbers back into characters
result = ''.join([unichr(x) for x in result_num]) result = ''.join([chr(x) for x in result_num])
# A wild JSON appears... # A wild JSON appears...
# You use "Master Ball"... # You use "Master Ball"...
escapist_cfg = json.loads(result) escapist_cfg = json.loads(result)

View file

@ -85,7 +85,7 @@ def call_extensions(func):
result.extend(cb_res) result.extend(cb_res)
elif cb_res is not None: elif cb_res is not None:
result = cb_res result = cb_res
except Exception, exception: except Exception as exception:
logger.error('Error in %s in %s: %s', container.filename, logger.error('Error in %s in %s: %s', container.filename,
method_name, exception, exc_info=True) method_name, exception, exc_info=True)
func(self, *args, **kwargs) func(self, *args, **kwargs)
@ -123,12 +123,12 @@ class ExtensionMetadata(object):
def __getattr__(self, name): def __getattr__(self, name):
try: try:
return self.DEFAULTS[name] return self.DEFAULTS[name]
except KeyError, e: except KeyError as e:
raise AttributeError(name, e) raise AttributeError(name, e)
def get_sorted(self): def get_sorted(self):
kf = lambda x: self.SORTKEYS.get(x[0], 99) kf = lambda x: self.SORTKEYS.get(x[0], 99)
return sorted([(k, v) for k, v in self.__dict__.items()], key=kf) return sorted([(k, v) for k, v in list(self.__dict__.items())], key=kf)
def check_ui(self, target, default): def check_ui(self, target, default):
"""Checks metadata information like """Checks metadata information like
@ -159,7 +159,7 @@ class ExtensionMetadata(object):
if not hasattr(self, target): if not hasattr(self, target):
return default return default
uis = filter(None, [x.strip() for x in getattr(self, target).split(',')]) uis = [_f for _f in [x.strip() for x in getattr(self, target).split(',')] if _f]
return any(getattr(gpodder.ui, ui.lower(), False) for ui in uis) return any(getattr(gpodder.ui, ui.lower(), False) for ui in uis)
@property @property
@ -253,15 +253,14 @@ class ExtensionContainer(object):
self.enabled = True self.enabled = True
if hasattr(self.module, 'on_load'): if hasattr(self.module, 'on_load'):
self.module.on_load() self.module.on_load()
except Exception, exception: except Exception as exception:
logger.error('Cannot load %s from %s: %s', self.name, logger.error('Cannot load %s from %s: %s', self.name,
self.filename, exception, exc_info=True) self.filename, exception, exc_info=True)
if isinstance(exception, ImportError): if isinstance(exception, ImportError):
# Wrap ImportError in MissingCommand for user-friendly # Wrap ImportError in MissingCommand for user-friendly
# message (might be displayed in the GUI) # message (might be displayed in the GUI)
match = re.match('No module named (.*)', exception.message) if exception.name:
if match: module = exception.name
module = match.group(1)
msg = _('Python module not found: %(module)s') % { msg = _('Python module not found: %(module)s') % {
'module': module 'module': module
} }
@ -272,7 +271,7 @@ class ExtensionContainer(object):
try: try:
if hasattr(self.module, 'on_unload'): if hasattr(self.module, 'on_unload'):
self.module.on_unload() self.module.on_unload()
except Exception, exception: except Exception as exception:
logger.error('Failed to on_unload %s: %s', self.name, logger.error('Failed to on_unload %s: %s', self.name,
exception, exc_info=True) exception, exc_info=True)
self.enabled = False self.enabled = False

View file

@ -29,9 +29,9 @@ from gpodder import util
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from urllib2 import HTTPError from urllib.error import HTTPError
from HTMLParser import HTMLParser from html.parser import HTMLParser
import urlparse import urllib.parse
try: try:
# Python 2 # Python 2
@ -67,7 +67,7 @@ class UnknownStatusCode(ExceptionWithData): pass
class AuthenticationRequired(Exception): pass class AuthenticationRequired(Exception): pass
# Successful status codes # Successful status codes
UPDATED_FEED, NEW_LOCATION, NOT_MODIFIED, CUSTOM_FEED = range(4) UPDATED_FEED, NEW_LOCATION, NOT_MODIFIED, CUSTOM_FEED = list(range(4))
class Result: class Result:
def __init__(self, status, feed=None): def __init__(self, status, feed=None):
@ -88,7 +88,7 @@ class FeedAutodiscovery(HTMLParser):
is_feed = attrs.get('type', '') in Fetcher.FEED_TYPES is_feed = attrs.get('type', '') in Fetcher.FEED_TYPES
is_alternate = attrs.get('rel', '') == 'alternate' is_alternate = attrs.get('rel', '') == 'alternate'
url = attrs.get('href', None) url = attrs.get('href', None)
url = urlparse.urljoin(self._base, url) url = urllib.parse.urljoin(self._base, url)
if is_feed and is_alternate and url: if is_feed and is_alternate and url:
logger.info('Feed autodiscovery: %s', url) logger.info('Feed autodiscovery: %s', url)
@ -175,10 +175,16 @@ class Fetcher(object):
data = stream data = stream
if autodiscovery and not is_local and stream.headers.get('content-type', '').startswith('text/html'): if autodiscovery and not is_local and stream.headers.get('content-type', '').startswith('text/html'):
# Not very robust attempt to detect encoding: http://stackoverflow.com/a/1495675/1072626
charset = stream.headers.get_param('charset')
if charset is None:
charset = 'utf-8' # utf-8 appears hard-coded elsewhere in this codebase
# We use StringIO in case the stream needs to be read again # We use StringIO in case the stream needs to be read again
data = StringIO(stream.read()) data = StringIO(stream.read().decode(charset))
ad = FeedAutodiscovery(url) ad = FeedAutodiscovery(url)
ad.feed(data.read())
ad.feed(data.getvalue())
if ad._resolved_url: if ad._resolved_url:
try: try:
self._parse_feed(ad._resolved_url, None, None, False) self._parse_feed(ad._resolved_url, None, None, False)
@ -190,15 +196,16 @@ class Fetcher(object):
url = self._resolve_url(url) url = self._resolve_url(url)
if url: if url:
return Result(NEW_LOCATION, url) return Result(NEW_LOCATION, url)
# Reset the stream so podcastparser can give it a go # Reset the stream so podcastparser can give it a go
data.seek(0) data.seek(0)
try: try:
feed = podcastparser.parse(url, data) feed = podcastparser.parse(url, data)
except ValueError as e: except ValueError as e:
raise InvalidFeed(u'Could not parse feed: {msg}'.format(msg=e)) raise InvalidFeed('Could not parse feed: {msg}'.format(msg=e))
if is_local: if is_local:
feed['headers'] = {} feed['headers'] = {}
return Result(UPDATED_FEED, feed) return Result(UPDATED_FEED, feed)

View file

@ -26,10 +26,10 @@ import re
import tokenize import tokenize
import gtk from gi.repository import Gtk
class GtkBuilderWidget(object): class GtkBuilderWidget(object):
def __init__(self, ui_folders, textdomain, **kwargs): def __init__(self, ui_folders, textdomain, parent, **kwargs):
""" """
Loads the UI file from the specified folder (with translations Loads the UI file from the specified folder (with translations
from the textdomain) and initializes attributes. from the textdomain) and initializes attributes.
@ -43,11 +43,16 @@ class GtkBuilderWidget(object):
**kwargs: **kwargs:
Keyword arguments will be set as attributes to this window Keyword arguments will be set as attributes to this window
""" """
for key, value in kwargs.items(): for key, value in list(kwargs.items()):
setattr(self, key, value) setattr(self, key, value)
self.builder = gtk.Builder() self.builder = Gtk.Builder()
if parent is not None:
self.builder.expose_object('parent_widget', parent)
self.builder.set_translation_domain(textdomain) self.builder.set_translation_domain(textdomain)
if hasattr(self, '_builder_expose'):
for (key, value) in list(self._builder_expose.items()):
self.builder.expose_object(key, value)
#print >>sys.stderr, 'Creating new from file', self.__class__.__name__ #print >>sys.stderr, 'Creating new from file', self.__class__.__name__
@ -74,11 +79,11 @@ class GtkBuilderWidget(object):
""" """
for widget in self.builder.get_objects(): for widget in self.builder.get_objects():
# Just to be safe - every widget from the builder is buildable # Just to be safe - every widget from the builder is buildable
if not isinstance(widget, gtk.Buildable): if not isinstance(widget, Gtk.Buildable):
continue continue
# The following call looks ugly, but see Gnome bug 591085 # The following call looks ugly, but see Gnome bug 591085
widget_name = gtk.Buildable.get_name(widget) widget_name = Gtk.Buildable.get_name(widget)
widget_api_name = '_'.join(re.findall(tokenize.Name, widget_name)) widget_api_name = '_'.join(re.findall(tokenize.Name, widget_name))
if hasattr(self, widget_api_name): if hasattr(self, widget_api_name):
@ -101,27 +106,27 @@ class GtkBuilderWidget(object):
def main(self): def main(self):
""" """
Starts the main loop of processing events. Starts the main loop of processing events.
The default implementation calls gtk.main() The default implementation calls Gtk.main()
Useful for applications that needs a non gtk main loop. Useful for applications that needs a non gtk main loop.
For example, applications based on gstreamer needs to override For example, applications based on gstreamer needs to override
this method with gst.main() this method with Gst.main()
Do not directly call this method in your programs. Do not directly call this method in your programs.
Use the method run() instead. Use the method run() instead.
""" """
gtk.main() Gtk.main()
def quit(self): def quit(self):
""" """
Quit processing events. Quit processing events.
The default implementation calls gtk.main_quit() The default implementation calls Gtk.main_quit()
Useful for applications that needs a non gtk main loop. Useful for applications that needs a non gtk main loop.
For example, applications based on gstreamer needs to override For example, applications based on gstreamer needs to override
this method with gst.main_quit() this method with Gst.main_quit()
""" """
gtk.main_quit() Gtk.main_quit()
def run(self): def run(self):
""" """

View file

@ -23,8 +23,9 @@
# #
import gtk from gi.repository import Gtk
import pango from gi.repository import Gdk
from gi.repository import Pango
import gpodder import gpodder
from gpodder import util from gpodder import util
@ -32,13 +33,12 @@ from gpodder import config
_ = gpodder.gettext _ = gpodder.gettext
class ConfigModel(gtk.ListStore): class ConfigModel(Gtk.ListStore):
C_NAME, C_TYPE_TEXT, C_VALUE_TEXT, C_TYPE, C_EDITABLE, C_FONT_STYLE, \ C_NAME, C_TYPE_TEXT, C_VALUE_TEXT, C_TYPE, C_EDITABLE, C_FONT_STYLE, \
C_IS_BOOLEAN, C_BOOLEAN_VALUE = range(8) C_IS_BOOLEAN, C_BOOLEAN_VALUE = list(range(8))
def __init__(self, config): def __init__(self, config):
gtk.ListStore.__init__(self, str, str, str, object, \ Gtk.ListStore.__init__(self, str, str, str, object, bool, int, bool, bool)
bool, int, bool, bool)
self._config = config self._config = config
self._fill_model() self._fill_model()
@ -65,11 +65,11 @@ class ConfigModel(gtk.ListStore):
value = self._config._lookup(key) value = self._config._lookup(key)
fieldtype = type(value) fieldtype = type(value)
style = pango.STYLE_NORMAL style = Pango.Style.NORMAL
#if value == default: #if value == default:
# style = pango.STYLE_NORMAL # style = Pango.Style.NORMAL
#else: #else:
# style = pango.STYLE_ITALIC # style = Pango.Style.ITALIC
self.append((key, self._type_as_string(fieldtype), self.append((key, self._type_as_string(fieldtype),
config.config_value_to_string(value), config.config_value_to_string(value),
@ -79,11 +79,11 @@ class ConfigModel(gtk.ListStore):
def _on_update(self, name, old_value, new_value): def _on_update(self, name, old_value, new_value):
for row in self: for row in self:
if row[self.C_NAME] == name: if row[self.C_NAME] == name:
style = pango.STYLE_NORMAL style = Pango.Style.NORMAL
#if new_value == self._config.Settings[name]: #if new_value == self._config.Settings[name]:
# style = pango.STYLE_NORMAL # style = Pango.Style.NORMAL
#else: #else:
# style = pango.STYLE_ITALIC # style = Pango.Style.ITALIC
new_value_text = config.config_value_to_string(new_value) new_value_text = config.config_value_to_string(new_value)
self.set(row.iter, \ self.set(row.iter, \
self.C_VALUE_TEXT, new_value_text, self.C_VALUE_TEXT, new_value_text,
@ -139,11 +139,11 @@ class UIConfig(config.Config):
cfg = getattr(self.ui.gtk.state, config_prefix) cfg = getattr(self.ui.gtk.state, config_prefix)
if gpodder.ui.win32: if gpodder.ui.win32:
window.set_gravity(gtk.gdk.GRAVITY_STATIC) window.set_gravity(Gdk.GRAVITY_STATIC)
window.resize(cfg.width, cfg.height) window.resize(cfg.width, cfg.height)
if cfg.x == -1 or cfg.y == -1: if cfg.x == -1 or cfg.y == -1:
window.set_position(gtk.WIN_POS_CENTER_ON_PARENT) window.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
else: else:
window.move(cfg.x, cfg.y) window.move(cfg.x, cfg.y)
@ -153,8 +153,7 @@ class UIConfig(config.Config):
def _receive_configure_event(widget, event): def _receive_configure_event(widget, event):
x_pos, y_pos = event.x, event.y x_pos, y_pos = event.x, event.y
width_size, height_size = event.width, event.height width_size, height_size = event.width, event.height
maximized = bool(event.window.get_state() & maximized = bool(event.window.get_state() & Gdk.WindowState.MAXIMIZED)
gtk.gdk.WINDOW_STATE_MAXIMIZED)
if not self.__ignore_window_events and not maximized: if not self.__ignore_window_events and not maximized:
cfg.x = x_pos cfg.x = x_pos
cfg.y = y_pos cfg.y = y_pos
@ -164,9 +163,10 @@ class UIConfig(config.Config):
window.connect('configure-event', _receive_configure_event) window.connect('configure-event', _receive_configure_event)
def _receive_window_state(widget, event): def _receive_window_state(widget, event):
new_value = bool(event.new_window_state & # ELL: why is it commented out?
gtk.gdk.WINDOW_STATE_MAXIMIZED) #new_value = bool(event.new_window_state & Gdk.WindowState.MAXIMIZED)
cfg.maximized = new_value #cfg.maximized = new_value
pass
window.connect('window-state-event', _receive_window_state) window.connect('window-state-event', _receive_window_state)

View file

@ -17,8 +17,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import gtk from gi.repository import Gtk
import gtk.gdk from gi.repository import Gdk
from gi.repository import GdkPixbuf
import gpodder import gpodder
@ -41,19 +42,19 @@ class gPodderChannel(BuilderWidget):
self.cbSkipFeedUpdate.set_active(self.channel.pause_subscription) self.cbSkipFeedUpdate.set_active(self.channel.pause_subscription)
self.cbEnableDeviceSync.set_active(self.channel.sync_to_mp3_player) self.cbEnableDeviceSync.set_active(self.channel.sync_to_mp3_player)
self.section_list = gtk.ListStore(str) self.section_list = Gtk.ListStore(str)
active_index = 0 active_index = 0
for index, section in enumerate(sorted(self.sections)): for index, section in enumerate(sorted(self.sections)):
self.section_list.append([section]) self.section_list.append([section])
if section == self.channel.section: if section == self.channel.section:
active_index = index active_index = index
self.combo_section.set_model(self.section_list) self.combo_section.set_model(self.section_list)
cell_renderer = gtk.CellRendererText() cell_renderer = Gtk.CellRendererText()
self.combo_section.pack_start(cell_renderer) self.combo_section.pack_start(cell_renderer, True)
self.combo_section.add_attribute(cell_renderer, 'text', 0) self.combo_section.add_attribute(cell_renderer, 'text', 0)
self.combo_section.set_active(active_index) self.combo_section.set_active(active_index)
self.strategy_list = gtk.ListStore(str, int) self.strategy_list = Gtk.ListStore(str, int)
active_index = 0 active_index = 0
for index, (checked, strategy_id, strategy) in \ for index, (checked, strategy_id, strategy) in \
enumerate(self.channel.get_download_strategies()): enumerate(self.channel.get_download_strategies()):
@ -61,8 +62,8 @@ class gPodderChannel(BuilderWidget):
if checked: if checked:
active_index = index active_index = index
self.combo_strategy.set_model(self.strategy_list) self.combo_strategy.set_model(self.strategy_list)
cell_renderer = gtk.CellRendererText() cell_renderer = Gtk.CellRendererText()
self.combo_strategy.pack_start(cell_renderer) self.combo_strategy.pack_start(cell_renderer, True)
self.combo_strategy.add_attribute(cell_renderer, 'text', 0) self.combo_strategy.add_attribute(cell_renderer, 'text', 0)
self.combo_strategy.set_active(active_index) self.combo_strategy.set_active(active_index)
@ -81,14 +82,14 @@ class gPodderChannel(BuilderWidget):
if not self.channel.link: if not self.channel.link:
self.btn_website.hide_all() self.btn_website.hide_all()
b = gtk.TextBuffer() b = Gtk.TextBuffer()
b.set_text( self.channel.description) b.set_text( self.channel.description)
self.channel_description.set_buffer( b) self.channel_description.set_buffer( b)
#Add Drag and Drop Support #Add Drag and Drop Support
flags = gtk.DEST_DEFAULT_ALL flags = Gtk.DestDefaults.ALL
targets = [('text/uri-list', 0, 2), ('text/plain', 0, 4)] targets = [Gtk.TargetEntry.new('text/uri-list', 0, 2), Gtk.TargetEntry.new('text/plain', 0, 4)]
actions = gtk.gdk.ACTION_DEFAULT | gtk.gdk.ACTION_COPY actions = Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY
self.imgCover.drag_dest_set(flags, targets, actions) self.imgCover.drag_dest_set(flags, targets, actions)
self.imgCover.connect('drag_data_received', self.drag_data_received) self.imgCover.connect('drag_data_received', self.drag_data_received)
border = 6 border = 6
@ -98,7 +99,7 @@ class gPodderChannel(BuilderWidget):
def on_button_add_section_clicked(self, widget): def on_button_add_section_clicked(self, widget):
text = self.show_text_edit_dialog(_('Add section'), _('New section:'), text = self.show_text_edit_dialog(_('Add section'), _('New section:'),
affirmative_text=gtk.STOCK_ADD) affirmative_text=Gtk.STOCK_ADD)
if text is not None: if text is not None:
for index, (section,) in enumerate(self.section_list): for index, (section,) in enumerate(self.section_list):
@ -110,31 +111,32 @@ class gPodderChannel(BuilderWidget):
self.combo_section.set_active(len(self.section_list)-1) self.combo_section.set_active(len(self.section_list)-1)
def on_cover_popup_menu(self, widget, event): def on_cover_popup_menu(self, widget, event):
if event.button != 3: if not event.triggers_context_menu():
return return
menu = gtk.Menu() menu = Gtk.Menu()
item = gtk.ImageMenuItem(gtk.STOCK_OPEN) item = Gtk.MenuItem.new_with_mnemonic(_('_Open'))
item.connect('activate', self.on_btnDownloadCover_clicked) item.connect('activate', self.on_btnDownloadCover_clicked)
menu.append(item) menu.append(item)
item = gtk.ImageMenuItem(gtk.STOCK_REFRESH) item = Gtk.MenuItem.new_with_mnemonic(_('_Refresh'))
item.connect('activate', self.on_btnClearCover_clicked) item.connect('activate', self.on_btnClearCover_clicked)
menu.append(item) menu.append(item)
menu.attach_to_widget(widget)
menu.show_all() menu.show_all()
menu.popup(None, None, None, event.button, event.time, None) menu.popup(None, None, None, None, event.button, event.time)
def on_btn_website_clicked(self, widget): def on_btn_website_clicked(self, widget):
util.open_website(self.channel.link) util.open_website(self.channel.link)
def on_btnDownloadCover_clicked(self, widget): def on_btnDownloadCover_clicked(self, widget):
dlg = gtk.FileChooserDialog(title=_('Select new podcast cover artwork'), parent=self.gPodderChannel, action=gtk.FILE_CHOOSER_ACTION_OPEN) dlg = Gtk.FileChooserDialog(title=_('Select new podcast cover artwork'), parent=self.gPodderChannel, action=Gtk.FileChooserAction.OPEN)
dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) dlg.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
dlg.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK) dlg.add_button(Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
if dlg.run() == gtk.RESPONSE_OK: if dlg.run() == Gtk.ResponseType.OK:
url = dlg.get_uri() url = dlg.get_uri()
self.clear_cover_cache(self.channel.url) self.clear_cover_cache(self.channel.url)
self.cover_downloader.replace_cover(self.channel, custom_url=url) self.cover_downloader.replace_cover(self.channel, custom_url=url)
@ -180,13 +182,13 @@ class gPodderChannel(BuilderWidget):
if pixbuf.get_width() > self.MAX_SIZE: if pixbuf.get_width() > self.MAX_SIZE:
f = float(self.MAX_SIZE)/pixbuf.get_width() f = float(self.MAX_SIZE)/pixbuf.get_width()
(width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f)) (width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
pixbuf = pixbuf.scale_simple(width, height, gtk.gdk.INTERP_BILINEAR) pixbuf = pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR)
# Resize if height is too large # Resize if height is too large
if pixbuf.get_height() > self.MAX_SIZE: if pixbuf.get_height() > self.MAX_SIZE:
f = float(self.MAX_SIZE)/pixbuf.get_height() f = float(self.MAX_SIZE)/pixbuf.get_height()
(width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f)) (width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
pixbuf = pixbuf.scale_simple(width, height, gtk.gdk.INTERP_BILINEAR) pixbuf = pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR)
return pixbuf return pixbuf

View file

@ -33,7 +33,7 @@ class gPodderDevicePlaylist(object):
self.linebreak = '\r\n' self.linebreak = '\r\n'
self.playlist_file=util.sanitize_filename(playlist_name + '.m3u') self.playlist_file=util.sanitize_filename(playlist_name + '.m3u')
self.playlist_folder = os.path.join(self._config.device_sync.device_folder, self._config.device_sync.playlists.folder) self.playlist_folder = os.path.join(self._config.device_sync.device_folder, self._config.device_sync.playlists.folder)
self.mountpoint = util.find_mount_point(util.sanitize_encoding(self.playlist_folder)) self.mountpoint = util.find_mount_point(self.playlist_folder)
if self.mountpoint == '/': if self.mountpoint == '/':
self.mountpoint = self.playlist_folder self.mountpoint = self.playlist_folder
logger.warning('MP3 player resides on / - using %s as MP3 player root', self.mountpoint) logger.warning('MP3 player resides on / - using %s as MP3 player root', self.mountpoint)

View file

@ -17,9 +17,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import gtk from gi.repository import Gtk
import pango from gi.repository import Pango
import cgi
import gpodder import gpodder
@ -66,7 +65,7 @@ class gPodderEpisodeSelector(BuilderWidget):
- stock_ok_button: (optional) Will replace the "OK" button with - stock_ok_button: (optional) Will replace the "OK" button with
another GTK+ stock item to be used for the another GTK+ stock item to be used for the
affirmative button of the dialog (e.g. can affirmative button of the dialog (e.g. can
be gtk.STOCK_DELETE when the episodes to be be Gtk.STOCK_DELETE when the episodes to be
selected will be deleted after closing the selected will be deleted after closing the
dialog) dialog)
- selection_buttons: (optional) A dictionary with labels as - selection_buttons: (optional) A dictionary with labels as
@ -140,25 +139,25 @@ class gPodderEpisodeSelector(BuilderWidget):
if hasattr(self, 'stock_ok_button'): if hasattr(self, 'stock_ok_button'):
if self.stock_ok_button == 'gpodder-download': if self.stock_ok_button == 'gpodder-download':
self.btnOK.set_image(gtk.image_new_from_stock(gtk.STOCK_GO_DOWN, gtk.ICON_SIZE_BUTTON)) self.btnOK.set_image(Gtk.Image.new_from_icon_name('go-down', Gtk.IconSize.BUTTON))
self.btnOK.set_label(_('Download')) self.btnOK.set_label(_('Download'))
else: else:
self.btnOK.set_label(self.stock_ok_button) self.btnOK.set_label(self.stock_ok_button)
self.btnOK.set_use_stock(True) self.btnOK.set_use_stock(True)
# check/uncheck column # check/uncheck column
toggle_cell = gtk.CellRendererToggle() toggle_cell = Gtk.CellRendererToggle()
toggle_cell.connect( 'toggled', self.toggle_cell_handler) toggle_cell.connect( 'toggled', self.toggle_cell_handler)
toggle_column = gtk.TreeViewColumn('', toggle_cell, active=self.COLUMN_TOGGLE) toggle_column = Gtk.TreeViewColumn('', toggle_cell, active=self.COLUMN_TOGGLE)
toggle_column.set_clickable(True) toggle_column.set_clickable(True)
self.treeviewEpisodes.append_column(toggle_column) self.treeviewEpisodes.append_column(toggle_column)
next_column = self.COLUMN_ADDITIONAL next_column = self.COLUMN_ADDITIONAL
for name, sort_name, sort_type, caption in self.columns: for name, sort_name, sort_type, caption in self.columns:
renderer = gtk.CellRendererText() renderer = Gtk.CellRendererText()
if next_column < self.COLUMN_ADDITIONAL + 1: if next_column < self.COLUMN_ADDITIONAL + 1:
renderer.set_property('ellipsize', pango.ELLIPSIZE_END) renderer.set_property('ellipsize', Pango.EllipsizeMode.END)
column = gtk.TreeViewColumn(caption, renderer, markup=next_column) column = Gtk.TreeViewColumn(caption, renderer, markup=next_column)
column.set_clickable(False) column.set_clickable(False)
column.set_resizable( True) column.set_resizable( True)
# Only set "expand" on the first column # Only set "expand" on the first column
@ -173,7 +172,7 @@ class gPodderEpisodeSelector(BuilderWidget):
if sort_name is not None: if sort_name is not None:
# add the sort column # add the sort column
column = gtk.TreeViewColumn() column = Gtk.TreeViewColumn()
column.set_clickable(False) column.set_clickable(False)
column.set_visible(False) column.set_visible(False)
self.treeviewEpisodes.append_column( column) self.treeviewEpisodes.append_column( column)
@ -185,7 +184,7 @@ class gPodderEpisodeSelector(BuilderWidget):
column_types.append(str) column_types.append(str)
if sort_name is not None: if sort_name is not None:
column_types.append(sort_type) column_types.append(sort_type)
self.model = gtk.ListStore( *column_types) self.model = Gtk.ListStore( *column_types)
tooltip = None tooltip = None
for index, episode in enumerate( self.episodes): for index, episode in enumerate( self.episodes):
@ -275,21 +274,21 @@ class gPodderEpisodeSelector(BuilderWidget):
return False return False
def treeview_episodes_button_pressed(self, treeview, event=None): def treeview_episodes_button_pressed(self, treeview, event=None):
if event is None or event.button == 3: if event is None or event.triggers_context_menu():
menu = gtk.Menu() menu = Gtk.Menu()
if len(self.selection_buttons): if len(self.selection_buttons):
for label in self.selection_buttons: for label in self.selection_buttons:
item = gtk.MenuItem(label) item = Gtk.MenuItem(label)
item.connect('activate', self.custom_selection_button_clicked, label) item.connect('activate', self.custom_selection_button_clicked, label)
menu.append(item) menu.append(item)
menu.append(gtk.SeparatorMenuItem()) menu.append(Gtk.SeparatorMenuItem())
item = gtk.MenuItem(_('Select all')) item = Gtk.MenuItem(_('Select all'))
item.connect('activate', self.on_btnCheckAll_clicked) item.connect('activate', self.on_btnCheckAll_clicked)
menu.append(item) menu.append(item)
item = gtk.MenuItem(_('Select none')) item = Gtk.MenuItem(_('Select none'))
item.connect('activate', self.on_btnCheckNone_clicked) item.connect('activate', self.on_btnCheckNone_clicked)
menu.append(item) menu.append(item)
@ -300,9 +299,9 @@ class gPodderEpisodeSelector(BuilderWidget):
menu.connect('deactivate', lambda menushell: self.episode_list_allow_tooltips()) menu.connect('deactivate', lambda menushell: self.episode_list_allow_tooltips())
if event is None: if event is None:
func = TreeViewHelper.make_popup_position_func(treeview) func = TreeViewHelper.make_popup_position_func(treeview)
menu.popup(None, None, func, 3, 0) menu.popup(None, None, func, None, 3, Gtk.get_current_event_time())
else: else:
menu.popup(None, None, None, event.button, event.time) menu.popup(None, None, None, None, event.button, event.time)
return True return True
@ -329,9 +328,9 @@ class gPodderEpisodeSelector(BuilderWidget):
self.btnOK.set_sensitive(count>0) self.btnOK.set_sensitive(count>0)
self.btnRemoveAction.set_sensitive(count>0) self.btnRemoveAction.set_sensitive(count>0)
if count > 0: if count > 0:
self.btnCancel.set_label(gtk.STOCK_CANCEL) self.btnCancel.set_label(Gtk.STOCK_CANCEL)
else: else:
self.btnCancel.set_label(gtk.STOCK_CLOSE) self.btnCancel.set_label(Gtk.STOCK_CLOSE)
else: else:
self.btnOK.set_sensitive(False) self.btnOK.set_sensitive(False)
self.btnRemoveAction.set_sensitive(False) self.btnRemoveAction.set_sensitive(False)

View file

@ -24,8 +24,9 @@
# #
import gtk from gi.repository import Gtk
import pango from gi.repository import GdkPixbuf
from gi.repository import Pango
import cgi import cgi
import os import os
@ -43,11 +44,11 @@ from gpodder.gtkui.interface.common import BuilderWidget
from gpodder.gtkui.interface.progress import ProgressIndicator from gpodder.gtkui.interface.progress import ProgressIndicator
from gpodder.gtkui.interface.tagcloud import TagCloud from gpodder.gtkui.interface.tagcloud import TagCloud
class DirectoryPodcastsModel(gtk.ListStore): class DirectoryPodcastsModel(Gtk.ListStore):
C_SELECTED, C_MARKUP, C_TITLE, C_URL = range(4) C_SELECTED, C_MARKUP, C_TITLE, C_URL = list(range(4))
def __init__(self, callback_can_subscribe): def __init__(self, callback_can_subscribe):
gtk.ListStore.__init__(self, bool, str, str, str) Gtk.ListStore.__init__(self, bool, str, str, str)
self.callback_can_subscribe = callback_can_subscribe self.callback_can_subscribe = callback_can_subscribe
def load(self, directory_entries): def load(self, directory_entries):
@ -74,13 +75,13 @@ class DirectoryPodcastsModel(gtk.ListStore):
return [(row[self.C_TITLE], row[self.C_URL]) for row in self if row[self.C_SELECTED]] return [(row[self.C_TITLE], row[self.C_URL]) for row in self if row[self.C_SELECTED]]
class DirectoryProvidersModel(gtk.ListStore): class DirectoryProvidersModel(Gtk.ListStore):
C_WEIGHT, C_TEXT, C_ICON, C_PROVIDER = range(4) C_WEIGHT, C_TEXT, C_ICON, C_PROVIDER = list(range(4))
SEPARATOR = (pango.WEIGHT_NORMAL, '', None, None) SEPARATOR = (Pango.Weight.NORMAL, '', None, None)
def __init__(self, providers): def __init__(self, providers):
gtk.ListStore.__init__(self, int, str, gtk.gdk.Pixbuf, object) Gtk.ListStore.__init__(self, int, str, GdkPixbuf.Pixbuf, object)
for provider in providers: for provider in providers:
self.add_provider(provider() if provider else None) self.add_provider(provider() if provider else None)
@ -89,11 +90,11 @@ class DirectoryProvidersModel(gtk.ListStore):
self.append(self.SEPARATOR) self.append(self.SEPARATOR)
else: else:
try: try:
pixbuf = gtk.gdk.pixbuf_new_from_file(os.path.join(gpodder.images_folder, provider.icon)) if provider.icon else None pixbuf = GdkPixbuf.Pixbuf.new_from_file(os.path.join(gpodder.images_folder, provider.icon)) if provider.icon else None
except Exception as e: except Exception as e:
logger.warn('Could not load icon: %s (%s)', provider.icon or '-', e) logger.warn('Could not load icon: %s (%s)', provider.icon or '-', e)
pixbuf = None pixbuf = None
self.append((pango.WEIGHT_NORMAL, provider.name, pixbuf, provider)) self.append((Pango.Weight.NORMAL, provider.name, pixbuf, provider))
def is_row_separator(self, model, it): def is_row_separator(self, model, it):
return self.get_value(it, self.C_PROVIDER) is None return self.get_value(it, self.C_PROVIDER) is None
@ -127,17 +128,17 @@ class gPodderPodcastDirectory(BuilderWidget):
self.tv_providers.set_cursor(len(self.providers_model)-1) self.tv_providers.set_cursor(len(self.providers_model)-1)
def setup_podcasts_treeview(self): def setup_podcasts_treeview(self):
column = gtk.TreeViewColumn('') column = Gtk.TreeViewColumn('')
cell = gtk.CellRendererToggle() cell = Gtk.CellRendererToggle()
column.pack_start(cell, False) column.pack_start(cell, False)
column.add_attribute(cell, 'active', DirectoryPodcastsModel.C_SELECTED) column.add_attribute(cell, 'active', DirectoryPodcastsModel.C_SELECTED)
cell.connect('toggled', lambda cell, path: self.podcasts_model.toggle(path)) cell.connect('toggled', lambda cell, path: self.podcasts_model.toggle(path))
self.tv_podcasts.append_column(column) self.tv_podcasts.append_column(column)
column = gtk.TreeViewColumn('') column = Gtk.TreeViewColumn('')
cell = gtk.CellRendererText() cell = Gtk.CellRendererText()
cell.set_property('ellipsize', pango.ELLIPSIZE_END) cell.set_property('ellipsize', Pango.EllipsizeMode.END)
column.pack_start(cell) column.pack_start(cell, True)
column.add_attribute(cell, 'markup', DirectoryPodcastsModel.C_MARKUP) column.add_attribute(cell, 'markup', DirectoryPodcastsModel.C_MARKUP)
self.tv_podcasts.append_column(column) self.tv_podcasts.append_column(column)
@ -145,13 +146,13 @@ class gPodderPodcastDirectory(BuilderWidget):
self.podcasts_model.append((False, 'a', 'b', 'c')) self.podcasts_model.append((False, 'a', 'b', 'c'))
def setup_providers_treeview(self): def setup_providers_treeview(self):
column = gtk.TreeViewColumn('') column = Gtk.TreeViewColumn('')
cell = gtk.CellRendererPixbuf() cell = Gtk.CellRendererPixbuf()
column.pack_start(cell, False) column.pack_start(cell, False)
column.add_attribute(cell, 'pixbuf', DirectoryProvidersModel.C_ICON) column.add_attribute(cell, 'pixbuf', DirectoryProvidersModel.C_ICON)
cell = gtk.CellRendererText() cell = Gtk.CellRendererText()
#cell.set_property('ellipsize', pango.ELLIPSIZE_END) #cell.set_property('ellipsize', Pango.EllipsizeMode.END)
column.pack_start(cell) column.pack_start(cell, True)
column.add_attribute(cell, 'text', DirectoryProvidersModel.C_TEXT) column.add_attribute(cell, 'text', DirectoryProvidersModel.C_TEXT)
column.add_attribute(cell, 'weight', DirectoryProvidersModel.C_WEIGHT) column.add_attribute(cell, 'weight', DirectoryProvidersModel.C_WEIGHT)
self.tv_providers.append_column(column) self.tv_providers.append_column(column)
@ -175,10 +176,10 @@ class gPodderPodcastDirectory(BuilderWidget):
it = self.providers_model.get_iter(path) it = self.providers_model.get_iter(path)
for row in self.providers_model: for row in self.providers_model:
row[DirectoryProvidersModel.C_WEIGHT] = pango.WEIGHT_NORMAL row[DirectoryProvidersModel.C_WEIGHT] = Pango.Weight.NORMAL
if it: if it:
self.providers_model.set_value(it, DirectoryProvidersModel.C_WEIGHT, pango.WEIGHT_BOLD) self.providers_model.set_value(it, DirectoryProvidersModel.C_WEIGHT, Pango.Weight.BOLD)
provider = self.providers_model.get_value(it, DirectoryProvidersModel.C_PROVIDER) provider = self.providers_model.get_value(it, DirectoryProvidersModel.C_PROVIDER)
self.use_provider(provider) self.use_provider(provider)
@ -229,7 +230,8 @@ class gPodderPodcastDirectory(BuilderWidget):
def on_tv_providers_cursor_changed(self, treeview): def on_tv_providers_cursor_changed(self, treeview):
path, column = treeview.get_cursor() path, column = treeview.get_cursor()
self.on_tv_providers_row_activated(treeview, path, column) if path is not None:
self.on_tv_providers_row_activated(treeview, path, column)
def obtain_podcasts_with(self, callback): def obtain_podcasts_with(self, callback):
if self.podcasts_progress_indicator is not None: if self.podcasts_progress_indicator is not None:
@ -260,15 +262,16 @@ class gPodderPodcastDirectory(BuilderWidget):
self.podcasts_progress_indicator.on_finished() self.podcasts_progress_indicator.on_finished()
self.podcasts_progress_indicator = None self.podcasts_progress_indicator = None
if original_provider != self.current_provider: if original_provider == self.current_provider:
self.podcasts_model.load(podcasts or [])
else:
logger.warn('Ignoring update from old thread') logger.warn('Ignoring update from old thread')
return
self.podcasts_model.load(podcasts or [])
self.en_query.set_sensitive(True) self.en_query.set_sensitive(True)
self.bt_search.set_sensitive(True) self.bt_search.set_sensitive(True)
self.tag_cloud.set_sensitive(True) self.tag_cloud.set_sensitive(True)
self.en_query.grab_focus() if self.en_query.get_realized():
self.en_query.grab_focus()
def on_bt_search_clicked(self, widget): def on_bt_search_clicked(self, widget):
if self.current_provider is None: if self.current_provider is None:

View file

@ -17,10 +17,11 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import gtk from gi.repository import Gtk
import pango from gi.repository import Gdk
from gi.repository import Pango
import cgi import cgi
import urlparse import urllib.parse
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -40,13 +41,13 @@ from gpodder.gtkui.interface.configeditor import gPodderConfigEditor
from gpodder.gtkui.desktopfile import PlayerListModel from gpodder.gtkui.desktopfile import PlayerListModel
class NewEpisodeActionList(gtk.ListStore): class NewEpisodeActionList(Gtk.ListStore):
C_CAPTION, C_AUTO_DOWNLOAD = range(2) C_CAPTION, C_AUTO_DOWNLOAD = list(range(2))
ACTION_NONE, ACTION_ASK, ACTION_MINIMIZED, ACTION_ALWAYS = range(4) ACTION_NONE, ACTION_ASK, ACTION_MINIMIZED, ACTION_ALWAYS = list(range(4))
def __init__(self, config): def __init__(self, config):
gtk.ListStore.__init__(self, str, str) Gtk.ListStore.__init__(self, str, str)
self._config = config self._config = config
self.append((_('Do nothing'), 'ignore')) self.append((_('Do nothing'), 'ignore'))
self.append((_('Show episode list'), 'show')) self.append((_('Show episode list'), 'show'))
@ -63,11 +64,11 @@ class NewEpisodeActionList(gtk.ListStore):
def set_index(self, index): def set_index(self, index):
self._config.auto_download = self[index][self.C_AUTO_DOWNLOAD] self._config.auto_download = self[index][self.C_AUTO_DOWNLOAD]
class DeviceTypeActionList(gtk.ListStore): class DeviceTypeActionList(Gtk.ListStore):
C_CAPTION, C_DEVICE_TYPE = range(2) C_CAPTION, C_DEVICE_TYPE = list(range(2))
def __init__(self, config): def __init__(self, config):
gtk.ListStore.__init__(self, str, str) Gtk.ListStore.__init__(self, str, str)
self._config = config self._config = config
self.append((_('None'), 'none')) self.append((_('None'), 'none'))
self.append((_('iPod'), 'ipod')) self.append((_('iPod'), 'ipod'))
@ -83,12 +84,12 @@ class DeviceTypeActionList(gtk.ListStore):
self._config.device_sync.device_type = self[index][self.C_DEVICE_TYPE] self._config.device_sync.device_type = self[index][self.C_DEVICE_TYPE]
class OnSyncActionList(gtk.ListStore): class OnSyncActionList(Gtk.ListStore):
C_CAPTION, C_ON_SYNC_DELETE, C_ON_SYNC_MARK_PLAYED = range(3) C_CAPTION, C_ON_SYNC_DELETE, C_ON_SYNC_MARK_PLAYED = list(range(3))
ACTION_NONE, ACTION_ASK, ACTION_MINIMIZED, ACTION_ALWAYS = range(4) ACTION_NONE, ACTION_ASK, ACTION_MINIMIZED, ACTION_ALWAYS = list(range(4))
def __init__(self, config): def __init__(self, config):
gtk.ListStore.__init__(self, str, bool, bool) Gtk.ListStore.__init__(self, str, bool, bool)
self._config = config self._config = config
self.append((_('Do nothing'), False, False)) self.append((_('Do nothing'), False, False))
self.append((_('Mark as played'), False, True)) self.append((_('Mark as played'), False, True))
@ -111,11 +112,11 @@ class OnSyncActionList(gtk.ListStore):
class YouTubeVideoFormatListModel(gtk.ListStore): class YouTubeVideoFormatListModel(Gtk.ListStore):
C_CAPTION, C_ID = range(2) C_CAPTION, C_ID = list(range(2))
def __init__(self, config): def __init__(self, config):
gtk.ListStore.__init__(self, str, int) Gtk.ListStore.__init__(self, str, int)
self._config = config self._config = config
self.custom_fmt_ids = self._config.youtube.preferred_fmt_ids self.custom_fmt_ids = self._config.youtube.preferred_fmt_ids
@ -150,11 +151,11 @@ class YouTubeVideoFormatListModel(gtk.ListStore):
self._config.youtube.preferred_fmt_ids = self.custom_fmt_ids self._config.youtube.preferred_fmt_ids = self.custom_fmt_ids
class VimeoVideoFormatListModel(gtk.ListStore): class VimeoVideoFormatListModel(Gtk.ListStore):
C_CAPTION, C_ID = range(2) C_CAPTION, C_ID = list(range(2))
def __init__(self, config): def __init__(self, config):
gtk.ListStore.__init__(self, str, str) Gtk.ListStore.__init__(self, str, str)
self._config = config self._config = config
for fileformat, description in vimeo.FORMATS: for fileformat, description in vimeo.FORMATS:
@ -168,20 +169,20 @@ class VimeoVideoFormatListModel(gtk.ListStore):
def set_index(self, index): def set_index(self, index):
value = self[index][self.C_ID] value = self[index][self.C_ID]
if value > 0: if value is not None:
self._config.vimeo.fileformat = value self._config.vimeo.fileformat = value
class gPodderPreferences(BuilderWidget): class gPodderPreferences(BuilderWidget):
C_TOGGLE, C_LABEL, C_EXTENSION, C_SHOW_TOGGLE = range(4) C_TOGGLE, C_LABEL, C_EXTENSION, C_SHOW_TOGGLE = list(range(4))
def new(self): def new(self):
for cb in (self.combo_audio_player_app, self.combo_video_player_app): for cb in (self.combo_audio_player_app, self.combo_video_player_app):
cellrenderer = gtk.CellRendererPixbuf() cellrenderer = Gtk.CellRendererPixbuf()
cb.pack_start(cellrenderer, False) cb.pack_start(cellrenderer, False)
cb.add_attribute(cellrenderer, 'pixbuf', PlayerListModel.C_ICON) cb.add_attribute(cellrenderer, 'pixbuf', PlayerListModel.C_ICON)
cellrenderer = gtk.CellRendererText() cellrenderer = Gtk.CellRendererText()
cellrenderer.set_property('ellipsize', pango.ELLIPSIZE_END) cellrenderer.set_property('ellipsize', Pango.EllipsizeMode.END)
cb.pack_start(cellrenderer, True) cb.pack_start(cellrenderer, True)
cb.add_attribute(cellrenderer, 'markup', PlayerListModel.C_NAME) cb.add_attribute(cellrenderer, 'markup', PlayerListModel.C_NAME)
cb.set_row_separator_func(PlayerListModel.is_separator) cb.set_row_separator_func(PlayerListModel.is_separator)
@ -198,14 +199,14 @@ class gPodderPreferences(BuilderWidget):
self.preferred_youtube_format_model = YouTubeVideoFormatListModel(self._config) self.preferred_youtube_format_model = YouTubeVideoFormatListModel(self._config)
self.combobox_preferred_youtube_format.set_model(self.preferred_youtube_format_model) self.combobox_preferred_youtube_format.set_model(self.preferred_youtube_format_model)
cellrenderer = gtk.CellRendererText() cellrenderer = Gtk.CellRendererText()
self.combobox_preferred_youtube_format.pack_start(cellrenderer, True) self.combobox_preferred_youtube_format.pack_start(cellrenderer, True)
self.combobox_preferred_youtube_format.add_attribute(cellrenderer, 'text', self.preferred_youtube_format_model.C_CAPTION) self.combobox_preferred_youtube_format.add_attribute(cellrenderer, 'text', self.preferred_youtube_format_model.C_CAPTION)
self.combobox_preferred_youtube_format.set_active(self.preferred_youtube_format_model.get_index()) self.combobox_preferred_youtube_format.set_active(self.preferred_youtube_format_model.get_index())
self.preferred_vimeo_format_model = VimeoVideoFormatListModel(self._config) self.preferred_vimeo_format_model = VimeoVideoFormatListModel(self._config)
self.combobox_preferred_vimeo_format.set_model(self.preferred_vimeo_format_model) self.combobox_preferred_vimeo_format.set_model(self.preferred_vimeo_format_model)
cellrenderer = gtk.CellRendererText() cellrenderer = Gtk.CellRendererText()
self.combobox_preferred_vimeo_format.pack_start(cellrenderer, True) self.combobox_preferred_vimeo_format.pack_start(cellrenderer, True)
self.combobox_preferred_vimeo_format.add_attribute(cellrenderer, 'text', self.preferred_vimeo_format_model.C_CAPTION) self.combobox_preferred_vimeo_format.add_attribute(cellrenderer, 'text', self.preferred_vimeo_format_model.C_CAPTION)
self.combobox_preferred_vimeo_format.set_active(self.preferred_vimeo_format_model.get_index()) self.combobox_preferred_vimeo_format.set_active(self.preferred_vimeo_format_model.get_index())
@ -217,7 +218,7 @@ class gPodderPreferences(BuilderWidget):
self.update_interval_presets = [0, 10, 30, 60, 2*60, 6*60, 12*60] self.update_interval_presets = [0, 10, 30, 60, 2*60, 6*60, 12*60]
adjustment_update_interval = self.hscale_update_interval.get_adjustment() adjustment_update_interval = self.hscale_update_interval.get_adjustment()
adjustment_update_interval.upper = len(self.update_interval_presets)-1 adjustment_update_interval.set_upper(len(self.update_interval_presets)-1)
if self._config.auto_update_frequency in self.update_interval_presets: if self._config.auto_update_frequency in self.update_interval_presets:
index = self.update_interval_presets.index(self._config.auto_update_frequency) index = self.update_interval_presets.index(self._config.auto_update_frequency)
self.hscale_update_interval.set_value(index) self.hscale_update_interval.set_value(index)
@ -226,7 +227,7 @@ class gPodderPreferences(BuilderWidget):
self.update_interval_presets.append(self._config.auto_update_frequency) self.update_interval_presets.append(self._config.auto_update_frequency)
self.update_interval_presets.sort() self.update_interval_presets.sort()
adjustment_update_interval.upper = len(self.update_interval_presets)-1 adjustment_update_interval.set_upper(len(self.update_interval_presets)-1)
index = self.update_interval_presets.index(self._config.auto_update_frequency) index = self.update_interval_presets.index(self._config.auto_update_frequency)
self.hscale_update_interval.set_value(index) self.hscale_update_interval.set_value(index)
@ -234,7 +235,7 @@ class gPodderPreferences(BuilderWidget):
self.auto_download_model = NewEpisodeActionList(self._config) self.auto_download_model = NewEpisodeActionList(self._config)
self.combo_auto_download.set_model(self.auto_download_model) self.combo_auto_download.set_model(self.auto_download_model)
cellrenderer = gtk.CellRendererText() cellrenderer = Gtk.CellRendererText()
self.combo_auto_download.pack_start(cellrenderer, True) self.combo_auto_download.pack_start(cellrenderer, True)
self.combo_auto_download.add_attribute(cellrenderer, 'text', NewEpisodeActionList.C_CAPTION) self.combo_auto_download.add_attribute(cellrenderer, 'text', NewEpisodeActionList.C_CAPTION)
self.combo_auto_download.set_active(self.auto_download_model.get_index()) self.combo_auto_download.set_active(self.auto_download_model.get_index())
@ -243,7 +244,7 @@ class gPodderPreferences(BuilderWidget):
adjustment_expiration = self.hscale_expiration.get_adjustment() adjustment_expiration = self.hscale_expiration.get_adjustment()
if self._config.episode_old_age > adjustment_expiration.get_upper(): if self._config.episode_old_age > adjustment_expiration.get_upper():
# Patch the adjustment to include the higher current value # Patch the adjustment to include the higher current value
adjustment_expiration.upper = self._config.episode_old_age adjustment_expiration.set_upper(self._config.episode_old_age)
self.hscale_expiration.set_value(self._config.episode_old_age) self.hscale_expiration.set_value(self._config.episode_old_age)
else: else:
@ -256,7 +257,7 @@ class gPodderPreferences(BuilderWidget):
self.device_type_model = DeviceTypeActionList(self._config) self.device_type_model = DeviceTypeActionList(self._config)
self.combobox_device_type.set_model(self.device_type_model) self.combobox_device_type.set_model(self.device_type_model)
cellrenderer = gtk.CellRendererText() cellrenderer = Gtk.CellRendererText()
self.combobox_device_type.pack_start(cellrenderer, True) self.combobox_device_type.pack_start(cellrenderer, True)
self.combobox_device_type.add_attribute(cellrenderer, 'text', self.combobox_device_type.add_attribute(cellrenderer, 'text',
DeviceTypeActionList.C_CAPTION) DeviceTypeActionList.C_CAPTION)
@ -264,7 +265,7 @@ class gPodderPreferences(BuilderWidget):
self.on_sync_model = OnSyncActionList(self._config) self.on_sync_model = OnSyncActionList(self._config)
self.combobox_on_sync.set_model(self.on_sync_model) self.combobox_on_sync.set_model(self.on_sync_model)
cellrenderer = gtk.CellRendererText() cellrenderer = Gtk.CellRendererText()
self.combobox_on_sync.pack_start(cellrenderer, True) self.combobox_on_sync.pack_start(cellrenderer, True)
self.combobox_on_sync.add_attribute(cellrenderer, 'text', OnSyncActionList.C_CAPTION) self.combobox_on_sync.add_attribute(cellrenderer, 'text', OnSyncActionList.C_CAPTION)
self.combobox_on_sync.set_active(self.on_sync_model.get_index()) self.combobox_on_sync.set_active(self.on_sync_model.get_index())
@ -292,12 +293,16 @@ class gPodderPreferences(BuilderWidget):
# Configure the extensions manager GUI # Configure the extensions manager GUI
self.set_extension_preferences() self.set_extension_preferences()
self.main_window.show()
def _extensions_select_function(self, selection, model, path, path_currently_selected):
return model.get_value(model.get_iter(path), self.C_SHOW_TOGGLE)
def set_extension_preferences(self): def set_extension_preferences(self):
def search_equal_func(model, column, key, it): def search_equal_func(model, column, key, it):
label = model.get_value(it, self.C_LABEL) label = model.get_value(it, self.C_LABEL)
if key.lower() in label.lower(): if key.lower() in label.lower():
# from http://www.pygtk.org/docs/pygtk/class-gtktreeview.html: # from http://www.pyGtk.org/docs/pygtk/class-gtktreeview.html:
# "func should return False to indicate that the row matches # "func should return False to indicate that the row matches
# the search criteria." # the search criteria."
return False return False
@ -305,24 +310,27 @@ class gPodderPreferences(BuilderWidget):
return True return True
self.treeviewExtensions.set_search_equal_func(search_equal_func) self.treeviewExtensions.set_search_equal_func(search_equal_func)
toggle_cell = gtk.CellRendererToggle() selection = self.treeviewExtensions.get_selection()
selection.set_select_function(self._extensions_select_function)
toggle_cell = Gtk.CellRendererToggle()
toggle_cell.connect('toggled', self.on_extensions_cell_toggled) toggle_cell.connect('toggled', self.on_extensions_cell_toggled)
toggle_column = gtk.TreeViewColumn('') toggle_column = Gtk.TreeViewColumn('')
toggle_column.pack_start(toggle_cell, True) toggle_column.pack_start(toggle_cell, True)
toggle_column.add_attribute(toggle_cell, 'active', self.C_TOGGLE) toggle_column.add_attribute(toggle_cell, 'active', self.C_TOGGLE)
toggle_column.add_attribute(toggle_cell, 'visible', self.C_SHOW_TOGGLE) toggle_column.add_attribute(toggle_cell, 'visible', self.C_SHOW_TOGGLE)
toggle_column.set_property('min-width', 32) toggle_column.set_property('min-width', 32)
self.treeviewExtensions.append_column(toggle_column) self.treeviewExtensions.append_column(toggle_column)
name_cell = gtk.CellRendererText() name_cell = Gtk.CellRendererText()
name_cell.set_property('ellipsize', pango.ELLIPSIZE_END) name_cell.set_property('ellipsize', Pango.EllipsizeMode.END)
extension_column = gtk.TreeViewColumn(_('Name')) extension_column = Gtk.TreeViewColumn(_('Name'))
extension_column.pack_start(name_cell, True) extension_column.pack_start(name_cell, True)
extension_column.add_attribute(name_cell, 'markup', self.C_LABEL) extension_column.add_attribute(name_cell, 'markup', self.C_LABEL)
extension_column.set_expand(True) extension_column.set_expand(True)
self.treeviewExtensions.append_column(extension_column) self.treeviewExtensions.append_column(extension_column)
self.extensions_model = gtk.ListStore(bool, str, object, bool) self.extensions_model = Gtk.ListStore(bool, str, object, bool)
def key_func(pair): def key_func(pair):
category, container = pair category, container = pair
@ -351,7 +359,7 @@ class gPodderPreferences(BuilderWidget):
if event.window != treeview.get_bin_window(): if event.window != treeview.get_bin_window():
return False return False
if event.type == gtk.gdk.BUTTON_RELEASE and event.button == 3: if event.type == Gdk.EventType.BUTTON_RELEASE and event.button == 3:
return self.on_treeview_extension_show_context_menu(treeview, event) return self.on_treeview_extension_show_context_menu(treeview, event)
return False return False
@ -364,29 +372,30 @@ class gPodderPreferences(BuilderWidget):
if not container: if not container:
return return
menu = gtk.Menu() menu = Gtk.Menu()
if container.metadata.doc: if container.metadata.doc:
menu_item = gtk.MenuItem(_('Documentation')) menu_item = Gtk.MenuItem(_('Documentation'))
menu_item.connect('activate', self.open_weblink, menu_item.connect('activate', self.open_weblink,
container.metadata.doc) container.metadata.doc)
menu.append(menu_item) menu.append(menu_item)
menu_item = gtk.MenuItem(_('Extension info')) menu_item = Gtk.MenuItem(_('Extension info'))
menu_item.connect('activate', self.show_extension_info, model, container) menu_item.connect('activate', self.show_extension_info, model, container)
menu.append(menu_item) menu.append(menu_item)
if container.metadata.payment: if container.metadata.payment:
menu_item = gtk.MenuItem(_('Support the author')) menu_item = Gtk.MenuItem(_('Support the author'))
menu_item.connect('activate', self.open_weblink, container.metadata.payment) menu_item.connect('activate', self.open_weblink, container.metadata.payment)
menu.append(menu_item) menu.append(menu_item)
menu.show_all() menu.show_all()
if event is None: if event is None:
func = TreeViewHelper.make_popup_position_func(treeview) func = TreeViewHelper.make_popup_position_func(treeview)
menu.popup(None, None, func, 3, 0) menu.popup(None, None, func, None, 3, Gtk.get_current_event_time())
else: else:
menu.popup(None, None, None, 3, 0) menu.popup(None, None, None, None, 3, Gtk.get_current_event_time())
return True return True
@ -414,7 +423,11 @@ class gPodderPreferences(BuilderWidget):
else: else:
self.on_extension_disabled(container.module) self.on_extension_disabled(container.module)
elif container.error is not None: elif container.error is not None:
self.show_message(container.error.message, if hasattr(container.error, 'message'):
error_msg = container.error.message
else:
error_msg = str(container.error)
self.show_message(error_msg,
_('Extension cannot be activated'), important=True) _('Extension cannot be activated'), important=True)
model.set_value(it, self.C_TOGGLE, False) model.set_value(it, self.C_TOGGLE, False)
@ -425,7 +438,7 @@ class gPodderPreferences(BuilderWidget):
# This is one ugly hack, but it displays the attributes of # This is one ugly hack, but it displays the attributes of
# the metadata object of the container.. # the metadata object of the container..
info = '\n'.join('<b>%s:</b> %s' % info = '\n'.join('<b>%s:</b> %s' %
tuple(map(cgi.escape, map(str, (key, value)))) tuple(map(cgi.escape, list(map(str, (key, value)))))
for key, value in container.metadata.get_sorted()) for key, value in container.metadata.get_sorted())
self.show_message(info, _('Extension module info'), important=True) self.show_message(info, _('Extension module info'), important=True)
@ -486,12 +499,19 @@ class gPodderPreferences(BuilderWidget):
def format_update_interval_value(self, scale, value): def format_update_interval_value(self, scale, value):
value = int(value) value = int(value)
ret = None
if value == 0: if value == 0:
return _('manually') ret = _('manually')
elif value > 0 and len(self.update_interval_presets) > value: elif value > 0 and len(self.update_interval_presets) > value:
return util.format_seconds_to_hour_min_sec(self.update_interval_presets[value]*60) ret = util.format_seconds_to_hour_min_sec(self.update_interval_presets[value]*60)
else: else:
return str(value) ret = str(value)
# bug in gtk3: value representation (pixels) must be smaller than value for highest value.
# this makes sense when formatting e.g. 0 to 1000 where '1000' is the longest
# string, but not when '10 minutes' is longer than '12 hours'
# so we replace spaces with non breaking spaces otherwise '10 minutes' is displayed as '10'
ret = ret.replace(' ', '\xa0')
return ret
def on_update_interval_value_changed(self, range): def on_update_interval_value_changed(self, range):
value = int(range.get_value()) value = int(range.get_value())
@ -626,12 +646,12 @@ class gPodderPreferences(BuilderWidget):
pass pass
def on_btn_device_mountpoint_clicked(self, widget): def on_btn_device_mountpoint_clicked(self, widget):
fs = gtk.FileChooserDialog(title=_('Select folder for mount point'), fs = Gtk.FileChooserDialog(title=_('Select folder for mount point'),
action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) action=Gtk.FileChooserAction.SELECT_FOLDER)
fs.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) fs.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
fs.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK) fs.add_button(Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
fs.set_current_folder(self.btn_filesystemMountpoint.get_label()) fs.set_current_folder(self.btn_filesystemMountpoint.get_label())
if fs.run() == gtk.RESPONSE_OK: if fs.run() == Gtk.ResponseType.OK:
filename = fs.get_filename() filename = fs.get_filename()
if self._config.device_sync.device_type == 'filesystem': if self._config.device_sync.device_type == 'filesystem':
self._config.device_sync.device_folder = filename self._config.device_sync.device_folder = filename
@ -643,12 +663,12 @@ class gPodderPreferences(BuilderWidget):
fs.destroy() fs.destroy()
def on_btn_playlist_folder_clicked(self, widget): def on_btn_playlist_folder_clicked(self, widget):
fs = gtk.FileChooserDialog(title=_('Select folder for playlists'), fs = Gtk.FileChooserDialog(title=_('Select folder for playlists'),
action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) action=Gtk.FileChooserAction.SELECT_FOLDER)
fs.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) fs.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
fs.add_button(gtk.STOCK_OPEN, gtk.RESPONSE_OK) fs.add_button(Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
fs.set_current_folder(self.btn_playlistfolder.get_label()) fs.set_current_folder(self.btn_playlistfolder.get_label())
if fs.run() == gtk.RESPONSE_OK: if fs.run() == Gtk.ResponseType.OK:
filename = util.relpath(self._config.device_sync.device_folder, filename = util.relpath(self._config.device_sync.device_folder,
fs.get_filename()) fs.get_filename())
if self._config.device_sync.device_type == 'filesystem': if self._config.device_sync.device_type == 'filesystem':

View file

@ -37,7 +37,7 @@ logger = logging.getLogger(__name__)
class gPodderSyncUI(object): class gPodderSyncUI(object):
def __init__(self, config, notification, parent_window, def __init__(self, config, notification, parent_window,
show_confirmation, show_confirmation,
preferences_widget, show_preferences,
channels, channels,
download_status_model, download_status_model,
download_queue_manager, download_queue_manager,
@ -51,7 +51,7 @@ class gPodderSyncUI(object):
self.parent_window = parent_window self.parent_window = parent_window
self.show_confirmation = show_confirmation self.show_confirmation = show_confirmation
self.preferences_widget = preferences_widget self.show_preferences = show_preferences
self.channels=channels self.channels=channels
self.download_status_model = download_status_model self.download_status_model = download_status_model
self.download_queue_manager = download_queue_manager self.download_queue_manager = download_queue_manager
@ -81,12 +81,13 @@ class gPodderSyncUI(object):
def _show_message_unconfigured(self): def _show_message_unconfigured(self):
title = _('No device configured') title = _('No device configured')
message = _('Please set up your device in the preferences dialog.') message = _('Please set up your device in the preferences dialog.')
self.notification(message, title, widget=self.preferences_widget, important=True) if self.show_confirmation(message, title):
self.show_preferences(self.parent_window, None)
def _show_message_cannot_open(self): def _show_message_cannot_open(self):
title = _('Cannot open device') title = _('Cannot open device')
message = _('Please check the settings in the preferences dialog.') message = _('Please check the settings in the preferences dialog.')
self.notification(message, title, widget=self.preferences_widget, important=True) self.notification(message, title, important=True)
def on_synchronize_episodes(self, channels, episodes=None, force_played=True): def on_synchronize_episodes(self, channels, episodes=None, force_played=True):
device = sync.open_device(self) device = sync.open_device(self)
@ -185,7 +186,7 @@ class gPodderSyncUI(object):
key=lambda ep: ep.published) key=lambda ep: ep.published)
#don't add played episodes to playlist if skip_played_episodes is True #don't add played episodes to playlist if skip_played_episodes is True
if self._config.device_sync.skip_played_episodes: if self._config.device_sync.skip_played_episodes:
episodes_for_playlist=filter(lambda ep: ep.is_new, episodes_for_playlist) episodes_for_playlist=[ep for ep in episodes_for_playlist if ep.is_new]
playlist.write_m3u(episodes_for_playlist) playlist.write_m3u(episodes_for_playlist)
#enable updating of UI #enable updating of UI
@ -194,7 +195,7 @@ class gPodderSyncUI(object):
if (self._config.device_sync.device_type=='filesystem' and self._config.device_sync.playlists.create): if (self._config.device_sync.device_type=='filesystem' and self._config.device_sync.playlists.create):
title = _('Update successful') title = _('Update successful')
message = _('The playlist on your MP3 player has been updated.') message = _('The playlist on your MP3 player has been updated.')
self.notification(message, title, widget=self.preferences_widget) self.notification(message, title)
# Finally start the synchronization process # Finally start the synchronization process
@util.run_in_background @util.run_in_background
@ -216,10 +217,10 @@ class gPodderSyncUI(object):
#get episodes to be written to playlist #get episodes to be written to playlist
episodes_for_playlist=sorted(current_channel.get_episodes(gpodder.STATE_DOWNLOADED), episodes_for_playlist=sorted(current_channel.get_episodes(gpodder.STATE_DOWNLOADED),
key=lambda ep: ep.published) key=lambda ep: ep.published)
episode_keys=map(playlist.get_absolute_filename_for_playlist, episode_keys=list(map(playlist.get_absolute_filename_for_playlist,
episodes_for_playlist) episodes_for_playlist))
episode_dict=dict(zip(episode_keys, episodes_for_playlist)) episode_dict=dict(list(zip(episode_keys, episodes_for_playlist)))
#then get episodes in playlist (if it exists) already on device #then get episodes in playlist (if it exists) already on device
episodes_in_playlists = playlist.read_m3u() episodes_in_playlists = playlist.read_m3u()
@ -233,7 +234,7 @@ class gPodderSyncUI(object):
#i.e. must have been deleted by user, so delete from gpodder #i.e. must have been deleted by user, so delete from gpodder
try: try:
episodes_to_delete.append(episode_dict[episode_filename]) episodes_to_delete.append(episode_dict[episode_filename])
except KeyError, ioe: except KeyError as ioe:
logger.warn('Episode %s, removed from device has already been deleted from gpodder', logger.warn('Episode %s, removed from device has already been deleted from gpodder',
episode_filename) episode_filename)
@ -277,10 +278,10 @@ class gPodderSyncUI(object):
logger.warning("Starting sync - no episodes to delete") logger.warning("Starting sync - no episodes to delete")
resume_sync([],[],None) resume_sync([],[],None)
except IOError, ioe: except IOError as ioe:
title = _('Error writing playlist files') title = _('Error writing playlist files')
message = _(str(ioe)) message = _(str(ioe))
self.notification(message, title, widget=self.preferences_widget) self.notification(message, title)
else: else:
logger.info ('Not creating playlists - starting sync') logger.info ('Not creating playlists - starting sync')
resume_sync([],[],None) resume_sync([],[],None)

View file

@ -17,7 +17,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import gtk from gi.repository import Gtk
import gpodder import gpodder
@ -31,12 +31,12 @@ class gPodderWelcome(BuilderWidget):
def new(self): def new(self):
for widget in self.vbox_buttons.get_children(): for widget in self.vbox_buttons.get_children():
for child in widget.get_children(): for child in widget.get_children():
if isinstance(child, gtk.Alignment): if isinstance(child, Gtk.Alignment):
child.set_padding(self.PADDING, self.PADDING, child.set_padding(self.PADDING, self.PADDING,
self.PADDING, self.PADDING) self.PADDING, self.PADDING)
else: else:
child.set_padding(self.PADDING, self.PADDING) child.set_padding(self.PADDING, self.PADDING)
def on_btnCancel_clicked(self, button): def on_btnCancel_clicked(self, button):
self.main_window.response(gtk.RESPONSE_CANCEL) self.main_window.response(Gtk.ResponseType.CANCEL)

View file

@ -30,11 +30,11 @@ import os
import os.path import os.path
import threading import threading
from ConfigParser import RawConfigParser from configparser import RawConfigParser
import gobject from gi.repository import GObject
import gtk from gi.repository import GdkPixbuf
import gtk.gdk from gi.repository import Gtk
import gpodder import gpodder
@ -49,11 +49,11 @@ userappsdirs = [ '/usr/share/applications/', '/usr/local/share/applications/', '
# the name of the section in the .desktop files # the name of the section in the .desktop files
sect = 'Desktop Entry' sect = 'Desktop Entry'
class PlayerListModel(gtk.ListStore): class PlayerListModel(Gtk.ListStore):
C_ICON, C_NAME, C_COMMAND, C_CUSTOM = range(4) C_ICON, C_NAME, C_COMMAND, C_CUSTOM = list(range(4))
def __init__(self): def __init__(self):
gtk.ListStore.__init__(self, gtk.gdk.Pixbuf, str, str, bool) Gtk.ListStore.__init__(self, GdkPixbuf.Pixbuf, str, str, bool)
def insert_app(self, pixbuf, name, command): def insert_app(self, pixbuf, name, command):
self.append((pixbuf, name, command, False)) self.append((pixbuf, name, command, False))
@ -92,13 +92,13 @@ class UserApplication(object):
# Load it from an absolute filename # Load it from an absolute filename
if os.path.exists(self.icon): if os.path.exists(self.icon):
try: try:
return gtk.gdk.pixbuf_new_from_file_at_size(self.icon, 24, 24) return GdkPixbuf.Pixbuf.new_from_file_at_size(self.icon, 24, 24)
except gobject.GError, ge: except GObject.GError as ge:
pass pass
# Load it from the current icon theme # Load it from the current icon theme
(icon_name, extension) = os.path.splitext(os.path.basename(self.icon)) (icon_name, extension) = os.path.splitext(os.path.basename(self.icon))
theme = gtk.IconTheme() theme = Gtk.IconTheme()
if theme.has_icon(icon_name): if theme.has_icon(icon_name):
return theme.load_icon(icon_name, 24, 0) return theme.load_icon(icon_name, 24, 0)
@ -116,23 +116,23 @@ WIN32_APP_REG_KEYS = [
def win32_read_registry_key(path): def win32_read_registry_key(path):
import _winreg import winreg
rootmap = { rootmap = {
'HKEY_CLASSES_ROOT': _winreg.HKEY_CLASSES_ROOT, 'HKEY_CLASSES_ROOT': winreg.HKEY_CLASSES_ROOT,
} }
parts = path.split('\\') parts = path.split('\\')
root = parts.pop(0) root = parts.pop(0)
key = _winreg.OpenKey(rootmap[root], parts.pop(0)) key = winreg.OpenKey(rootmap[root], parts.pop(0))
while parts: while parts:
key = _winreg.OpenKey(key, parts.pop(0)) key = winreg.OpenKey(key, parts.pop(0))
value, type_ = _winreg.QueryValueEx(key, '') value, type_ = winreg.QueryValueEx(key, '')
if type_ == _winreg.REG_EXPAND_SZ: if type_ == winreg.REG_EXPAND_SZ:
cmdline = re.sub(r'%([^%]+)%', lambda m: os.environ[m.group(1)], value) cmdline = re.sub(r'%([^%]+)%', lambda m: os.environ[m.group(1)], value)
elif type_ == _winreg.REG_SZ: elif type_ == winreg.REG_SZ:
cmdline = value cmdline = value
else: else:
raise ValueError('Not a string: ' + path) raise ValueError('Not a string: ' + path)
@ -151,7 +151,7 @@ class UserAppsReader(object):
self.__has_read = False self.__has_read = False
self.__finished = threading.Event() self.__finished = threading.Event()
self.__has_sep = False self.__has_sep = False
self.apps.append(UserApplication(_('Default application'), 'default', ';'.join((mime+'/*' for mime in self.mimetypes)), gtk.STOCK_OPEN)) self.apps.append(UserApplication(_('Default application'), 'default', ';'.join((mime+'/*' for mime in self.mimetypes)), Gtk.STOCK_OPEN))
def add_separator(self): def add_separator(self):
self.apps.append(UserApplication('', '', ';'.join((mime+'/*' for mime in self.mimetypes)), '')) self.apps.append(UserApplication('', '', ';'.join((mime+'/*' for mime in self.mimetypes)), ''))
@ -163,7 +163,7 @@ class UserAppsReader(object):
self.__has_read = True self.__has_read = True
if gpodder.ui.win32: if gpodder.ui.win32:
import _winreg import winreg
for caption, types, hkey in WIN32_APP_REG_KEYS: for caption, types, hkey in WIN32_APP_REG_KEYS:
try: try:
cmdline = win32_read_registry_key(hkey) cmdline = win32_read_registry_key(hkey)

View file

@ -23,35 +23,40 @@
# Based on code from gpodder.services (thp, 2007-08-24) # Based on code from gpodder.services (thp, 2007-08-24)
# #
import gpodder import gpodder
from gpodder import util from gpodder import util
from gpodder import download from gpodder import download
import gtk from gi.repository import Gtk
import cgi import cgi
import collections import collections
import threading
_ = gpodder.gettext _ = gpodder.gettext
class DownloadStatusModel(gtk.ListStore): class DownloadStatusModel(Gtk.ListStore):
# Symbolic names for our columns, so we know what we're up to # Symbolic names for our columns, so we know what we're up to
C_TASK, C_NAME, C_URL, C_PROGRESS, C_PROGRESS_TEXT, C_ICON_NAME = range(6) C_TASK, C_NAME, C_URL, C_PROGRESS, C_PROGRESS_TEXT, C_ICON_NAME = list(range(6))
SEARCH_COLUMNS = (C_NAME, C_URL) SEARCH_COLUMNS = (C_NAME, C_URL)
def __init__(self): def __init__(self):
gtk.ListStore.__init__(self, object, str, str, int, str, str) Gtk.ListStore.__init__(self, object, str, str, int, str, str)
self.set_downloading_access = threading.RLock()
# Set up stock icon IDs for tasks # Set up stock icon IDs for tasks
self._status_ids = collections.defaultdict(lambda: None) self._status_ids = collections.defaultdict(lambda: None)
self._status_ids[download.DownloadTask.DOWNLOADING] = gtk.STOCK_GO_DOWN self._status_ids[download.DownloadTask.DOWNLOADING] = 'go-down'
self._status_ids[download.DownloadTask.DONE] = gtk.STOCK_APPLY self._status_ids[download.DownloadTask.DONE] = Gtk.STOCK_APPLY
self._status_ids[download.DownloadTask.FAILED] = gtk.STOCK_STOP self._status_ids[download.DownloadTask.FAILED] = 'dialog-error'
self._status_ids[download.DownloadTask.CANCELLED] = gtk.STOCK_CANCEL self._status_ids[download.DownloadTask.CANCELLED] = 'media-playback-stop'
self._status_ids[download.DownloadTask.PAUSED] = gtk.STOCK_MEDIA_PAUSE self._status_ids[download.DownloadTask.PAUSED] = 'media-playback-pause'
def _format_message(self, episode, message, podcast): def _format_message(self, episode, message, podcast):
episode = cgi.escape(episode) episode = cgi.escape(episode)
@ -138,6 +143,27 @@ class DownloadStatusModel(gtk.ListStore):
return False return False
def has_work(self):
return any(task for task in
(row[DownloadStatusModel.C_TASK] for row in self)
if task.status == task.QUEUED)
def get_next(self):
with self.set_downloading_access:
result = next(task for task in
(row[DownloadStatusModel.C_TASK] for row in self)
if task.status == task.QUEUED)
self.set_downloading(result)
return result
def set_downloading(self, task):
with self.set_downloading_access:
if task.status is task.DOWNLOADING:
# Task was already set as DOWNLOADING by get_next
return False
task.status = task.DOWNLOADING
return True
class DownloadTaskMonitor(object): class DownloadTaskMonitor(object):
"""A helper class that abstracts download events""" """A helper class that abstracts download events"""

View file

@ -25,11 +25,18 @@
import gpodder import gpodder
import gtk import gi
import pango gi.require_version('PangoCairo', '1.0')
import pangocairo
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GdkPixbuf
from gi.repository import Pango
from gi.repository import PangoCairo
import cairo import cairo
import StringIO
import io
import math import math
@ -87,25 +94,25 @@ def rounded_rectangle(ctx, x, y, width, height, radius=4.):
def draw_text_box_centered(ctx, widget, w_width, w_height, text, font_desc=None, add_progress=None): def draw_text_box_centered(ctx, widget, w_width, w_height, text, font_desc=None, add_progress=None):
style = widget.rc_get_style() style_context = widget.get_style_context()
text_color = style.text[gtk.STATE_PRELIGHT] text_color = style_context.get_color(Gtk.StateFlags.PRELIGHT)
red, green, blue = text_color.red, text_color.green, text_color.blue red, green, blue = text_color.red, text_color.green, text_color.blue
text_color = [float(x)/65535. for x in (red, green, blue)] text_color = [x/65535. for x in (red, green, blue)]
text_color.append(.5) text_color.append(.5)
if font_desc is None: if font_desc is None:
font_desc = style.font_desc font_desc = style_context.get_font(Gtk.StateFlags.NORMAL)
font_desc.set_size(14*pango.SCALE) font_desc.set_size(14*Pango.SCALE)
pango_context = widget.create_pango_context() pango_context = widget.create_pango_context()
layout = pango.Layout(pango_context) layout = Pango.Layout(pango_context)
layout.set_font_description(font_desc) layout.set_font_description(font_desc)
layout.set_text(text) layout.set_text(text, -1)
width, height = layout.get_pixel_size() width, height = layout.get_pixel_size()
ctx.move_to(w_width/2-width/2, w_height/2-height/2) ctx.move_to(w_width/2-width/2, w_height/2-height/2)
ctx.set_source_rgba(*text_color) ctx.set_source_rgba(*text_color)
ctx.show_layout(layout) PangoCairo.show_layout(ctx, layout)
# Draw an optional progress bar below the text (same width) # Draw an optional progress bar below the text (same width)
if add_progress is not None: if add_progress is not None:
@ -126,13 +133,17 @@ def draw_cake(percentage, text=None, emblem=None, size=None):
size = EPISODE_LIST_ICON_SIZE size = EPISODE_LIST_ICON_SIZE
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size, size) surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size, size)
ctx = pangocairo.CairoContext(cairo.Context(surface)) ctx = cairo.Context(surface)
widget = gtk.ProgressBar() # ELL: get all black
style = widget.rc_get_style() #widget = Gtk.ProgressBar()
bgc = style.bg[gtk.STATE_NORMAL] #style_context = widget.get_style_context()
fgc = style.bg[gtk.STATE_SELECTED] bgc = Gdk.RGBA() #style_context.get_background_color(Gtk.StateFlags.NORMAL)
txc = style.text[gtk.STATE_NORMAL] bgc.parse('white')
fgc = Gdk.RGBA() #style_context.get_background_color(Gtk.StateFlags.SELECTED)
fgc.parse('#4a90d9')
txc = Gdk.RGBA() #style_context.get_color(Gtk.StateFlags.NORMAL)
txc.parse('#333333')
border = 1.5 border = 1.5
height = int(size*.4) height = int(size*.4)
@ -142,19 +153,19 @@ def draw_cake(percentage, text=None, emblem=None, size=None):
# Background # Background
ctx.rectangle(x, y, width, height) ctx.rectangle(x, y, width, height)
ctx.set_source_rgb(bgc.red_float, bgc.green_float, bgc.blue_float) ctx.set_source_rgb(bgc.red, bgc.green, bgc.blue)
ctx.fill() ctx.fill()
# Filling # Filling
if percentage > 0: if percentage > 0:
fill_width = max(1, min(width-2, (width-2)*percentage+.5)) fill_width = max(1, min(width-2, (width-2)*percentage+.5))
ctx.rectangle(x+1, y+1, fill_width, height-2) ctx.rectangle(x+1, y+1, fill_width, height-2)
ctx.set_source_rgb(fgc.red_float, fgc.green_float, fgc.blue_float) ctx.set_source_rgb(0.289, 0.5625, 0.84765625)
ctx.fill() ctx.fill()
# Border # Border
ctx.rectangle(x, y, width, height) ctx.rectangle(x, y, width, height)
ctx.set_source_rgb(txc.red_float, txc.green_float, txc.blue_float) ctx.set_source_rgb(txc.red, txc.green, txc.blue)
ctx.set_line_width(1) ctx.set_line_width(1)
ctx.stroke() ctx.stroke()
@ -162,12 +173,10 @@ def draw_cake(percentage, text=None, emblem=None, size=None):
return surface return surface
def draw_text_pill(left_text, right_text, x=0, y=0, border=2, radius=14, font_desc=None): def draw_text_pill(left_text, right_text, x=0, y=0, border=2, radius=14, font_desc=None):
# Create temporary context to calculate the text size
ctx = cairo.Context(cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1))
# Use GTK+ style of a normal Button # Use GTK+ style of a normal Button
widget = gtk.Label() widget = Gtk.Label()
style = widget.rc_get_style() style_context = widget.get_style_context()
# Padding (in px) at the right edge of the image (for Ubuntu; bug 1533) # Padding (in px) at the right edge of the image (for Ubuntu; bug 1533)
padding_right = 7 padding_right = 7
@ -175,16 +184,16 @@ def draw_text_pill(left_text, right_text, x=0, y=0, border=2, radius=14, font_de
x_border = border*2 x_border = border*2
if font_desc is None: if font_desc is None:
font_desc = style.font_desc font_desc = style_context.get_font(Gtk.StateFlags.NORMAL)
font_desc.set_weight(pango.WEIGHT_BOLD) font_desc.set_weight(Pango.Weight.BOLD)
pango_context = widget.create_pango_context() pango_context = widget.create_pango_context()
layout_left = pango.Layout(pango_context) layout_left = Pango.Layout(pango_context)
layout_left.set_font_description(font_desc) layout_left.set_font_description(font_desc)
layout_left.set_text(left_text) layout_left.set_text(left_text, -1)
layout_right = pango.Layout(pango_context) layout_right = Pango.Layout(pango_context)
layout_right.set_font_description(font_desc) layout_right.set_font_description(font_desc)
layout_right.set_text(right_text) layout_right.set_text(right_text, -1)
width_left, height_left = layout_left.get_pixel_size() width_left, height_left = layout_left.get_pixel_size()
width_right, height_right = layout_right.get_pixel_size() width_right, height_right = layout_right.get_pixel_size()
@ -193,9 +202,9 @@ def draw_text_pill(left_text, right_text, x=0, y=0, border=2, radius=14, font_de
image_height = int(y+text_height+border*2) image_height = int(y+text_height+border*2)
image_width = int(x+width_left+width_right+x_border*4+padding_right) image_width = int(x+width_left+width_right+x_border*4+padding_right)
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, image_width, image_height)
ctx = pangocairo.CairoContext(cairo.Context(surface)) surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, image_width, image_height)
ctx = cairo.Context(surface)
# Clip so as to not draw on the right padding (for Ubuntu; bug 1533) # Clip so as to not draw on the right padding (for Ubuntu; bug 1533)
ctx.rectangle(0, 0, image_width - padding_right, image_height) ctx.rectangle(0, 0, image_width - padding_right, image_height)
@ -235,39 +244,39 @@ def draw_text_pill(left_text, right_text, x=0, y=0, border=2, radius=14, font_de
ctx.move_to(x+x_border, y+1+border) ctx.move_to(x+x_border, y+1+border)
ctx.set_source_rgba( 0, 0, 0, 1) ctx.set_source_rgba( 0, 0, 0, 1)
ctx.show_layout(layout_left) PangoCairo.show_layout(ctx, layout_left)
ctx.move_to(x-1+x_border, y+border) ctx.move_to(x-1+x_border, y+border)
ctx.set_source_rgba( 1, 1, 1, 1) ctx.set_source_rgba( 1, 1, 1, 1)
ctx.show_layout(layout_left) PangoCairo.show_layout(ctx, layout_left)
if right_text is not None: if right_text is not None:
draw_rounded_rectangle(ctx, x, y, rect_width, rect_height, radius, left_side_width, RRECT_RIGHT_SIDE, left_text is None) draw_rounded_rectangle(ctx, x, y, rect_width, rect_height, radius, left_side_width, RRECT_RIGHT_SIDE, left_text is None)
linear = cairo.LinearGradient(x+left_side_width, y, x+left_side_width+right_side_width/2, y+rect_height) linear = cairo.LinearGradient(x+left_side_width, y, x+left_side_width+right_side_width/2, y+rect_height)
linear.add_color_stop_rgba(0, .2, .2, .2, .9) linear.add_color_stop_rgba(0, .2, .2, .2, .9)
linear.add_color_stop_rgba(.4, .2, .2, .2, .8) linear.add_color_stop_rgba(.4, .2, .2, .2, .8)
linear.add_color_stop_rgba(.6, .2, .2, .2, .6) linear.add_color_stop_rgba(.6, .2, .2, .2, .6)
linear.add_color_stop_rgba(.9, .2, .2, .2, .7) linear.add_color_stop_rgba(.9, .2, .2, .2, .7)
linear.add_color_stop_rgba(1, .2, .2, .2, .5) linear.add_color_stop_rgba(1, .2, .2, .2, .5)
ctx.set_source(linear) ctx.set_source(linear)
ctx.fill() ctx.fill()
xpos, ypos, width, height = x, y+1, rect_width-1, rect_height-2 xpos, ypos, width, height = x, y+1, rect_width-1, rect_height-2
if left_text is None: if left_text is None:
xpos, width = x+1, rect_width-2 xpos, width = x+1, rect_width-2
draw_rounded_rectangle(ctx, xpos, ypos, width, height, radius, left_side_width, RRECT_RIGHT_SIDE, left_text is None) draw_rounded_rectangle(ctx, xpos, ypos, width, height, radius, left_side_width, RRECT_RIGHT_SIDE, left_text is None)
ctx.set_source_rgba(1., 1., 1., .3) ctx.set_source_rgba(1., 1., 1., .3)
ctx.set_line_width(1) ctx.set_line_width(1)
ctx.stroke() ctx.stroke()
draw_rounded_rectangle(ctx, x, y, rect_width, rect_height, radius, left_side_width, RRECT_RIGHT_SIDE, left_text is None) draw_rounded_rectangle(ctx, x, y, rect_width, rect_height, radius, left_side_width, RRECT_RIGHT_SIDE, left_text is None)
ctx.set_source_rgba(.1, .1, .1, .6) ctx.set_source_rgba(.1, .1, .1, .6)
ctx.set_line_width(1) ctx.set_line_width(1)
ctx.stroke() ctx.stroke()
ctx.move_to(x+left_side_width+x_border, y+1+border) ctx.move_to(x+left_side_width+x_border, y+1+border)
ctx.set_source_rgba( 0, 0, 0, 1) ctx.set_source_rgba( 0, 0, 0, 1)
ctx.show_layout(layout_right) PangoCairo.show_layout(ctx, layout_right)
ctx.move_to(x-1+left_side_width+x_border, y+border) ctx.move_to(x-1+left_side_width+x_border, y+border)
ctx.set_source_rgba( 1, 1, 1, 1) ctx.set_source_rgba( 1, 1, 1, 1)
ctx.show_layout(layout_right) PangoCairo.show_layout(ctx, layout_right)
return surface return surface
@ -284,19 +293,19 @@ def cairo_surface_to_pixbuf(s):
Converts a Cairo surface to a Gtk Pixbuf by Converts a Cairo surface to a Gtk Pixbuf by
encoding it as PNG and using the PixbufLoader. encoding it as PNG and using the PixbufLoader.
""" """
sio = StringIO.StringIO() bio = io.BytesIO()
try: try:
s.write_to_png(sio) s.write_to_png(bio)
except: except:
# Write an empty PNG file to the StringIO, so # Write an empty PNG file to the StringIO, so
# in case of an error we have "something" to # in case of an error we have "something" to
# load. This happens in PyCairo < 1.1.6, see: # load. This happens in PyCairo < 1.1.6, see:
# http://webcvs.cairographics.org/pycairo/NEWS?view=markup # http://webcvs.cairographics.org/pycairo/NEWS?view=markup
# Thanks to Chris Arnold for reporting this bug # Thanks to Chris Arnold for reporting this bug
sio.write('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A\n/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9cMEQkqIyxn3RkAAAAZdEVYdENv\nbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAADUlEQVQI12NgYGBgAAAABQABXvMqOgAAAABJ\nRU5ErkJggg==\n'.decode('base64')) bio.write('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A\n/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9cMEQkqIyxn3RkAAAAZdEVYdENv\nbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAADUlEQVQI12NgYGBgAAAABQABXvMqOgAAAABJ\nRU5ErkJggg==\n'.decode('base64'))
pbl = gtk.gdk.PixbufLoader() pbl = GdkPixbuf.PixbufLoader()
pbl.write(sio.getvalue()) pbl.write(bio.getvalue())
pbl.close() pbl.close()
pixbuf = pbl.get_pixbuf() pixbuf = pbl.get_pixbuf()
@ -312,7 +321,7 @@ def progressbar_pixbuf(width, height, percentage):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
ctx = cairo.Context(surface) ctx = cairo.Context(surface)
padding = int(float(width)/8.0) padding = int(width/8.0)
bar_width = 2*padding bar_width = 2*padding
bar_height = height - 2*padding bar_height = height - 2*padding
bar_height_fill = bar_height*percentage bar_height_fill = bar_height*percentage
@ -337,4 +346,3 @@ def progressbar_pixbuf(width, height, percentage):
ctx.stroke() ctx.stroke()
return cairo_surface_to_pixbuf(surface) return cairo_surface_to_pixbuf(surface)

View file

@ -17,7 +17,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import gtk from gi.repository import Gtk
from gi.repository import Gdk
import gpodder import gpodder
@ -43,8 +44,11 @@ class gPodderAddPodcast(BuilderWidget):
if not hasattr(self, 'preset_url'): if not hasattr(self, 'preset_url'):
# Fill the entry if a valid URL is in the clipboard, but # Fill the entry if a valid URL is in the clipboard, but
# only if there's no preset_url available (see bug 1132) # only if there's no preset_url available (see bug 1132).
clipboard = gtk.Clipboard(selection='PRIMARY') # First try from CLIPBOARD (everyday copy-paste),
# then from SELECTION (text selected and pasted via
# middle mouse button).
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
def receive_clipboard_text(clipboard, text, second_try): def receive_clipboard_text(clipboard, text, second_try):
# Heuristic: If there is a space in the clipboard # Heuristic: If there is a space in the clipboard
# text, assume it's some arbitrary text, and no URL # text, assume it's some arbitrary text, and no URL
@ -56,7 +60,7 @@ class gPodderAddPodcast(BuilderWidget):
return return
if not second_try: if not second_try:
clipboard = gtk.Clipboard() clipboard = Gtk.Clipboard.get(Gdk.SELECTION_PRIMARY)
clipboard.request_text(receive_clipboard_text, True) clipboard.request_text(receive_clipboard_text, True)
clipboard.request_text(receive_clipboard_text, False) clipboard.request_text(receive_clipboard_text, False)
@ -64,7 +68,7 @@ class gPodderAddPodcast(BuilderWidget):
self.gPodderAddPodcast.destroy() self.gPodderAddPodcast.destroy()
def on_btn_paste_clicked(self, widget): def on_btn_paste_clicked(self, widget):
clipboard = gtk.Clipboard() clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
clipboard.request_text(self.receive_clipboard_text) clipboard.request_text(self.receive_clipboard_text)
def receive_clipboard_text(self, clipboard, text, data=None): def receive_clipboard_text(self, clipboard, text, data=None):

View file

@ -17,7 +17,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import gtk from gi.repository import Gtk
from gi.repository import Gdk
import os import os
import shutil import shutil
@ -33,40 +35,17 @@ from gpodder.gtkui.base import GtkBuilderWidget
class BuilderWidget(GtkBuilderWidget): class BuilderWidget(GtkBuilderWidget):
def __init__(self, parent, **kwargs): def __init__(self, parent, **kwargs):
self._window_iconified = False self._window_iconified = False
self._window_visible = False
GtkBuilderWidget.__init__(self, gpodder.ui_folders, gpodder.textdomain, **kwargs) GtkBuilderWidget.__init__(self, gpodder.ui_folders, gpodder.textdomain, parent, **kwargs)
# Enable support for tracking iconified state # Enable support for tracking iconified state
if hasattr(self, 'on_iconify') and hasattr(self, 'on_uniconify'): if hasattr(self, 'on_iconify') and hasattr(self, 'on_uniconify'):
self.main_window.connect('window-state-event', \ self.main_window.connect('window-state-event', \
self._on_window_state_event_iconified) self._on_window_state_event_iconified)
# Enable support for tracking visibility state
self.main_window.connect('visibility-notify-event', \
self._on_window_state_event_visibility)
if parent is not None:
self.main_window.set_transient_for(parent)
if hasattr(self, 'center_on_widget'):
(x, y) = parent.get_position()
a = self.center_on_widget.allocation
(x, y) = (x + a.x, y + a.y)
(w, h) = (a.width, a.height)
(pw, ph) = self.main_window.get_size()
self.main_window.move(x + w/2 - pw/2, y + h/2 - ph/2)
def _on_window_state_event_visibility(self, widget, event):
if event.state & gtk.gdk.VISIBILITY_FULLY_OBSCURED:
self._window_visible = False
else:
self._window_visible = True
return False
def _on_window_state_event_iconified(self, widget, event): def _on_window_state_event_iconified(self, widget, event):
if event.new_window_state & gtk.gdk.WINDOW_STATE_ICONIFIED: if event.new_window_state & Gdk.WindowState.ICONIFIED:
if not self._window_iconified: if not self._window_iconified:
self._window_iconified = True self._window_iconified = True
self.on_iconify() self.on_iconify()
@ -84,12 +63,12 @@ class BuilderWidget(GtkBuilderWidget):
util.idle_add(self.show_message, message, title, important, widget) util.idle_add(self.show_message, message, title, important, widget)
def get_dialog_parent(self): def get_dialog_parent(self):
"""Return a gtk.Window that should be the parent of dialogs""" """Return a Gtk.Window that should be the parent of dialogs"""
return self.main_window return self.main_window
def show_message(self, message, title=None, important=False, widget=None): def show_message(self, message, title=None, important=False, widget=None):
if important: if important:
dlg = gtk.MessageDialog(self.main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO, gtk.BUTTONS_OK) dlg = Gtk.MessageDialog(self.main_window, Gtk.DialogFlags.MODAL, Gtk.MessageType.INFO, Gtk.ButtonsType.OK)
if title: if title:
dlg.set_title(str(title)) dlg.set_title(str(title))
dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message)) dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
@ -101,7 +80,7 @@ class BuilderWidget(GtkBuilderWidget):
gpodder.user_extensions.on_notification_show(title, message) gpodder.user_extensions.on_notification_show(title, message)
def show_confirmation(self, message, title=None): def show_confirmation(self, message, title=None):
dlg = gtk.MessageDialog(self.main_window, gtk.DIALOG_MODAL, gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO) dlg = Gtk.MessageDialog(self.main_window, Gtk.DialogFlags.MODAL, Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO)
if title: if title:
dlg.set_title(str(title)) dlg.set_title(str(title))
dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message)) dlg.set_markup('<span weight="bold" size="larger">%s</span>\n\n%s' % (title, message))
@ -109,21 +88,20 @@ class BuilderWidget(GtkBuilderWidget):
dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message)) dlg.set_markup('<span weight="bold" size="larger">%s</span>' % (message))
response = dlg.run() response = dlg.run()
dlg.destroy() dlg.destroy()
return response == gtk.RESPONSE_YES return response == Gtk.ResponseType.YES
def show_text_edit_dialog(self, title, prompt, text=None, empty=False, \ def show_text_edit_dialog(self, title, prompt, text=None, empty=False, \
is_url=False, affirmative_text=gtk.STOCK_OK): is_url=False, affirmative_text=Gtk.STOCK_OK):
dialog = gtk.Dialog(title, self.get_dialog_parent(), \ dialog = Gtk.Dialog(title, self.get_dialog_parent(), \
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT) Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT)
dialog.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
dialog.add_button(affirmative_text, gtk.RESPONSE_OK) dialog.add_button(affirmative_text, Gtk.ResponseType.OK)
dialog.set_has_separator(False)
dialog.set_default_size(300, -1) dialog.set_default_size(300, -1)
dialog.set_default_response(gtk.RESPONSE_OK) dialog.set_default_response(Gtk.ResponseType.OK)
text_entry = gtk.Entry() text_entry = Gtk.Entry()
text_entry.set_activates_default(True) text_entry.set_activates_default(True)
if text is not None: if text is not None:
text_entry.set_text(text) text_entry.set_text(text)
@ -132,24 +110,24 @@ class BuilderWidget(GtkBuilderWidget):
if not empty: if not empty:
def on_text_changed(editable): def on_text_changed(editable):
can_confirm = (editable.get_text() != '') can_confirm = (editable.get_text() != '')
dialog.set_response_sensitive(gtk.RESPONSE_OK, can_confirm) dialog.set_response_sensitive(Gtk.ResponseType.OK, can_confirm)
text_entry.connect('changed', on_text_changed) text_entry.connect('changed', on_text_changed)
if text is None: if text is None:
dialog.set_response_sensitive(gtk.RESPONSE_OK, False) dialog.set_response_sensitive(Gtk.ResponseType.OK, False)
hbox = gtk.HBox() hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
hbox.set_border_width(10) hbox.set_border_width(10)
hbox.set_spacing(10) hbox.set_spacing(10)
hbox.pack_start(gtk.Label(prompt), False, False) hbox.pack_start(Gtk.Label(prompt, True, True, 0), False, False, 0)
hbox.pack_start(text_entry, True, True) hbox.pack_start(text_entry, True, True, 0)
dialog.vbox.pack_start(hbox, True, True) dialog.vbox.pack_start(hbox, True, True, 0)
dialog.show_all() dialog.show_all()
response = dialog.run() response = dialog.run()
result = text_entry.get_text() result = text_entry.get_text()
dialog.destroy() dialog.destroy()
if response == gtk.RESPONSE_OK: if response == Gtk.ResponseType.OK:
return result return result
else: else:
return None return None
@ -162,25 +140,25 @@ class BuilderWidget(GtkBuilderWidget):
if register_text is None: if register_text is None:
register_text = _('New user') register_text = _('New user')
dialog = gtk.MessageDialog( dialog = Gtk.MessageDialog(
self.main_window, self.main_window,
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
gtk.MESSAGE_QUESTION, Gtk.MessageType.QUESTION,
gtk.BUTTONS_CANCEL) Gtk.ButtonsType.CANCEL)
dialog.add_button(_('Login'), gtk.RESPONSE_OK) dialog.add_button(_('Login'), Gtk.ResponseType.OK)
dialog.set_image(gtk.image_new_from_stock(gtk.STOCK_DIALOG_AUTHENTICATION, gtk.ICON_SIZE_DIALOG)) dialog.set_image(Gtk.Image.new_from_icon_name('dialog-password', Gtk.IconSize.DIALOG))
dialog.set_title(_('Authentication required')) dialog.set_title(_('Authentication required'))
dialog.set_markup('<span weight="bold" size="larger">' + title + '</span>') dialog.set_markup('<span weight="bold" size="larger">' + title + '</span>')
dialog.format_secondary_markup(message) dialog.format_secondary_markup(message)
dialog.set_default_response(gtk.RESPONSE_OK) dialog.set_default_response(Gtk.ResponseType.OK)
if register_callback is not None: if register_callback is not None:
dialog.add_button(register_text, gtk.RESPONSE_HELP) dialog.add_button(register_text, Gtk.ResponseType.HELP)
server_entry = gtk.Entry() server_entry = Gtk.Entry()
server_entry.set_tooltip_text(_('hostname or root URL (e.g. https://gpodder.net)')) server_entry.set_tooltip_text(_('hostname or root URL (e.g. https://gpodder.net)'))
username_entry = gtk.Entry() username_entry = Gtk.Entry()
password_entry = gtk.Entry() password_entry = Gtk.Entry()
server_entry.connect('activate', lambda w: username_entry.grab_focus()) server_entry.connect('activate', lambda w: username_entry.grab_focus())
username_entry.connect('activate', lambda w: password_entry.grab_focus()) username_entry.connect('activate', lambda w: password_entry.grab_focus())
@ -194,17 +172,17 @@ class BuilderWidget(GtkBuilderWidget):
if password is not None: if password is not None:
password_entry.set_text(password) password_entry.set_text(password)
table = gtk.Table(3, 2) table = Gtk.Table(3, 2)
table.set_row_spacings(6) table.set_row_spacings(6)
table.set_col_spacings(6) table.set_col_spacings(6)
server_label = gtk.Label() server_label = Gtk.Label()
server_label.set_markup('<b>' + _('Server') + ':</b>') server_label.set_markup('<b>' + _('Server') + ':</b>')
username_label = gtk.Label() username_label = Gtk.Label()
username_label.set_markup('<b>' + username_prompt + ':</b>') username_label.set_markup('<b>' + username_prompt + ':</b>')
password_label = gtk.Label() password_label = Gtk.Label()
password_label.set_markup('<b>' + _('Password') + ':</b>') password_label.set_markup('<b>' + _('Password') + ':</b>')
label_entries = [(username_label, username_entry), label_entries = [(username_label, username_entry),
@ -215,7 +193,7 @@ class BuilderWidget(GtkBuilderWidget):
for i, (label, entry) in enumerate(label_entries): for i, (label, entry) in enumerate(label_entries):
label.set_alignment(0.0, 0.5) label.set_alignment(0.0, 0.5)
table.attach(label, 0, 1, i, i + 1, gtk.FILL, 0) table.attach(label, 0, 1, i, i + 1, Gtk.AttachOptions.FILL, 0)
table.attach(entry, 1, 2, i, i + 1) table.attach(entry, 1, 2, i, i + 1)
dialog.vbox.pack_end(table, True, True, 0) dialog.vbox.pack_end(table, True, True, 0)
@ -223,7 +201,7 @@ class BuilderWidget(GtkBuilderWidget):
username_entry.grab_focus() username_entry.grab_focus()
response = dialog.run() response = dialog.run()
while response == gtk.RESPONSE_HELP: while response == Gtk.ResponseType.HELP:
register_callback() register_callback()
response = dialog.run() response = dialog.run()
@ -231,7 +209,7 @@ class BuilderWidget(GtkBuilderWidget):
root_url = server_entry.get_text() root_url = server_entry.get_text()
username = username_entry.get_text() username = username_entry.get_text()
password = password_entry.get_text() password = password_entry.get_text()
success = (response == gtk.RESPONSE_OK) success = (response == Gtk.ResponseType.OK)
dialog.destroy() dialog.destroy()
@ -240,36 +218,22 @@ class BuilderWidget(GtkBuilderWidget):
else: else:
return (success, (username, password)) return (success, (username, password))
def show_copy_dialog(self, src_filename, dst_filename=None, dst_directory=None, title=_('Select destination')): def show_folder_select_dialog(self, initial_directory=None, title=_('Select destination')):
if dst_filename is None: if initial_directory is None:
dst_filename = src_filename initial_directory = os.path.expanduser('~')
if dst_directory is None: dlg = Gtk.FileChooserDialog(title=title, parent=self.main_window, action=Gtk.FileChooserAction.SELECT_FOLDER)
dst_directory = os.path.expanduser('~') dlg.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
dlg.add_button(Gtk.STOCK_SAVE, Gtk.ResponseType.OK)
base, extension = os.path.splitext(src_filename)
if not dst_filename.endswith(extension):
dst_filename += extension
dlg = gtk.FileChooserDialog(title=title, parent=self.main_window, action=gtk.FILE_CHOOSER_ACTION_SAVE)
dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
dlg.add_button(gtk.STOCK_SAVE, gtk.RESPONSE_OK)
dlg.set_do_overwrite_confirmation(True) dlg.set_do_overwrite_confirmation(True)
dlg.set_current_name(os.path.basename(dst_filename)) dlg.set_current_folder(initial_directory)
dlg.set_current_folder(dst_directory)
result = False result = False
folder = dst_directory folder = initial_directory
if dlg.run() == gtk.RESPONSE_OK: if dlg.run() == Gtk.ResponseType.OK:
result = True result = True
dst_filename = dlg.get_filename()
folder = dlg.get_current_folder() folder = dlg.get_current_folder()
if not dst_filename.endswith(extension):
dst_filename += extension
shutil.copyfile(src_filename, dst_filename)
dlg.destroy() dlg.destroy()
return (result, folder) return (result, folder)
@ -282,7 +246,7 @@ class TreeViewHelper(object):
COLUMNS = '_gpodder_columns' COLUMNS = '_gpodder_columns'
# Enum for the role attribute # Enum for the role attribute
ROLE_PODCASTS, ROLE_EPISODES, ROLE_DOWNLOADS = range(3) ROLE_PODCASTS, ROLE_EPISODES, ROLE_DOWNLOADS = list(range(3))
@classmethod @classmethod
def set(cls, treeview, role): def set(cls, treeview, role):

View file

@ -17,7 +17,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import gtk from gi.repository import Gtk
import cgi import cgi
import gpodder import gpodder
@ -31,28 +31,30 @@ from gpodder.gtkui.interface.common import BuilderWidget
class gPodderConfigEditor(BuilderWidget): class gPodderConfigEditor(BuilderWidget):
def new(self): def new(self):
name_column = gtk.TreeViewColumn(_('Setting')) name_column = Gtk.TreeViewColumn(_('Setting'))
name_renderer = gtk.CellRendererText() name_renderer = Gtk.CellRendererText()
name_column.pack_start(name_renderer) name_column.pack_start(name_renderer, True)
name_column.add_attribute(name_renderer, 'text', 0) name_column.add_attribute(name_renderer, 'text', 0)
name_column.add_attribute(name_renderer, 'style', 5) name_column.add_attribute(name_renderer, 'style', 5)
name_column.set_expand(True)
self.configeditor.append_column(name_column) self.configeditor.append_column(name_column)
value_column = gtk.TreeViewColumn(_('Set to')) value_column = Gtk.TreeViewColumn(_('Set to'))
value_check_renderer = gtk.CellRendererToggle() value_check_renderer = Gtk.CellRendererToggle()
value_column.pack_start(value_check_renderer, expand=False) value_column.pack_start(value_check_renderer, False)
value_column.add_attribute(value_check_renderer, 'active', 7) value_column.add_attribute(value_check_renderer, 'active', 7)
value_column.add_attribute(value_check_renderer, 'visible', 6) value_column.add_attribute(value_check_renderer, 'visible', 6)
value_column.add_attribute(value_check_renderer, 'activatable', 6) value_column.add_attribute(value_check_renderer, 'activatable', 6)
value_check_renderer.connect('toggled', self.value_toggled) value_check_renderer.connect('toggled', self.value_toggled)
value_renderer = gtk.CellRendererText() value_renderer = Gtk.CellRendererText()
value_column.pack_start(value_renderer) value_column.pack_start(value_renderer, True)
value_column.add_attribute(value_renderer, 'text', 2) value_column.add_attribute(value_renderer, 'text', 2)
value_column.add_attribute(value_renderer, 'visible', 4) value_column.add_attribute(value_renderer, 'visible', 4)
value_column.add_attribute(value_renderer, 'editable', 4) value_column.add_attribute(value_renderer, 'editable', 4)
value_column.add_attribute(value_renderer, 'style', 5) value_column.add_attribute(value_renderer, 'style', 5)
value_renderer.connect('edited', self.value_edited) value_renderer.connect('edited', self.value_edited)
value_column.set_expand(False)
self.configeditor.append_column(value_column) self.configeditor.append_column(value_column)
self.model = ConfigModel(self._config) self.model = ConfigModel(self._config)

View file

@ -17,9 +17,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import gtk from gi.repository import Gtk
import gobject from gi.repository import GObject
import pango from gi.repository import Pango
import gpodder import gpodder
@ -45,16 +45,16 @@ class ProgressIndicator(object):
self._initial_message = None self._initial_message = None
self._initial_progress = None self._initial_progress = None
self._progress_set = False self._progress_set = False
self.source_id = gobject.timeout_add(self.DELAY, self._create_progress) self.source_id = GObject.timeout_add(self.DELAY, self._create_progress)
def _on_delete_event(self, window, event): def _on_delete_event(self, window, event):
if self.cancellable: if self.cancellable:
self.dialog.response(gtk.RESPONSE_CANCEL) self.dialog.response(Gtk.ResponseType.CANCEL)
return True return True
def _create_progress(self): def _create_progress(self):
self.dialog = gtk.MessageDialog(self.parent, \ self.dialog = Gtk.MessageDialog(self.parent, \
0, 0, gtk.BUTTONS_CANCEL, self.subtitle or self.title) 0, 0, Gtk.ButtonsType.CANCEL, self.subtitle or self.title)
self.dialog.set_modal(True) self.dialog.set_modal(True)
self.dialog.connect('delete-event', self._on_delete_event) self.dialog.connect('delete-event', self._on_delete_event)
self.dialog.set_title(self.title) self.dialog.set_title(self.title)
@ -63,14 +63,15 @@ class ProgressIndicator(object):
# Avoid selectable text (requires PyGTK >= 2.22) # Avoid selectable text (requires PyGTK >= 2.22)
if hasattr(self.dialog, 'get_message_area'): if hasattr(self.dialog, 'get_message_area'):
for label in self.dialog.get_message_area(): for label in self.dialog.get_message_area():
if isinstance(label, gtk.Label): if isinstance(label, Gtk.Label):
label.set_selectable(False) label.set_selectable(False)
self.dialog.set_response_sensitive(gtk.RESPONSE_CANCEL, \ self.dialog.set_response_sensitive(Gtk.ResponseType.CANCEL, \
self.cancellable) self.cancellable)
self.progressbar = gtk.ProgressBar() self.progressbar = Gtk.ProgressBar()
self.progressbar.set_ellipsize(pango.ELLIPSIZE_END) self.progressbar.set_show_text(True)
self.progressbar.set_ellipsize(Pango.EllipsizeMode.END)
# If the window is shown after the first update, set the progress # If the window is shown after the first update, set the progress
# info so that when the window appears, data is there already # info so that when the window appears, data is there already
@ -84,8 +85,8 @@ class ProgressIndicator(object):
self.dialog.set_image(self.indicator) self.dialog.set_image(self.indicator)
self.dialog.show_all() self.dialog.show_all()
gobject.source_remove(self.source_id) GObject.source_remove(self.source_id)
self.source_id = gobject.timeout_add(self.INTERVAL, self._update_gui) self.source_id = GObject.timeout_add(self.INTERVAL, self._update_gui)
return False return False
def _update_gui(self): def _update_gui(self):
@ -111,5 +112,5 @@ class ProgressIndicator(object):
def on_finished(self): def on_finished(self):
if self.dialog is not None: if self.dialog is not None:
self.dialog.destroy() self.dialog.destroy()
gobject.source_remove(self.source_id) GObject.source_remove(self.source_id)

View file

@ -18,19 +18,18 @@
# #
import gtk from gi.repository import Gtk
import gobject from gi.repository import GObject
import cgi import cgi
class TagCloud(gtk.Layout): class TagCloud(Gtk.Layout):
__gsignals__ = { __gsignals__ = {
'selected': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, 'selected': (GObject.SignalFlags.RUN_LAST, None,
(gobject.TYPE_STRING,)) (GObject.TYPE_STRING,))
} }
def __init__(self, min_size=20, max_size=36): def __init__(self, min_size=20, max_size=36):
self.__gobject_init__() Gtk.Layout.__init__(self)
gtk.Layout.__init__(self)
self._min_weight = 0 self._min_weight = 0
self._max_weight = 0 self._max_weight = 0
self._min_size = min_size self._min_size = min_size
@ -49,10 +48,10 @@ class TagCloud(gtk.Layout):
self._max_weight = max(weight for tag, weight in tags) self._max_weight = max(weight for tag, weight in tags)
for tag, weight in tags: for tag, weight in tags:
label = gtk.Label() label = Gtk.Label()
markup = '<span size="%d">%s</span>' % (1000*self._scale(weight), cgi.escape(tag)) markup = '<span size="%d">%s</span>' % (1000*self._scale(weight), cgi.escape(tag))
label.set_markup(markup) label.set_markup(markup)
button = gtk.ToolButton(label) button = Gtk.ToolButton(label)
button.connect('clicked', lambda b, t: self.emit('selected', t), tag) button.connect('clicked', lambda b, t: self.emit('selected', t), tag)
self.put(button, 1, 1) self.put(button, 1, 1)
button.show_all() button.show_all()
@ -65,8 +64,8 @@ class TagCloud(gtk.Layout):
self.relayout() self.relayout()
def _scale(self, weight): def _scale(self, weight):
weight_range = float(self._max_weight-self._min_weight) weight_range = self._max_weight-self._min_weight
ratio = float(weight-self._min_weight)/weight_range ratio = (weight-self._min_weight)/weight_range
return int(self._min_size + (self._max_size-self._min_size)*ratio) return int(self._min_size + (self._max_size-self._min_size)*ratio)
def relayout(self): def relayout(self):
@ -76,10 +75,10 @@ class TagCloud(gtk.Layout):
pw, ph = self._size pw, ph = self._size
def fixup_row(widgets, x, y, max_h): def fixup_row(widgets, x, y, max_h):
residue = (pw - x) residue = (pw - x)
x = int(residue/2) x = int(residue//2)
for widget in widgets: for widget in widgets:
cw, ch = widget.size_request() cw, ch = widget.size_request()
self.move(widget, x, y+max(0, int((max_h-ch)/2))) self.move(widget, x, y+max(0, int(max_h-ch)//2))
x += cw + 10 x += cw + 10
for child in self.get_children(): for child in self.get_children():
w, h = child.size_request() w, h = child.size_request()
@ -98,6 +97,6 @@ class TagCloud(gtk.Layout):
def unrelayout(): def unrelayout():
self._in_relayout = False self._in_relayout = False
return False return False
gobject.idle_add(unrelayout) GObject.idle_add(unrelayout)
gobject.type_register(TagCloud) GObject.type_register(TagCloud)

View file

@ -29,7 +29,10 @@ def aeKeyword(fourCharCode):
# for the kCoreEventClass, kAEOpenDocuments, ... constants # for the kCoreEventClass, kAEOpenDocuments, ... constants
# comes with macpython # comes with macpython
from Carbon.AppleEvents import * try:
from Carbon.AppleEvents import *
except ImportError:
...
# all this depends on pyObjc (http://pyobjc.sourceforge.net/). # all this depends on pyObjc (http://pyobjc.sourceforge.net/).
# There may be a way to achieve something equivalent with only # There may be a way to achieve something equivalent with only
@ -78,7 +81,7 @@ try:
util.idle_add(self.gp.on_item_import_from_file_activate, None,url) util.idle_add(self.gp.on_item_import_from_file_activate, None,url)
urls.append(str(url)) urls.append(str(url))
print >>sys.stderr,("open Files :",urls) print(("open Files :",urls), file=sys.stderr)
result = NSAppleEventDescriptor.descriptorWithInt32_(42) result = NSAppleEventDescriptor.descriptorWithInt32_(42)
reply.setParamDescriptor_forKeyword_(result, aeKeyword('----')) reply.setParamDescriptor_forKeyword_(result, aeKeyword('----'))
@ -88,7 +91,7 @@ try:
fileURLData = filelist.data() fileURLData = filelist.data()
url = buffer(fileURLData.bytes(),0,fileURLData.length()) url = buffer(fileURLData.bytes(),0,fileURLData.length())
url = str(url) url = str(url)
print >>sys.stderr,("Subscribe to :"+url) print(("Subscribe to :"+url), file=sys.stderr)
util.idle_add(self.gp.subscribe_to_url, url) util.idle_add(self.gp.subscribe_to_url, url)
result = NSAppleEventDescriptor.descriptorWithInt32_(42) result = NSAppleEventDescriptor.descriptorWithInt32_(42)
@ -97,9 +100,9 @@ try:
# global reference to the handler (mustn't be destroyed) # global reference to the handler (mustn't be destroyed)
handler = gPodderEventHandler.alloc().init() handler = gPodderEventHandler.alloc().init()
except ImportError: except ImportError:
print >> sys.stderr, """ print("""
Warning: pyobjc not found. Disabling "Subscribe with" events handling Warning: pyobjc not found. Disabling "Subscribe with" events handling
""" """, file=sys.stderr)
handler = None handler = None
def register_handlers(gp): def register_handlers(gp):

File diff suppressed because it is too large Load diff

View file

@ -38,14 +38,15 @@ logger = logging.getLogger(__name__)
from gpodder.gtkui import draw from gpodder.gtkui import draw
import os import os
import gtk from gi.repository import Gtk
import gobject from gi.repository import GObject
from gi.repository import GdkPixbuf
import cgi import cgi
import re import re
import time import time
try: try:
import gio from gi.repository import Gio
have_gio = True have_gio = True
except ImportError: except ImportError:
have_gio = False have_gio = False
@ -140,15 +141,17 @@ class BackgroundUpdate(object):
return bool(self.episodes) return bool(self.episodes)
class EpisodeListModel(gtk.ListStore): class EpisodeListModel(Gtk.ListStore):
C_URL, C_TITLE, C_FILESIZE_TEXT, C_EPISODE, C_STATUS_ICON, \ C_URL, C_TITLE, C_FILESIZE_TEXT, C_EPISODE, C_STATUS_ICON, \
C_PUBLISHED_TEXT, C_DESCRIPTION, C_TOOLTIP, \ C_PUBLISHED_TEXT, C_DESCRIPTION, C_TOOLTIP, \
C_VIEW_SHOW_UNDELETED, C_VIEW_SHOW_DOWNLOADED, \ C_VIEW_SHOW_UNDELETED, C_VIEW_SHOW_DOWNLOADED, \
C_VIEW_SHOW_UNPLAYED, C_FILESIZE, C_PUBLISHED, \ C_VIEW_SHOW_UNPLAYED, C_FILESIZE, C_PUBLISHED, \
C_TIME, C_TIME_VISIBLE, C_TOTAL_TIME, \ C_TIME, C_TIME_VISIBLE, C_TOTAL_TIME, \
C_LOCKED = range(17) C_LOCKED = list(range(17))
VIEW_ALL, VIEW_UNDELETED, VIEW_DOWNLOADED, VIEW_UNPLAYED = range(4) VIEW_ALL, VIEW_UNDELETED, VIEW_DOWNLOADED, VIEW_UNPLAYED = list(range(4))
VIEWS = ['VIEW_ALL', 'VIEW_UNDELETED', 'VIEW_DOWNLOADED', 'VIEW_UNPLAYED']
# In which steps the UI is updated for "loading" animations # In which steps the UI is updated for "loading" animations
_UI_UPDATE_STEP = .03 _UI_UPDATE_STEP = .03
@ -157,9 +160,9 @@ class EpisodeListModel(gtk.ListStore):
PROGRESS_STEPS = 20 PROGRESS_STEPS = 20
def __init__(self, config, on_filter_changed=lambda has_episodes: None): def __init__(self, config, on_filter_changed=lambda has_episodes: None):
gtk.ListStore.__init__(self, str, str, str, object, \ Gtk.ListStore.__init__(self, str, str, str, object, \
str, str, str, str, bool, bool, bool, \ str, str, str, str, bool, bool, bool, \
gobject.TYPE_INT64, gobject.TYPE_INT64, str, bool, gobject.TYPE_INT64, bool) GObject.TYPE_INT64, GObject.TYPE_INT64, str, bool, GObject.TYPE_INT64, bool)
self._config = config self._config = config
@ -169,7 +172,7 @@ class EpisodeListModel(gtk.ListStore):
# Filter to allow hiding some episodes # Filter to allow hiding some episodes
self._filter = self.filter_new() self._filter = self.filter_new()
self._sorter = gtk.TreeModelSort(self._filter) self._sorter = Gtk.TreeModelSort(self._filter)
self._view_mode = self.VIEW_ALL self._view_mode = self.VIEW_ALL
self._search_term = None self._search_term = None
self._search_term_eql = None self._search_term_eql = None
@ -182,8 +185,8 @@ class EpisodeListModel(gtk.ListStore):
self.ICON_VIDEO_FILE = 'video-x-generic' self.ICON_VIDEO_FILE = 'video-x-generic'
self.ICON_IMAGE_FILE = 'image-x-generic' self.ICON_IMAGE_FILE = 'image-x-generic'
self.ICON_GENERIC_FILE = 'text-x-generic' self.ICON_GENERIC_FILE = 'text-x-generic'
self.ICON_DOWNLOADING = gtk.STOCK_GO_DOWN self.ICON_DOWNLOADING = Gtk.STOCK_GO_DOWN
self.ICON_DELETED = gtk.STOCK_DELETE self.ICON_DELETED = Gtk.STOCK_DELETE
self.background_update = None self.background_update = None
self.background_update_tag = None self.background_update_tag = None
@ -201,7 +204,7 @@ class EpisodeListModel(gtk.ListStore):
else: else:
return None return None
def _filter_visible_func(self, model, iter): def _filter_visible_func(self, model, iter, misc):
# If searching is active, set visibility based on search text # If searching is active, set visibility based on search text
if self._search_term is not None: if self._search_term is not None:
episode = model.get_value(iter, self.C_EPISODE) episode = model.get_value(iter, self.C_EPISODE)
@ -210,7 +213,7 @@ class EpisodeListModel(gtk.ListStore):
try: try:
return self._search_term_eql.match(episode) return self._search_term_eql.match(episode)
except Exception, e: except Exception as e:
return True return True
if self._view_mode == self.VIEW_ALL: if self._view_mode == self.VIEW_ALL:
@ -314,10 +317,10 @@ class EpisodeListModel(gtk.ListStore):
def _update_from_episodes(self, episodes, include_description): def _update_from_episodes(self, episodes, include_description):
if self.background_update_tag is not None: if self.background_update_tag is not None:
gobject.source_remove(self.background_update_tag) GObject.source_remove(self.background_update_tag)
self.background_update = BackgroundUpdate(self, episodes, include_description) self.background_update = BackgroundUpdate(self, episodes, include_description)
self.background_update_tag = gobject.idle_add(self._update_background) self.background_update_tag = GObject.idle_add(self._update_background)
def _update_background(self): def _update_background(self):
if self.background_update is not None: if self.background_update is not None:
@ -349,7 +352,7 @@ class EpisodeListModel(gtk.ListStore):
def update_by_filter_iter(self, iter, include_description=False): def update_by_filter_iter(self, iter, include_description=False):
# Convenience function for use by "outside" methods that use iters # Convenience function for use by "outside" methods that use iters
# from the filtered episode list model (i.e. all UI things normally) # from the filtered episode list model (i.e. all UI things normally)
iter = self._sorter.convert_iter_to_child_iter(None, iter) iter = self._sorter.convert_iter_to_child_iter(iter)
self.update_by_iter(self._filter.convert_iter_to_child_iter(iter), self.update_by_iter(self._filter.convert_iter_to_child_iter(iter),
include_description) include_description)
@ -362,7 +365,7 @@ class EpisodeListModel(gtk.ListStore):
view_show_undeleted = True view_show_undeleted = True
view_show_downloaded = False view_show_downloaded = False
view_show_unplayed = False view_show_unplayed = False
icon_theme = gtk.icon_theme_get_default() icon_theme = Gtk.IconTheme.get_default()
if episode.downloading: if episode.downloading:
tooltip.append('%s %d%%' % (_('Downloading'), tooltip.append('%s %d%%' % (_('Downloading'),
@ -408,9 +411,9 @@ class EpisodeListModel(gtk.ListStore):
# Try to find a themed icon for this file # Try to find a themed icon for this file
if filename is not None and have_gio: if filename is not None and have_gio:
file = gio.File(filename) file = Gio.File.new_for_path(filename)
if file.query_exists(): if file.query_exists():
file_info = file.query_info('*') file_info = file.query_info('*', Gio.FileQueryInfoFlags.NONE, None)
icon = file_info.get_icon() icon = file_info.get_icon()
for icon_name in icon.get_names(): for icon_name in icon.get_names():
if icon_theme.has_icon(icon_name): if icon_theme.has_icon(icon_name):
@ -504,12 +507,12 @@ class PodcastChannelProxy(object):
pass pass
class PodcastListModel(gtk.ListStore): class PodcastListModel(Gtk.ListStore):
C_URL, C_TITLE, C_DESCRIPTION, C_PILL, C_CHANNEL, \ C_URL, C_TITLE, C_DESCRIPTION, C_PILL, C_CHANNEL, \
C_COVER, C_ERROR, C_PILL_VISIBLE, \ C_COVER, C_ERROR, C_PILL_VISIBLE, \
C_VIEW_SHOW_UNDELETED, C_VIEW_SHOW_DOWNLOADED, \ C_VIEW_SHOW_UNDELETED, C_VIEW_SHOW_DOWNLOADED, \
C_VIEW_SHOW_UNPLAYED, C_HAS_EPISODES, C_SEPARATOR, \ C_VIEW_SHOW_UNPLAYED, C_HAS_EPISODES, C_SEPARATOR, \
C_DOWNLOADS, C_COVER_VISIBLE, C_SECTION = range(16) C_DOWNLOADS, C_COVER_VISIBLE, C_SECTION = list(range(16))
SEARCH_COLUMNS = (C_TITLE, C_DESCRIPTION, C_SECTION) SEARCH_COLUMNS = (C_TITLE, C_DESCRIPTION, C_SECTION)
@ -518,8 +521,8 @@ class PodcastListModel(gtk.ListStore):
return model.get_value(iter, cls.C_SEPARATOR) return model.get_value(iter, cls.C_SEPARATOR)
def __init__(self, cover_downloader): def __init__(self, cover_downloader):
gtk.ListStore.__init__(self, str, str, str, gtk.gdk.Pixbuf, \ Gtk.ListStore.__init__(self, str, str, str, GdkPixbuf.Pixbuf, \
object, gtk.gdk.Pixbuf, str, bool, bool, bool, bool, \ object, GdkPixbuf.Pixbuf, str, bool, bool, bool, bool, \
bool, bool, int, bool, str) bool, bool, int, bool, str)
# Filter to allow hiding some episodes # Filter to allow hiding some episodes
@ -534,7 +537,7 @@ class PodcastListModel(gtk.ListStore):
self.ICON_DISABLED = 'gtk-media-pause' self.ICON_DISABLED = 'gtk-media-pause'
def _filter_visible_func(self, model, iter): def _filter_visible_func(self, model, iter, misc):
# If searching is active, set visibility based on search text # If searching is active, set visibility based on search text
if self._search_term is not None: if self._search_term is not None:
if model.get_value(iter, self.C_CHANNEL) == SectionMarker: if model.get_value(iter, self.C_CHANNEL) == SectionMarker:
@ -608,14 +611,14 @@ class PodcastListModel(gtk.ListStore):
if pixbuf.get_width() > self._max_image_side: if pixbuf.get_width() > self._max_image_side:
f = float(self._max_image_side)/pixbuf.get_width() f = float(self._max_image_side)/pixbuf.get_width()
(width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f)) (width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
pixbuf = pixbuf.scale_simple(width, height, gtk.gdk.INTERP_BILINEAR) pixbuf = pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR)
changed = True changed = True
# Resize if too high # Resize if too high
if pixbuf.get_height() > self._max_image_side: if pixbuf.get_height() > self._max_image_side:
f = float(self._max_image_side)/pixbuf.get_height() f = float(self._max_image_side)/pixbuf.get_height()
(width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f)) (width, height) = (int(pixbuf.get_width()*f), int(pixbuf.get_height()*f))
pixbuf = pixbuf.scale_simple(width, height, gtk.gdk.INTERP_BILINEAR) pixbuf = pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR)
changed = True changed = True
if changed: if changed:
@ -632,7 +635,7 @@ class PodcastListModel(gtk.ListStore):
def _overlay_pixbuf(self, pixbuf, icon): def _overlay_pixbuf(self, pixbuf, icon):
try: try:
icon_theme = gtk.icon_theme_get_default() icon_theme = Gtk.IconTheme.get_default()
emblem = icon_theme.load_icon(icon, self._max_image_side/2, 0) emblem = icon_theme.load_icon(icon, self._max_image_side/2, 0)
(width, height) = (emblem.get_width(), emblem.get_height()) (width, height) = (emblem.get_width(), emblem.get_height())
xpos = pixbuf.get_width() - width xpos = pixbuf.get_width() - width
@ -643,7 +646,7 @@ class PodcastListModel(gtk.ListStore):
(width, height) = (emblem.get_width(), emblem.get_height()) (width, height) = (emblem.get_width(), emblem.get_height())
xpos = pixbuf.get_width() - width xpos = pixbuf.get_width() - width
ypos = pixbuf.get_height() - height ypos = pixbuf.get_height() - height
emblem.composite(pixbuf, xpos, ypos, width, height, xpos, ypos, 1, 1, gtk.gdk.INTERP_BILINEAR, 255) emblem.composite(pixbuf, xpos, ypos, width, height, xpos, ypos, 1, 1, GdkPixbuf.InterpType.BILINEAR, 255)
except: except:
pass pass
@ -654,11 +657,11 @@ class PodcastListModel(gtk.ListStore):
return None return None
try: try:
loader = gtk.gdk.PixbufLoader('png') loader = GdkPixbuf.PixbufLoader()
loader.write(channel.cover_thumb) loader.write(channel.cover_thumb)
loader.close() loader.close()
return loader.get_pixbuf() return loader.get_pixbuf()
except Exception, e: except Exception as e:
logger.warn('Could not load cached cover art for %s', channel.url, exc_info=True) logger.warn('Could not load cached cover art for %s', channel.url, exc_info=True)
channel.cover_thumb = None channel.cover_thumb = None
channel.save() channel.save()
@ -666,8 +669,11 @@ class PodcastListModel(gtk.ListStore):
def _save_cached_thumb(self, channel, pixbuf): def _save_cached_thumb(self, channel, pixbuf):
bufs = [] bufs = []
pixbuf.save_to_callback(lambda buf, data: data.append(buf), 'png', {}, bufs) def save_callback(buf, length, user_data):
channel.cover_thumb = buffer(''.join(bufs)) user_data.append(buf)
return True
pixbuf.save_to_callbackv(save_callback, bufs, 'png', [None], [])
channel.cover_thumb = bytes(b''.join(bufs))
channel.save() channel.save()
def _get_cover_image(self, channel, add_overlay=False): def _get_cover_image(self, channel, add_overlay=False):
@ -799,7 +805,7 @@ class PodcastListModel(gtk.ListStore):
def iter_is_first_row(self, iter): def iter_is_first_row(self, iter):
iter = self._filter.convert_iter_to_child_iter(iter) iter = self._filter.convert_iter_to_child_iter(iter)
path = self.get_path(iter) path = self.get_path(iter)
return (path == (0,)) return (path == Gtk.TreePath.new_first())
def update_by_filter_iter(self, iter): def update_by_filter_iter(self, iter):
self.update_by_iter(self._filter.convert_iter_to_child_iter(iter)) self.update_by_iter(self._filter.convert_iter_to_child_iter(iter))
@ -828,8 +834,11 @@ class PodcastListModel(gtk.ListStore):
if isinstance(c, GPodcast) and c.section == section] if isinstance(c, GPodcast) and c.section == section]
# Calculate the stats over all podcasts of this section # Calculate the stats over all podcasts of this section
total, deleted, new, downloaded, unplayed = map(sum, if len(channels) is 0:
zip(*[c.get_statistics() for c in channels])) total = deleted = new = downloaded = unplayed = 0
else:
total, deleted, new, downloaded, unplayed = list(map(sum,
list(zip(*[c.get_statistics() for c in channels]))))
# We could customized the section header here with the list # We could customized the section header here with the list
# of channels and their stats (i.e. add some "new" indicator) # of channels and their stats (i.e. add some "new" indicator)

View file

@ -34,7 +34,8 @@ logger = logging.getLogger(__name__)
from gpodder import util from gpodder import util
from gpodder import coverart from gpodder import coverart
import gtk from gi.repository import Gtk
from gi.repository import GdkPixbuf
class CoverDownloader(ObservableService): class CoverDownloader(ObservableService):
@ -117,15 +118,15 @@ class CoverDownloader(ObservableService):
pixbuf = None pixbuf = None
try: try:
pixbuf = gtk.gdk.pixbuf_new_from_file(filename) pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename)
except Exception, e: except Exception as e:
logger.warn('Cannot load cover art', exc_info=True) logger.warn('Cannot load cover art', exc_info=True)
if pixbuf is None and filename.startswith(channel.cover_file): if pixbuf is None and filename.startswith(channel.cover_file):
logger.info('Deleting broken cover: %s', filename) logger.info('Deleting broken cover: %s', filename)
util.delete_file(filename) util.delete_file(filename)
filename = get_filename() filename = get_filename()
try: try:
pixbuf = gtk.gdk.pixbuf_new_from_file(filename) pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename)
except Exception as e: except Exception as e:
logger.warn('Corrupt cover art on server, deleting', exc_info=True) logger.warn('Corrupt cover art on server, deleting', exc_info=True)
util.delete_file(filename) util.delete_file(filename)

View file

@ -16,39 +16,54 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from urllib.parse import urlparse
import gtk from gi.repository import Gtk
import gtk.gdk from gi.repository import Gdk
import gobject from gi.repository import Pango
import pango
import os
import cgi
import html
import logging
import gpodder import gpodder
_ = gpodder.gettext
import logging
logger = logging.getLogger(__name__)
from gpodder import util from gpodder import util
from gpodder.gtkui.draw import draw_text_box_centered from gpodder.gtkui.draw import draw_text_box_centered
_ = gpodder.gettext
logger = logging.getLogger(__name__)
has_webkit2 = False
try:
import gi
gi.require_version('WebKit2', '4.0')
from gi.repository import WebKit2
has_webkit2 = True
except (ImportError, ValueError):
logger.info('No WebKit2 gobject bindings, so no HTML shownotes')
def get_shownotes(enable_html, pane):
if enable_html and has_webkit2:
return gPodderShownotesHTML(pane)
else:
return gPodderShownotesText(pane)
class gPodderShownotes: class gPodderShownotes:
def __init__(self, shownotes_pane): def __init__(self, shownotes_pane):
self.shownotes_pane = shownotes_pane self.shownotes_pane = shownotes_pane
self.scrolled_window = gtk.ScrolledWindow() self.scrolled_window = Gtk.ScrolledWindow()
self.scrolled_window.set_shadow_type(gtk.SHADOW_IN) self.scrolled_window.set_shadow_type(Gtk.ShadowType.IN)
self.scrolled_window.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) self.scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
self.scrolled_window.add(self.init()) self.scrolled_window.add(self.init())
self.scrolled_window.show_all() self.scrolled_window.show_all()
self.da_message = gtk.DrawingArea() self.da_message = Gtk.DrawingArea()
self.da_message.connect('expose-event', \ self.da_message.set_property('expand', True)
self.on_shownotes_message_expose_event) self.da_message.connect('draw', self.on_shownotes_message_expose_event)
self.shownotes_pane.add(self.da_message) self.shownotes_pane.add(self.da_message)
self.shownotes_pane.add(self.scrolled_window) self.shownotes_pane.add(self.scrolled_window)
@ -90,19 +105,14 @@ class gPodderShownotes:
else: else:
self.show_pane(selected_episodes) self.show_pane(selected_episodes)
def on_shownotes_message_expose_event(self, drawingarea, event): def on_shownotes_message_expose_event(self, drawingarea, ctx):
ctx = event.window.cairo_create()
ctx.rectangle(event.area.x, event.area.y, \
event.area.width, event.area.height)
ctx.clip()
# paint the background white # paint the background white
colormap = event.window.get_colormap() ctx.set_source_rgba(1, 1, 1)
gc = event.window.new_gc(foreground=colormap.alloc_color('white')) x1, y1, x2, y2 = ctx.clip_extents()
event.window.draw_rectangle(gc, True, event.area.x, event.area.y, \ ctx.rectangle(x1, y1, x2 - x1, y2 - y1)
event.area.width, event.area.height) ctx.fill()
x, y, width, height, depth = event.window.get_geometry() width, height = drawingarea.get_allocated_width(), drawingarea.get_allocated_height(),
text = _('Please select an episode') text = _('Please select an episode')
draw_text_box_centered(ctx, drawingarea, width, height, text, None, None) draw_text_box_centered(ctx, drawingarea, width, height, text, None, None)
return False return False
@ -110,19 +120,18 @@ class gPodderShownotes:
class gPodderShownotesText(gPodderShownotes): class gPodderShownotesText(gPodderShownotes):
def init(self): def init(self):
self.text_view = gtk.TextView() self.text_view = Gtk.TextView()
self.text_view.set_wrap_mode(gtk.WRAP_WORD_CHAR) self.text_view.set_property('expand', True)
self.text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
self.text_view.set_border_width(10) self.text_view.set_border_width(10)
self.text_view.set_editable(False) self.text_view.set_editable(False)
self.text_view.connect('button-release-event', self.on_button_release) self.text_view.connect('button-release-event', self.on_button_release)
self.text_view.connect('key-press-event', self.on_key_press) self.text_view.connect('key-press-event', self.on_key_press)
self.text_buffer = gtk.TextBuffer() self.text_buffer = Gtk.TextBuffer()
self.text_buffer.create_tag('heading', scale=pango.SCALE_LARGE, weight=pango.WEIGHT_BOLD) self.text_buffer.create_tag('heading', scale=2, weight=Pango.Weight.BOLD)
self.text_buffer.create_tag('subheading', scale=pango.SCALE_SMALL) self.text_buffer.create_tag('subheading', scale=1.5)
self.text_buffer.create_tag('hyperlink', foreground="#0000FF", underline=pango.UNDERLINE_SINGLE) self.text_buffer.create_tag('hyperlink', foreground="#0000FF", underline=Pango.Underline.SINGLE)
self.text_view.set_buffer(self.text_buffer) self.text_view.set_buffer(self.text_buffer)
self.text_view.modify_bg(gtk.STATE_NORMAL,
gtk.gdk.color_parse('#ffffff'))
return self.text_view return self.text_view
def update(self, heading, subheading, episode): def update(self, heading, subheading, episode):
@ -149,7 +158,7 @@ class gPodderShownotesText(gPodderShownotes):
self.activate_links() self.activate_links()
def on_key_press(self, widget, event): def on_key_press(self, widget, event):
if gtk.gdk.keyval_name(event.keyval) == 'Return': if event.keyval == Gdk.KEY_Return:
self.activate_links() self.activate_links()
return True return True
@ -161,3 +170,123 @@ class gPodderShownotesText(gPodderShownotes):
target = next((url for start, end, url in self.hyperlinks if start < pos < end), None) target = next((url for start, end, url in self.hyperlinks if start < pos < end), None)
if target is not None: if target is not None:
util.open_website(target) util.open_website(target)
class gPodderShownotesHTML(gPodderShownotes):
def init(self):
# basic restrictions
settings = WebKit2.Settings()
settings.set_enable_java(False)
settings.set_enable_plugins(False)
settings.set_enable_javascript(False)
self.html_view = WebKit2.WebView.new_with_settings(settings)
self.html_view.set_property('expand', True)
self.html_view.connect('mouse-target-changed', self.on_mouse_over)
self.html_view.connect('context-menu', self.on_context_menu)
self.html_view.connect('decide-policy', self.on_decide_policy)
self.header = Gtk.Label.new()
self.header.set_halign(Gtk.Align.START)
self.header.set_valign(Gtk.Align.START)
self.header.set_property('margin', 10)
self.header.set_selectable(True)
self.status = Gtk.Label.new()
self.status.set_halign(Gtk.Align.START)
self.status.set_valign(Gtk.Align.END)
self.set_status(None)
grid = Gtk.Grid()
grid.attach(self.header, 0, 0, 1, 1)
grid.attach(self.html_view, 0, 1, 1, 1)
grid.attach(self.status, 0, 2, 1, 1)
return grid
def update(self, heading, subheading, episode):
tmpl = '<span size="x-large" font_weight="bold">%s</span>\n' \
+ '<span size="medium">%s</span>'
self.header.set_markup(tmpl % (html.escape(heading), html.escape(subheading)))
if episode.has_website_link:
self._base_uri = episode.link
else:
self._base_uri = episode.channel.url
# for incomplete base URI (e.g. http://919.noagendanotes.com)
baseURI = urlparse(self._base_uri)
if baseURI.path == '':
self._base_uri += '/'
self._loaded = False
description_html = episode.description_html
if description_html:
self.html_view.load_html(description_html, self._base_uri)
else:
self.html_view.load_plain_text(episode.description)
def on_mouse_over(self, webview, hit_test_result, modifiers):
if hit_test_result.context_is_link():
self.set_status(hit_test_result.get_link_uri())
else:
self.set_status(None)
def on_context_menu(self, webview, context_menu, event, hit_test_result):
whitelist_actions = [
WebKit2.ContextMenuAction.NO_ACTION,
WebKit2.ContextMenuAction.STOP,
WebKit2.ContextMenuAction.RELOAD,
WebKit2.ContextMenuAction.COPY,
WebKit2.ContextMenuAction.CUT,
WebKit2.ContextMenuAction.PASTE,
WebKit2.ContextMenuAction.DELETE,
WebKit2.ContextMenuAction.SELECT_ALL,
WebKit2.ContextMenuAction.INPUT_METHODS,
WebKit2.ContextMenuAction.COPY_VIDEO_LINK_TO_CLIPBOARD,
WebKit2.ContextMenuAction.COPY_AUDIO_LINK_TO_CLIPBOARD,
WebKit2.ContextMenuAction.COPY_LINK_TO_CLIPBOARD,
WebKit2.ContextMenuAction.COPY_IMAGE_TO_CLIPBOARD,
WebKit2.ContextMenuAction.COPY_IMAGE_URL_TO_CLIPBOARD
]
items = context_menu.get_items()
for item in items:
if item.get_stock_action() not in whitelist_actions:
context_menu.remove(item)
if hit_test_result.get_context() == WebKit2.HitTestResultContext.DOCUMENT:
item = self.create_open_item(
'shownotes-in-browser',
_('Open shownotes in web browser'),
self._base_uri)
context_menu.insert(item, -1)
elif hit_test_result.context_is_link():
item = self.create_open_item(
'link-in-browser',
_('Open link in web browser'),
hit_test_result.get_link_uri())
context_menu.insert(item, -1)
return False
def on_decide_policy(self, webview, decision, decision_type):
if decision_type == WebKit2.PolicyDecisionType.NEW_WINDOW_ACTION:
decision.ignore()
return False
elif decision_type == WebKit2.PolicyDecisionType.NAVIGATION_ACTION:
req = decision.get_request()
# about:blank is for plain text shownotes
if req.get_uri() in (self._base_uri, 'about:blank'):
decision.use()
else:
logger.debug("refusing to go to %s (base URI=%s)", req.get_uri(), self._base_uri)
decision.ignore()
return False
else:
decision.use()
return False
def on_open_in_browser(self, action):
util.open_website(action.url)
def create_open_item(self, name, label, url):
action = Gtk.Action.new(name, label, None, Gtk.STOCK_OPEN)
action.url = url
action.connect('activate', self.on_open_in_browser)
return WebKit2.ContextMenuItem.new(action)
def set_status(self, text):
self.status.set_label(text or " ")

View file

@ -24,36 +24,37 @@
# Thomas Perl <thp@gpodder.org> 2009-03-31 # Thomas Perl <thp@gpodder.org> 2009-03-31
# #
import gtk from gi.repository import Gdk
import gobject from gi.repository import Gtk
import pango from gi.repository import GObject
from gi.repository import Pango
import cgi import cgi
class SimpleMessageArea(gtk.HBox): class SimpleMessageArea(Gtk.HBox):
"""A simple, yellow message area. Inspired by gedit. """A simple, yellow message area. Inspired by gedit.
Original C source code: Original C source code:
http://svn.gnome.org/viewvc/gedit/trunk/gedit/gedit-message-area.c http://svn.gnome.org/viewvc/gedit/trunk/gedit/gedit-message-area.c
""" """
def __init__(self, message, buttons=()): def __init__(self, message, buttons=()):
gtk.HBox.__init__(self, spacing=6) Gtk.HBox.__init__(self, spacing=6)
self.set_border_width(6) self.set_border_width(6)
self.__in_style_set = False self.__in_style_updated = False
self.connect('style-set', self.__style_set) self.connect('style-updated', self.__style_updated)
self.connect('expose-event', self.__expose_event) self.connect('draw', self.__on_draw)
self.__label = gtk.Label() self.__label = Gtk.Label()
self.__label.set_alignment(0.0, 0.5) self.__label.set_alignment(0.0, 0.5)
self.__label.set_line_wrap(False) self.__label.set_line_wrap(False)
self.__label.set_ellipsize(pango.ELLIPSIZE_END) self.__label.set_ellipsize(Pango.EllipsizeMode.END)
self.__label.set_markup('<b>%s</b>' % cgi.escape(message)) self.__label.set_markup('<b>%s</b>' % cgi.escape(message))
self.pack_start(self.__label, expand=True, fill=True) self.pack_start(self.__label, True, True, 0)
hbox = gtk.HBox() hbox = Gtk.HBox()
for button in buttons: for button in buttons:
hbox.pack_start(button, expand=True, fill=False) hbox.pack_start(button, True, False, 0)
self.pack_start(hbox, expand=False, fill=False) self.pack_start(hbox, False, False, 0)
def set_markup(self, markup, line_wrap=True, min_width=3, max_width=100): def set_markup(self, markup, line_wrap=True, min_width=3, max_width=100):
# The longest line should determine the size of the label # The longest line should determine the size of the label
@ -66,11 +67,11 @@ class SimpleMessageArea(gtk.HBox):
self.__label.set_markup(markup) self.__label.set_markup(markup)
self.__label.set_line_wrap(line_wrap) self.__label.set_line_wrap(line_wrap)
def __style_set(self, widget, previous_style): def __style_updated(self, widget):
if self.__in_style_set: if self.__in_style_updated:
return return
w = gtk.Window(gtk.WINDOW_POPUP) w = Gtk.Window(Gtk.WindowType.POPUP)
w.set_name('gtk-tooltip') w.set_name('gtk-tooltip')
w.ensure_style() w.ensure_style()
style = w.get_style() style = w.get_style()
@ -84,33 +85,33 @@ class SimpleMessageArea(gtk.HBox):
self.queue_draw() self.queue_draw()
def __expose_event(self, widget, event): def __on_draw(self, widget, cr):
style = widget.get_style() style = widget.get_style()
rect = widget.get_allocation() x, rect = Gdk.cairo_get_clip_rectangle(cr)
style.paint_flat_box(widget.window, gtk.STATE_NORMAL, Gtk.paint_flat_box(style, cr, Gtk.StateType.NORMAL,
gtk.SHADOW_OUT, None, widget, "tooltip", Gtk.ShadowType.OUT, widget, "tooltip",
rect.x, rect.y, rect.width, rect.height) rect.x, rect.y, rect.width, rect.height)
return False return False
class SpinningProgressIndicator(gtk.Image): class SpinningProgressIndicator(Gtk.Image):
# Progress indicator loading inspired by glchess from gnome-games-clutter # Progress indicator loading inspired by glchess from gnome-games-clutter
def __init__(self, size=32): def __init__(self, size=32):
gtk.Image.__init__(self) Gtk.Image.__init__(self)
self._frames = [] self._frames = []
self._frame_id = 0 self._frame_id = 0
# Load the progress indicator # Load the progress indicator
icon_theme = gtk.icon_theme_get_default() icon_theme = Gtk.IconTheme.get_default()
try: try:
icon = icon_theme.load_icon('process-working', size, 0) icon = icon_theme.load_icon('process-working', size, 0)
width, height = icon.get_width(), icon.get_height() width, height = icon.get_width(), icon.get_height()
if width < size or height < size: if width < size or height < size:
size = min(width, height) size = min(width, height)
for row in range(height/size): for row in range(height//size):
for column in range(width/size): for column in range(width//size):
frame = icon.subpixbuf(column*size, row*size, size, size) frame = icon.subpixbuf(column*size, row*size, size, size)
self._frames.append(frame) self._frames.append(frame)
# Remove the first frame (the "idle" icon) # Remove the first frame (the "idle" icon)
@ -119,7 +120,7 @@ class SpinningProgressIndicator(gtk.Image):
self.step_animation() self.step_animation()
except: except:
# FIXME: This is not very beautiful :/ # FIXME: This is not very beautiful :/
self.set_from_stock(gtk.STOCK_EXECUTE, gtk.ICON_SIZE_BUTTON) self.set_from_icon_name('system-run', Gtk.IconSize.BUTTON)
def step_animation(self): def step_animation(self):
if len(self._frames) > 1: if len(self._frames) > 1:

View file

@ -24,13 +24,9 @@
# #
import copy import copy
from functools import reduce
try: import json
# For Python < 2.6, we use the "simplejson" add-on module
import simplejson as json
except ImportError:
# Python 2.6 already ships with a nice "json" module
import json
class JsonConfigSubtree(object): class JsonConfigSubtree(object):
@ -89,7 +85,7 @@ class JsonConfig(object):
For newly-set keys, on_key_changed is also called. In this case, For newly-set keys, on_key_changed is also called. In this case,
None will be the old_value: None will be the old_value:
>>> def callback(*args): print 'callback:', args >>> def callback(*args): print('callback:', args)
>>> c = JsonConfig(on_key_changed=callback) >>> c = JsonConfig(on_key_changed=callback)
>>> c.a.b = 10 >>> c.a.b = 10
callback: ('a.b', None, 10) callback: ('a.b', None, 10)
@ -102,7 +98,7 @@ class JsonConfig(object):
Please note that dict-style access will not call on_key_changed: Please note that dict-style access will not call on_key_changed:
>>> def callback(*args): print 'callback:', args >>> def callback(*args): print('callback:', args)
>>> c = JsonConfig(on_key_changed=callback) >>> c = JsonConfig(on_key_changed=callback)
>>> c.a.b = 1 # This works as expected >>> c.a.b = 1 # This works as expected
callback: ('a.b', None, 1) callback: ('a.b', None, 1)
@ -129,14 +125,14 @@ class JsonConfig(object):
>>> c = JsonConfig() >>> c = JsonConfig()
>>> c.a.b = 10 >>> c.a.b = 10
>>> backup = repr(c) >>> backup = repr(c)
>>> print c.a.b >>> print(c.a.b)
10 10
>>> c.a.b = 11 >>> c.a.b = 11
>>> print c.a.b >>> print(c.a.b)
11 11
>>> c._restore(backup) >>> c._restore(backup)
False False
>>> print c.a.b >>> print(c.a.b)
10 10
""" """
self._data = json.loads(backup) self._data = json.loads(backup)
@ -156,7 +152,7 @@ class JsonConfig(object):
work_queue = [(self._data, merge_source)] work_queue = [(self._data, merge_source)]
while work_queue: while work_queue:
data, default = work_queue.pop() data, default = work_queue.pop()
for key, value in default.iteritems(): for key, value in default.items():
if key not in data: if key not in data:
# Copy defaults for missing key # Copy defaults for missing key
data[key] = copy.deepcopy(value) data[key] = copy.deepcopy(value)
@ -175,7 +171,7 @@ class JsonConfig(object):
def __repr__(self): def __repr__(self):
""" """
>>> c = JsonConfig('{"a": 1}') >>> c = JsonConfig('{"a": 1}')
>>> print c >>> print(c)
{ {
"a": 1 "a": 1
} }

View file

@ -29,7 +29,7 @@
# For Python 2.5, we need to request the "with" statement # For Python 2.5, we need to request the "with" statement
from __future__ import with_statement
try: try:
import sqlite3.dbapi2 as sqlite import sqlite3.dbapi2 as sqlite
@ -55,7 +55,7 @@ class Store(object):
# necessary. The value None is special-cased and never cast. # necessary. The value None is special-cased and never cast.
cls = o.__class__.__slots__[slot] cls = o.__class__.__slots__[slot]
if value is not None: if value is not None:
if isinstance(value, unicode): if isinstance(value, bytes):
value = value.decode('utf-8') value = value.decode('utf-8')
value = cls(value) value = cls(value)
setattr(o, slot, value) setattr(o, slot, value)
@ -66,7 +66,9 @@ class Store(object):
def close(self): def close(self):
with self.lock: with self.lock:
self.db.isolation_level = None
self.db.execute('VACUUM') self.db.execute('VACUUM')
self.db.isolation_level = ''
self.db.close() self.db.close()
def _register(self, class_): def _register(self, class_):
@ -86,7 +88,7 @@ class Store(object):
', '.join('%s TEXT'%s for s in slots))) ', '.join('%s TEXT'%s for s in slots)))
def convert(self, v): def convert(self, v):
if isinstance(v, unicode): if isinstance(v, str):
return v return v
elif isinstance(v, str): elif isinstance(v, str):
# XXX: Rewrite ^^^ as "isinstance(v, bytes)" in Python 3 # XXX: Rewrite ^^^ as "isinstance(v, bytes)" in Python 3
@ -96,7 +98,7 @@ class Store(object):
def update(self, o, **kwargs): def update(self, o, **kwargs):
self.remove(o) self.remove(o)
for k, v in kwargs.items(): for k, v in list(kwargs.items()):
setattr(o, k, v) setattr(o, k, v)
self.save(o) self.save(o)
@ -134,9 +136,9 @@ class Store(object):
if kwargs: if kwargs:
sql += ' WHERE %s' % (' AND '.join('%s=?' % k for k in kwargs)) sql += ' WHERE %s' % (' AND '.join('%s=?' % k for k in kwargs))
try: try:
self.db.execute(sql, kwargs.values()) self.db.execute(sql, list(kwargs.values()))
return True return True
except Exception, e: except Exception as e:
return False return False
def remove(self, o): def remove(self, o):
@ -164,18 +166,18 @@ class Store(object):
if kwargs: if kwargs:
sql += ' WHERE %s' % (' AND '.join('%s=?' % k for k in kwargs)) sql += ' WHERE %s' % (' AND '.join('%s=?' % k for k in kwargs))
try: try:
cur = self.db.execute(sql, kwargs.values()) cur = self.db.execute(sql, list(kwargs.values()))
except Exception, e: except Exception as e:
raise raise
def apply(row): def apply(row):
o = class_.__new__(class_) o = class_.__new__(class_)
for attr, value in zip(slots, row): for attr, value in zip(slots, row):
try: try:
self._set(o, attr, value) self._set(o, attr, value)
except ValueError, ve: except ValueError as ve:
return None return None
return o return o
return filter(lambda x: x is not None, [apply(row) for row in cur]) return [x for x in [apply(row) for row in cur] if x is not None]
def get(self, class_, **kwargs): def get(self, class_, **kwargs):
result = self.load(class_, **kwargs) result = self.load(class_, **kwargs)
@ -199,7 +201,7 @@ if __name__ == '__main__':
m.save(Person('User %d' % x, x*20) for x in range(50)) m.save(Person('User %d' % x, x*20) for x in range(50))
p = m.get(Person, id=200) p = m.get(Person, id=200)
print p print(p)
m.remove(p) m.remove(p)
p = m.get(Person, id=200) p = m.get(Person, id=200)
@ -219,5 +221,5 @@ if __name__ == '__main__':
# A schema update takes place here # A schema update takes place here
m.save(Person('User %d' % x, x*20, 'user@home.com') for x in range(50)) m.save(Person('User %d' % x, x*20, 'user@home.com') for x in range(50))
print m.load(Person) print(m.load(Person))

View file

@ -112,7 +112,7 @@ class PodcastModelObject(object):
o = cls(*args) o = cls(*args)
# XXX: all(map(lambda k: hasattr(o, k), d))? # XXX: all(map(lambda k: hasattr(o, k), d))?
for k, v in d.iteritems(): for k, v in d.items():
setattr(o, k, v) setattr(o, k, v)
return o return o
@ -433,7 +433,7 @@ class PodcastEpisode(PodcastModelObject):
if self.download_filename is None and (check_only or not create): if self.download_filename is None and (check_only or not create):
return None return None
ext = self.extension(may_call_local_filename=False).encode('utf-8', 'ignore') ext = self.extension(may_call_local_filename=False)
if not check_only and (force_update or not self.download_filename): if not check_only and (force_update or not self.download_filename):
# Avoid and catch gPodder bug 1440 and similar situations # Avoid and catch gPodder bug 1440 and similar situations
@ -500,8 +500,7 @@ class PodcastEpisode(PodcastModelObject):
self.download_filename = wanted_filename self.download_filename = wanted_filename
self.save() self.save()
return os.path.join(util.sanitize_encoding(self.channel.save_dir), return os.path.join(self.channel.save_dir, self.download_filename)
util.sanitize_encoding(self.download_filename))
def extension(self, may_call_local_filename=True): def extension(self, may_call_local_filename=True):
filename, ext = util.filename_from_url(self.url) filename, ext = util.filename_from_url(self.url)
@ -640,10 +639,10 @@ class PodcastEpisode(PodcastModelObject):
class PodcastChannel(PodcastModelObject): class PodcastChannel(PodcastModelObject):
__slots__ = schema.PodcastColumns + ('_common_prefix',) __slots__ = schema.PodcastColumns + ('_common_prefix',)
UNICODE_TRANSLATE = {ord(u'ö'): u'o', ord(u'ä'): u'a', ord(u'ü'): u'u'} UNICODE_TRANSLATE = {ord('ö'): 'o', ord('ä'): 'a', ord('ü'): 'u'}
# Enumerations for download strategy # Enumerations for download strategy
STRATEGY_DEFAULT, STRATEGY_LATEST = range(2) STRATEGY_DEFAULT, STRATEGY_LATEST = list(range(2))
# Description and ordering of strategies # Description and ordering of strategies
STRATEGIES = [ STRATEGIES = [
@ -812,12 +811,8 @@ class PodcastChannel(PodcastModelObject):
return re.sub('^the ', '', key).translate(cls.UNICODE_TRANSLATE) return re.sub('^the ', '', key).translate(cls.UNICODE_TRANSLATE)
@classmethod @classmethod
def load(cls, model, url, create=True, authentication_tokens=None,\ def load(cls, model, url, create=True, authentication_tokens=None, max_episodes=0):
max_episodes=0): existing = [p for p in model.get_podcasts() if p.url == url]
if isinstance(url, unicode):
url = url.encode('utf-8')
existing = filter(lambda p: p.url == url, model.get_podcasts())
if existing: if existing:
return existing[0] return existing[0]
@ -835,7 +830,7 @@ class PodcastChannel(PodcastModelObject):
try: try:
tmp.update(max_episodes) tmp.update(max_episodes)
except Exception, e: except Exception as e:
logger.debug('Fetch failed. Removing buggy feed.') logger.debug('Fetch failed. Removing buggy feed.')
tmp.remove_downloaded() tmp.remove_downloaded()
tmp.delete() tmp.delete()
@ -1034,7 +1029,7 @@ class PodcastChannel(PodcastModelObject):
pass pass
self.save() self.save()
except Exception, e: except Exception as e:
# "Not really" errors # "Not really" errors
#feedcore.AuthenticationRequired #feedcore.AuthenticationRequired
# Temporary errors # Temporary errors
@ -1153,7 +1148,7 @@ class PodcastChannel(PodcastModelObject):
return self.children return self.children
def get_episodes(self, state): def get_episodes(self, state):
return filter(lambda e: e.state == state, self.get_all_episodes()) return [e for e in self.get_all_episodes() if e.state == state]
def find_unique_folder_name(self, download_folder): def find_unique_folder_name(self, download_folder):
# Remove trailing dots to avoid errors on Windows (bug 600) # Remove trailing dots to avoid errors on Windows (bug 600)
@ -1191,9 +1186,6 @@ class PodcastChannel(PodcastModelObject):
save_dir = os.path.join(gpodder.downloads, self.download_folder) save_dir = os.path.join(gpodder.downloads, self.download_folder)
# Avoid encoding errors for OS-specific functions (bug 1570)
save_dir = util.sanitize_encoding(save_dir)
# Create save_dir if it does not yet exist # Create save_dir if it does not yet exist
if not util.make_directory(save_dir): if not util.make_directory(save_dir):
logger.error('Could not create save_dir: %s', save_dir) logger.error('Could not create save_dir: %s', save_dir)

View file

@ -49,13 +49,13 @@ MYGPOCLIENT_REQUIRED = '1.4'
if not hasattr(mygpoclient, 'require_version') or \ if not hasattr(mygpoclient, 'require_version') or \
not mygpoclient.require_version(MYGPOCLIENT_REQUIRED): not mygpoclient.require_version(MYGPOCLIENT_REQUIRED):
print >>sys.stderr, """ print("""
Please upgrade your mygpoclient library. Please upgrade your mygpoclient library.
See http://thp.io/2010/mygpoclient/ See http://thp.io/2010/mygpoclient/
Required version: %s Required version: %s
Installed version: %s Installed version: %s
""" % (MYGPOCLIENT_REQUIRED, mygpoclient.__version__) """ % (MYGPOCLIENT_REQUIRED, mygpoclient.__version__), file=sys.stderr)
sys.exit(1) sys.exit(1)
try: try:
@ -79,7 +79,7 @@ class SinceValue(object):
__slots__ = {'host': str, 'device_id': str, 'category': int, 'since': int} __slots__ = {'host': str, 'device_id': str, 'category': int, 'since': int}
# Possible values for the "category" field # Possible values for the "category" field
PODCASTS, EPISODES = range(2) PODCASTS, EPISODES = list(range(2))
def __init__(self, host, device_id, category, since=0): def __init__(self, host, device_id, category, since=0):
self.host = host self.host = host
@ -91,7 +91,7 @@ class SubscribeAction(object):
__slots__ = {'action_type': int, 'url': str} __slots__ = {'action_type': int, 'url': str}
# Possible values for the "action_type" field # Possible values for the "action_type" field
ADD, REMOVE = range(2) ADD, REMOVE = list(range(2))
def __init__(self, action_type, url): def __init__(self, action_type, url):
self.action_type = action_type self.action_type = action_type
@ -506,7 +506,7 @@ class MygPoClient(object):
# handle outside # handle outside
raise raise
except Exception, e: except Exception as e:
logger.warn('Exception while polling for episodes.', exc_info=True) logger.warn('Exception while polling for episodes.', exc_info=True)
# Step 2: Upload Episode actions # Step 2: Upload Episode actions
@ -533,7 +533,7 @@ class MygPoClient(object):
self._config.mygpo.enabled = False self._config.mygpo.enabled = False
return False return False
except Exception, e: except Exception as e:
logger.error('Cannot upload episode actions: %s', str(e), exc_info=True) logger.error('Cannot upload episode actions: %s', str(e), exc_info=True)
return False return False
@ -598,7 +598,7 @@ class MygPoClient(object):
self._config.mygpo.enabled = False self._config.mygpo.enabled = False
return False return False
except Exception, e: except Exception as e:
logger.error('Cannot upload subscriptions: %s', str(e), exc_info=True) logger.error('Cannot upload subscriptions: %s', str(e), exc_info=True)
return False return False
@ -615,7 +615,7 @@ class MygPoClient(object):
self._config.mygpo.enabled = False self._config.mygpo.enabled = False
return False return False
except Exception, e: except Exception as e:
logger.error('Cannot update device %s: %s', self.device_id, logger.error('Cannot update device %s: %s', self.device_id,
str(e), exc_info=True) str(e), exc_info=True)
return False return False

View file

@ -71,6 +71,7 @@ class Importer(object):
if os.path.exists(url): if os.path.exists(url):
doc = xml.dom.minidom.parse(url) doc = xml.dom.minidom.parse(url)
else: else:
# FIXME: is it ok to pass bytes to parseString?
doc = xml.dom.minidom.parseString(util.urlopen(url).read()) doc = xml.dom.minidom.parseString(util.urlopen(url).read())
for outline in doc.getElementsByTagName('outline'): for outline in doc.getElementsByTagName('outline'):

View file

@ -51,7 +51,7 @@
import gpodder import gpodder
import urllib import urllib.request, urllib.parse, urllib.error
class MediaPlayerDBusReceiver(object): class MediaPlayerDBusReceiver(object):
INTERFACE = 'org.gpodder.player' INTERFACE = 'org.gpodder.player'
@ -77,12 +77,7 @@ class MediaPlayerDBusReceiver(object):
pass pass
def on_playback_stopped(self, start, end, total, file_uri): def on_playback_stopped(self, start, end, total, file_uri):
# Assume the URI comes as quoted UTF-8 string, so decode
# it first to utf-8 (should be no problem) for unquoting
# to work correctly on this later on (Maemo bug 11811)
if isinstance(file_uri, unicode):
file_uri = file_uri.encode('utf-8')
if file_uri.startswith('/'): if file_uri.startswith('/'):
file_uri = 'file://' + urllib.quote(file_uri) file_uri = 'file://' + urllib.parse.quote(file_uri)
self.on_play_event(start, end, total, file_uri) self.on_play_event(start, end, total, file_uri)

View file

@ -28,12 +28,7 @@ _ = gpodder.gettext
from gpodder import model from gpodder import model
from gpodder import util from gpodder import util
try: import json
# For Python < 2.6, we use the "simplejson" add-on module
import simplejson as json
except ImportError:
# Python 2.6 already ships with a nice "json" module
import json
import logging import logging
import os import os
@ -41,7 +36,7 @@ import time
import re import re
import email import email
import urllib import urllib.request, urllib.parse, urllib.error
# gPodder's consumer key for the Soundcloud API # gPodder's consumer key for the Soundcloud API
@ -58,7 +53,7 @@ def soundcloud_parsedate(s):
parsed with this function (2009/11/03 13:37:00). parsed with this function (2009/11/03 13:37:00).
""" """
m = re.match(r'(\d{4})/(\d{2})/(\d{2}) (\d{2}):(\d{2}):(\d{2})', s) m = re.match(r'(\d{4})/(\d{2})/(\d{2}) (\d{2}):(\d{2}):(\d{2})', s)
return time.mktime([int(x) for x in m.groups()]+[0, 0, -1]) return time.mktime(tuple([int(x) for x in m.groups()]+[0, 0, -1]))
def get_param(s, param='filename', header='content-disposition'): def get_param(s, param='filename', header='content-disposition'):
"""Get a parameter from a string of headers """Get a parameter from a string of headers
@ -76,8 +71,8 @@ def get_param(s, param='filename', header='content-disposition'):
if encoding: if encoding:
value.append(part.decode(encoding)) value.append(part.decode(encoding))
else: else:
value.append(unicode(part)) value.append(str(part))
return u''.join(value) return ''.join(value)
return None return None
@ -92,7 +87,7 @@ def get_metadata(url):
headers = track_fp.info() headers = track_fp.info()
filesize = headers['content-length'] or '0' filesize = headers['content-length'] or '0'
filetype = headers['content-type'] or 'application/octet-stream' filetype = headers['content-type'] or 'application/octet-stream'
headers_s = '\n'.join('%s:%s'%(k,v) for k, v in headers.items()) headers_s = '\n'.join('%s:%s'%(k,v) for k, v in list(headers.items()))
filename = get_param(headers_s) or os.path.basename(os.path.dirname(url)) filename = get_param(headers_s) or os.path.basename(os.path.dirname(url))
track_fp.close() track_fp.close()
return filesize, filetype, filename return filesize, filetype, filename
@ -121,7 +116,7 @@ class SoundcloudUser(object):
try: try:
json_url = 'https://api.soundcloud.com/users/%s.json?consumer_key=%s' % (self.username, CONSUMER_KEY) json_url = 'https://api.soundcloud.com/users/%s.json?consumer_key=%s' % (self.username, CONSUMER_KEY)
user_info = json.load(util.urlopen(json_url)) user_info = json.loads(util.urlopen(json_url).read().decode('utf-8'))
self.cache[key] = user_info self.cache[key] = user_info
finally: finally:
self.commit_cache() self.commit_cache()
@ -252,5 +247,5 @@ model.register_custom_handler(SoundcloudFeed)
model.register_custom_handler(SoundcloudFavFeed) model.register_custom_handler(SoundcloudFavFeed)
def search_for_user(query): def search_for_user(query):
json_url = 'https://api.soundcloud.com/users.json?q=%s&consumer_key=%s' % (urllib.quote(query), CONSUMER_KEY) json_url = 'https://api.soundcloud.com/users.json?q=%s&consumer_key=%s' % (urllib.parse.quote(query), CONSUMER_KEY)
return json.load(util.urlopen(json_url)) return json.loads(util.urlopen(json_url).read().decode('utf-8'))

View file

@ -40,8 +40,8 @@ class Matcher(object):
def match(self, term): def match(self, term):
try: try:
return bool(eval(term, {'__builtins__': None}, self)) return bool(eval(term, {'__builtins__': None}, self))
except Exception, e: except Exception as e:
print e print(e)
return False return False
def __getitem__(self, k): def __getitem__(self, k):
@ -69,7 +69,7 @@ class Matcher(object):
# Nouns (for comparisons) # Nouns (for comparisons)
if k in ('megabytes', 'mb'): if k in ('megabytes', 'mb'):
return float(episode.file_size) / (1024*1024) return episode.file_size / (1024*1024)
elif k == 'title': elif k == 'title':
return episode.title return episode.title
elif k == 'description': elif k == 'description':
@ -79,9 +79,9 @@ class Matcher(object):
elif k == 'age': elif k == 'age':
return episode.age_in_days() return episode.age_in_days()
elif k in ('minutes', 'min'): elif k in ('minutes', 'min'):
return float(episode.total_time) / 60 return episode.total_time / 60
elif k in ('remaining', 'rem'): elif k in ('remaining', 'rem'):
return float(episode.total_time - episode.current_position) / 60 return episode.total_time - episode.current_position / 60
raise KeyError(k) raise KeyError(k)
@ -140,8 +140,8 @@ class EQL(object):
if not self._regex and not self._string: if not self._regex and not self._string:
try: try:
self._query = compile(query, '<eql-string>', 'eval') self._query = compile(query, '<eql-string>', 'eval')
except Exception, e: except Exception as e:
print e print(e)
self._query = None self._query = None
@ -157,7 +157,7 @@ class EQL(object):
return Matcher(episode).match(self._query) return Matcher(episode).match(self._query)
def filter(self, episodes): def filter(self, episodes):
return filter(self.match, episodes) return list(filter(self.match, episodes))
def UserEQL(query): def UserEQL(query):

View file

@ -210,7 +210,7 @@ def upgrade(db, filename):
backup = '%s_upgraded-v%d_%d' % (filename, int(version), int(time.time())) backup = '%s_upgraded-v%d_%d' % (filename, int(version), int(time.time()))
try: try:
shutil.copy(filename, backup) shutil.copy(filename, backup)
except Exception, e: except Exception as e:
raise Exception('Cannot create DB backup before upgrade: ' + e) raise Exception('Cannot create DB backup before upgrade: ' + e)
db.execute("DELETE FROM version") db.execute("DELETE FROM version")
@ -246,7 +246,7 @@ def convert_gpodder2_db(old_db, new_db):
old_cur = old_db.cursor() old_cur = old_db.cursor()
columns = [x[1] for x in old_cur.execute('PRAGMA table_info(channels)')] columns = [x[1] for x in old_cur.execute('PRAGMA table_info(channels)')]
for row in old_cur.execute('SELECT * FROM channels'): for row in old_cur.execute('SELECT * FROM channels'):
row = dict(zip(columns, row)) row = dict(list(zip(columns, row)))
values = ( values = (
row['id'], row['id'],
row['override_title'] or row['title'], row['override_title'] or row['title'],
@ -276,7 +276,7 @@ def convert_gpodder2_db(old_db, new_db):
old_cur = old_db.cursor() old_cur = old_db.cursor()
columns = [x[1] for x in old_cur.execute('PRAGMA table_info(episodes)')] columns = [x[1] for x in old_cur.execute('PRAGMA table_info(episodes)')]
for row in old_cur.execute('SELECT * FROM episodes'): for row in old_cur.execute('SELECT * FROM episodes'):
row = dict(zip(columns, row)) row = dict(list(zip(columns, row)))
values = ( values = (
row['id'], row['id'],
row['channel_id'], row['channel_id'],

View file

@ -85,8 +85,8 @@ if pymtp_available:
folder = folder.contents folder = folder.contents
name = self.sep.join([path, folder.name]).lstrip(self.sep) name = self.sep.join([path, folder.name]).lstrip(self.sep)
result[name] = folder.folder_id result[name] = folder.folder_id
if folder.child: if folder.get_child():
result.update(self.unfold(folder.child, name)) result.update(self.unfold(folder.get_child(), name))
folder = folder.sibling folder = folder.sibling
return result return result
@ -97,7 +97,7 @@ if pymtp_available:
while parts: while parts:
prefix.append(parts[0]) prefix.append(parts[0])
tmpath = self.sep.join(prefix) tmpath = self.sep.join(prefix)
if self.folders.has_key(tmpath): if tmpath in self.folders:
folder_id = self.folders[tmpath] folder_id = self.folders[tmpath]
else: else:
folder_id = self.create_folder(parts[0], parent=folder_id) folder_id = self.create_folder(parts[0], parent=folder_id)
@ -136,7 +136,7 @@ def get_track_length(filename):
try: try:
mp3file = eyed3.mp3.Mp3AudioFile(filename) mp3file = eyed3.mp3.Mp3AudioFile(filename)
return int(mp3file.info.time_secs * 1000) return int(mp3file.info.time_secs * 1000)
except Exception, e: except Exception as e:
logger.warn('Could not determine length: %s', filename, exc_info=True) logger.warn('Could not determine length: %s', filename, exc_info=True)
return int(60*60*1000*3) # Default is three hours (to be on the safe side) return int(60*60*1000*3) # Default is three hours (to be on the safe side)
@ -225,16 +225,16 @@ class Device(services.ObservableService):
sync_task.status=sync_task.QUEUED sync_task.status=sync_task.QUEUED
sync_task.device=self sync_task.device=self
# New Task, we must wait on the GTK Loop
self.download_status_model.register_task(sync_task) self.download_status_model.register_task(sync_task)
self.download_queue_manager.add_task(sync_task) # Executes after task has been registered
util.idle_add(self.download_queue_manager.queue_task, sync_task)
else: else:
logger.warning("No episodes to sync") logger.warning("No episodes to sync")
if done_callback: if done_callback:
done_callback() done_callback()
return True
def remove_tracks(self, tracklist): def remove_tracks(self, tracklist):
for idx, track in enumerate(tracklist): for idx, track in enumerate(tracklist):
if self.cancelled: if self.cancelled:
@ -379,7 +379,7 @@ class iPodDevice(Device):
try: try:
released = gpod.itdb_time_mac_to_host(track.time_released) released = gpod.itdb_time_mac_to_host(track.time_released)
released = util.format_date(released) released = util.format_date(released)
except ValueError, ve: except ValueError as ve:
# timestamp out of range for platform time_t (bug 418) # timestamp out of range for platform time_t (bug 418)
logger.info('Cannot convert track time: %s', ve) logger.info('Cannot convert track time: %s', ve)
released = 0 released = 0
@ -502,7 +502,7 @@ class MP3PlayerDevice(Device):
download_status_model, download_status_model,
download_queue_manager): download_queue_manager):
Device.__init__(self, config) Device.__init__(self, config)
self.destination = util.sanitize_encoding(self._config.device_sync.device_folder) self.destination = self._config.device_sync.device_folder
self.buffer_size = 1024*1024 # 1 MiB self.buffer_size = 1024*1024 # 1 MiB
self.download_status_model = download_status_model self.download_status_model = download_status_model
self.download_queue_manager = download_queue_manager self.download_queue_manager = download_queue_manager
@ -532,11 +532,11 @@ class MP3PlayerDevice(Device):
else: else:
folder = self.destination folder = self.destination
return util.sanitize_encoding(folder) return folder
def get_episode_file_on_device(self, episode): def get_episode_file_on_device(self, episode):
# get the local file # get the local file
from_file = util.sanitize_encoding(episode.local_filename(create=False)) from_file = episode.local_filename(create=False)
# get the formated base name # get the formated base name
filename_base = util.sanitize_filename(episode.sync_filename( filename_base = util.sanitize_filename(episode.sync_filename(
self._config.device_sync.custom_sync_name_enabled, self._config.device_sync.custom_sync_name_enabled,
@ -554,7 +554,7 @@ class MP3PlayerDevice(Device):
return to_file return to_file
def add_track(self, episode,reporthook=None): def add_track(self, episode,reporthook=None):
self.notify('status', _('Adding %s') % episode.title.decode('utf-8', 'ignore')) self.notify('status', _('Adding %s') % episode.title)
# get the folder on the device # get the folder on the device
folder = self.get_episode_folder_on_device(episode) folder = self.get_episode_folder_on_device(episode)
@ -564,7 +564,7 @@ class MP3PlayerDevice(Device):
# local_filename(create=False) must never return None as filename # local_filename(create=False) must never return None as filename
assert filename is not None assert filename is not None
from_file = util.sanitize_encoding(filename) from_file = filename
# verify free space # verify free space
needed = util.calculate_size(from_file) needed = util.calculate_size(from_file)
@ -578,7 +578,7 @@ class MP3PlayerDevice(Device):
# get the filename that will be used on the device # get the filename that will be used on the device
to_file = self.get_episode_file_on_device(episode) to_file = self.get_episode_file_on_device(episode)
to_file = util.sanitize_encoding(os.path.join(folder, to_file)) to_file = os.path.join(folder, to_file)
if not os.path.exists(folder): if not os.path.exists(folder):
try: try:
@ -590,7 +590,7 @@ class MP3PlayerDevice(Device):
if not os.path.exists(to_file): if not os.path.exists(to_file):
logger.info('Copying %s => %s', logger.info('Copying %s => %s',
os.path.basename(from_file), os.path.basename(from_file),
to_file.decode(util.encoding)) to_file)
self.copy_file_progress(from_file, to_file, reporthook) self.copy_file_progress(from_file, to_file, reporthook)
return True return True
@ -598,7 +598,7 @@ class MP3PlayerDevice(Device):
def copy_file_progress(self, from_file, to_file, reporthook=None): def copy_file_progress(self, from_file, to_file, reporthook=None):
try: try:
out_file = open(to_file, 'wb') out_file = open(to_file, 'wb')
except IOError, ioerror: except IOError as ioerror:
d = {'filename': ioerror.filename, 'message': ioerror.strerror} d = {'filename': ioerror.filename, 'message': ioerror.strerror}
self.errors.append(_('Error opening %(filename)s: %(message)s') % d) self.errors.append(_('Error opening %(filename)s: %(message)s') % d)
self.cancel() self.cancel()
@ -606,7 +606,7 @@ class MP3PlayerDevice(Device):
try: try:
in_file = open(from_file, 'rb') in_file = open(from_file, 'rb')
except IOError, ioerror: except IOError as ioerror:
d = {'filename': ioerror.filename, 'message': ioerror.strerror} d = {'filename': ioerror.filename, 'message': ioerror.strerror}
self.errors.append(_('Error opening %(filename)s: %(message)s') % d) self.errors.append(_('Error opening %(filename)s: %(message)s') % d)
self.cancel() self.cancel()
@ -622,7 +622,7 @@ class MP3PlayerDevice(Device):
bytes_read += len(s) bytes_read += len(s)
try: try:
out_file.write(s) out_file.write(s)
except IOError, ioerror: except IOError as ioerror:
self.errors.append(ioerror.strerror) self.errors.append(ioerror.strerror)
try: try:
out_file.close() out_file.close()
@ -697,7 +697,7 @@ class MTPDevice(Device):
self.__model_name = None self.__model_name = None
try: try:
self.__MTPDevice = MTP() self.__MTPDevice = MTP()
except NameError, e: except NameError as e:
# pymtp not available / not installed (see bug 924) # pymtp not available / not installed (see bug 924)
logger.error('pymtp not found: %s', str(e)) logger.error('pymtp not found: %s', str(e))
self.__MTPDevice = None self.__MTPDevice = None
@ -705,7 +705,7 @@ class MTPDevice(Device):
def __callback(self, sent, total): def __callback(self, sent, total):
if self.cancelled: if self.cancelled:
return -1 return -1
percentage = round(float(sent)/float(total)*100) percentage = round(sent/total*100)
text = ('%i%%' % percentage) text = ('%i%%' % percentage)
self.notify('progress', sent, total, text) self.notify('progress', sent, total, text)
@ -722,7 +722,7 @@ class MTPDevice(Device):
try: try:
d = time.gmtime(date) d = time.gmtime(date)
return time.strftime("%Y%m%d-%H%M%S.0Z", d) return time.strftime("%Y%m%d-%H%M%S.0Z", d)
except Exception, exc: except Exception as exc:
logger.error('ERROR: An error has happend while trying to convert date to an mtp string') logger.error('ERROR: An error has happend while trying to convert date to an mtp string')
return None return None
@ -752,10 +752,10 @@ class MTPDevice(Device):
_date -= shift_in_sec _date -= shift_in_sec
else: else:
raise ValueError("Expected + or -") raise ValueError("Expected + or -")
except Exception, exc: except Exception as exc:
logger.warning('WARNING: ignoring invalid time zone information for %s (%s)') logger.warning('WARNING: ignoring invalid time zone information for %s (%s)')
return max( 0, _date ) return max( 0, _date )
except Exception, exc: except Exception as exc:
logger.warning('WARNING: the mtp date "%s" can not be parsed against mtp specification (%s)') logger.warning('WARNING: the mtp date "%s" can not be parsed against mtp specification (%s)')
return None return None
@ -796,7 +796,7 @@ class MTPDevice(Device):
self.__MTPDevice.connect() self.__MTPDevice.connect()
# build the initial tracks_list # build the initial tracks_list
self.tracks_list = self.get_all_tracks() self.tracks_list = self.get_all_tracks()
except Exception, exc: except Exception as exc:
logger.error('unable to find an MTP device (%s)') logger.error('unable to find an MTP device (%s)')
return False return False
@ -809,7 +809,7 @@ class MTPDevice(Device):
try: try:
self.__MTPDevice.disconnect() self.__MTPDevice.disconnect()
except Exception, exc: except Exception as exc:
logger.error('unable to close %s (%s)', self.get_name()) logger.error('unable to close %s (%s)', self.get_name())
return False return False
@ -876,7 +876,7 @@ class MTPDevice(Device):
try: try:
self.__MTPDevice.delete_object(sync_track.mtptrack.item_id) self.__MTPDevice.delete_object(sync_track.mtptrack.item_id)
except Exception, exc: except Exception as exc:
logger.error('unable remove file %s (%s)', sync_track.mtptrack.filename) logger.error('unable remove file %s (%s)', sync_track.mtptrack.filename)
logger.info('%s removed', sync_track.mtptrack.title) logger.info('%s removed', sync_track.mtptrack.title)
@ -884,7 +884,7 @@ class MTPDevice(Device):
def get_all_tracks(self): def get_all_tracks(self):
try: try:
listing = self.__MTPDevice.get_tracklisting(callback=self.__callback) listing = self.__MTPDevice.get_tracklisting(callback=self.__callback)
except Exception, exc: except Exception as exc:
logger.error('unable to get file listing %s (%s)') logger.error('unable to get file listing %s (%s)')
tracks = [] tracks = []
@ -923,7 +923,7 @@ class SyncTask(download.DownloadTask):
# Possible states this sync task can be in # Possible states this sync task can be in
STATUS_MESSAGE = (_('Added'), _('Queued'), _('Synchronizing'), STATUS_MESSAGE = (_('Added'), _('Queued'), _('Synchronizing'),
_('Finished'), _('Failed'), _('Cancelled'), _('Paused')) _('Finished'), _('Failed'), _('Cancelled'), _('Paused'))
(INIT, QUEUED, DOWNLOADING, DONE, FAILED, CANCELLED, PAUSED) = range(7) (INIT, QUEUED, DOWNLOADING, DONE, FAILED, CANCELLED, PAUSED) = list(range(7))
def __str__(self): def __str__(self):
@ -1040,7 +1040,7 @@ class SyncTask(download.DownloadTask):
self.total_size = float(totalSize) self.total_size = float(totalSize)
if self.total_size > 0: if self.total_size > 0:
self.progress = max(0.0, min(1.0, float(count*blockSize)/self.total_size)) self.progress = max(0.0, min(1.0, (count*blockSize)/self.total_size))
self._progress_updated(self.progress) self._progress_updated(self.progress)
if self.status == SyncTask.CANCELLED: if self.status == SyncTask.CANCELLED:
@ -1064,8 +1064,8 @@ class SyncTask(download.DownloadTask):
self.speed = 0.0 self.speed = 0.0
return False return False
# We only start this download if its status is "queued" # We only start this download if its status is "downloading"
if self.status != SyncTask.QUEUED: if self.status != SyncTask.DOWNLOADING:
return False return False
# We are synching this file right now # We are synching this file right now
@ -1075,7 +1075,7 @@ class SyncTask(download.DownloadTask):
try: try:
logger.info('Starting SyncTask') logger.info('Starting SyncTask')
self.device.add_track(self.episode, reporthook=self.status_updated) self.device.add_track(self.episode, reporthook=self.status_updated)
except Exception, e: except Exception as e:
self.status = SyncTask.FAILED self.status = SyncTask.FAILED
logger.error('Sync failed: %s', str(e), exc_info=True) logger.error('Sync failed: %s', str(e), exc_info=True)
self.error_message = _('Error: %s') % (str(e),) self.error_message = _('Error: %s') % (str(e),)

View file

@ -30,11 +30,11 @@ try:
# Unused here locally, but we import it to be able to give an early # Unused here locally, but we import it to be able to give an early
# warning about this missing dependency in order to avoid bogus errors. # warning about this missing dependency in order to avoid bogus errors.
import minimock import minimock
except ImportError, e: except ImportError as e:
print >>sys.stderr, """ print("""
Error: Unit tests require the "minimock" module (python-minimock). Error: Unit tests require the "minimock" module (python-minimock).
Please install it before running the unit tests. Please install it before running the unit tests.
""" """, file=sys.stderr)
sys.exit(2) sys.exit(2)
# Main package and test package (for modules in main package) # Main package and test package (for modules in main package)
@ -73,9 +73,9 @@ try:
import HTMLTestRunner import HTMLTestRunner
REPORT_FILENAME = 'test_report.html' REPORT_FILENAME = 'test_report.html'
runner = HTMLTestRunner.HTMLTestRunner(stream=open(REPORT_FILENAME, 'w')) runner = HTMLTestRunner.HTMLTestRunner(stream=open(REPORT_FILENAME, 'w'))
print """ print("""
HTML Test Report will be written to %s HTML Test Report will be written to %s
""" % REPORT_FILENAME """ % REPORT_FILENAME)
except ImportError: except ImportError:
runner = unittest.TextTestRunner(verbosity=2) runner = unittest.TextTestRunner(verbosity=2)
@ -100,7 +100,7 @@ if __name__ == '__main__':
cov.report(coverage_modules) cov.report(coverage_modules)
cov.erase() cov.erase()
else: else:
print >>sys.stderr, """ print("""
No coverage reporting done (Python module "coverage" is missing) No coverage reporting done (Python module "coverage" is missing)
Please install the python-coverage package to get coverage reporting. Please install the python-coverage package to get coverage reporting.
""" """, file=sys.stderr)

View file

@ -49,28 +49,28 @@ import string
import re import re
import subprocess import subprocess
from htmlentitydefs import entitydefs from html.entities import entitydefs
import time import time
import gzip import gzip
import datetime import datetime
import threading import threading
import urlparse import urllib.parse
import urllib import urllib.request, urllib.parse, urllib.error
import urllib2 import urllib.request, urllib.error, urllib.parse
import httplib import http.client
import webbrowser import webbrowser
import mimetypes import mimetypes
import itertools import itertools
import StringIO import io
import xml.dom.minidom import xml.dom.minidom
import collections import collections
if sys.hexversion < 0x03000000: if sys.hexversion < 0x03000000:
from HTMLParser import HTMLParser from html.parser import HTMLParser
from htmlentitydefs import name2codepoint from html.entities import name2codepoint
else: else:
from html.parser import HTMLParser from html.parser import HTMLParser
from html.entities import name2codepoint from html.entities import name2codepoint
@ -95,7 +95,7 @@ N_ = gpodder.ngettext
import locale import locale
try: try:
locale.setlocale(locale.LC_ALL, '') locale.setlocale(locale.LC_ALL, '')
except Exception, e: except Exception as e:
logger.warn('Cannot set locale (%s)', e, exc_info=True) logger.warn('Cannot set locale (%s)', e, exc_info=True)
# Native filesystem encoding detection # Native filesystem encoding detection
@ -119,15 +119,15 @@ if encoding is None:
# Filename / folder name sanitization # Filename / folder name sanitization
def _sanitize_char(c): def _sanitize_char(c):
if c in string.whitespace: if c in string.whitespace:
return ' ' return b' '
elif c in ',-.()': elif c in ',-.()':
return c return c.encode('utf-8')
elif c in string.punctuation or ord(c) <= 31: elif c in string.punctuation or ord(c) <= 31 or ord(c) >= 127:
return '_' return b'_'
return c return c.encode('utf-8')
SANITIZATION_TABLE = ''.join(map(_sanitize_char, map(chr, range(256)))) SANITIZATION_TABLE = b''.join(map(_sanitize_char, list(map(chr, list(range(256))))))
del _sanitize_char del _sanitize_char
_MIME_TYPE_LIST = [ _MIME_TYPE_LIST = [
@ -232,7 +232,7 @@ def normalize_feed_url(url):
'ytpl:': 'http://gdata.youtube.com/feeds/api/playlists/%s', 'ytpl:': 'http://gdata.youtube.com/feeds/api/playlists/%s',
} }
for prefix, expansion in PREFIXES.iteritems(): for prefix, expansion in PREFIXES.items():
if url.startswith(prefix): if url.startswith(prefix):
url = expansion % (url[len(prefix):],) url = expansion % (url[len(prefix):],)
break break
@ -241,7 +241,7 @@ def normalize_feed_url(url):
if not '://' in url: if not '://' in url:
url = 'http://' + url url = 'http://' + url
scheme, netloc, path, query, fragment = urlparse.urlsplit(url) scheme, netloc, path, query, fragment = urllib.parse.urlsplit(url)
# Domain name is case insensitive, but username/password is not (bug 1942) # Domain name is case insensitive, but username/password is not (bug 1942)
if '@' in netloc: if '@' in netloc:
@ -265,7 +265,7 @@ def normalize_feed_url(url):
return None return None
# urlunsplit might return "a slighty different, but equivalent URL" # urlunsplit might return "a slighty different, but equivalent URL"
return urlparse.urlunsplit((scheme, netloc, path, query, fragment)) return urllib.parse.urlunsplit((scheme, netloc, path, query, fragment))
def username_password_from_url(url): def username_password_from_url(url):
@ -287,11 +287,11 @@ def username_password_from_url(url):
>>> username_password_from_url(1) >>> username_password_from_url(1)
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValueError: URL has to be a string or unicode object. ValueError: URL has to be a string.
>>> username_password_from_url(None) >>> username_password_from_url(None)
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValueError: URL has to be a string or unicode object. ValueError: URL has to be a string.
>>> username_password_from_url('http://a@b:c@host.com/') >>> username_password_from_url('http://a@b:c@host.com/')
('a@b', 'c') ('a@b', 'c')
>>> username_password_from_url('ftp://a:b:c@host.com/') >>> username_password_from_url('ftp://a:b:c@host.com/')
@ -299,18 +299,18 @@ def username_password_from_url(url):
>>> username_password_from_url('http://i%2Fo:P%40ss%3A@host.com/') >>> username_password_from_url('http://i%2Fo:P%40ss%3A@host.com/')
('i/o', 'P@ss:') ('i/o', 'P@ss:')
>>> username_password_from_url('ftp://%C3%B6sterreich@host.com/') >>> username_password_from_url('ftp://%C3%B6sterreich@host.com/')
('\xc3\xb6sterreich', None) ('österreich', None)
>>> username_password_from_url('http://w%20x:y%20z@example.org/') >>> username_password_from_url('http://w%20x:y%20z@example.org/')
('w x', 'y z') ('w x', 'y z')
>>> username_password_from_url('http://example.com/x@y:z@test.com/') >>> username_password_from_url('http://example.com/x@y:z@test.com/')
(None, None) (None, None)
""" """
if type(url) not in (str, unicode): if not isinstance(url, str):
raise ValueError('URL has to be a string or unicode object.') raise ValueError('URL has to be a string.')
(username, password) = (None, None) (username, password) = (None, None)
(scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url) (scheme, netloc, path, params, query, fragment) = urllib.parse.urlparse(url)
if '@' in netloc: if '@' in netloc:
(authentication, netloc) = netloc.rsplit('@', 1) (authentication, netloc) = netloc.rsplit('@', 1)
@ -330,10 +330,10 @@ def username_password_from_url(url):
# is handled by the authentication.split(':', 1) above, and # is handled by the authentication.split(':', 1) above, and
# will cause any extraneous ':'s to be part of the password. # will cause any extraneous ':'s to be part of the password.
username = urllib.unquote(username) username = urllib.parse.unquote(username)
password = urllib.unquote(password) password = urllib.parse.unquote(password)
else: else:
username = urllib.unquote(authentication) username = urllib.parse.unquote(authentication)
return (username, password) return (username, password)
@ -353,10 +353,10 @@ def calculate_size( path):
to list all subdirectories of the given path. to list all subdirectories of the given path.
""" """
if path is None: if path is None:
return 0L return 0
if os.path.dirname( path) == '/': if os.path.dirname( path) == '/':
return 0L return 0
if os.path.isfile( path): if os.path.isfile( path):
return os.path.getsize( path) return os.path.getsize( path)
@ -375,7 +375,7 @@ def calculate_size( path):
return sum return sum
return 0L return 0
def file_modification_datetime(filename): def file_modification_datetime(filename):
@ -433,9 +433,9 @@ def file_age_to_string(days):
>>> file_age_to_string(0) >>> file_age_to_string(0)
'' ''
>>> file_age_to_string(1) >>> file_age_to_string(1)
u'1 day ago' '1 day ago'
>>> file_age_to_string(2) >>> file_age_to_string(2)
u'2 days ago' '2 days ago'
""" """
if days < 1: if days < 1:
return '' return ''
@ -511,10 +511,10 @@ def format_date(timestamp):
yesterday = time.localtime(time.time() - seconds_in_a_day)[:3] yesterday = time.localtime(time.time() - seconds_in_a_day)[:3]
try: try:
timestamp_date = time.localtime(timestamp)[:3] timestamp_date = time.localtime(timestamp)[:3]
except ValueError, ve: except ValueError as ve:
logger.warn('Cannot convert timestamp', exc_info=True) logger.warn('Cannot convert timestamp', exc_info=True)
return None return None
except TypeError, te: except TypeError as te:
logger.warn('Cannot convert timestamp', exc_info=True) logger.warn('Cannot convert timestamp', exc_info=True)
return None return None
@ -536,10 +536,10 @@ def format_date(timestamp):
if diff < 7: if diff < 7:
# Weekday name # Weekday name
return str(timestamp.strftime('%A').decode(encoding)) return timestamp.strftime('%A')
else: else:
# Locale's appropriate date representation # Locale's appropriate date representation
return str(timestamp.strftime('%x')) return timestamp.strftime('%x')
def format_filesize(bytesize, use_si_units=False, digits=2): def format_filesize(bytesize, use_si_units=False, digits=2):
@ -636,10 +636,10 @@ def remove_html_tags(html):
result = re_strip_tags.sub('', result) result = re_strip_tags.sub('', result)
# Convert numeric XML entities to their unicode character # Convert numeric XML entities to their unicode character
result = re_unicode_entities.sub(lambda x: unichr(int(x.group(1))), result) result = re_unicode_entities.sub(lambda x: chr(int(x.group(1))), result)
# Convert named HTML entities to their unicode character # Convert named HTML entities to their unicode character
result = re_html_entities.sub(lambda x: unicode(entitydefs.get(x.group(1),''), 'iso-8859-1'), result) result = re_html_entities.sub(lambda x: entitydefs.get(x.group(1),''), result)
# Convert more than two newlines to two newlines # Convert more than two newlines to two newlines
result = re.sub('([\r\n]{2})([\r\n])+', '\\1', result) result = re.sub('([\r\n]{2})([\r\n])+', '\\1', result)
@ -658,7 +658,7 @@ class HyperlinkExtracter(object):
group_it = itertools.groupby(self.parts, key=lambda x: x[0]) group_it = itertools.groupby(self.parts, key=lambda x: x[0])
result = [] result = []
for target, parts in group_it: for target, parts in group_it:
t = u''.join(text for _, text in parts if text is not None) t = ''.join(text for _, text in parts if text is not None)
# Remove trailing spaces # Remove trailing spaces
t = re.sub(' +\n', '\n', t) t = re.sub(' +\n', '\n', t)
# Convert more than two newlines to two newlines # Convert more than two newlines to two newlines
@ -705,14 +705,14 @@ class HyperlinkExtracter(object):
self.output(self.htmlws(data)) self.output(self.htmlws(data))
def handle_entityref(self, name): def handle_entityref(self, name):
c = unichr(name2codepoint[name]) c = chr(name2codepoint[name])
self.output(c) self.output(c)
def handle_charref(self, name): def handle_charref(self, name):
if name.startswith('x'): if name.startswith('x'):
c = unichr(int(name[1:], 16)) c = chr(int(name[1:], 16))
else: else:
c = unichr(int(name)) c = chr(int(name))
self.output(c) self.output(c)
def output_newline(self, attrs=None): def output_newline(self, attrs=None):
@ -740,7 +740,7 @@ class ExtractHyperlinkedText(object):
def visit(self, element): def visit(self, element):
NS = '{http://www.w3.org/1999/xhtml}' NS = '{http://www.w3.org/1999/xhtml}'
tag_name = (element.tag[len(NS):] if element.tag.startswith(NS) else element.tag).lower() tag_name = (element.tag[len(NS):] if element.tag.startswith(NS) else element.tag).lower()
self.extracter.handle_starttag(tag_name, element.items()) self.extracter.handle_starttag(tag_name, list(element.items()))
if element.text is not None: if element.text is not None:
self.extracter.handle_data(element.text) self.extracter.handle_data(element.text)
@ -940,8 +940,8 @@ def filename_from_url(url):
http://server/get.jsp?file=/episode0815.MOV => ("episode0815", ".mov") http://server/get.jsp?file=/episode0815.MOV => ("episode0815", ".mov")
http://s/redirect.mp4?http://serv2/test.mp4 => ("test", ".mp4") http://s/redirect.mp4?http://serv2/test.mp4 => ("test", ".mp4")
""" """
(scheme, netloc, path, para, query, fragid) = urlparse.urlparse(url) (scheme, netloc, path, para, query, fragid) = urllib.parse.urlparse(url)
(filename, extension) = os.path.splitext(os.path.basename( urllib.unquote(path))) (filename, extension) = os.path.splitext(os.path.basename( urllib.parse.unquote(path)))
if file_type_by_extension(extension) is not None and not \ if file_type_by_extension(extension) is not None and not \
query.startswith(scheme+'://'): query.startswith(scheme+'://'):
@ -951,7 +951,7 @@ def filename_from_url(url):
# If the query string looks like a possible URL, try that first # If the query string looks like a possible URL, try that first
if len(query.strip()) > 0 and query.find('/') != -1: if len(query.strip()) > 0 and query.find('/') != -1:
query_url = '://'.join((scheme, urllib.unquote(query))) query_url = '://'.join((scheme, urllib.parse.unquote(query)))
(query_filename, query_extension) = filename_from_url(query_url) (query_filename, query_extension) = filename_from_url(query_url)
if file_type_by_extension(query_extension) is not None: if file_type_by_extension(query_extension) is not None:
@ -1033,7 +1033,7 @@ def object_string_formatter(s, **kwargs):
'Hi 123 456' 'Hi 123 456'
""" """
result = s result = s
for key, o in kwargs.iteritems(): for key, o in kwargs.items():
matches = re.findall(r'\{%s\.([^\}]+)\}' % key, s) matches = re.findall(r'\{%s\.([^\}]+)\}' % key, s)
for attr in matches: for attr in matches:
if hasattr(o, attr): if hasattr(o, attr):
@ -1116,14 +1116,14 @@ def url_strip_authentication(url):
>>> url_strip_authentication('http://x@x.com:s3cret@example.com/') >>> url_strip_authentication('http://x@x.com:s3cret@example.com/')
'http://example.com/' 'http://example.com/'
""" """
url_parts = list(urlparse.urlsplit(url)) url_parts = list(urllib.parse.urlsplit(url))
# url_parts[1] is the HOST part of the URL # url_parts[1] is the HOST part of the URL
# Remove existing authentication data # Remove existing authentication data
if '@' in url_parts[1]: if '@' in url_parts[1]:
url_parts[1] = url_parts[1].rsplit('@', 1)[1] url_parts[1] = url_parts[1].rsplit('@', 1)[1]
return urlparse.urlunsplit(url_parts) return urllib.parse.urlunsplit(url_parts)
def url_add_authentication(url, username, password): def url_add_authentication(url, username, password):
@ -1158,21 +1158,21 @@ def url_add_authentication(url, username, password):
# Relaxations of the strict quoting rules (bug 1521): # Relaxations of the strict quoting rules (bug 1521):
# 1. Accept '@' in username and password # 1. Accept '@' in username and password
# 2. Acecpt ':' in password only # 2. Acecpt ':' in password only
username = urllib.quote(username, safe='@') username = urllib.parse.quote(username, safe='@')
if password is not None: if password is not None:
password = urllib.quote(password, safe='@:') password = urllib.parse.quote(password, safe='@:')
auth_string = ':'.join((username, password)) auth_string = ':'.join((username, password))
else: else:
auth_string = username auth_string = username
url = url_strip_authentication(url) url = url_strip_authentication(url)
url_parts = list(urlparse.urlsplit(url)) url_parts = list(urllib.parse.urlsplit(url))
# url_parts[1] is the HOST part of the URL # url_parts[1] is the HOST part of the URL
url_parts[1] = '@'.join((auth_string, url_parts[1])) url_parts[1] = '@'.join((auth_string, url_parts[1]))
return urlparse.urlunsplit(url_parts) return urllib.parse.urlunsplit(url_parts)
def urlopen(url, headers=None, data=None, timeout=None): def urlopen(url, headers=None, data=None, timeout=None):
@ -1182,12 +1182,12 @@ def urlopen(url, headers=None, data=None, timeout=None):
username, password = username_password_from_url(url) username, password = username_password_from_url(url)
if username is not None or password is not None: if username is not None or password is not None:
url = url_strip_authentication(url) url = url_strip_authentication(url)
password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm() password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
password_mgr.add_password(None, url, username, password) password_mgr.add_password(None, url, username, password)
handler = urllib2.HTTPBasicAuthHandler(password_mgr) handler = urllib.request.HTTPBasicAuthHandler(password_mgr)
opener = urllib2.build_opener(handler) opener = urllib.request.build_opener(handler)
else: else:
opener = urllib2.build_opener() opener = urllib.request.build_opener()
if headers is None: if headers is None:
headers = {} headers = {}
@ -1195,7 +1195,7 @@ def urlopen(url, headers=None, data=None, timeout=None):
headers = dict(headers) headers = dict(headers)
headers.update({'User-agent': gpodder.user_agent}) headers.update({'User-agent': gpodder.user_agent})
request = urllib2.Request(url, data=data, headers=headers) request = urllib.request.Request(url, data=data, headers=headers)
if timeout is None: if timeout is None:
return opener.open(request) return opener.open(request)
else: else:
@ -1251,8 +1251,8 @@ def idle_add(func, *args):
as possible from the main UI thread. as possible from the main UI thread.
""" """
if gpodder.ui.gtk: if gpodder.ui.gtk:
import gobject from gi.repository import GObject
gobject.idle_add(func, *args) GObject.idle_add(func, *args)
else: else:
func(*args) func(*args)
@ -1358,11 +1358,11 @@ def format_seconds_to_hour_min_sec(seconds):
human-readable string (duration). human-readable string (duration).
>>> format_seconds_to_hour_min_sec(3834) >>> format_seconds_to_hour_min_sec(3834)
u'1 hour, 3 minutes and 54 seconds' '1 hour, 3 minutes and 54 seconds'
>>> format_seconds_to_hour_min_sec(3600) >>> format_seconds_to_hour_min_sec(3600)
u'1 hour' '1 hour'
>>> format_seconds_to_hour_min_sec(62) >>> format_seconds_to_hour_min_sec(62)
u'1 minute and 2 seconds' '1 minute and 2 seconds'
""" """
if seconds < 1: if seconds < 1:
@ -1372,10 +1372,10 @@ def format_seconds_to_hour_min_sec(seconds):
seconds = int(seconds) seconds = int(seconds)
hours = seconds/3600 hours = seconds//3600
seconds = seconds%3600 seconds = seconds%3600
minutes = seconds/60 minutes = seconds//60
seconds = seconds%60 seconds = seconds%60
if hours: if hours:
@ -1393,8 +1393,8 @@ def format_seconds_to_hour_min_sec(seconds):
return result[0] return result[0]
def http_request(url, method='HEAD'): def http_request(url, method='HEAD'):
(scheme, netloc, path, parms, qry, fragid) = urlparse.urlparse(url) (scheme, netloc, path, parms, qry, fragid) = urllib.parse.urlparse(url)
conn = httplib.HTTPConnection(netloc) conn = http.client.HTTPConnection(netloc)
start = len(scheme) + len('://') + len(netloc) start = len(scheme) + len('://') + len(netloc)
conn.request(method, url[start:]) conn.request(method, url[start:])
return conn.getresponse() return conn.getresponse()
@ -1437,75 +1437,38 @@ def convert_bytes(d):
strings. Any other data types will be left alone. strings. Any other data types will be left alone.
>>> convert_bytes(None) >>> convert_bytes(None)
>>> convert_bytes(1) >>> convert_bytes(4711)
1 4711
>>> convert_bytes(4711L)
4711L
>>> convert_bytes(True) >>> convert_bytes(True)
True True
>>> convert_bytes(3.1415) >>> convert_bytes(3.1415)
3.1415 3.1415
>>> convert_bytes('Hello') >>> convert_bytes('Hello')
u'Hello' 'Hello'
>>> convert_bytes(u'Hey') >>> type(convert_bytes(b'hoho'))
u'Hey' <class 'bytes'>
>>> type(convert_bytes(buffer('hoho')))
<type 'buffer'>
""" """
if d is None: if d is None:
return d return d
if isinstance(d, buffer): elif isinstance(d, bytes):
return d return d
elif any(isinstance(d, t) for t in (int, long, bool, float)): elif any(isinstance(d, t) for t in (int, int, bool, float)):
return d return d
elif not isinstance(d, unicode): elif not isinstance(d, str):
return d.decode('utf-8', 'ignore') return d.decode('utf-8', 'ignore')
return d return d
def sanitize_encoding(filename):
r"""
Generate a sanitized version of a string (i.e.
remove invalid characters and encode in the
detected native language encoding).
>>> sanitize_encoding('\x80') def sanitize_filename(filename, max_length=0):
''
>>> sanitize_encoding(u'unicode')
'unicode'
""" """
# The encoding problem goes away in Python 3.. hopefully! Generate a sanitized version of a filename; trim filename
if sys.version_info >= (3, 0): if greater than max_length (0 = no limit).
return filename
global encoding
if not isinstance(filename, unicode):
filename = filename.decode(encoding, 'ignore')
return filename.encode(encoding, 'ignore')
def sanitize_filename(filename, max_length=0, use_ascii=False):
""" """
Generate a sanitized version of a filename that can
be written on disk (i.e. remove/replace invalid
characters and encode in the native language) and
trim filename if greater than max_length (0 = no limit).
If use_ascii is True, don't encode in the native language,
but use only characters from the ASCII character set.
"""
if not isinstance(filename, unicode):
filename = filename.decode(encoding, 'ignore')
if max_length > 0 and len(filename) > max_length: if max_length > 0 and len(filename) > max_length:
logger.info('Limiting file/folder name "%s" to %d characters.', logger.info('Limiting file/folder name "%s" to %d characters.', filename, max_length)
filename, max_length)
filename = filename[:max_length] filename = filename[:max_length]
filename = filename.encode('ascii' if use_ascii else encoding, 'ignore') return filename.strip('.' + string.whitespace)
filename = filename.translate(SANITIZATION_TABLE)
filename = filename.strip('.' + string.whitespace)
return filename
def find_mount_point(directory): def find_mount_point(directory):
@ -1519,10 +1482,10 @@ def find_mount_point(directory):
>>> find_mount_point('/') >>> find_mount_point('/')
'/' '/'
>>> find_mount_point(u'/something') >>> find_mount_point(b'/something')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValueError: Convert unicode objects to str first. ValueError: Convert bytes objects to str first.
>>> find_mount_point(None) >>> find_mount_point(None)
Traceback (most recent call last): Traceback (most recent call last):
@ -1573,15 +1536,14 @@ def find_mount_point(directory):
'/media/usbdisk' '/media/usbdisk'
>>> restore() >>> restore()
""" """
if isinstance(directory, unicode): if isinstance(directory, bytes):
# XXX: This is only valid for Python 2 - misleading error in Python 3? # We do not accept byte strings, because they could fail when
# We do not accept unicode strings, because they could fail when
# trying to be converted to some native encoding, so fail loudly # trying to be converted to some native encoding, so fail loudly
# and leave it up to the callee to encode into the proper encoding. # and leave it up to the callee to decode from the proper encoding.
raise ValueError('Convert unicode objects to str first.') raise ValueError('Convert bytes objects to str first.')
if not isinstance(directory, str): if not isinstance(directory, str):
# In Python 2, we assume it's a byte str; in Python 3, we assume # In Python 2, we assumed it's a byte str; in Python 3, we assume
# that it's a unicode str. The abspath/ismount/split functions of # that it's a unicode str. The abspath/ismount/split functions of
# os.path work with unicode str in Python 3, but not in Python 2. # os.path work with unicode str in Python 3, but not in Python 2.
raise ValueError('Directory names should be of type str.') raise ValueError('Directory names should be of type str.')
@ -1751,7 +1713,6 @@ def atomic_rename(old_name, new_name):
def check_command(self, cmd): def check_command(self, cmd):
"""Check if a command line command/program exists""" """Check if a command line command/program exists"""
# Prior to Python 2.7.3, this module (shlex) did not support Unicode input. # Prior to Python 2.7.3, this module (shlex) did not support Unicode input.
cmd = sanitize_encoding(cmd)
program = shlex.split(cmd)[0] program = shlex.split(cmd)[0]
return (find_command(program) is not None) return (find_command(program) is not None)
@ -1818,7 +1779,7 @@ def linux_get_active_interfaces():
""" """
process = subprocess.Popen(['ip', 'link'], stdout=subprocess.PIPE) process = subprocess.Popen(['ip', 'link'], stdout=subprocess.PIPE)
data, _ = process.communicate() data, _ = process.communicate()
for interface, _ in re.findall(r'\d+: ([^:]+):.*state (UP|UNKNOWN)', data): for interface, _ in re.findall(r'\d+: ([^:]+):.*state (UP|UNKNOWN)', data.decode(locale.getpreferredencoding())):
if interface != 'lo': if interface != 'lo':
yield interface yield interface
@ -1832,7 +1793,7 @@ def osx_get_active_interfaces():
""" """
process = subprocess.Popen(['ifconfig'], stdout=subprocess.PIPE) process = subprocess.Popen(['ifconfig'], stdout=subprocess.PIPE)
stdout, _ = process.communicate() stdout, _ = process.communicate()
for i in re.split('\n(?!\t)', stdout, re.MULTILINE): for i in re.split('\n(?!\t)', stdout.decode('utf-8'), re.MULTILINE):
b = re.match('(\\w+):.*status: (active|associated)$', i, re.MULTILINE | re.DOTALL) b = re.match('(\\w+):.*status: (active|associated)$', i, re.MULTILINE | re.DOTALL)
if b: if b:
yield b.group(1) yield b.group(1)
@ -1846,7 +1807,7 @@ def unix_get_active_interfaces():
""" """
process = subprocess.Popen(['ifconfig'], stdout=subprocess.PIPE) process = subprocess.Popen(['ifconfig'], stdout=subprocess.PIPE)
stdout, _ = process.communicate() stdout, _ = process.communicate()
for i in re.split('\n(?!\t)', stdout, re.MULTILINE): for i in re.split('\n(?!\t)', stdout.decode(locale.getpreferredencoding()), re.MULTILINE):
b = re.match('(\\w+):.*status: (active|associated)$', i, re.MULTILINE | re.DOTALL) b = re.match('(\\w+):.*status: (active|associated)$', i, re.MULTILINE | re.DOTALL)
if b: if b:
yield b.group(1) yield b.group(1)
@ -1885,7 +1846,7 @@ def connection_available():
return not offline return not offline
return False return False
except Exception, e: except Exception as e:
logger.warn('Cannot get connection status: %s', e, exc_info=True) logger.warn('Cannot get connection status: %s', e, exc_info=True)
# When we can't determine the connection status, act as if we're online (bug 1730) # When we can't determine the connection status, act as if we're online (bug 1730)
return True return True
@ -1900,9 +1861,9 @@ def website_reachable(url):
return (False, None) return (False, None)
try: try:
response = urllib2.urlopen(url, timeout=1) response = urllib.request.urlopen(url, timeout=1)
return (True, response) return (True, response)
except urllib2.URLError as err: except urllib.error.URLError as err:
pass pass
return (False, None) return (False, None)

View file

@ -32,12 +32,7 @@ from gpodder import util
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
try: import json
# For Python < 2.6, we use the "simplejson" add-on module
import simplejson as json
except ImportError:
# Python 2.6 already ships with a nice "json" module
import json
import re import re
@ -64,7 +59,7 @@ def get_real_download_url(url, preferred_fileformat=None):
def get_urls(data_config_url): def get_urls(data_config_url):
data_config_data = util.urlopen(data_config_url).read().decode('utf-8') data_config_data = util.urlopen(data_config_url).read().decode('utf-8')
data_config = json.loads(data_config_data) data_config = json.loads(data_config_data)
for fileinfo in data_config['request']['files'].values(): for fileinfo in list(data_config['request']['files'].values()):
if not isinstance(fileinfo, list): if not isinstance(fileinfo, list):
continue continue

View file

@ -30,20 +30,12 @@ import os.path
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
try: import json
import simplejson as json
except ImportError:
import json
import re import re
import urllib import urllib.request, urllib.parse, urllib.error
try: from urllib.parse import parse_qs
# Python >= 2.6
from urlparse import parse_qs
except ImportError:
# Python < 2.6
from cgi import parse_qs
# http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs # http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs
# format id, (preferred ids, path(?), description) # video bitrate, audio bitrate # format id, (preferred ids, path(?), description) # video bitrate, audio bitrate
@ -117,7 +109,7 @@ def get_real_download_url(url, preferred_fmt_ids=None):
def find_urls(page): def find_urls(page):
r4 = re.search('url_encoded_fmt_stream_map=([^&]+)', page) r4 = re.search('url_encoded_fmt_stream_map=([^&]+)', page)
if r4 is not None: if r4 is not None:
fmt_url_map = urllib.unquote(r4.group(1)) fmt_url_map = urllib.parse.unquote(r4.group(1))
for fmt_url_encoded in fmt_url_map.split(','): for fmt_url_encoded in fmt_url_map.split(','):
video_info = parse_qs(fmt_url_encoded) video_info = parse_qs(fmt_url_encoded)
yield int(video_info['itag'][0]), video_info['url'][0] yield int(video_info['itag'][0]), video_info['url'][0]
@ -212,7 +204,7 @@ def get_real_cover(url):
def return_user_cover(url, channel): def return_user_cover(url, channel):
try: try:
api_url = 'https://www.youtube.com/channel/{0}'.format(channel) api_url = 'https://www.youtube.com/channel/{0}'.format(channel)
data = util.urlopen(api_url).read() data = util.urlopen(api_url).read().decode('utf-8')
# Look for 900x900px image first. # Look for 900x900px image first.
m = re.search('<link rel="image_src"[^>]* href=[\'"]([^\'"]+)[\'"][^>]*>', data) m = re.search('<link rel="image_src"[^>]* href=[\'"]([^\'"]+)[\'"][^>]*>', data)
if m is None: if m is None:

View file

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# create-desktop-icon.py: Create a Desktop icon # create-desktop-icon.py: Create a Desktop icon
# 2016-12-22 Thomas Perl <m@thp.io> # 2016-12-22 Thomas Perl <m@thp.io>
@ -19,11 +19,11 @@ Type=Application
DESTINATION = os.path.expanduser('~/Desktop/gpodder-git.desktop') DESTINATION = os.path.expanduser('~/Desktop/gpodder-git.desktop')
if os.path.exists(DESTINATION): if os.path.exists(DESTINATION):
print '%(DESTINATION)s already exists, not overwriting' print('%(DESTINATION)s already exists, not overwriting')
sys.exit(1) sys.exit(1)
with open(DESTINATION, 'w') as fp: with open(DESTINATION, 'w') as fp:
fp.write(TEMPLATE) fp.write(TEMPLATE)
os.chmod(DESTINATION, 0755) os.chmod(DESTINATION, 0o755)
print 'Wrote %(DESTINATION)s' % locals() print('Wrote %(DESTINATION)s' % locals())

View file

@ -4,4 +4,4 @@ import re
here = os.path.dirname(__file__) or '.' here = os.path.dirname(__file__) or '.'
main_module = open(os.path.join(here, '../src/gpodder/__init__.py')).read() main_module = open(os.path.join(here, '../src/gpodder/__init__.py')).read()
metadata = dict(re.findall("__([a-z_]+)__\s*=\s*'([^']+)'", main_module)) metadata = dict(re.findall("__([a-z_]+)__\s*=\s*'([^']+)'", main_module))
print metadata['version'] print(metadata['version'])

View file

@ -1,4 +1,4 @@
#!/usr/bin/python #!/usr/bin/python3
# summary.py - Text-based visual translation completeness summary # summary.py - Text-based visual translation completeness summary
# Thomas Perl <thp@gpodder.org>, 2009-01-03 # Thomas Perl <thp@gpodder.org>, 2009-01-03
# #
@ -22,13 +22,13 @@ class Language(object):
self.untranslated = int(untranslated) self.untranslated = int(untranslated)
def get_translated_ratio(self): def get_translated_ratio(self):
return float(self.translated)/float(self.translated+self.fuzzy+self.untranslated) return self.translated/(self.translated+self.fuzzy+self.untranslated)
def get_fuzzy_ratio(self): def get_fuzzy_ratio(self):
return float(self.fuzzy)/float(self.translated+self.fuzzy+self.untranslated) return self.fuzzy/(self.translated+self.fuzzy+self.untranslated)
def get_untranslated_ratio(self): def get_untranslated_ratio(self):
return float(self.untranslated)/float(self.translated+self.fuzzy+self.untranslated) return self.untranslated/(self.translated+self.fuzzy+self.untranslated)
def __cmp__(self, other): def __cmp__(self, other):
return cmp(self.get_translated_ratio(), other.get_translated_ratio()) return cmp(self.get_translated_ratio(), other.get_translated_ratio())
@ -47,15 +47,15 @@ for filename in glob.glob(os.path.join(po_folder, '*.po')):
match = re.match(COUNTS_RE, stderr).groups() match = re.match(COUNTS_RE, stderr).groups()
languages.append(Language(language, match[1] or '0', match[3] or '0', match[5] or '0')) languages.append(Language(language, match[1] or '0', match[3] or '0', match[5] or '0'))
print '' print('')
for language in sorted(languages): for language in sorted(languages):
tc = '#'*(int(math.floor(width*language.get_translated_ratio()))) tc = '#'*(int(math.floor(width*language.get_translated_ratio())))
fc = '~'*(int(math.floor(width*language.get_fuzzy_ratio()))) fc = '~'*(int(math.floor(width*language.get_fuzzy_ratio())))
uc = ' '*(width-len(tc)-len(fc)) uc = ' '*(width-len(tc)-len(fc))
print ' %5s [%s%s%s] -- %3.0f %% translated' % (language.language, tc, fc, uc, language.get_translated_ratio()*100) print(' %5s [%s%s%s] -- %3.0f %% translated' % (language.language, tc, fc, uc, language.get_translated_ratio()*100))
print """ print("""
Total translations: %s Total translations: %s
""" % (len(languages)) """ % (len(languages)))

View file

@ -1,4 +1,4 @@
#!/usr/bin/python #!/usr/bin/python3
# #
# gPodder dependency installer for running the CLI from the source tree # gPodder dependency installer for running the CLI from the source tree
# #
@ -8,10 +8,10 @@
# Thomas Perl <thp.io/about>; 2012-02-11 # Thomas Perl <thp.io/about>; 2012-02-11
# #
import urllib2 import urllib.request, urllib.error, urllib.parse
import re import re
import sys import sys
import StringIO import io
import tarfile import tarfile
import os import os
import shutil import shutil
@ -30,33 +30,33 @@ MODULES = [
def get_tarball_url(modulename): def get_tarball_url(modulename):
url = 'http://pypi.python.org/pypi/' + modulename url = 'http://pypi.python.org/pypi/' + modulename
html = urllib2.urlopen(url).read() html = urllib.request.urlopen(url).read().decode('utf-8')
match = re.search(r'(http[s]?://[^>]*%s-([0-9.]*)(?:\.post\d+)?\.tar\.gz)' % modulename, html) match = re.search(r'(http[s]?://[^>]*%s-([0-9.]*)(?:\.post\d+)?\.tar\.gz)' % modulename, html)
return match.group(0) if match is not None else None return match.group(0) if match is not None else None
for module, required_files in MODULES: for module, required_files in MODULES:
print 'Fetching', module, '...', print('Fetching', module, '...', end=' ')
tarball_url = get_tarball_url(module) tarball_url = get_tarball_url(module)
if tarball_url is None: if tarball_url is None:
print 'Cannot determine download URL for', module, '- aborting!' print('Cannot determine download URL for', module, '- aborting!')
break break
data = urllib2.urlopen(tarball_url).read() data = urllib.request.urlopen(tarball_url).read()
print '%d KiB' % (len(data)/1024) print('%d KiB' % (len(data)//1024))
tar = tarfile.open(fileobj=StringIO.StringIO(data)) tar = tarfile.open(fileobj=io.BytesIO(data))
for name in tar.getnames(): for name in tar.getnames():
match = re.match(required_files, name) match = re.match(required_files, name)
if match is not None: if match is not None:
target_name = match.group(1) target_name = match.group(1)
target_file = os.path.join(src_dir, target_name) target_file = os.path.join(src_dir, target_name)
if os.path.exists(target_file): if os.path.exists(target_file):
print 'Skipping:', target_file print('Skipping:', target_file)
continue continue
target_dir = os.path.dirname(target_file) target_dir = os.path.dirname(target_file)
if not os.path.isdir(target_dir): if not os.path.isdir(target_dir):
os.mkdir(target_dir) os.mkdir(target_dir)
print 'Extracting:', target_name print('Extracting:', target_name)
tar.extract(name, tmp_dir) tar.extract(name, tmp_dir)
shutil.move(os.path.join(tmp_dir, name), target_file) shutil.move(os.path.join(tmp_dir, name), target_file)

View file

@ -1,4 +1,4 @@
#!/usr/bin/python #!/usr/bin/python3
# Progressbar icon tester # Progressbar icon tester
# Thomas Perl <thp.io/about>; 2012-02-05 # Thomas Perl <thp.io/about>; 2012-02-05
# #
@ -8,26 +8,26 @@
import sys import sys
sys.path.insert(0, 'src') sys.path.insert(0, 'src')
import gtk from gi.repository import Gtk
from gpodder.gtkui.draw import draw_cake_pixbuf from gpodder.gtkui.draw import draw_cake_pixbuf
def gen(percentage): def gen(percentage):
pixbuf = draw_cake_pixbuf(percentage) pixbuf = draw_cake_pixbuf(percentage)
return gtk.image_new_from_pixbuf(pixbuf) return Gtk.Image.new_from_pixbuf(pixbuf)
w = gtk.Window() w = Gtk.Window()
w.connect('destroy', gtk.main_quit) w.connect('destroy', Gtk.main_quit)
v = gtk.VBox() v = Gtk.VBox()
w.add(v) w.add(v)
for y in xrange(1): for y in range(1):
h = gtk.HBox() h = Gtk.HBox()
h.set_homogeneous(True) h.set_homogeneous(True)
v.add(h) v.add(h)
PARTS = 20 PARTS = 20
for x in xrange(PARTS + 1): for x in range(PARTS + 1):
h.add(gen(float(x)/float(PARTS))) h.add(gen(x/PARTS))
w.set_default_size(400, 100) w.set_default_size(400, 100)
w.show_all() w.show_all()
gtk.main() Gtk.main()

View file

@ -1,10 +1,10 @@
#!/usr/bin/python #!/usr/bin/python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Simple HTTP web server for testing HTTP Authentication (see bug 1539) # Simple HTTP web server for testing HTTP Authentication (see bug 1539)
# from our crappy-but-does-the-job department # from our crappy-but-does-the-job department
# Thomas Perl <thp.io/about>; 2012-01-20 # Thomas Perl <thp.io/about>; 2012-01-20
import BaseHTTPServer import http.server
import sys import sys
import re import re
import hashlib import hashlib
@ -47,7 +47,7 @@ def mkrss(items=EP_COUNT):
type="%(EPISODES_MIME)s" type="%(EPISODES_MIME)s"
length="%(SIZE)s"/> length="%(SIZE)s"/>
</item> </item>
""" % dict(locals().items()+globals().items()) """ % dict(list(locals().items())+list(globals().items()))
for INDEX, PUBDATE in enumerate(mkpubdates(items))) for INDEX, PUBDATE in enumerate(mkpubdates(items)))
return """ return """
@ -56,13 +56,13 @@ def mkrss(items=EP_COUNT):
%(ITEMS)s %(ITEMS)s
</channel> </channel>
</rss> </rss>
""" % dict(locals().items()+globals().items()) """ % dict(list(locals().items())+list(globals().items()))
def mkdata(size=SIZE): def mkdata(size=SIZE):
"""Generate dummy data of a given size (in bytes)""" """Generate dummy data of a given size (in bytes)"""
return ''.join(chr(32+(i%(127-32))) for i in range(size)) return ''.join(chr(32+(i%(127-32))) for i in range(size))
class AuthRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): class AuthRequestHandler(http.server.BaseHTTPRequestHandler):
FEEDFILE_PATH = '/%s' % FEEDFILE FEEDFILE_PATH = '/%s' % FEEDFILE
EPISODES_PATH = '/%s' % EPISODES EPISODES_PATH = '/%s' % EPISODES
@ -77,21 +77,21 @@ class AuthRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
auth_data = m.group(1).decode('base64').split(':', 1) auth_data = m.group(1).decode('base64').split(':', 1)
if len(auth_data) == 2: if len(auth_data) == 2:
username, password = auth_data username, password = auth_data
print 'Got username:', username print('Got username:', username)
print 'Got password:', password print('Got password:', password)
if (username, password) == (USERNAME, PASSWORD): if (username, password) == (USERNAME, PASSWORD):
print 'Valid credentials provided.' print('Valid credentials provided.')
authorized = True authorized = True
if self.path == self.FEEDFILE_PATH: if self.path == self.FEEDFILE_PATH:
print 'Feed request.' print('Feed request.')
is_feed = True is_feed = True
elif self.path.startswith(self.EPISODES_PATH): elif self.path.startswith(self.EPISODES_PATH):
print 'Episode request.' print('Episode request.')
is_episode = True is_episode = True
if not authorized: if not authorized:
print 'Not authorized - sending WWW-Authenticate header.' print('Not authorized - sending WWW-Authenticate header.')
self.send_response(401) self.send_response(401)
self.send_header('WWW-Authenticate', self.send_header('WWW-Authenticate',
'Basic realm="%s"' % sys.argv[0]) 'Basic realm="%s"' % sys.argv[0])
@ -108,12 +108,12 @@ class AuthRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
if __name__ == '__main__': if __name__ == '__main__':
httpd = BaseHTTPServer.HTTPServer((HOST, PORT), AuthRequestHandler) httpd = http.server.HTTPServer((HOST, PORT), AuthRequestHandler)
print """ print("""
Feed URL: %(URL)s/%(FEEDFILE)s Feed URL: %(URL)s/%(FEEDFILE)s
Username: %(USERNAME)s Username: %(USERNAME)s
Password: %(PASSWORD)s Password: %(PASSWORD)s
""" % locals() """ % locals())
while True: while True:
httpd.handle_request() httpd.handle_request()

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