Merge branch 'master' into dev-adaptive
This commit is contained in:
commit
5eee09ce49
|
@ -0,0 +1,32 @@
|
|||
name: lint and test
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
linttest:
|
||||
name: lint and unit tests
|
||||
if: >-
|
||||
github.event_name == 'push' ||
|
||||
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.9]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update -q
|
||||
sudo apt-get install intltool desktop-file-utils
|
||||
pip3 install pytest-cov minimock pycodestyle isort requests pytest pytest-httpserver
|
||||
python3 tools/localdepends.py
|
||||
- name: Lint
|
||||
run: make lint
|
||||
- name: Test
|
||||
run: make releasetest
|
13
.travis.yml
13
.travis.yml
|
@ -1,13 +0,0 @@
|
|||
language: python
|
||||
dist: focal
|
||||
sudo: required
|
||||
python:
|
||||
- "3.8"
|
||||
install:
|
||||
- sudo apt-get update -q
|
||||
- sudo apt-get install intltool desktop-file-utils
|
||||
- pip3 install pytest-cov minimock pycodestyle isort requests pytest pytest-httpserver
|
||||
- python3 tools/localdepends.py
|
||||
script:
|
||||
- make lint
|
||||
- make releasetest
|
|
@ -43,6 +43,7 @@ PyPI. With this, you get a self-contained gPodder CLI codebase.
|
|||
### GTK3 UI - Additional Dependencies
|
||||
|
||||
- [PyGObject](https://wiki.gnome.org/PyGObject) 3.22.0 or newer
|
||||
- [GTK+3](https://www.gtk.org/) 3.10 or newer
|
||||
|
||||
|
||||
### Optional Dependencies
|
||||
|
|
|
@ -24,6 +24,7 @@ __category__ = 'post-download'
|
|||
DefaultConfig = {
|
||||
'add_sortdate': False, # Add the sortdate as prefix
|
||||
'add_podcast_title': False, # Add the podcast title as prefix
|
||||
'sortdate_after_podcast_title': False, # put the sortdate after podcast title
|
||||
}
|
||||
|
||||
|
||||
|
@ -50,10 +51,16 @@ class gPodderExtension:
|
|||
|
||||
new_basename = []
|
||||
new_basename.append(title)
|
||||
if self.config.add_podcast_title:
|
||||
new_basename.insert(0, podcast_title)
|
||||
if self.config.add_sortdate:
|
||||
new_basename.insert(0, sortdate)
|
||||
if self.config.sortdate_after_podcast_title:
|
||||
if self.config.add_sortdate:
|
||||
new_basename.insert(0, sortdate)
|
||||
if self.config.add_podcast_title:
|
||||
new_basename.insert(0, podcast_title)
|
||||
else:
|
||||
if self.config.add_podcast_title:
|
||||
new_basename.insert(0, podcast_title)
|
||||
if self.config.add_sortdate:
|
||||
new_basename.insert(0, sortdate)
|
||||
new_basename = ' - '.join(new_basename)
|
||||
|
||||
# Remove unwanted characters and shorten filename (#494)
|
||||
|
|
|
@ -15,7 +15,8 @@ from youtube_dl.utils import DownloadError, ExtractorError, sanitize_url
|
|||
|
||||
import gpodder
|
||||
from gpodder import download, feedcore, model, registry, youtube
|
||||
from gpodder.util import mimetype_from_extension, remove_html_tags
|
||||
from gpodder.util import (mimetype_from_extension, nice_html_description,
|
||||
remove_html_tags)
|
||||
|
||||
_ = gpodder.gettext
|
||||
|
||||
|
@ -213,7 +214,7 @@ class YoutubeFeed(model.Feed):
|
|||
for en in self._ie_result['entries']:
|
||||
guid = video_guid(en['id'])
|
||||
description = remove_html_tags(en.get('description') or _('No description available'))
|
||||
html_description = self.nice_html_description(en, description)
|
||||
html_description = nice_html_description(en.get('thumbnail'), description)
|
||||
if en.get('ext'):
|
||||
mime_type = mimetype_from_extension('.{}'.format(en['ext']))
|
||||
else:
|
||||
|
@ -250,25 +251,6 @@ class YoutubeFeed(model.Feed):
|
|||
"""
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def nice_html_description(en, description):
|
||||
"""
|
||||
basic html formating + hyperlink highlighting + video thumbnail
|
||||
"""
|
||||
description = re.sub(r'''https?://[^\s]+''',
|
||||
r'''<a href="\g<0>">\g<0></a>''',
|
||||
description)
|
||||
description = description.replace('\n', '<br>')
|
||||
html = """<style type="text/css">
|
||||
body > img { float: left; max-width: 30vw; margin: 0 1em 1em 0; }
|
||||
</style>
|
||||
"""
|
||||
img = en.get('thumbnail')
|
||||
if img:
|
||||
html += '<img src="{}">'.format(img)
|
||||
html += '<p>{}</p>'.format(description)
|
||||
return html
|
||||
|
||||
|
||||
class gPodderYoutubeDL(download.CustomDownloader):
|
||||
def __init__(self, gpodder_config, my_config, force=False):
|
||||
|
|
|
@ -209,6 +209,10 @@
|
|||
<attribute name="action">win.viewAlwaysShowNewEpisodes</attribute>
|
||||
<attribute name="label" translatable="yes">Always show New Episodes</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="action">win.viewCtrlClickToSortEpisodes</attribute>
|
||||
<attribute name="label" translatable="yes">Require control click to sort episodes</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<submenu id="menuViewColumns">
|
||||
<attribute name="label" translatable="yes">Visible columns</attribute>
|
||||
|
|
|
@ -26,7 +26,10 @@ __copyright__ = '© 2005-2021 The gPodder Team'
|
|||
__license__ = 'GNU General Public License, version 3 or later'
|
||||
__url__ = 'http://gpodder.org/'
|
||||
|
||||
# __version_info__ = tuple(int(x) for x in __version__.split('.'))
|
||||
# Use public version part for __version_info__, see PEP 440
|
||||
__public_version__, __local_version__ = next(
|
||||
(v[0], v[1] if len(v) > 1 else '') for v in (__version__.split('+'),))
|
||||
__version_info__ = tuple(int(x) for x in __public_version__.split('.'))
|
||||
|
||||
import gettext
|
||||
import locale
|
||||
|
|
|
@ -169,6 +169,7 @@ defaults = {
|
|||
'view_mode': 1,
|
||||
'columns': int('110', 2), # bitfield of visible columns
|
||||
'always_show_new': True,
|
||||
'ctrl_click_to_sort': False,
|
||||
},
|
||||
|
||||
'download_list': {
|
||||
|
|
|
@ -24,9 +24,10 @@ import gpodder
|
|||
from gpodder import util
|
||||
from gpodder.sync import (episode_filename_on_device,
|
||||
episode_foldername_on_device)
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import GLib, Gio
|
||||
|
||||
import gi # isort:skip
|
||||
gi.require_version('Gtk', '3.0') # isort:skip
|
||||
from gi.repository import Gio, GLib # isort:skip
|
||||
|
||||
_ = gpodder.gettext
|
||||
|
||||
|
|
|
@ -435,8 +435,7 @@ class DownloadQueueManager(object):
|
|||
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)
|
||||
spawn_limit = max(int(self._config.max_downloads), 1)
|
||||
else:
|
||||
spawn_limit = self._config.limit.downloads.concurrent_max
|
||||
running = len(self.worker_threads)
|
||||
|
@ -538,9 +537,9 @@ class DownloadTask(object):
|
|||
The same thing works for failed downloads ("notify_as_failed()").
|
||||
"""
|
||||
# Possible states this download task can be in
|
||||
STATUS_MESSAGE = (_('Added'), _('Queued'), _('Downloading'),
|
||||
STATUS_MESSAGE = (_('Queued'), _('Downloading'),
|
||||
_('Finished'), _('Failed'), _('Cancelled'), _('Paused'))
|
||||
(INIT, QUEUED, DOWNLOADING, DONE, FAILED, CANCELLED, PAUSED) = list(range(7))
|
||||
(QUEUED, DOWNLOADING, DONE, FAILED, CANCELLED, PAUSED) = list(range(6))
|
||||
|
||||
# Wheter this task represents a file download or a device sync operation
|
||||
ACTIVITY_DOWNLOAD, ACTIVITY_SYNCHRONIZE = list(range(2))
|
||||
|
@ -612,7 +611,7 @@ class DownloadTask(object):
|
|||
|
||||
def __init__(self, episode, config, downloader=None):
|
||||
assert episode.download_task is None
|
||||
self.__status = DownloadTask.INIT
|
||||
self.__status = DownloadTask.QUEUED
|
||||
self.__activity = DownloadTask.ACTIVITY_DOWNLOAD
|
||||
self.__status_changed = True
|
||||
self.__episode = episode
|
||||
|
|
|
@ -26,7 +26,8 @@ from gi.repository import Gdk, Gtk, Pango
|
|||
import gpodder
|
||||
from gpodder import util, vimeo, youtube
|
||||
from gpodder.gtkui.desktopfile import PlayerListModel
|
||||
from gpodder.gtkui.interface.common import BuilderWidget, TreeViewHelper, show_message_dialog
|
||||
from gpodder.gtkui.interface.common import (BuilderWidget, TreeViewHelper,
|
||||
show_message_dialog)
|
||||
from gpodder.gtkui.interface.configeditor import gPodderConfigEditor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
|
@ -34,6 +34,7 @@ from gpodder import download, util
|
|||
|
||||
_ = gpodder.gettext
|
||||
|
||||
|
||||
class TaskQueue:
|
||||
def __init__(self):
|
||||
self.lock = threading.Lock()
|
||||
|
@ -81,6 +82,7 @@ class TaskQueue:
|
|||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
class DownloadStatusModel:
|
||||
# Symbolic names for our columns, so we know what we're up to
|
||||
C_TASK, C_NAME, C_URL, C_PROGRESS, C_PROGRESS_TEXT, C_ICON_NAME = list(range(6))
|
||||
|
@ -220,6 +222,7 @@ class DownloadStatusModel:
|
|||
task.status = task.DOWNLOADING
|
||||
return True
|
||||
|
||||
|
||||
class DownloadTaskMonitor(object):
|
||||
"""A helper class that abstracts download events"""
|
||||
def __init__(self, episode, on_can_resume, on_can_pause, on_finished):
|
||||
|
|
|
@ -169,7 +169,8 @@ def draw_cake(percentage, text=None, emblem=None, size=None):
|
|||
return surface
|
||||
|
||||
|
||||
def draw_text_pill(left_text, right_text, x=0, y=0, border=2, radius=14, widget=None):
|
||||
def draw_text_pill(left_text, right_text, x=0, y=0, border=2, radius=14,
|
||||
widget=None, scale=1):
|
||||
|
||||
# Padding (in px) at the right edge of the image (for Ubuntu; bug 1533)
|
||||
padding_right = 7
|
||||
|
@ -200,10 +201,13 @@ def draw_text_pill(left_text, right_text, x=0, y=0, border=2, radius=14, widget=
|
|||
left_side_width = width_left + x_border * 2
|
||||
right_side_width = width_right + x_border * 2
|
||||
|
||||
image_height = int(y + text_height + border * 2)
|
||||
image_width = int(x + left_side_width + right_side_width + padding_right)
|
||||
image_height = int(scale * (y + text_height + border * 2))
|
||||
image_width = int(scale * (x + left_side_width + right_side_width
|
||||
+ padding_right))
|
||||
|
||||
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, image_width, image_height)
|
||||
surface.set_device_scale(scale, scale)
|
||||
|
||||
ctx = cairo.Context(surface)
|
||||
|
||||
# Clip so as to not draw on the right padding (for Ubuntu; bug 1533)
|
||||
|
@ -288,8 +292,9 @@ def draw_cake_pixbuf(percentage, text=None, emblem=None, size=None):
|
|||
return cairo_surface_to_pixbuf(draw_cake(percentage, text, emblem, size=size))
|
||||
|
||||
|
||||
def draw_pill_pixbuf(left_text, right_text, widget=None):
|
||||
return cairo_surface_to_pixbuf(draw_text_pill(left_text, right_text, widget=widget))
|
||||
def draw_pill_pixbuf(left_text, right_text, widget=None, scale=1):
|
||||
return cairo_surface_to_pixbuf(draw_text_pill(left_text, right_text,
|
||||
widget=widget, scale=scale))
|
||||
|
||||
|
||||
def cake_size_from_widget(widget=None):
|
||||
|
@ -479,3 +484,33 @@ def investigate_widget_colors(type_classes_and_widgets):
|
|||
|
||||
f.write("</dl></td></tr>\n")
|
||||
f.write("</table></html>\n")
|
||||
|
||||
|
||||
def draw_iconcell_scale(column, cell, model, iter, scale):
|
||||
"""
|
||||
Draw cell's pixbuf to a surface with proper scaling for high resolution
|
||||
displays. To be used as gtk.TreeViewColumn.set_cell_data_func.
|
||||
|
||||
:param column: gtk.TreeViewColumn (ignored)
|
||||
:param cell: gtk.CellRenderer
|
||||
:param model: gtk.TreeModel (ignored)
|
||||
:param iter: gtk.TreeIter (ignored)
|
||||
:param scale: factor of the target display (e.g. 1 or 2)
|
||||
"""
|
||||
pixbuf = cell.props.pixbuf
|
||||
if not pixbuf:
|
||||
return
|
||||
|
||||
width = pixbuf.get_width()
|
||||
height = pixbuf.get_height()
|
||||
scale_inv = 1 / scale
|
||||
|
||||
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
|
||||
surface.set_device_scale(scale, scale)
|
||||
|
||||
cr = cairo.Context(surface)
|
||||
cr.scale(scale_inv, scale_inv)
|
||||
Gdk.cairo_set_source_pixbuf(cr, cell.props.pixbuf, 0, 0)
|
||||
cr.paint()
|
||||
|
||||
cell.props.surface = surface
|
||||
|
|
|
@ -48,7 +48,7 @@ from .desktop.welcome import gPodderWelcome
|
|||
from .desktopfile import UserAppsReader
|
||||
from .download import DownloadStatusModel
|
||||
from .draw import (cake_size_from_widget, draw_cake_pixbuf,
|
||||
draw_text_box_centered)
|
||||
draw_iconcell_scale, draw_text_box_centered)
|
||||
from .interface.addpodcast import gPodderAddPodcast
|
||||
from .interface.common import (BuilderWidget, TreeViewHelper,
|
||||
ExtensionMenuHelper, Dummy)
|
||||
|
@ -332,6 +332,11 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
action.connect('activate', self.on_item_view_always_show_new_episodes_toggled)
|
||||
g.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new_stateful(
|
||||
'viewCtrlClickToSortEpisodes', None, GLib.Variant.new_boolean(self.config.ui.gtk.episode_list.ctrl_click_to_sort))
|
||||
action.connect('activate', self.on_item_view_ctrl_click_to_sort_episodes_toggled)
|
||||
g.add_action(action)
|
||||
|
||||
action = Gio.SimpleAction.new_stateful(
|
||||
'searchAlwaysVisible', None, GLib.Variant.new_boolean(self.config.ui.gtk.search_always_visible))
|
||||
action.connect('activate', self.on_item_view_search_always_visible_toggled)
|
||||
|
@ -789,7 +794,8 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
|
||||
def init_podcast_list_treeview(self):
|
||||
size = cake_size_from_widget(self.treeChannels) * 2
|
||||
self.podcast_list_model.set_max_image_size(size)
|
||||
scale = self.treeChannels.get_scale_factor()
|
||||
self.podcast_list_model.set_max_image_size(size, scale)
|
||||
# Set up podcast channel tree view widget
|
||||
column = Gtk.TreeViewColumn('')
|
||||
iconcell = Gtk.CellRendererPixbuf()
|
||||
|
@ -797,6 +803,8 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
column.pack_start(iconcell, False)
|
||||
column.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_COVER)
|
||||
column.add_attribute(iconcell, 'visible', PodcastListModel.C_COVER_VISIBLE)
|
||||
if scale != 1:
|
||||
column.set_cell_data_func(iconcell, draw_iconcell_scale, scale)
|
||||
|
||||
namecell = Gtk.CellRendererText()
|
||||
namecell.set_property('ellipsize', Pango.EllipsizeMode.END)
|
||||
|
@ -808,6 +816,8 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
column.pack_start(iconcell, False)
|
||||
column.add_attribute(iconcell, 'pixbuf', PodcastListModel.C_PILL)
|
||||
column.add_attribute(iconcell, 'visible', PodcastListModel.C_PILL_VISIBLE)
|
||||
if scale != 1:
|
||||
column.set_cell_data_func(iconcell, draw_iconcell_scale, scale)
|
||||
|
||||
self.treeChannels.append_column(column)
|
||||
|
||||
|
@ -995,11 +1005,13 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
return True
|
||||
|
||||
def on_episode_list_header_clicked(self, button, event):
|
||||
if event.button != 3:
|
||||
return False
|
||||
|
||||
if self.episode_columns_menu is not None:
|
||||
self.episode_columns_menu.popup(None, None, None, None, event.button, event.time)
|
||||
if event.button == 1:
|
||||
# Require control click to sort episodes, when enabled
|
||||
if self.config.ui.gtk.episode_list.ctrl_click_to_sort and (event.state & Gdk.ModifierType.CONTROL_MASK) == 0:
|
||||
return True
|
||||
elif event.button == 3:
|
||||
if self.episode_columns_menu is not None:
|
||||
self.episode_columns_menu.popup(None, None, None, None, event.button, event.time)
|
||||
|
||||
return False
|
||||
|
||||
|
@ -1514,8 +1526,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
title.append(N_('%(queued)d task queued',
|
||||
'%(queued)d tasks queued',
|
||||
queued) % {'queued': queued})
|
||||
if ((downloading + synchronizing + queued) == 0 and
|
||||
self.things_adding_tasks == 0):
|
||||
if (downloading + synchronizing + queued) == 0 and self.things_adding_tasks == 0:
|
||||
self.set_download_progress(1.)
|
||||
self.downloads_finished(self.download_tasks_seen)
|
||||
gpodder.user_extensions.on_all_episodes_downloaded()
|
||||
|
@ -3482,6 +3493,11 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
self.config.ui.gtk.episode_list.always_show_new = not state
|
||||
action.set_state(GLib.Variant.new_boolean(not state))
|
||||
|
||||
def on_item_view_ctrl_click_to_sort_episodes_toggled(self, action, param):
|
||||
state = action.get_state()
|
||||
self.config.ui.gtk.episode_list.ctrl_click_to_sort = not state
|
||||
action.set_state(GLib.Variant.new_boolean(not state))
|
||||
|
||||
def on_item_view_search_always_visible_toggled(self, action, param):
|
||||
state = action.get_state()
|
||||
self.config.ui.gtk.search_always_visible = not state
|
||||
|
@ -4128,7 +4144,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
file.mount_enclosing_volume_finish(res)
|
||||
except GLib.Error as err:
|
||||
if (not err.matches(Gio.io_error_quark(), Gio.IOErrorEnum.NOT_SUPPORTED) and
|
||||
not err.matches(Gio.io_error_quark(), Gio.IOErrorEnum.ALREADY_MOUNTED)):
|
||||
not err.matches(Gio.io_error_quark(), Gio.IOErrorEnum.ALREADY_MOUNTED)):
|
||||
logger.error('mounting volume %s failed: %s' % (file.get_uri(), err.message))
|
||||
result = False
|
||||
finally:
|
||||
|
|
|
@ -587,6 +587,7 @@ class PodcastListModel(Gtk.ListStore):
|
|||
|
||||
self._cover_cache = {}
|
||||
self._max_image_side = 40
|
||||
self._scale = 1
|
||||
self._cover_downloader = cover_downloader
|
||||
|
||||
self.ICON_DISABLED = 'media-playback-pause'
|
||||
|
@ -655,8 +656,9 @@ class PodcastListModel(Gtk.ListStore):
|
|||
def get_search_term(self):
|
||||
return self._search_term
|
||||
|
||||
def set_max_image_size(self, size):
|
||||
self._max_image_side = size
|
||||
def set_max_image_size(self, size, scale):
|
||||
self._max_image_side = size * scale
|
||||
self._scale = scale
|
||||
self._cover_cache = {}
|
||||
|
||||
def _resize_pixbuf_keep_ratio(self, url, pixbuf):
|
||||
|
@ -665,31 +667,27 @@ class PodcastListModel(Gtk.ListStore):
|
|||
Returns None if the pixbuf does not need to be
|
||||
resized or the newly resized pixbuf if it does.
|
||||
"""
|
||||
changed = False
|
||||
result = None
|
||||
|
||||
if url in self._cover_cache:
|
||||
return self._cover_cache[url]
|
||||
|
||||
# Resize if too wide
|
||||
if pixbuf.get_width() > self._max_image_side:
|
||||
f = float(self._max_image_side) / pixbuf.get_width()
|
||||
(width, height) = (int(pixbuf.get_width() * f), int(pixbuf.get_height() * f))
|
||||
pixbuf = pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR)
|
||||
changed = True
|
||||
max_side = self._max_image_side
|
||||
w_cur = pixbuf.get_width()
|
||||
h_cur = pixbuf.get_height()
|
||||
|
||||
# Resize if too high
|
||||
if pixbuf.get_height() > self._max_image_side:
|
||||
f = float(self._max_image_side) / pixbuf.get_height()
|
||||
(width, height) = (int(pixbuf.get_width() * f), int(pixbuf.get_height() * f))
|
||||
pixbuf = pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR)
|
||||
changed = True
|
||||
if w_cur <= max_side and h_cur <= max_side:
|
||||
return None
|
||||
|
||||
if changed:
|
||||
self._cover_cache[url] = pixbuf
|
||||
result = pixbuf
|
||||
f = max_side / (w_cur if w_cur >= h_cur else h_cur)
|
||||
w_new = int(w_cur * f)
|
||||
h_new = int(h_cur * f)
|
||||
|
||||
return result
|
||||
logger.debug("Scaling cover image: url=%s from %ix%i to %ix%i",
|
||||
url, w_cur, h_cur, w_new, h_new)
|
||||
pixbuf = pixbuf.scale_simple(w_new, h_new,
|
||||
GdkPixbuf.InterpType.BILINEAR)
|
||||
|
||||
self._cover_cache[url] = pixbuf
|
||||
return pixbuf
|
||||
|
||||
def _resize_pixbuf(self, url, pixbuf):
|
||||
if pixbuf is None:
|
||||
|
@ -728,6 +726,7 @@ class PodcastListModel(Gtk.ListStore):
|
|||
if self._max_image_side not in (pixbuf.get_width(), pixbuf.get_height()):
|
||||
logger.debug("cached thumb wrong size: %r != %i", (pixbuf.get_width(), pixbuf.get_height()), self._max_image_side)
|
||||
return None
|
||||
return pixbuf
|
||||
except Exception as e:
|
||||
logger.warn('Could not load cached cover art for %s', channel.url, exc_info=True)
|
||||
channel.cover_thumb = None
|
||||
|
@ -774,7 +773,10 @@ class PodcastListModel(Gtk.ListStore):
|
|||
|
||||
def _get_pill_image(self, channel, count_downloaded, count_unplayed):
|
||||
if count_unplayed > 0 or count_downloaded > 0:
|
||||
return draw.draw_pill_pixbuf('{:n}'.format(count_unplayed), '{:n}'.format(count_downloaded), widget=self.widget)
|
||||
return draw.draw_pill_pixbuf('{:n}'.format(count_unplayed),
|
||||
'{:n}'.format(count_downloaded),
|
||||
widget=self.widget,
|
||||
scale=self._scale)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
|
|
@ -283,6 +283,10 @@ class PodcastEpisode(PodcastModelObject):
|
|||
episode.description = entry['description']
|
||||
if entry.get('description_html'):
|
||||
episode.description_html = entry['description_html']
|
||||
else:
|
||||
thumbnail = entry.get('episode_art_url')
|
||||
description = util.remove_html_tags(episode.description or _('No description available'))
|
||||
episode.description_html = util.nice_html_description(thumbnail, description)
|
||||
|
||||
episode.total_time = entry['total_time']
|
||||
episode.published = entry['published']
|
||||
|
|
|
@ -32,6 +32,7 @@ from urllib.parse import urlparse
|
|||
|
||||
import gpodder
|
||||
from gpodder import download, services, util
|
||||
|
||||
import gi # isort:skip
|
||||
gi.require_version('Gtk', '3.0') # isort:skip
|
||||
from gi.repository import GLib, Gio, Gtk # isort:skip
|
||||
|
@ -503,6 +504,7 @@ class iPodDevice(Device):
|
|||
except:
|
||||
logger.warning('Seems like your python-gpod is out-of-date.')
|
||||
|
||||
|
||||
class MP3PlayerDevice(Device):
|
||||
def __init__(self, config,
|
||||
download_status_model,
|
||||
|
@ -597,7 +599,8 @@ class MP3PlayerDevice(Device):
|
|||
to_file.get_uri())
|
||||
from_file = Gio.File.new_for_path(from_file)
|
||||
try:
|
||||
hookconvert = lambda current_bytes, total_bytes, user_data : reporthook(current_bytes, 1, total_bytes)
|
||||
def hookconvert(current_bytes, total_bytes, user_data):
|
||||
return reporthook(current_bytes, 1, total_bytes)
|
||||
from_file.copy(to_file, Gio.FileCopyFlags.OVERWRITE, None, hookconvert, None)
|
||||
except GLib.Error as err:
|
||||
logger.error('Error copying %s to %s: %s', from_file.get_uri(), to_file.get_uri(), err.message)
|
||||
|
@ -691,9 +694,9 @@ class SyncTask(download.DownloadTask):
|
|||
# An object representing the synchronization task of an episode
|
||||
|
||||
# Possible states this sync task can be in
|
||||
STATUS_MESSAGE = (_('Added'), _('Queued'), _('Synchronizing'),
|
||||
STATUS_MESSAGE = (_('Queued'), _('Synchronizing'),
|
||||
_('Finished'), _('Failed'), _('Cancelled'), _('Paused'))
|
||||
(INIT, QUEUED, DOWNLOADING, DONE, FAILED, CANCELLED, PAUSED) = list(range(7))
|
||||
(QUEUED, DOWNLOADING, DONE, FAILED, CANCELLED, PAUSED) = list(range(6))
|
||||
|
||||
def __str__(self):
|
||||
return self.__episode.title
|
||||
|
@ -753,7 +756,7 @@ class SyncTask(download.DownloadTask):
|
|||
pass
|
||||
|
||||
def __init__(self, episode):
|
||||
self.__status = SyncTask.INIT
|
||||
self.__status = SyncTask.QUEUED
|
||||
self.__activity = SyncTask.ACTIVITY_SYNCHRONIZE
|
||||
self.__status_changed = True
|
||||
self.__episode = episode
|
||||
|
|
|
@ -218,7 +218,7 @@ class gPodderSyncUI(object):
|
|||
message = _('The playlist on your MP3 player has been updated.')
|
||||
self.notification(message, title)
|
||||
|
||||
# called from the main thread to complete adding tasks_
|
||||
# called from the main thread to complete adding tasks
|
||||
def add_downloads_complete():
|
||||
self.set_download_list_state(gPodderSyncUI.DL_ADDED_TASKS)
|
||||
|
||||
|
@ -252,8 +252,7 @@ class gPodderSyncUI(object):
|
|||
# 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 playlist.mountpoint.resolve_relative_path(
|
||||
episode_filename).query_exists():
|
||||
if not playlist.mountpoint.resolve_relative_path(episode_filename).query_exists():
|
||||
# episode was synced but no longer on device
|
||||
# i.e. must have been deleted by user, so delete from gpodder
|
||||
try:
|
||||
|
|
|
@ -61,10 +61,6 @@ import xml.dom.minidom
|
|||
from html.entities import entitydefs, name2codepoint
|
||||
from html.parser import HTMLParser
|
||||
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0') # isort:skip
|
||||
from gi.repository import Gio, GLib, Gtk # isort:skip
|
||||
|
||||
import requests
|
||||
import requests.exceptions
|
||||
from requests.packages.urllib3.util.retry import Retry
|
||||
|
@ -152,6 +148,7 @@ _MIME_TYPE_LIST = [
|
|||
('.wmv', 'video/x-ms-wmv'),
|
||||
('.opus', 'audio/opus'),
|
||||
('.webm', 'video/webm'),
|
||||
('.webm', 'audio/webm'),
|
||||
]
|
||||
|
||||
_MIME_TYPES = dict((k, v) for v, k in _MIME_TYPE_LIST)
|
||||
|
@ -173,6 +170,8 @@ def new_gio_file(path):
|
|||
"""
|
||||
Create a new Gio.File given a path or uri
|
||||
"""
|
||||
from gi.repository import Gio
|
||||
|
||||
if is_absolute_url(path):
|
||||
return Gio.File.new_for_uri(path)
|
||||
else:
|
||||
|
@ -185,6 +184,8 @@ def make_directory(path):
|
|||
Returns True if the directory exists after the function
|
||||
call, False otherwise.
|
||||
"""
|
||||
from gi.repository import Gio, GLib
|
||||
|
||||
if not isinstance(path, Gio.File):
|
||||
path = new_gio_file(path)
|
||||
|
||||
|
@ -832,6 +833,24 @@ def extract_hyperlinked_text(html):
|
|||
return ExtractHyperlinkedTextHTMLParser()(html)
|
||||
|
||||
|
||||
def nice_html_description(img, description):
|
||||
"""
|
||||
basic html formating + hyperlink highlighting + video thumbnail
|
||||
"""
|
||||
description = re.sub(r'''https?://[^\s]+''',
|
||||
r'''<a href="\g<0>">\g<0></a>''',
|
||||
description)
|
||||
description = description.replace('\n', '<br>')
|
||||
html = """<style type="text/css">
|
||||
body > img { float: left; max-width: 30vw; margin: 0 1em 1em 0; }
|
||||
</style>
|
||||
"""
|
||||
if img:
|
||||
html += '<img src="{}">'.format(img)
|
||||
html += '<p>{}</p>'.format(description)
|
||||
return html
|
||||
|
||||
|
||||
def wrong_extension(extension):
|
||||
"""
|
||||
Determine if a given extension looks like it's
|
||||
|
@ -1835,7 +1854,11 @@ def get_update_info():
|
|||
days_since_release = (datetime.datetime.today() - release_parsed).days
|
||||
|
||||
def convert(s):
|
||||
return tuple(int(x) for x in s.split('.'))
|
||||
# Use both public and local version label, see PEP 440
|
||||
pubv, locv = next(
|
||||
(v[0], v[1] if len(v) > 1 else '') for v in (s.split('+'),))
|
||||
return tuple(int(x) if x.isdigit() else x.lower()
|
||||
for x in pubv.split('.') + (locv.split('.') if locv else []))
|
||||
|
||||
up_to_date = (convert(gpodder.__version__) >= convert(latest_version))
|
||||
|
||||
|
@ -2241,11 +2264,15 @@ def response_text(response, default_encoding='utf-8'):
|
|||
return response.content.decode(default_encoding)
|
||||
|
||||
|
||||
def mount_volume_for_file(file, op = None):
|
||||
def mount_volume_for_file(file, op=None):
|
||||
"""
|
||||
Utility method to mount the enclosing volume for the given file in a blocking
|
||||
fashion
|
||||
"""
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gio, GLib, Gtk
|
||||
|
||||
result = True
|
||||
message = None
|
||||
|
||||
|
@ -2256,7 +2283,7 @@ def mount_volume_for_file(file, op = None):
|
|||
result = True
|
||||
except GLib.Error as err:
|
||||
if (not err.matches(Gio.io_error_quark(), Gio.IOErrorEnum.NOT_SUPPORTED) and
|
||||
not err.matches(Gio.io_error_quark(), Gio.IOErrorEnum.ALREADY_MOUNTED)):
|
||||
not err.matches(Gio.io_error_quark(), Gio.IOErrorEnum.ALREADY_MOUNTED)):
|
||||
message = err.message
|
||||
result = False
|
||||
finally:
|
||||
|
|
Loading…
Reference in New Issue