Merge branch 'master' into gtk3-win_installer

This commit is contained in:
Eric Le Lay 2018-05-27 10:05:11 +02:00
commit 92dcb1c0c3
82 changed files with 11252 additions and 9948 deletions

24
bin/gpo
View File

@ -48,6 +48,7 @@
- Episode management -
download [URL] [GUID] Download new episodes (all or only from URL) or single GUID
delete [URL] [GUID] Delete from feed at URL an episode with given GUID
pending [URL] List new episodes (all or only from URL)
episodes [--guid] [URL] List episodes with or without GUIDs (all or only from URL)
@ -559,6 +560,29 @@ class gPodderCli(object):
print(len(episodes), 'episodes downloaded.')
return True
@FirstArgumentIsPodcastURL
def delete(self, url, guid):
podcast = self.get_podcast(url)
episode_to_delete = None
if podcast is None:
self._error(_('You are not subscribed to %s.') % url)
else:
for episode in podcast.get_all_episodes():
if (episode.guid == guid):
episode_to_delete = episode
if not episode_to_delete:
self._error(_('No episode with the specified GUID found.'))
else:
if episode_to_delete.state != gpodder.STATE_DELETED:
episode_to_delete.delete_from_disk()
self._info(_('Deleted episode "%s".') % episode_to_delete.title)
else:
self._error(_('Episode has already been deleted.'))
return True
@FirstArgumentIsPodcastURL
def disable(self, url):
podcast = self.get_podcast(url)

585
po/ca.po

File diff suppressed because it is too large Load Diff

576
po/cs.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

577
po/da.po

File diff suppressed because it is too large Load Diff

577
po/de.po

File diff suppressed because it is too large Load Diff

577
po/el.po

File diff suppressed because it is too large Load Diff

577
po/es.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

577
po/eu.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

577
po/fi.po

File diff suppressed because it is too large Load Diff

1174
po/fr.po

File diff suppressed because it is too large Load Diff

577
po/gl.po

File diff suppressed because it is too large Load Diff

577
po/he.po

File diff suppressed because it is too large Load Diff

577
po/hu.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

577
po/it.po

File diff suppressed because it is too large Load Diff

577
po/kk.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

577
po/nb.po

File diff suppressed because it is too large Load Diff

576
po/nl.po

File diff suppressed because it is too large Load Diff

576
po/nn.po

File diff suppressed because it is too large Load Diff

577
po/pl.po

File diff suppressed because it is too large Load Diff

577
po/pt.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

576
po/ro.po

File diff suppressed because it is too large Load Diff

576
po/ru.po

File diff suppressed because it is too large Load Diff

577
po/sv.po

File diff suppressed because it is too large Load Diff

588
po/tr.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

576
po/uk.po

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
[pycodestyle]
count=1
select = W1, W2, W3, E11, E125, E129, E211, E221, E222, E225, E226, E227, E228, E262, E266, E271, E272, E3, E703, E711, E712, E713, E721, E9
select = W1, W2, W3, E11, E124, E125, E129, E211, E22, E26, E271, E272, E3, E401, E502, E703, E711, E712, E713, E721, E731, E9
# https://pycodestyle.readthedocs.io/en/latest/intro.html#error-codes
max-line-length = 100

View File

@ -27,7 +27,7 @@ import dbus
import gpodder
session_bus = dbus.SessionBus()
proxy = session_bus.get_object(gpodder.dbus_bus_name, \
proxy = session_bus.get_object(gpodder.dbus_bus_name,
gpodder.dbus_gui_object_path)
interface = dbus.Interface(proxy, gpodder.dbus_interface)

View File

@ -26,8 +26,8 @@ __category__ = 'post-download'
DefaultConfig = {
'use_ogg': False, # Set to True to convert to .ogg (otherwise .mp3)
'context_menu': True, # Show the conversion option in the context menu
'use_ogg': False, # Set to True to convert to .ogg (otherwise .mp3)
'context_menu': True, # Show the conversion option in the context menu
}
@ -36,11 +36,11 @@ class gPodderExtension:
EXT = ('.m4a', '.ogg')
CMD = {'avconv': {'.mp3': ['-i', '%(old_file)s', '-q:a', '2', '-id3v2_version', '3', '-write_id3v1', '1', '%(new_file)s'],
'.ogg': ['-i', '%(old_file)s', '-q:a', '2', '%(new_file)s']
},
},
'ffmpeg': {'.mp3': ['-i', '%(old_file)s', '-q:a', '2', '-id3v2_version', '3', '-write_id3v1', '1', '%(new_file)s'],
'.ogg': ['-i', '%(old_file)s', '-q:a', '2', '%(new_file)s']
}
}
}
}
def __init__(self, container):
self.container = container

View File

@ -23,7 +23,7 @@ import os.path
from gpodder.gtkui import draw
DefaultConfig = {
'download_progress_bar': False, # draw progress bar on icon while downloading?
'download_progress_bar': False, # draw progress bar on icon while downloading?
}
@ -108,7 +108,7 @@ class gPodderExtension:
return
if progress == 1:
self.set_icon() # no progress bar
self.set_icon() # no progress bar
self.last_progress = progress
return

View File

@ -19,13 +19,16 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import collections
import dbus
import dbus.service
import gpodder
import logging
import time
import urllib.request, urllib.parse, urllib.error
import urllib.error
import urllib.parse
import urllib.request
import dbus
import dbus.service
import gpodder
logger = logging.getLogger(__name__)
_ = gpodder.gettext
@ -102,7 +105,7 @@ class CurrentTrackTracker(object):
uri = kwargs.pop('uri', None)
if uri is not None:
length = kwargs.pop('length') # don't know how to handle uri with no length
length = kwargs.pop('length') # don't know how to handle uri with no length
if uri != cur['uri']:
# if this is a new uri, and the previous state was 'Playing',
# notify that the previous track has stopped before updating to
@ -119,9 +122,8 @@ class CurrentTrackTracker(object):
# If the status *is* playing, and *was* playing, but the position
# has changed discontinuously, notify a stop for the old position
if (cur['status'] == 'Playing' and
('status' not in kwargs or kwargs['status'] == 'Playing') and not
subsecond_difference(cur['pos'], kwargs['pos'])
):
('status' not in kwargs or kwargs['status'] == 'Playing') and not
subsecond_difference(cur['pos'], kwargs['pos'])):
logger.debug('notify Stopped: playback discontinuity:' +
'calc: %f observed: %f', cur['pos'], kwargs['pos'])
self.notify_stop()
@ -134,7 +136,7 @@ class CurrentTrackTracker(object):
self.pos / USECS_IN_SEC, self.length / USECS_IN_SEC,
(self.pos / USECS_IN_SEC) - (self.length / USECS_IN_SEC))
self.pos = self.length
kwargs.pop('pos') # remove 'pos' even though we're not using it
kwargs.pop('pos') # remove 'pos' even though we're not using it
else:
if self.pos is not None:
logger.debug("%r %r", self.pos, self.length)
@ -213,10 +215,9 @@ class MPRISDBusReceiver(object):
INTERFACE_MPRIS = 'org.mpris.MediaPlayer2.Player'
SIGNAL_SEEKED = 'Seeked'
OBJECT_VLC = 'org.mpris.MediaPlayer2.vlc'
OTHER_MPRIS_INTERFACES = [ 'org.mpris.MediaPlayer2',
'org.mpris.MediaPlayer2.TrackList',
'org.mpris.MediaPlayer2.Playlists'
]
OTHER_MPRIS_INTERFACES = ['org.mpris.MediaPlayer2',
'org.mpris.MediaPlayer2.TrackList',
'org.mpris.MediaPlayer2.Playlists']
def __init__(self, bus, notifier):
self.bus = bus

View File

@ -27,7 +27,7 @@ __category__ = 'post-download'
DefaultConfig = {
'context_menu': True, # Show action in the episode list context menu
'context_menu': True, # Show action in the episode list context menu
}
# a tuple of (extension, command)

View File

@ -21,8 +21,8 @@ __payment__ = 'https://flattr.com/submit/auto?user_id=BerndSch&url=http://wiki.g
__category__ = 'post-download'
DefaultConfig = {
'add_sortdate': False, # Add the sortdate as prefix
'add_podcast_title': False, # Add the podcast title as prefix
'add_sortdate': False, # Add the sortdate as prefix
'add_podcast_title': False, # Add the podcast title as prefix
}

View File

@ -43,7 +43,7 @@ __category__ = 'post-download'
DefaultConfig = {
'context_menu': True, # Show item in context menu
'context_menu': True, # Show item in context menu
}

View File

@ -4,8 +4,8 @@
# To use, copy it as a Python script into ~/.config/gpodder/extensions/rockbox_mp4_convert.py
# See the module "gpodder.extensions" for a description of when each extension
# gets called and what the parameters of each extension are.
#Based on Rename files after download based on the episode title
#And patch in Bug https://bugs.gpodder.org/show_bug.cgi?id=1263
# Based on Rename files after download based on the episode title
# And patch in Bug https://bugs.gpodder.org/show_bug.cgi?id=1263
# Copyright (c) 2011-04-06 Guy Sheffer <guysoft at gmail.com>
# Copyright (c) 2011-04-04 Thomas Perl <thp.io>
# Licensed under the same terms as gPodder itself

View File

@ -17,7 +17,7 @@ __only_for__ = 'gtk, cli'
__authors__ = 'Alex Mayer <magictrick4906@aim.com>'
DefaultConfig = {
"art_name_on_device": "cover.jpg" # The file name that will be used on the device for cover art
"art_name_on_device": "cover.jpg" # The file name that will be used on the device for cover art
}

View File

@ -23,7 +23,8 @@ __category__ = 'interface'
__only_for__ = 'gtk'
SONOS_CAN_PLAY = lambda e: 'audio' in e.file_type()
def SONOS_CAN_PLAY(e):
return 'audio' in e.file_type()
class gPodderExtension:

View File

@ -163,7 +163,7 @@ class Mp3File(AudioFile):
audio.tags.add(
APIC(
encoding = 3, # 3 is for utf-8
encoding = 3, # 3 is for utf-8
mime = mimetypes.guess_type(self.cover)[0],
type = 3,
desc = 'Cover',

View File

@ -25,7 +25,7 @@ logger = logging.getLogger(__name__)
DefaultConfig = {
'visible': True, # Set to False if you don't want to show the appindicator
'visible': True, # Set to False if you don't want to show the appindicator
}

View File

@ -27,8 +27,8 @@ __payment__ = 'https://flattr.com/submit/auto?user_id=BerndSch&url=http://wiki.g
__category__ = 'post-download'
DefaultConfig = {
'output_format': 'mp4', # At the moment we support/test only mp4, m4v and avi
'context_menu': True, # Show the conversion option in the context menu
'output_format': 'mp4', # At the moment we support/test only mp4, m4v and avi
'context_menu': True, # Show the conversion option in the context menu
}
@ -37,7 +37,7 @@ class gPodderExtension:
EXT = ('.mp4', '.m4v', '.flv', )
CMD = {'avconv': ['-i', '%(old_file)s', '-codec', 'copy', '%(new_file)s'],
'ffmpeg': ['-i', '%(old_file)s', '-codec', 'copy', '%(new_file)s']
}
}
def __init__(self, container):
self.container = container

View File

@ -10,7 +10,7 @@
<property name="page_size">0</property>
</object>
<object class="GtkAdjustment" id="adjustment2">
<property name="upper">50</property>
<property name="upper">16</property>
<property name="lower">1</property>
<property name="page_increment">0</property>
<property name="step_increment">1</property>

View File

@ -56,7 +56,7 @@
</section>
</submenu>
<submenu id="menuSubscriptions">
<attribute name="label">Subscriptions</attribute>
<attribute name="label" translatable="yes">Subscriptions</attribute>
<section>
<item>
<attribute name="action">win.discover</attribute>
@ -95,7 +95,7 @@
</section>
</submenu>
<submenu id="menuChannels">
<attribute name="label">Episodes</attribute>
<attribute name="label" translatable="yes">Episodes</attribute>
<section>
<item>
<attribute name="action">win.play</attribute>
@ -150,7 +150,7 @@
</item>
</submenu>
<submenu id="menuView">
<attribute name="label">View</attribute>
<attribute name="label" translatable="yes">View</attribute>
<section>
<item>
<attribute name="action">win.showToolbar</attribute>

View File

@ -60,25 +60,26 @@ defaults = {
'limit': {
'bandwidth': {
'enabled': False,
'kbps': 500.0, # maximum kB/s per download
'kbps': 500.0, # maximum kB/s per download
},
'downloads': {
'enabled': True,
'concurrent': 1,
'concurrent_max': 16,
},
'episodes': 200, # max episodes per feed
'episodes': 200, # max episodes per feed
},
# Behavior of downloads
'downloads': {
'chronological_order': True, # download older episodes first
'chronological_order': True, # download older episodes first
},
# Automatic feed updates, download removal and retry on download timeout
'auto': {
'update': {
'enabled': False,
'frequency': 20, # minutes
'frequency': 20, # minutes
},
'cleanup': {
@ -88,14 +89,14 @@ defaults = {
'unfinished': True,
},
'retries': 3, # number of retries when downloads time out
'retries': 3, # number of retries when downloads time out
},
# Software updates from gpodder.org
'software_update': {
'check_on_startup': True, # check for updates on start
'last_check': 0, # unix timestamp of last update check
'interval': 5, # interval (in days) to check for updates
'check_on_startup': True, # check for updates on start
'last_check': 0, # unix timestamp of last update check
'interval': 5, # interval (in days) to check for updates
},
'ui': {
@ -128,7 +129,7 @@ defaults = {
},
'toolbar': False,
'new_episodes': 'show', # ignore, show, queue, download
'new_episodes': 'show', # ignore, show, queue, download
'live_search_delay': 200,
'podcast_list': {
@ -141,7 +142,7 @@ defaults = {
'episode_list': {
'descriptions': True,
'view_mode': 1,
'columns': int('110', 2), # bitfield of visible columns
'columns': int('110', 2), # bitfield of visible columns
},
'download_list': {
@ -154,7 +155,7 @@ defaults = {
# Synchronization with portable devices (MP3 players, etc..)
'device_sync': {
'device_type': 'none', # Possible values: 'none', 'filesystem', 'ipod'
'device_type': 'none', # Possible values: 'none', 'filesystem', 'ipod'
'device_folder': '/media',
'one_folder_per_podcast': True,
@ -181,13 +182,13 @@ defaults = {
},
'youtube': {
'preferred_fmt_id': 18, # default fmt_id (see fallbacks in youtube.py)
'preferred_fmt_ids': [], # for advanced uses (custom fallback sequence)
'api_key_v3': '', # API key, register for one at https://developers.google.com/youtube/v3/
'preferred_fmt_id': 18, # default fmt_id (see fallbacks in youtube.py)
'preferred_fmt_ids': [], # for advanced uses (custom fallback sequence)
'api_key_v3': '', # API key, register for one at https://developers.google.com/youtube/v3/
},
'vimeo': {
'fileformat': '720p', # preferred file format (see vimeo.py)
'fileformat': '720p', # preferred file format (see vimeo.py)
},
'extensions': {

View File

@ -54,18 +54,18 @@ class DBusPodcastsProxy(dbus.service.Object):
for parameter and return values.
"""
#DBusPodcastsProxy(lambda: self.channels, self.on_itemUpdate_activate(), self.playback_episodes, self.download_episode_list, bus_name)
def __init__(self, get_podcast_list, \
check_for_updates, playback_episodes, \
download_episodes, episode_from_uri, \
# DBusPodcastsProxy(lambda: self.channels, self.on_itemUpdate_activate(), self.playback_episodes, self.download_episode_list, bus_name)
def __init__(self, get_podcast_list,
check_for_updates, playback_episodes,
download_episodes, episode_from_uri,
bus_name):
self._get_podcasts = get_podcast_list
self._on_check_for_updates = check_for_updates
self._playback_episodes = playback_episodes
self._download_episodes = download_episodes
self._episode_from_uri = episode_from_uri
dbus.service.Object.__init__(self, \
object_path=gpodder.dbus_podcasts_object_path, \
dbus.service.Object.__init__(self,
object_path=gpodder.dbus_podcasts_object_path,
bus_name=bus_name)
def _get_episode_refs(self, urls):

View File

@ -27,9 +27,11 @@ import gpodder
_ = gpodder.gettext
import urllib.request, urllib.parse, urllib.error
import json
import os
import urllib.error
import urllib.parse
import urllib.request
from gpodder import opml
from gpodder import util
@ -186,10 +188,10 @@ PROVIDERS = [
None,
GPodderNetSearchProvider,
GPodderNetToplistProvider,
#GPodderNetTagsProvider,
# GPodderNetTagsProvider,
None,
OpmlWebImportProvider,
#OpmlFileImportProvider,
# OpmlFileImportProvider,
None,
SoundcloudSearchProvider,
]

View File

@ -36,8 +36,6 @@ import gpodder
import socket
import threading
import urllib.request, urllib.parse, urllib.error
import urllib.parse
import shutil
import os.path
import os
@ -46,6 +44,9 @@ import collections
import mimetypes
import email
import urllib.error
import urllib.parse
import urllib.request
from email.header import decode_header
@ -230,7 +231,7 @@ class DownloadURLOpener(urllib.request.FancyURLopener):
return
# This blocks forever(?) with certain servers (see bug #465)
#void = fp.read()
# void = fp.read()
fp.close()
# In case the server sent a relative URL, join with original:
@ -258,7 +259,7 @@ class DownloadURLOpener(urllib.request.FancyURLopener):
try:
current_size = os.path.getsize(filename)
tfp = open(filename, 'ab')
#If the file exists, then only download the remainder
# If the file exists, then only download the remainder
if current_size > 0:
self.addheader('Range', 'bytes=%s-' % (current_size))
except:
@ -401,12 +402,15 @@ class DownloadQueueManager(object):
"""Spawn new worker threads if necessary
"""
with self.worker_threads_access:
if not self.tasks.has_work():
return
if len(self.worker_threads) == 0 or \
len(self.worker_threads) < self._config.max_downloads or \
not self._config.max_downloads_enabled:
work_count = self.tasks.available_work_count()
if self._config.max_downloads_enabled:
# always allow at least 1 download
max_downloads = max(int(self._config.max_downloads), 1)
spawn_limit = max_downloads - len(self.worker_threads)
else:
spawn_limit = self._config.limit.downloads.concurrent_max
logger.info('%r tasks to do, can start at most %r threads', work_count, spawn_limit)
for i in range(0, min(work_count, spawn_limit)):
# We have to create a new thread here, there's work to do
logger.info('Starting new worker thread.')
@ -803,19 +807,12 @@ class DownloadTask(object):
# Look at the Content-disposition header; use if if available
disposition_filename = get_header_param(headers, 'filename', 'content-disposition')
if disposition_filename is not None:
try:
disposition_filename.decode('ascii')
except:
logger.warn('Content-disposition header contains non-ASCII characters - ignoring')
disposition_filename = None
# Some servers do send the content-disposition header, but provide
# an empty filename, resulting in an empty string here (bug 1440)
if disposition_filename is not None and disposition_filename != '':
# The server specifies a download filename - try to use it
disposition_filename = os.path.basename(disposition_filename)
self.filename = self.__episode.local_filename(create=True, \
self.filename = self.__episode.local_filename(create=True,
force_update=True, template=disposition_filename)
new_mimetype, encoding = mimetypes.guess_type(self.filename)
if new_mimetype is not None:

View File

@ -33,7 +33,9 @@ logger = logging.getLogger(__name__)
import json
import re
import urllib.request, urllib.parse, urllib.error
import urllib.error
import urllib.parse
import urllib.request
# This matches the more reliable URL
ESCAPIST_NUMBER_RE = re.compile(r'http://www.escapistmagazine.com/videos/view/(\d+)', re.IGNORECASE)
@ -68,7 +70,7 @@ def get_real_download_url(url):
data_config_data = util.urlopen(data_config_url).read().decode('utf-8')
#TODO: This second argument should get a real name
# TODO: This second argument should get a real name
real_url = get_escapist_real_url(data_config_data, data_config_frag.group(1))
if real_url is None:
@ -174,5 +176,5 @@ def get_escapist_real_url(data, config_json):
escapist_cfg = json.loads(result)
# It's super effective!
#TODO: There's a way to choose different video types, for now just pick MP4@480p
# TODO: There's a way to choose different video types, for now just pick MP4@480p
return escapist_cfg["files"]["videos"][2]["src"]

View File

@ -127,7 +127,10 @@ class ExtensionMetadata(object):
raise AttributeError(name, e)
def get_sorted(self):
kf = lambda x: self.SORTKEYS.get(x[0], 99)
def kf(x):
return self.SORTKEYS.get(x[0], 99)
return sorted([(k, v) for k, v in list(self.__dict__.items())], key=kf)
def check_ui(self, target, default):

View File

@ -193,7 +193,7 @@ class Fetcher(object):
# 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
charset = 'utf-8' # utf-8 appears hard-coded elsewhere in this codebase
# We use StringIO in case the stream needs to be read again
data = StringIO(stream.read().decode(charset))

View File

@ -66,8 +66,8 @@ def update_using_feedservice(podcasts):
podcast.link = feed.get('link', podcast.link)
podcast.description = feed.get('description', podcast.description)
podcast.cover_url = feed.get('logo', podcast.cover_url)
#podcast.http_etag = feed.get('http_etag', podcast.http_etag)
#podcast.http_last_modified = feed.get('http_last_modified', \
# podcast.http_etag = feed.get('http_etag', podcast.http_etag)
# podcast.http_last_modified = feed.get('http_last_modified', \
# podcast.http_last_modified)
podcast.save()

View File

@ -55,7 +55,7 @@ class GtkBuilderWidget(object):
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__
ui_file = '%s.ui' % self.__class__.__name__.lower()

View File

@ -67,10 +67,10 @@ class ConfigModel(Gtk.ListStore):
fieldtype = type(value)
style = Pango.Style.NORMAL
#if value == default:
# style = Pango.Style.NORMAL
#else:
# style = Pango.Style.ITALIC
# if value == default:
# style = Pango.Style.NORMAL
# else:
# style = Pango.Style.ITALIC
self.append((key, self._type_as_string(fieldtype),
config.config_value_to_string(value),
@ -81,12 +81,12 @@ class ConfigModel(Gtk.ListStore):
for row in self:
if row[self.C_NAME] == name:
style = Pango.Style.NORMAL
#if new_value == self._config.Settings[name]:
# style = Pango.Style.NORMAL
#else:
# style = Pango.Style.ITALIC
# if new_value == self._config.Settings[name]:
# style = Pango.Style.NORMAL
# else:
# style = Pango.Style.ITALIC
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_BOOLEAN_VALUE, bool(new_value),
self.C_FONT_STYLE, style)
@ -109,10 +109,24 @@ class UIConfig(config.Config):
setattr(self, name, editable.get_chars(0, -1))
editable.connect('changed', _editable_changed)
def connect_gtk_spinbutton(self, name, spinbutton):
def connect_gtk_spinbutton(self, name, spinbutton, forced_upper=None):
"""
bind a Gtk.SpinButton to a configuration entry.
It's now possible to specify an upper value (forced_upper).
It's not done automatically (always look for name + '_max') because it's
used only once. If it becomes commonplace, better make it automatic.
:param str name: configuration key (e.g. 'max_downloads' or 'limit.downloads.concurrent')
:param Gtk.SpinButton spinbutton: button to bind to config
:param float forced_upper: forced upper limit on spinbutton.
Overrides value in .ui to be consistent with code
"""
current_value = getattr(self, name)
adjustment = spinbutton.get_adjustment()
if forced_upper is not None:
adjustment.set_upper(forced_upper)
if current_value > adjustment.get_upper():
adjustment.set_upper(current_value)
@ -166,8 +180,8 @@ class UIConfig(config.Config):
def _receive_window_state(widget, event):
# ELL: why is it commented out?
#new_value = bool(event.new_window_state & Gdk.WindowState.MAXIMIZED)
#cfg.maximized = new_value
# new_value = bool(event.new_window_state & Gdk.WindowState.MAXIMIZED)
# cfg.maximized = new_value
pass
window.connect('window-state-event', _receive_window_state)

View File

@ -86,7 +86,7 @@ class gPodderChannel(BuilderWidget):
b.set_text( self.channel.description)
self.channel_description.set_buffer( b)
#Add Drag and Drop Support
# Add Drag and Drop Support
flags = Gtk.DestDefaults.ALL
targets = [Gtk.TargetEntry.new('text/uri-list', 0, 2), Gtk.TargetEntry.new('text/plain', 0, 4)]
actions = Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY

View File

@ -41,7 +41,7 @@ class gPodderDevicePlaylist(object):
self.playlist_absolute_filename = os.path.join(self.playlist_folder, self.playlist_file)
def build_extinf(self, filename):
#TO DO: Windows playlists
# TODO: Windows playlists
# if self._config.mp3_player_playlist_win_path:
# filename = filename.replace('\\', os.sep)

View File

@ -152,7 +152,7 @@ class gPodderPodcastDirectory(BuilderWidget):
column.pack_start(cell, False)
column.add_attribute(cell, 'pixbuf', DirectoryProvidersModel.C_ICON)
cell = Gtk.CellRendererText()
#cell.set_property('ellipsize', Pango.EllipsizeMode.END)
# cell.set_property('ellipsize', Pango.EllipsizeMode.END)
column.pack_start(cell, True)
column.add_attribute(cell, 'text', DirectoryProvidersModel.C_TEXT)
column.add_attribute(cell, 'weight', DirectoryProvidersModel.C_WEIGHT)

View File

@ -60,7 +60,7 @@ class NewEpisodeActionList(Gtk.ListStore):
if self._config.auto_download == row[self.C_AUTO_DOWNLOAD]:
return index
return 1 # Some sane default
return 1 # Some sane default
def set_index(self, index):
self._config.auto_download = self[index][self.C_AUTO_DOWNLOAD]
@ -80,7 +80,7 @@ class DeviceTypeActionList(Gtk.ListStore):
for index, row in enumerate(self):
if self._config.device_sync.device_type == row[self.C_DEVICE_TYPE]:
return index
return 0 # Some sane default
return 0 # Some sane default
def set_index(self, index):
self._config.device_sync.device_type = self[index][self.C_DEVICE_TYPE]
@ -106,7 +106,7 @@ class OnSyncActionList(Gtk.ListStore):
row[self.C_ON_SYNC_MARK_PLAYED] and not
self._config.device_sync.after_sync.delete_episodes):
return index
return 0 # Some sane default
return 0 # Some sane default
def set_index(self, index):
self._config.device_sync.after_sync.delete_episodes = self[index][self.C_ON_SYNC_DELETE]
@ -478,8 +478,8 @@ class gPodderPreferences(BuilderWidget):
self.preferred_vimeo_format_model.set_index(index)
def on_button_audio_player_clicked(self, widget):
result = self.show_text_edit_dialog(_('Configure audio player'), \
_('Command:'), \
result = self.show_text_edit_dialog(_('Configure audio player'),
_('Command:'),
self._config.player)
if result:
@ -488,8 +488,8 @@ class gPodderPreferences(BuilderWidget):
self.combo_audio_player_app.set_active(index)
def on_button_video_player_clicked(self, widget):
result = self.show_text_edit_dialog(_('Configure video player'), \
_('Command:'), \
result = self.show_text_edit_dialog(_('Configure video player'),
_('Command:'),
self._config.videoplayer)
if result:
@ -583,8 +583,8 @@ class gPodderPreferences(BuilderWidget):
if not widget.get_active():
self._config.device_sync.playlists.create = False
self.toggle_playlist_interface(False)
#need to read value of checkbutton from interface,
#rather than value of parameter
# need to read value of checkbutton from interface,
# rather than value of parameter
else:
self._config.device_sync.playlists.create = True
self.toggle_playlist_interface(True)

View File

@ -142,55 +142,56 @@ class gPodderSyncUI(object):
device.close()
return
#enable updating of UI
# enable updating of UI
self.enable_download_list_update()
#Update device playlists
#General approach is as follows:
"""Update device playlists
General approach is as follows:
#When a episode is downloaded and synched, it is added to the
#standard playlist for that podcast which is then written to
#the device.
When a episode is downloaded and synched, it is added to the
standard playlist for that podcast which is then written to
the device.
#After the user has played that episode on their device, they
#can delete that episode from their device.
After the user has played that episode on their device, they
can delete that episode from their device.
#At the next sync, gPodder will then compare the standard
#podcast-specific playlists on the device (as written by
#gPodder during the last sync), with the episodes on the
#device.If there is an episode referenced in the playlist
#that is no longer on the device, gPodder will assume that
#the episode has already been synced and subsequently deleted
#from the device, and will hence mark that episode as deleted
#in gPodder. If there are no playlists, nothing is deleted.
At the next sync, gPodder will then compare the standard
podcast-specific playlists on the device (as written by
gPodder during the last sync), with the episodes on the
device.If there is an episode referenced in the playlist
that is no longer on the device, gPodder will assume that
the episode has already been synced and subsequently deleted
from the device, and will hence mark that episode as deleted
in gPodder. If there are no playlists, nothing is deleted.
#At the next sync, the playlists will be refreshed based on
#the downloaded, undeleted episodes in gPodder, and the
#cycle begins again...
At the next sync, the playlists will be refreshed based on
the downloaded, undeleted episodes in gPodder, and the
cycle begins again...
"""
def resume_sync(episode_urls, channel_urls,progress):
if progress is not None:
progress.on_finished()
#rest of sync process should continue here
# rest of sync process should continue here
self.commit_changes_to_database()
for current_channel in self.channels:
#only sync those channels marked for syncing
# only sync those channels marked for syncing
if (self._config.device_sync.device_type == 'filesystem' and current_channel.sync_to_mp3_player and self._config.device_sync.playlists.create):
#get playlist object
# get playlist object
playlist = gPodderDevicePlaylist(self._config,
current_channel.title)
#need to refresh episode list so that
#deleted episodes aren't included in playlists
# need to refresh episode list so that
# deleted episodes aren't included in playlists
episodes_for_playlist = sorted(current_channel.get_episodes(gpodder.STATE_DOWNLOADED),
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:
episodes_for_playlist = [ep for ep in episodes_for_playlist if ep.is_new]
playlist.write_m3u(episodes_for_playlist)
#enable updating of UI
# enable updating of UI
self.enable_download_list_update()
if (self._config.device_sync.device_type == 'filesystem' and self._config.device_sync.playlists.create):
@ -211,11 +212,11 @@ class gPodderSyncUI(object):
episodes_to_delete = []
if self._config.device_sync.playlists.two_way_sync:
for current_channel in self.channels:
#only include channels that are included in the sync
# only include channels that are included in the sync
if current_channel.sync_to_mp3_player:
#get playlist object
# get playlist object
playlist = gPodderDevicePlaylist(self._config, current_channel.title)
#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),
key=lambda ep: ep.published)
episode_keys = list(map(playlist.get_absolute_filename_for_playlist,
@ -223,34 +224,34 @@ class gPodderSyncUI(object):
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()
#if playlist doesn't exist (yet) episodes_in_playlist will be empty
# if playlist doesn't exist (yet) episodes_in_playlist will be empty
if episodes_in_playlists:
for episode_filename in episodes_in_playlists:
if not(os.path.exists(os.path.join(playlist.mountpoint,
episode_filename))):
#episode was synced but no longer on device
#i.e. must have been deleted by user, so delete from gpodder
# episode was synced but no longer on device
# i.e. must have been deleted by user, so delete from gpodder
try:
episodes_to_delete.append(episode_dict[episode_filename])
except KeyError as ioe:
logger.warn('Episode %s, removed from device has already been deleted from gpodder',
episode_filename)
#delete all episodes from gpodder (will prompt user)
# delete all episodes from gpodder (will prompt user)
#not using playlists to delete
# not using playlists to delete
def auto_delete_callback(episodes):
if not episodes:
#episodes were deleted on device
#but user decided not to delete them from gpodder
#so jump straight to sync
# episodes were deleted on device
# but user decided not to delete them from gpodder
# so jump straight to sync
logger.info('Starting sync - no episodes selected for deletion')
resume_sync([],[],None)
else:
#episodes need to be deleted from gpodder
# episodes need to be deleted from gpodder
for episode_to_delete in episodes:
logger.info("Deleting episode %s",
episode_to_delete.title)

View File

@ -72,13 +72,13 @@ class DownloadStatusModel(Gtk.ListStore):
self.C_URL, task.url)
if task.status == task.FAILED:
status_message = '%s: %s' % (\
task.STATUS_MESSAGE[task.status], \
status_message = '%s: %s' % (
task.STATUS_MESSAGE[task.status],
task.error_message)
elif task.status == task.DOWNLOADING:
status_message = '%s (%.0f%%, %s/s)' % (\
task.STATUS_MESSAGE[task.status], \
task.progress * 100, \
status_message = '%s (%.0f%%, %s/s)' % (
task.STATUS_MESSAGE[task.status],
task.progress * 100,
util.format_filesize(task.speed))
else:
status_message = task.STATUS_MESSAGE[task.status]
@ -96,7 +96,7 @@ class DownloadStatusModel(Gtk.ListStore):
progress_message = ' / '.join((current, total))
elif task.total_size > 0:
progress_message = util.format_filesize(task.total_size, \
progress_message = util.format_filesize(task.total_size,
digits=1)
else:
progress_message = ('unknown size')
@ -104,8 +104,8 @@ class DownloadStatusModel(Gtk.ListStore):
self.set(iter,
self.C_NAME, self._format_message(task.episode.title,
status_message, task.episode.channel.title),
self.C_PROGRESS, 100. * task.progress, \
self.C_PROGRESS_TEXT, progress_message, \
self.C_PROGRESS, 100. * task.progress,
self.C_PROGRESS_TEXT, progress_message,
self.C_ICON_NAME, self._status_ids[task.status])
def __add_new_task(self, task):
@ -135,25 +135,29 @@ class DownloadStatusModel(Gtk.ListStore):
for row in self:
task = row[DownloadStatusModel.C_TASK]
if task is not None and \
task.status in (task.DOWNLOADING, \
task.status in (task.DOWNLOADING,
task.QUEUED):
return True
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)
return any(self._work_gen())
def available_work_count(self):
return len(list(self._work_gen()))
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)
result = next(self._work_gen())
self.set_downloading(result)
return result
def _work_gen(self):
return (task for task in
(row[DownloadStatusModel.C_TASK] for row in self)
if task.status == task.QUEUED)
def set_downloading(self, task):
with self.set_downloading_access:
if task.status is task.DOWNLOADING:

View File

@ -88,5 +88,5 @@ class gPodderAddPodcast(BuilderWidget):
url = self.entry_url.get_text()
self.on_btn_close_clicked(widget)
if self.add_podcast_list is not None:
title = None # FIXME: Add title GUI element
title = None # FIXME: Add title GUI element
self.add_podcast_list([(title, url)])

View File

@ -40,7 +40,7 @@ class BuilderWidget(GtkBuilderWidget):
# Enable support for tracking iconified state
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)
def _on_window_state_event_iconified(self, widget, event):
@ -89,9 +89,9 @@ class BuilderWidget(GtkBuilderWidget):
dlg.destroy()
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):
dialog = Gtk.Dialog(title, self.get_dialog_parent(), \
dialog = Gtk.Dialog(title, self.get_dialog_parent(),
Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT)
dialog.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)

View File

@ -54,7 +54,7 @@ class ProgressIndicator(object):
return True
def _create_progress(self):
self.dialog = Gtk.MessageDialog(self.parent, \
self.dialog = Gtk.MessageDialog(self.parent,
0, 0, Gtk.ButtonsType.CANCEL, self.subtitle or self.title)
self.dialog.set_modal(True)
self.dialog.connect('delete-event', self._on_delete_event)
@ -67,7 +67,7 @@ class ProgressIndicator(object):
if isinstance(label, Gtk.Label):
label.set_selectable(False)
self.dialog.set_response_sensitive(Gtk.ResponseType.CANCEL, \
self.dialog.set_response_sensitive(Gtk.ResponseType.CANCEL,
self.cancellable)
self.progressbar = Gtk.ProgressBar()

View File

@ -38,7 +38,9 @@ import time
import threading
import tempfile
import collections
import urllib.request, urllib.parse, urllib.error
import urllib.error
import urllib.parse
import urllib.request
import cgi
@ -162,14 +164,18 @@ class gPodder(BuilderWidget, dbus.service.Object):
self.download_status_model = DownloadStatusModel()
self.download_queue_manager = download.DownloadQueueManager(self.config, self.download_status_model)
self.config.connect_gtk_spinbutton('max_downloads', self.spinMaxDownloads)
self.config.connect_gtk_spinbutton('limit.downloads.concurrent', self.spinMaxDownloads,
self.config.limit.downloads.concurrent_max)
self.config.connect_gtk_togglebutton('max_downloads_enabled', self.cbMaxDownloads)
self.config.connect_gtk_spinbutton('limit_rate_value', self.spinLimitDownloads)
self.config.connect_gtk_togglebutton('limit_rate', self.cbLimitDownloads)
# When the amount of maximum downloads changes, notify the queue manager
changed_cb = lambda spinbutton: self.download_queue_manager.update_max_downloads()
def changed_cb(spinbutton):
return self.download_queue_manager.update_max_downloads()
self.spinMaxDownloads.connect('value-changed', changed_cb)
self.cbMaxDownloads.connect('toggled', changed_cb)
# Keep a reference to the last add podcast dialog instance
self._add_podcast_dialog = None
@ -471,8 +477,12 @@ class gPodder(BuilderWidget, dbus.service.Object):
# By default, assume we can't pre-select any channel
# but can match episodes simply via the download URL
is_channel = lambda c: True
is_episode = lambda e: e.url == uri
def is_channel(c):
return True
def is_episode(e):
return e.url == uri
if uri.startswith(prefix):
# File is on the local filesystem in the download folder
@ -487,8 +497,11 @@ class gPodder(BuilderWidget, dbus.service.Object):
foldername, filename = file_parts
is_channel = lambda c: c.download_folder == foldername
is_episode = lambda e: e.download_filename == filename
def is_channel(c):
return c.download_folder == foldername
def is_episode(e):
return e.download_filename == filename
# Deep search through channels and episodes for a match
for channel in filter(is_channel, self.channels):
@ -587,14 +600,14 @@ class gPodder(BuilderWidget, dbus.service.Object):
def ask():
# We're abusing the Episode Selector again ;) -- thp
gPodderEpisodeSelector(self.main_window, \
title=_('Confirm changes from gpodder.net'), \
instructions=_('Select the actions you want to carry out.'), \
episodes=changes, \
columns=columns, \
size_attribute=None, \
stock_ok_button=Gtk.STOCK_APPLY, \
callback=execute_podcast_actions, \
gPodderEpisodeSelector(self.main_window,
title=_('Confirm changes from gpodder.net'),
instructions=_('Select the actions you want to carry out.'),
episodes=changes,
columns=columns,
size_attribute=None,
stock_ok_button=Gtk.STOCK_APPLY,
callback=execute_podcast_actions,
_config=self.config)
# There are some actions that need the user's attention
@ -629,8 +642,8 @@ class gPodder(BuilderWidget, dbus.service.Object):
def on_send_full_subscriptions(self):
# Send the full subscription list to the gpodder.net client
# (this will overwrite the subscription list on the server)
indicator = ProgressIndicator(_('Uploading subscriptions'), \
_('Your subscriptions are being uploaded to the server.'), \
indicator = ProgressIndicator(_('Uploading subscriptions'),
_('Your subscriptions are being uploaded to the server.'),
False, self.get_dialog_parent())
try:
@ -641,8 +654,8 @@ class gPodder(BuilderWidget, dbus.service.Object):
message = str(e)
if not message:
message = e.__class__.__name__
self.show_message(message, \
_('Error while uploading'), \
self.show_message(message,
_('Error while uploading'),
important=True)
util.idle_add(show_error, e)
@ -657,10 +670,10 @@ class gPodder(BuilderWidget, dbus.service.Object):
def for_each_episode_set_task_status(self, episodes, status):
episode_urls = set(episode.url for episode in episodes)
model = self.treeDownloads.get_model()
selected_tasks = [(Gtk.TreeRowReference.new(model, row.path), \
model.get_value(row.iter, \
DownloadStatusModel.C_TASK)) for row in model \
if model.get_value(row.iter, DownloadStatusModel.C_TASK).url \
selected_tasks = [(Gtk.TreeRowReference.new(model, row.path),
model.get_value(row.iter,
DownloadStatusModel.C_TASK)) for row in model
if model.get_value(row.iter, DownloadStatusModel.C_TASK).url
in episode_urls]
self._for_each_task_set_status(selected_tasks, status)
@ -1010,15 +1023,14 @@ class gPodder(BuilderWidget, dbus.service.Object):
self.treeAvailable.connect('popup-menu', self.treeview_available_show_context_menu)
self.treeAvailable.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, \
self.treeAvailable.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK,
(('text/uri-list', 0, 0),), Gdk.DragAction.COPY)
def drag_data_get(tree, context, selection_data, info, timestamp):
uris = ['file://' + e.local_filename(create=False) \
for e in self.get_selected_episodes() \
uris = ['file://' + e.local_filename(create=False)
for e in self.get_selected_episodes()
if e.was_downloaded(and_exists=True)]
uris.append('') # for the trailing '\r\n'
selection_data.set(selection_data.target, 8, '\r\n'.join(uris))
selection_data.set_uris(uris)
self.treeAvailable.connect('drag-data-get', drag_data_get)
selection = self.treeAvailable.get_selection()
@ -1043,7 +1055,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
cell = Gtk.CellRendererPixbuf()
cell.set_property('stock-size', Gtk.IconSize.BUTTON)
column.pack_start(cell, False)
column.add_attribute(cell, 'icon-name', \
column.add_attribute(cell, 'icon-name',
DownloadStatusModel.C_ICON_NAME)
cell = Gtk.CellRendererText()
@ -1059,7 +1071,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
cell.set_property('yalign', .5)
cell.set_property('ypad', 6)
column = Gtk.TreeViewColumn(_('Progress'), cell,
value=DownloadStatusModel.C_PROGRESS, \
value=DownloadStatusModel.C_PROGRESS,
text=DownloadStatusModel.C_PROGRESS_TEXT)
column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
column.set_expand(False)
@ -1385,14 +1397,14 @@ class gPodder(BuilderWidget, dbus.service.Object):
selection = treeview.get_selection()
model, paths = selection.get_selected_rows()
if path is None or (path not in paths and \
if path is None or (path not in paths and
event.button == 3):
# We have right-clicked, but not into the selection,
# assume we don't want to operate on the selection
paths = []
if path is not None and not paths and \
event.button == 3:
if (path is not None and not paths and
event.button == 3):
# No selection or clicked outside selection;
# select the single item where we clicked
treeview.grab_focus()
@ -1415,27 +1427,27 @@ class gPodder(BuilderWidget, dbus.service.Object):
model, paths = selection.get_selected_rows()
can_queue, can_cancel, can_pause, can_remove, can_force = (True,) * 5
selected_tasks = [(Gtk.TreeRowReference.new(model, path), \
model.get_value(model.get_iter(path), \
selected_tasks = [(Gtk.TreeRowReference.new(model, path),
model.get_value(model.get_iter(path),
DownloadStatusModel.C_TASK)) for path in paths]
for row_reference, task in selected_tasks:
if task.status != download.DownloadTask.QUEUED:
can_force = False
if task.status not in (download.DownloadTask.PAUSED, \
download.DownloadTask.FAILED, \
if task.status not in (download.DownloadTask.PAUSED,
download.DownloadTask.FAILED,
download.DownloadTask.CANCELLED):
can_queue = False
if task.status not in (download.DownloadTask.PAUSED, \
download.DownloadTask.QUEUED, \
download.DownloadTask.DOWNLOADING, \
if task.status not in (download.DownloadTask.PAUSED,
download.DownloadTask.QUEUED,
download.DownloadTask.DOWNLOADING,
download.DownloadTask.FAILED):
can_cancel = False
if task.status not in (download.DownloadTask.QUEUED, \
if task.status not in (download.DownloadTask.QUEUED,
download.DownloadTask.DOWNLOADING):
can_pause = False
if task.status not in (download.DownloadTask.CANCELLED, \
download.DownloadTask.FAILED, \
if task.status not in (download.DownloadTask.CANCELLED,
download.DownloadTask.FAILED,
download.DownloadTask.DONE):
can_remove = False
@ -2048,18 +2060,19 @@ class gPodder(BuilderWidget, dbus.service.Object):
logger.error('Exception in D-Bus call: %s', str(err))
# Fallback: use the command line client
for command in util.format_desktop_command('panucci', \
for command in util.format_desktop_command('panucci',
[filename]):
logger.info('Executing: %s', repr(command))
subprocess.Popen(command)
on_error = lambda err: error_handler(filename, err)
def on_error(err):
return error_handler(filename, err)
# This method only exists in Panucci > 0.9 ('new Panucci')
i.playback_from(filename, resume_position, \
i.playback_from(filename, resume_position,
reply_handler=on_reply, error_handler=on_error)
continue # This file was handled by the D-Bus call
continue # This file was handled by the D-Bus call
except Exception as e:
logger.error('Calling Panucci using D-Bus', exc_info=True)
@ -2087,14 +2100,14 @@ class gPodder(BuilderWidget, dbus.service.Object):
def playback_episodes(self, episodes):
# We need to create a list, because we run through it more than once
episodes = list(Model.sort_episodes_by_pubdate(e for e in episodes if \
episodes = list(Model.sort_episodes_by_pubdate(e for e in episodes if
e.was_downloaded(and_exists=True) or self.streaming_possible()))
try:
self.playback_episodes_for_real(episodes)
except Exception as e:
logger.error('Error in playback!', exc_info=True)
self.show_message(_('Please check your media player settings in the preferences dialog.'), \
self.show_message(_('Please check your media player settings in the preferences dialog.'),
_('Error opening player'))
self.episode_list_status_changed(episodes)
@ -2157,9 +2170,6 @@ class gPodder(BuilderWidget, dbus.service.Object):
self.delete_action.set_enabled(can_delete)
self.toggle_episode_new_action.set_enabled(can_play)
self.toggle_episode_lock_action.set_enabled(can_play)
# XXX: how to hide menu items?
#self.itemOpenSelected.set_visible(open_instead_of_play)
#self.itemPlaySelected.set_visible(not open_instead_of_play)
return (can_play, can_download, can_cancel, can_delete, open_instead_of_play)
@ -2193,8 +2203,12 @@ class gPodder(BuilderWidget, dbus.service.Object):
selection = self.treeChannels.get_selection()
model, iter = selection.get_selected()
is_section = lambda r: r[PodcastListModel.C_URL] == '-'
is_separator = lambda r: r[PodcastListModel.C_SEPARATOR]
def is_section(r):
return r[PodcastListModel.C_URL] == '-'
def is_separator(r):
return r[PodcastListModel.C_SEPARATOR]
sections_active = any(is_section(x) for x in self.podcast_list_model)
if self.config.podcast_list_view_all:
@ -2210,7 +2224,10 @@ class gPodder(BuilderWidget, dbus.service.Object):
# Filter items in the list model that are not podcasts, so we get the
# correct podcast list count (ignore section headers and separators)
is_not_podcast = lambda r: is_section(r) or is_separator(r)
def is_not_podcast(r):
return is_section(r) or is_separator(r)
list_model_length -= len(list(filter(is_not_podcast, self.podcast_list_model)))
if selected and not force_update:
@ -2326,8 +2343,8 @@ class gPodder(BuilderWidget, dbus.service.Object):
error_messages = {}
redirections = {}
progress = ProgressIndicator(_('Adding podcasts'), \
_('Please wait while episode information is downloaded.'), \
progress = ProgressIndicator(_('Adding podcasts'),
_('Please wait while episode information is downloaded.'),
parent=self.get_dialog_parent())
def on_after_update():
@ -2371,7 +2388,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
if failed:
title = _('Could not add some podcasts')
message = _('Some podcasts could not be added to your list:') \
+ '\n\n' + '\n'.join(cgi.escape('%s: %s' % (url, \
+ '\n\n' + '\n'.join(cgi.escape('%s: %s' % (url,
error_messages.get(url, _('Unknown')))) for url in failed)
self.show_message(message, title, important=True)
@ -2407,9 +2424,9 @@ class gPodder(BuilderWidget, dbus.service.Object):
episodes.extend(podcast.get_all_episodes())
if episodes:
episodes = list(Model.sort_episodes_by_pubdate(episodes, \
episodes = list(Model.sort_episodes_by_pubdate(episodes,
reverse=True))
self.new_episodes_show(episodes, \
self.new_episodes_show(episodes,
selected=[e.check_is_new() for e in episodes])
@util.run_in_background
@ -2422,8 +2439,8 @@ class gPodder(BuilderWidget, dbus.service.Object):
progress.on_message(title or url)
try:
# The URL is valid and does not exist already - subscribe!
channel = self.model.load_podcast(url=url, create=True, \
authentication_tokens=auth_tokens.get(url, None), \
channel = self.model.load_podcast(url=url, create=True,
authentication_tokens=auth_tokens.get(url, None),
max_episodes=self.config.max_episodes_per_feed)
try:
@ -2489,8 +2506,8 @@ class gPodder(BuilderWidget, dbus.service.Object):
the server to the local database and update the
status of the affected episodes as necessary.
"""
indicator = ProgressIndicator(_('Merging episode actions'), \
_('Episode actions from gpodder.net are merged.'), \
indicator = ProgressIndicator(_('Merging episode actions'),
_('Episode actions from gpodder.net are merged.'),
False, self.get_dialog_parent())
Gtk.main_iteration()
@ -2637,7 +2654,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
if (show_new_episodes_dialog and
self.config.auto_download == 'show'):
self.new_episodes_show(episodes, notification=True)
else: # !show_new_episodes_dialog or auto_download == 'ignore'
else: # !show_new_episodes_dialog or auto_download == 'ignore'
message = N_('%(count)d new episode available', '%(count)d new episodes available', count) % {'count':count}
self.pbFeedUpdate.set_text(message)
@ -2720,8 +2737,8 @@ class gPodder(BuilderWidget, dbus.service.Object):
if confirm and not self.show_confirmation(message, title):
return False
progress = ProgressIndicator(_('Deleting episodes'), \
_('Please wait while episodes are deleted'), \
progress = ProgressIndicator(_('Deleting episodes'),
_('Please wait while episodes are deleted'),
parent=self.get_dialog_parent())
def finish_deletion(episode_urls, channel_urls):
@ -2797,9 +2814,9 @@ class gPodder(BuilderWidget, dbus.service.Object):
selected = [not e.is_new or not e.file_exists() for e in episodes]
gPodderEpisodeSelector(self.main_window, title = _('Delete episodes'), instructions = instructions, \
episodes = episodes, selected = selected, columns = columns, \
stock_ok_button = 'edit-delete', callback = self.delete_episode_list, \
gPodderEpisodeSelector(self.main_window, title = _('Delete episodes'), instructions = instructions,
episodes = episodes, selected = selected, columns = columns,
stock_ok_button = 'edit-delete', callback = self.delete_episode_list,
selection_buttons = selection_buttons, _config=self.config)
def on_selected_episodes_status_changed(self):
@ -2989,23 +3006,23 @@ class gPodder(BuilderWidget, dbus.service.Object):
# Select all by default
selected = [True] * len(episodes)
self.new_episodes_window = gPodderEpisodeSelector(self.main_window, \
title=_('New episodes available'), \
instructions=instructions, \
episodes=episodes, \
columns=columns, \
selected=selected, \
stock_ok_button = 'gpodder-download', \
callback=download_episodes_callback, \
remove_callback=lambda e: e.mark_old(), \
remove_action=_('Mark as old'), \
remove_finished=self.episode_new_status_changed, \
_config=self.config, \
self.new_episodes_window = gPodderEpisodeSelector(self.main_window,
title=_('New episodes available'),
instructions=instructions,
episodes=episodes,
columns=columns,
selected=selected,
stock_ok_button = 'gpodder-download',
callback=download_episodes_callback,
remove_callback=lambda e: e.mark_old(),
remove_action=_('Mark as old'),
remove_finished=self.episode_new_status_changed,
_config=self.config,
show_notification=False)
def on_itemDownloadAllNew_activate(self, action, param):
if not self.offer_new_episodes():
self.show_message(_('Please check for new episodes later.'), \
self.show_message(_('Please check for new episodes later.'),
_('No new episodes available'))
def get_new_episodes(self, channels=None):
@ -3079,7 +3096,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
util.idle_add(after_login)
def on_itemAddChannel_activate(self, action=None, param=None):
self._add_podcast_dialog = gPodderAddPodcast(self.gPodder, \
self._add_podcast_dialog = gPodderAddPodcast(self.gPodder,
add_podcast_list=self.add_podcast_list)
def on_itemEditChannel_activate(self, action, param=None):
@ -3104,14 +3121,14 @@ class gPodder(BuilderWidget, dbus.service.Object):
# We're abusing the Episode Selector for selecting Podcasts here,
# but it works and looks good, so why not? -- thp
gPodderEpisodeSelector(self.main_window, \
title=_('Delete podcasts'), \
instructions=_('Select the podcast you want to delete.'), \
episodes=self.channels, \
columns=columns, \
size_attribute=None, \
stock_ok_button=_('Delete'), \
callback=self.remove_podcast_list, \
gPodderEpisodeSelector(self.main_window,
title=_('Delete podcasts'),
instructions=_('Select the podcast you want to delete.'),
episodes=self.channels,
columns=columns,
size_attribute=None,
stock_ok_button=_('Delete'),
callback=self.remove_podcast_list,
_config=self.config)
def remove_podcast_list(self, channels, confirm=True):
@ -3213,8 +3230,8 @@ class gPodder(BuilderWidget, dbus.service.Object):
dlg.destroy()
if filename is not None:
dir = gPodderPodcastDirectory(self.gPodder, _config=self.config, \
custom_title=_('Import podcasts from OPML file'), \
dir = gPodderPodcastDirectory(self.gPodder, _config=self.config,
custom_title=_('Import podcasts from OPML file'),
add_podcast_list=self.add_podcast_list,
hide_url_entry=True)
dir.download_opml_file(filename)
@ -3330,7 +3347,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
def get_podcast_urls_from_selected_episodes(self):
"""Get a set of podcast URLs based on the selected episodes"""
return set(episode.channel.url for episode in \
return set(episode.channel.url for episode in
self.get_selected_episodes())
def get_selected_episodes(self):
@ -3365,12 +3382,12 @@ class gPodder(BuilderWidget, dbus.service.Object):
GObject.source_remove(self._auto_update_timer_source_id)
self._auto_update_timer_source_id = None
if self.config.auto_update_feeds and \
self.config.auto_update_frequency:
if (self.config.auto_update_feeds and
self.config.auto_update_frequency):
interval = 60 * 1000 * self.config.auto_update_frequency
logger.debug('Setting up auto update timer with interval %d.',
self.config.auto_update_frequency)
self._auto_update_timer_source_id = GObject.timeout_add(\
self._auto_update_timer_source_id = GObject.timeout_add(
interval, self._on_auto_update_timer)
def _on_auto_update_timer(self):
@ -3411,14 +3428,14 @@ class gPodder(BuilderWidget, dbus.service.Object):
if self.wNotebook.get_current_page() == 0:
selection = self.treeAvailable.get_selection()
(model, paths) = selection.get_selected_rows()
urls = [model.get_value(model.get_iter(path), \
urls = [model.get_value(model.get_iter(path),
self.episode_list_model.C_URL) for path in paths]
selected_tasks = [task for task in self.download_tasks_seen \
selected_tasks = [task for task in self.download_tasks_seen
if task.url in urls]
else:
selection = self.treeDownloads.get_selection()
(model, paths) = selection.get_selected_rows()
selected_tasks = [model.get_value(model.get_iter(path), \
selected_tasks = [model.get_value(model.get_iter(path),
self.download_status_model.C_TASK) for path in paths]
self.cancel_task_list(selected_tasks)
@ -3654,7 +3671,7 @@ class gPodderApplication(Gtk.Application):
Gtk.IconTheme.add_builtin_icon(icon_name, pixbuf.get_width(), pixbuf)
Gtk.Window.set_default_icon_name('gpodder')
#Gtk.AboutDialog.set_url_hook(lambda dlg, link, data: util.open_website(link), None)
# Gtk.AboutDialog.set_url_hook(lambda dlg, link, data: util.open_website(link), None)
try:
dbus_main_loop = dbus.glib.DBusGMainLoop(set_as_default=True)
@ -3663,7 +3680,7 @@ class gPodderApplication(Gtk.Application):
self.bus_name = dbus.service.BusName(gpodder.dbus_bus_name, bus=gpodder.dbus_session_bus)
except dbus.exceptions.DBusException as dbe:
logger.warn('Cannot get "on the bus".', exc_info=True)
dlg = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR, \
dlg = Gtk.MessageDialog(None, Gtk.DialogFlags.MODAL, Gtk.MessageType.ERROR,
Gtk.ButtonsType.CLOSE, _('Cannot start gPodder'))
dlg.format_secondary_markup(_('D-Bus error: %s') % (str(dbe),))
dlg.set_title('gPodder')
@ -3687,7 +3704,7 @@ class gPodderApplication(Gtk.Application):
self.window.gPodder.present()
def on_about(self, action, param):
dlg = Gtk.Dialog(_('About gPodder'), self.window.gPodder, \
dlg = Gtk.Dialog(_('About gPodder'), self.window.gPodder,
Gtk.DialogFlags.MODAL)
dlg.add_button(Gtk.STOCK_CLOSE, Gtk.ResponseType.OK).show()
dlg.set_resizable(False)
@ -3733,13 +3750,13 @@ class gPodderApplication(Gtk.Application):
util.open_website('https://gpodder.github.io/docs/')
def on_itemPreferences_activate(self, action, param=None):
gPodderPreferences(self.window.gPodder, \
_config=self.window.config, \
user_apps_reader=self.window.user_apps_reader, \
parent_window=self.window.main_window, \
mygpo_client=self.window.mygpo_client, \
on_send_full_subscriptions=self.window.on_send_full_subscriptions, \
on_itemExportChannels_activate=self.window.on_itemExportChannels_activate, \
gPodderPreferences(self.window.gPodder,
_config=self.window.config,
user_apps_reader=self.window.user_apps_reader,
parent_window=self.window.main_window,
mygpo_client=self.window.mygpo_client,
on_send_full_subscriptions=self.window.on_send_full_subscriptions,
on_itemExportChannels_activate=self.window.on_itemExportChannels_activate,
on_extension_enabled=self.on_extension_enabled,
on_extension_disabled=self.on_extension_disabled)

View File

@ -68,11 +68,11 @@ class GEpisode(model.PodcastEpisode):
length_str = '%s; ' % util.format_filesize(self.file_size)
else:
length_str = ''
return ('<b>%s</b>\n<small>%s' + _('released %s') + \
'; ' + _('from %s') + '</small>') % (\
cgi.escape(re.sub('\s+', ' ', self.title)), \
cgi.escape(length_str), \
cgi.escape(self.pubdate_prop), \
return ('<b>%s</b>\n<small>%s' + _('released %s') +
'; ' + _('from %s') + '</small>') % (
cgi.escape(re.sub('\s+', ' ', self.title)),
cgi.escape(length_str),
cgi.escape(self.pubdate_prop),
cgi.escape(re.sub('\s+', ' ', self.channel.title)))
@property
@ -86,12 +86,12 @@ class GEpisode(model.PodcastEpisode):
downloaded_string = self.get_age_string()
if not downloaded_string:
downloaded_string = _('today')
return ('<b>%s</b>\n<small>%s; %s; ' + _('downloaded %s') + \
'; ' + _('from %s') + '</small>') % (\
cgi.escape(self.title), \
cgi.escape(util.format_filesize(self.file_size)), \
cgi.escape(played_string), \
cgi.escape(downloaded_string), \
return ('<b>%s</b>\n<small>%s; %s; ' + _('downloaded %s') +
'; ' + _('from %s') + '</small>') % (
cgi.escape(self.title),
cgi.escape(util.format_filesize(self.file_size)),
cgi.escape(played_string),
cgi.escape(downloaded_string),
cgi.escape(self.channel.title))
@ -149,11 +149,11 @@ class BackgroundUpdate(object):
class EpisodeListModel(Gtk.ListStore):
C_URL, C_TITLE, C_FILESIZE_TEXT, C_EPISODE, C_STATUS_ICON, \
C_PUBLISHED_TEXT, C_DESCRIPTION, C_TOOLTIP, \
C_VIEW_SHOW_UNDELETED, C_VIEW_SHOW_DOWNLOADED, \
C_VIEW_SHOW_UNPLAYED, C_FILESIZE, C_PUBLISHED, \
C_TIME, C_TIME_VISIBLE, C_TOTAL_TIME, \
C_LOCKED = list(range(17))
C_PUBLISHED_TEXT, C_DESCRIPTION, C_TOOLTIP, \
C_VIEW_SHOW_UNDELETED, C_VIEW_SHOW_DOWNLOADED, \
C_VIEW_SHOW_UNPLAYED, C_FILESIZE, C_PUBLISHED, \
C_TIME, C_TIME_VISIBLE, C_TOTAL_TIME, \
C_LOCKED = list(range(17))
VIEW_ALL, VIEW_UNDELETED, VIEW_DOWNLOADED, VIEW_UNPLAYED = list(range(4))
@ -166,8 +166,8 @@ class EpisodeListModel(Gtk.ListStore):
PROGRESS_STEPS = 20
def __init__(self, config, on_filter_changed=lambda has_episodes: None):
Gtk.ListStore.__init__(self, str, str, str, object, \
str, str, str, str, bool, bool, bool, \
Gtk.ListStore.__init__(self, str, str, str, object,
str, str, str, str, bool, bool, bool,
GObject.TYPE_INT64, GObject.TYPE_INT64, str, bool, GObject.TYPE_INT64, bool)
self._config = config
@ -488,7 +488,7 @@ class PodcastChannelProxy(object):
self.channels = channels
self.title = _('All episodes')
self.description = _('from all podcasts')
#self.parse_error = ''
# self.parse_error = ''
self.url = ''
self.section = ''
self.id = None
@ -516,10 +516,10 @@ class PodcastChannelProxy(object):
class PodcastListModel(Gtk.ListStore):
C_URL, C_TITLE, C_DESCRIPTION, C_PILL, C_CHANNEL, \
C_COVER, C_ERROR, C_PILL_VISIBLE, \
C_VIEW_SHOW_UNDELETED, C_VIEW_SHOW_DOWNLOADED, \
C_VIEW_SHOW_UNPLAYED, C_HAS_EPISODES, C_SEPARATOR, \
C_DOWNLOADS, C_COVER_VISIBLE, C_SECTION = list(range(16))
C_COVER, C_ERROR, C_PILL_VISIBLE, \
C_VIEW_SHOW_UNDELETED, C_VIEW_SHOW_DOWNLOADED, \
C_VIEW_SHOW_UNPLAYED, C_HAS_EPISODES, C_SEPARATOR, \
C_DOWNLOADS, C_COVER_VISIBLE, C_SECTION = list(range(16))
SEARCH_COLUMNS = (C_TITLE, C_DESCRIPTION, C_SECTION)
@ -528,8 +528,8 @@ class PodcastListModel(Gtk.ListStore):
return model.get_value(iter, cls.C_SEPARATOR)
def __init__(self, cover_downloader):
Gtk.ListStore.__init__(self, str, str, str, GdkPixbuf.Pixbuf, \
object, GdkPixbuf.Pixbuf, str, bool, bool, bool, bool, \
Gtk.ListStore.__init__(self, str, str, str, GdkPixbuf.Pixbuf,
object, GdkPixbuf.Pixbuf, str, bool, bool, bool, bool,
bool, bool, int, bool, str)
# Filter to allow hiding some episodes
@ -707,7 +707,7 @@ class PodcastListModel(Gtk.ListStore):
else:
return None
def _format_description(self, channel, total, deleted, \
def _format_description(self, channel, total, deleted,
new, downloaded, unplayed):
title_markup = cgi.escape(channel.title)
if not channel.pause_subscription:
@ -727,10 +727,10 @@ class PodcastListModel(Gtk.ListStore):
return ''.join(d)
def _format_error(self, channel):
#if channel.parse_error:
# return str(channel.parse_error)
#else:
# return None
# if channel.parse_error:
# return str(channel.parse_error)
# else:
# return None
return None
def set_channels(self, db, config, channels):
@ -865,22 +865,22 @@ class PodcastListModel(Gtk.ListStore):
return
total, deleted, new, downloaded, unplayed = channel.get_statistics()
description = self._format_description(channel, total, deleted, new, \
description = self._format_description(channel, total, deleted, new,
downloaded, unplayed)
pill_image = self._get_pill_image(channel, downloaded, unplayed)
self.set(iter, \
self.C_TITLE, channel.title, \
self.C_DESCRIPTION, description, \
self.C_SECTION, channel.section, \
self.C_ERROR, self._format_error(channel), \
self.C_PILL, pill_image, \
self.C_PILL_VISIBLE, pill_image is not None, \
self.C_VIEW_SHOW_UNDELETED, total - deleted > 0, \
self.C_VIEW_SHOW_DOWNLOADED, downloaded + new > 0, \
self.C_VIEW_SHOW_UNPLAYED, unplayed + new > 0, \
self.C_HAS_EPISODES, total > 0, \
self.set(iter,
self.C_TITLE, channel.title,
self.C_DESCRIPTION, description,
self.C_SECTION, channel.section,
self.C_ERROR, self._format_error(channel),
self.C_PILL, pill_image,
self.C_PILL_VISIBLE, pill_image is not None,
self.C_VIEW_SHOW_UNDELETED, total - deleted > 0,
self.C_VIEW_SHOW_DOWNLOADED, downloaded + new > 0,
self.C_VIEW_SHOW_UNPLAYED, unplayed + new > 0,
self.C_HAS_EPISODES, total > 0,
self.C_DOWNLOADS, downloaded)
def clear_cover_cache(self, podcast_url):

View File

@ -160,7 +160,7 @@ class JsonConfig(object):
elif isinstance(value, dict):
# Recurse into sub-dictionaries
work_queue.append((data[key], value))
elif type(value) != type(data[key]): # noqa
elif type(value) != type(data[key]): # noqa
# Type mismatch of current value and default
if type(value) == int and type(data[key]) == float:
# Convert float to int if default value is int

View File

@ -138,7 +138,7 @@ class PodcastEpisode(PodcastModelObject):
is_locked = property(fget=_deprecated, fset=_deprecated)
def has_website_link(self):
return bool(self.link) and (self.link != self.url or \
return bool(self.link) and (self.link != self.url or
youtube.is_video_link(self.link))
@classmethod
@ -336,7 +336,7 @@ class PodcastEpisode(PodcastModelObject):
self.save()
def age_in_days(self):
return util.file_age_in_days(self.local_filename(create=False, \
return util.file_age_in_days(self.local_filename(create=False,
check_only=True))
age_int_prop = property(fget=age_in_days)
@ -576,8 +576,8 @@ class PodcastEpisode(PodcastModelObject):
value is the canonical representation of this episode
in playlists (for example, M3U playlists).
"""
return '%s - %s (%s)' % (self.channel.title, \
self.title, \
return '%s - %s (%s)' % (self.channel.title,
self.title,
self.cute_pubdate())
def cute_pubdate(self):
@ -616,9 +616,9 @@ class PodcastEpisode(PodcastModelObject):
current position is greater than 99 percent of the
total time or inside the last 10 seconds of a track.
"""
return self.current_position > 0 and self.total_time > 0 and \
(self.current_position + 10 >= self.total_time or \
self.current_position >= self.total_time * .99)
return (self.current_position > 0 and self.total_time > 0 and
(self.current_position + 10 >= self.total_time or
self.current_position >= self.total_time * .99))
def get_play_info_string(self, duration_only=False):
duration = util.format_time(self.total_time)
@ -752,8 +752,8 @@ class PodcastChannel(PodcastModelObject):
known_files.add(filename)
existing_files = set(filename for filename in \
glob.glob(os.path.join(self.save_dir, '*')) \
existing_files = set(filename for filename in
glob.glob(os.path.join(self.save_dir, '*'))
if not filename.endswith('.partial'))
ignore_files = ['folder' + ext for ext in
@ -779,7 +779,7 @@ class PodcastChannel(PodcastModelObject):
continue
for episode in all_episodes:
wanted_filename = episode.local_filename(create=True, \
wanted_filename = episode.local_filename(create=True,
return_wanted_filename=True)
if basename == wanted_filename:
logger.info('Importing external download: %s', filename)
@ -1007,7 +1007,7 @@ class PodcastChannel(PodcastModelObject):
# max_episodes_per_feed items added to the feed between updates.
# The benefit is that it prevents old episodes from apearing as new
# in certain situations (see bug #340).
self.db.purge(max_episodes, self.id) # TODO: Remove from self.children!
self.db.purge(max_episodes, self.id) # TODO: Remove from self.children!
# Sort episodes by pubdate, descending
self.children.sort(key=lambda e: e.published, reverse=True)
@ -1034,18 +1034,18 @@ class PodcastChannel(PodcastModelObject):
self.save()
except Exception as e:
# "Not really" errors
#feedcore.AuthenticationRequired
# Temporary errors
#feedcore.Offline
#feedcore.BadRequest
#feedcore.InternalServerError
#feedcore.WifiLogin
# Permanent errors
#feedcore.Unsubscribe
#feedcore.NotFound
#feedcore.InvalidFeed
#feedcore.UnknownStatusCode
# "Not really" errors
# feedcore.AuthenticationRequired
# Temporary errors
# feedcore.Offline
# feedcore.BadRequest
# feedcore.InternalServerError
# feedcore.WifiLogin
# Permanent errors
# feedcore.Unsubscribe
# feedcore.NotFound
# feedcore.InvalidFeed
# feedcore.UnknownStatusCode
gpodder.user_extensions.on_podcast_update_failed(self, e)
raise

View File

@ -47,8 +47,8 @@ mygpoclient.user_agent += ' ' + gpodder.user_agent
# 2013-02-08: We should update this to 1.7 once we use the new features
MYGPOCLIENT_REQUIRED = '1.4'
if not hasattr(mygpoclient, 'require_version') or \
not mygpoclient.require_version(MYGPOCLIENT_REQUIRED):
if (not hasattr(mygpoclient, 'require_version')
or not mygpoclient.require_version(MYGPOCLIENT_REQUIRED)):
print("""
Please upgrade your mygpoclient library.
See http://thp.io/2010/mygpoclient/
@ -143,7 +143,7 @@ class EpisodeAction(object):
'action': str, 'timestamp': int,
'started': int, 'position': int, 'total': int}
def __init__(self, podcast_url, episode_url, device_id, \
def __init__(self, podcast_url, episode_url, device_id,
action, timestamp, started, position, total):
self.podcast_url = podcast_url
self.episode_url = episode_url
@ -212,8 +212,8 @@ class MygPoClient(object):
self._store.remove(self._store.load(UpdateDeviceAction))
# Insert our new update action
action = UpdateDeviceAction(self.device_id, \
self._config.mygpo.device.caption, \
action = UpdateDeviceAction(self.device_id,
self._config.mygpo.device.caption,
self._config.mygpo.device.type)
self._store.save(action)
@ -343,13 +343,13 @@ class MygPoClient(object):
raise Exception('Webservice access not enabled')
def _convert_played_episode(self, episode, start, end, total):
return EpisodeAction(episode.channel.url, \
episode.url, self.device_id, 'play', \
return EpisodeAction(episode.channel.url,
episode.url, self.device_id, 'play',
int(time.time()), start, end, total)
def _convert_episode(self, episode, action):
return EpisodeAction(episode.channel.url, \
episode.url, self.device_id, action, \
return EpisodeAction(episode.channel.url,
episode.url, self.device_id, action,
int(time.time()), None, None, None)
def on_delete(self, episodes):
@ -474,23 +474,23 @@ class MygPoClient(object):
def convert_to_api(action):
dt = datetime.datetime.utcfromtimestamp(action.timestamp)
action_ts = mygpoutil.datetime_to_iso8601(dt)
return api.EpisodeAction(action.podcast_url, \
action.episode_url, action.action, \
action.device_id, action_ts, \
return api.EpisodeAction(action.podcast_url,
action.episode_url, action.action,
action.device_id, action_ts,
action.started, action.position, action.total)
def convert_from_api(action):
dt = mygpoutil.iso8601_to_datetime(action.timestamp)
action_ts = calendar.timegm(dt.timetuple())
return ReceivedEpisodeAction(action.podcast, \
action.episode, action.device, \
action.action, action_ts, \
return ReceivedEpisodeAction(action.podcast,
action.episode, action.device,
action.action, action_ts,
action.started, action.position, action.total)
try:
# Load the "since" value from the database
since_o = self._store.get(SinceValue, host=self.host, \
device_id=self.device_id, \
since_o = self._store.get(SinceValue, host=self.host,
device_id=self.device_id,
category=SinceValue.EPISODES)
# Use a default since object for the first-time case
@ -547,8 +547,8 @@ class MygPoClient(object):
logger.debug('Starting subscription sync.')
try:
# Load the "since" value from the database
since_o = self._store.get(SinceValue, host=self.host, \
device_id=self.device_id, \
since_o = self._store.get(SinceValue, host=self.host,
device_id=self.device_id,
category=SinceValue.PODCASTS)
# Use a default since object for the first-time case
@ -611,7 +611,7 @@ class MygPoClient(object):
def update_device(self, action):
try:
logger.debug('Uploading device settings...')
self._client.update_device_settings(action.device_id, \
self._client.update_device_settings(action.device_id,
action.caption, action.device_type)
logger.debug('Device settings uploaded.')
return True
@ -646,8 +646,8 @@ class MygPoClient(object):
def get_download_user_subscriptions_url(self):
OPML_URL = self._client.locator.subscriptions_uri()
url = util.url_add_authentication(OPML_URL, \
self._config.mygpo.username, \
url = util.url_add_authentication(OPML_URL,
self._config.mygpo.username,
self._config.mygpo.password)
return url

View File

@ -50,8 +50,11 @@
#
import urllib.error
import urllib.parse
import urllib.request
import gpodder
import urllib.request, urllib.parse, urllib.error
class MediaPlayerDBusReceiver(object):
@ -63,15 +66,15 @@ class MediaPlayerDBusReceiver(object):
self.on_play_event = on_play_event
self.bus = gpodder.dbus_session_bus
self.bus.add_signal_receiver(self.on_playback_started, \
self.SIGNAL_STARTED, \
self.INTERFACE, \
None, \
self.bus.add_signal_receiver(self.on_playback_started,
self.SIGNAL_STARTED,
self.INTERFACE,
None,
None)
self.bus.add_signal_receiver(self.on_playback_stopped, \
self.SIGNAL_STOPPED, \
self.INTERFACE, \
None, \
self.bus.add_signal_receiver(self.on_playback_stopped,
self.SIGNAL_STOPPED,
self.INTERFACE,
None,
None)
def on_playback_started(self, position, file_uri):

View File

@ -21,22 +21,20 @@
# Soundcloud.com API client module for gPodder
# Thomas Perl <thp@gpodder.org>; 2009-11-03
import gpodder
_ = gpodder.gettext
from gpodder import model
from gpodder import util
import email
import json
import logging
import os
import time
import re
import email
import urllib.request, urllib.parse, urllib.error
import time
import urllib.error
import urllib.parse
import urllib.request
import gpodder
from gpodder import model, util
_ = gpodder.gettext
# gPodder's consumer key for the Soundcloud API

View File

@ -48,12 +48,12 @@ except:
gpod_available = False
logger.warning('Could not find gpod')
#pymtp_available = True
#try:
# import gpodder.gpopymtp as pymtp
#except:
# pymtp_available = False
# logger.warning('Could not load gpopymtp (libmtp not installed?).')
# pymtp_available = True
# try:
# import gpodder.gpopymtp as pymtp
# except:
# pymtp_available = False
# logger.warning('Could not load gpopymtp (libmtp not installed?).')
try:
import eyed3.mp3
@ -141,7 +141,8 @@ def get_track_length(filename):
except Exception as e:
logger.warn('Could not determine length: %s', filename, exc_info=True)
return int(60 * 60 * 1000 * 3) # Default is three hours (to be on the safe side)
return int(60 * 60 * 1000 * 3)
# Default is three hours (to be on the safe side)
class SyncTrack(object):
@ -507,7 +508,7 @@ class MP3PlayerDevice(Device):
download_queue_manager):
Device.__init__(self, config)
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_queue_manager = download_queue_manager
@ -741,7 +742,8 @@ class MTPDevice(Device):
return None
try:
mtp = mtp.replace(" ", "0") # replace blank with 0 to fix some invalid string
mtp = mtp.replace(" ", "0")
# replace blank with 0 to fix some invalid string
d = time.strptime(mtp[:8] + mtp[9:13],"%Y%m%d%H%M%S")
_date = calendar.timegm(d)
if len(mtp) == 20:
@ -784,7 +786,8 @@ class MTPDevice(Device):
if self.__MTPDevice is None:
return _('MTP device')
self.__model_name = self.__MTPDevice.get_devicename() # actually libmtp.Get_Friendlyname
self.__model_name = self.__MTPDevice.get_devicename()
# actually libmtp.Get_Friendlyname
if not self.__model_name or self.__model_name == "?????":
self.__model_name = self.__MTPDevice.get_modelname()
if not self.__model_name:
@ -903,8 +906,8 @@ class MTPDevice(Device):
age_in_days = 0
date = self.__mtp_to_date(track.date)
if not date:
modified = track.date # not a valid mtp date. Display what mtp gave anyway
modified_sort = -1 # no idea how to sort invalid date
modified = track.date # not a valid mtp date. Display what mtp gave anyway
modified_sort = -1 # no idea how to sort invalid date
else:
modified = util.format_date(date)
modified_sort = date

View File

@ -55,13 +55,13 @@ import gzip
import datetime
import threading
import urllib.parse
import urllib.request, urllib.parse, urllib.error
import urllib.request, urllib.error, urllib.parse
import http.client
import webbrowser
import mimetypes
import itertools
import urllib.error
import urllib.parse
import urllib.request
import io
import xml.dom.minidom
@ -452,8 +452,8 @@ def is_system_file(filename):
"""
if gpodder.ui.win32 and win32file is not None:
result = win32file.GetFileAttributes(filename)
#-1 / 0xffffffff is returned by GetFileAttributes when an error occurs
#0x4 is the FILE_ATTRIBUTE_SYSTEM constant
# -1 / 0xffffffff is returned by GetFileAttributes when an error occurs
# 0x4 is the FILE_ATTRIBUTE_SYSTEM constant
return result != -1 and result != 0xffffffff and result & 0x4 != 0
else:
return False
@ -1610,7 +1610,8 @@ def relpath(p1, p2):
Finds relative path from p1 to p2
Source: http://code.activestate.com/recipes/208993/
"""
pathsplit = lambda s: s.split(os.path.sep)
def pathsplit(s):
return s.split(os.path.sep)
(common,l1,l2) = commonpath(pathsplit(p1), pathsplit(p2))
p = []
@ -1779,7 +1780,9 @@ def get_update_info():
release_parsed = datetime.datetime.strptime(release_date, '%Y-%m-%dT%H:%M:%SZ')
days_since_release = (datetime.datetime.today() - release_parsed).days
convert = lambda s: tuple(int(x) for x in s.split('.'))
def convert(s):
return tuple(int(x) for x in s.split('.'))
up_to_date = (convert(gpodder.__version__) >= convert(latest_version))
return up_to_date, latest_version, release_date, days_since_release

View File

@ -276,10 +276,14 @@ def parse_youtube_url(url):
>>> parse_youtube_url("https://www.youtube.com/playlist?list=PLAYLIST_ID")
'https://www.youtube.com/feeds/videos.xml?playlist_id=PLAYLIST_ID'
>>> parse_youtube_url(None)
None
@param url: the path to the channel, user or playlist
@return: the feed url if successful or the given url if not
"""
if url is None:
return url
scheme, netloc, path, query, fragment = urllib.parse.urlsplit(url)
logger.debug("Analyzing URL: {}".format(" ".join([scheme, netloc, path, query, fragment])))

View File

@ -8,14 +8,16 @@
# Thomas Perl <thp.io/about>; 2012-02-11
#
import urllib.request, urllib.error, urllib.parse
import re
import sys
import io
import tarfile
import os
import re
import shutil
import sys
import tarfile
import tempfile
import urllib.error
import urllib.parse
import urllib.request
sys.stdout = sys.stderr

View File

@ -20,13 +20,13 @@ GUID = hashlib.sha1(open(__file__).read()).hexdigest()
URL = 'http://%(HOST)s:%(PORT)s' % locals()
FEEDNAME = sys.argv[0] # The title of the RSS feed
FEEDFILE = 'feed.rss' # The "filename" of the feed on the server
EPISODES = 'episode' # Base name for the episode files
EPISODES_EXT = '.mp3' # Extension for the episode files
EPISODES_MIME = 'audio/mpeg' # Mime type for the episode files
EP_COUNT = 7 # Number of episodes in the feed
SIZE = 500000 # Size (in bytes) of the episode downloads)
FEEDNAME = sys.argv[0] # The title of the RSS feed
FEEDFILE = 'feed.rss' # The "filename" of the feed on the server
EPISODES = 'episode' # Base name for the episode files
EPISODES_EXT = '.mp3' # Extension for the episode files
EPISODES_MIME = 'audio/mpeg' # Mime type for the episode files
EP_COUNT = 7 # Number of episodes in the feed
SIZE = 500000 # Size (in bytes) of the episode downloads)
def mkpubdates(items):