Merge branch 'master' into gtk3-win_installer
This commit is contained in:
commit
92dcb1c0c3
24
bin/gpo
24
bin/gpo
|
@ -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)
|
||||
|
|
577
po/cs_CZ.po
577
po/cs_CZ.po
File diff suppressed because it is too large
Load Diff
577
po/es_ES.po
577
po/es_ES.po
File diff suppressed because it is too large
Load Diff
577
po/es_MX.po
577
po/es_MX.po
File diff suppressed because it is too large
Load Diff
587
po/fa_IR.po
587
po/fa_IR.po
File diff suppressed because it is too large
Load Diff
585
po/id_ID.po
585
po/id_ID.po
File diff suppressed because it is too large
Load Diff
577
po/ko_KR.po
577
po/ko_KR.po
File diff suppressed because it is too large
Load Diff
585
po/messages.pot
585
po/messages.pot
File diff suppressed because it is too large
Load Diff
577
po/pt_BR.po
577
po/pt_BR.po
File diff suppressed because it is too large
Load Diff
585
po/tr_TR.po
585
po/tr_TR.po
File diff suppressed because it is too large
Load Diff
587
po/zh_CN.po
587
po/zh_CN.po
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ __category__ = 'post-download'
|
|||
|
||||
|
||||
DefaultConfig = {
|
||||
'context_menu': True, # Show item in context menu
|
||||
'context_menu': True, # Show item in context menu
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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': {
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)])
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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])))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue