Merge tag 'merge5' into dev-adaptive

This commit is contained in:
Teemu Ikonen 2022-02-04 12:22:51 +02:00
commit 4453316aa8
19 changed files with 1081 additions and 709 deletions

View File

@ -43,7 +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
- [GTK+3](https://www.gtk.org/) 3.16 or newer
### Optional Dependencies

View File

@ -623,6 +623,7 @@ class gPodderCli(object):
task.add_progress_callback(self._update_action)
task.status = download.DownloadTask.DOWNLOADING
task.run()
task.recycle()
def _download_episodes(self, episodes):
if self._config.downloads.chronological_order:
@ -954,6 +955,7 @@ class gPodderCli(object):
task.status = sync.SyncTask.DOWNLOADING
task.add_progress_callback(progress_updated)
task.run()
task.recycle()
done_lock = threading.Lock()
self.mygpo_client = my.MygPoClient(self._config)

View File

@ -1,92 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<!--*- mode: xml -*-->
<interface>
<requires lib="gtk+" version="3.16"/>
<object class="GtkDialog" id="gPodderAddPodcast">
<property name="can-focus">False</property>
<property name="title" translatable="yes">Add a new podcast</property>
<property name="type_hint">dialog</property>
<property name="modal">True</property>
<property name="transient-for">parent_widget</property>
<property name="default_width">400</property>
<property name="default-width">400</property>
<property name="type-hint">dialog</property>
<child internal-child="vbox">
<object class="GtkBox" id="vboxmain">
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child internal-child="action_area">
<object class="GtkHButtonBox" id="hbuttonbox">
<property name="layout_style">GTK_BUTTONBOX_END</property>
<object class="GtkButtonBox" id="hbuttonbox">
<property name="can-focus">False</property>
<property name="layout-style">end</property>
<child>
<object class="GtkButton" id="btn_close">
<property name="label" translatable="yes">_Cancel</property>
<property name="visible">True</property>
<property name="label">gtk-cancel</property>
<property name="use_stock">True</property>
<signal handler="on_btn_close_clicked" name="clicked"/>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="use-underline">True</property>
<signal name="clicked" handler="on_btn_close_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="btn_add">
<property name="label" translatable="yes">_Add</property>
<property name="visible">True</property>
<property name="label">gtk-add</property>
<property name="sensitive">false</property>
<property name="use_stock">True</property>
<signal handler="on_btn_add_clicked" name="clicked"/>
<property name="sensitive">False</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="use-underline">True</property>
<signal name="clicked" handler="on_btn_add_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="padding">0</property>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">GTK_PACK_END</property>
<property name="pack-type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="hboxurlentry">
<property name="border_width">10</property>
<property name="visible">True</property>
<property name="homogeneous">False</property>
<property name="can-focus">False</property>
<property name="border-width">10</property>
<property name="spacing">5</property>
<property name="orientation">horizontal</property>
<child>
<object class="GtkLabel" id="label_add">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">URL:</property>
</object>
<packing>
<property name="padding">0</property>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="entry_url">
<property name="visible">True</property>
<property name="has_focus">True</property>
<property name="activates_default">True</property>
<signal handler="on_entry_url_changed" name="changed"/>
<property name="can-focus">True</property>
<property name="has-focus">True</property>
<property name="activates-default">True</property>
<signal name="changed" handler="on_entry_url_changed" swapped="no"/>
</object>
<packing>
<property name="padding">0</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="btn_paste">
<property name="label">gtk-paste</property>
<property name="use_stock">True</property>
<property name="label" translatable="yes">_Paste</property>
<property name="visible">True</property>
<signal handler="on_btn_paste_clicked" name="clicked"/>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="use-underline">True</property>
<signal name="clicked" handler="on_btn_paste_clicked" swapped="no"/>
</object>
<packing>
<property name="padding">0</property>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="padding">0</property>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<requires lib="gtk+" version="3.16"/>
<object class="GtkDialog" id="gPodderChannel">
<property name="can-focus">False</property>
<property name="title" translatable="yes">Channel Editor</property>

View File

@ -1,153 +1,136 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<!--*- mode: xml -*-->
<interface>
<requires lib="gtk+" version="3.16"/>
<object class="GtkDialog" id="gPodderConfigEditor">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">gPodder Configuration Editor</property>
<property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
<property name="modal">True</property>
<property name="transient-for">parent_widget</property>
<property name="default_width">750</property>
<property name="default_height">450</property>
<property name="destroy_with_parent">False</property>
<property name="skip_taskbar_hint">False</property>
<property name="skip_pager_hint">False</property>
<property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
<property name="focus_on_map">True</property>
<property name="urgency_hint">False</property>
<signal handler="on_gPodderConfigEditor_destroy" name="destroy"/>
<property name="window-position">center-on-parent</property>
<property name="default-width">750</property>
<property name="default-height">450</property>
<property name="type-hint">dialog</property>
<signal name="destroy" handler="on_gPodderConfigEditor_destroy" swapped="no"/>
<child internal-child="vbox">
<object class="GtkBox" id="vbox13">
<property name="visible">True</property>
<property name="homogeneous">False</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="vbox_for_episode_selector">
<property name="border_width">5</property>
<property name="visible">True</property>
<property name="spacing">5</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="hbox38">
<property name="visible">True</property>
<property name="homogeneous">False</property>
<property name="spacing">6</property>
<property name="orientation">horizontal</property>
<child>
<object class="GtkLabel" id="label121">
<property name="visible">True</property>
<property name="label" translatable="yes">Search for:</property>
<property name="use_underline">False</property>
<property name="use_markup">False</property>
<property name="wrap">False</property>
<property name="selectable">False</property>
<property name="width_chars">-1</property>
<property name="single_line_mode">False</property>
</object>
<packing>
<property name="padding">0</property>
<property name="expand">False</property>
<property name="fill">False</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="entryFilter">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="has_focus">True</property>
<property name="max_length">0</property>
<property name="has_frame">True</property>
<property name="invisible_char">●</property>
<property name="activates_default">False</property>
<signal handler="on_entryFilter_changed" name="changed"/>
</object>
<packing>
<property name="padding">0</property>
<property name="expand">True</property>
<property name="fill">True</property>
</packing>
</child>
<child>
<object class="GtkButton" id="btnShowAll">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="label" translatable="yes">Show All</property>
<property name="use_underline">True</property>
<property name="focus_on_click">True</property>
<signal handler="on_btnShowAll_clicked" name="clicked"/>
</object>
<packing>
<property name="padding">0</property>
<property name="expand">False</property>
<property name="fill">False</property>
</packing>
</child>
</object>
<packing>
<property name="padding">0</property>
<property name="expand">False</property>
<property name="fill">False</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scrolledwindow8">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="shadow_type">GTK_SHADOW_IN</property>
<property name="window_placement">GTK_CORNER_TOP_LEFT</property>
<child>
<object class="GtkTreeView" id="configeditor">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="headers_visible">True</property>
<property name="rules_hint">False</property>
<property name="reorderable">False</property>
<property name="enable_search">True</property>
<property name="fixed_height_mode">False</property>
<property name="hover_selection">False</property>
<property name="hover_expand">False</property>
</object>
</child>
</object>
<packing>
<property name="padding">0</property>
<property name="expand">True</property>
<property name="fill">True</property>
</packing>
</child>
</object>
<packing>
<property name="padding">0</property>
<property name="expand">True</property>
<property name="fill">True</property>
</packing>
</child>
<child>
<object class="GtkHButtonBox" id="hbuttonbox2">
<property name="visible">True</property>
<property name="layout_style">GTK_BUTTONBOX_END</property>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can-focus">False</property>
<child>
<object class="GtkButton" id="btnClose">
<property name="label" translatable="yes">_Close</property>
<property name="visible">True</property>
<property name="can_default">True</property>
<property name="has_default">True</property>
<property name="can_focus">True</property>
<property name="label">gtk-close</property>
<property name="use_stock">True</property>
<property name="focus_on_click">True</property>
<signal handler="on_btnClose_clicked" name="clicked"/>
<property name="can-focus">True</property>
<property name="can-default">True</property>
<property name="has-default">True</property>
<property name="receives-default">False</property>
<property name="use-underline">True</property>
<signal name="clicked" handler="on_btnClose_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="vbox_for_episode_selector">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">5</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkBox" id="hbox38">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel" id="label121">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Search for:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="entryFilter">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="has-focus">True</property>
<property name="invisible-char">●</property>
<signal name="changed" handler="on_entryFilter_changed" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="btnShowAll">
<property name="label" translatable="yes">_Show All</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="use-underline">True</property>
<signal name="clicked" handler="on_btnShowAll_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scrolledwindow8">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkTreeView" id="configeditor">
<property name="visible">True</property>
<property name="can-focus">True</property>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>

View File

@ -1,36 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.0"/>
<requires lib="gtk+" version="3.16"/>
<object class="GtkCheckButton" id="allsamefolder">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="draw-indicator">True</property>
</object>
<object class="GtkFileChooserDialog" id="gPodderExportToLocalFolder">
<property name="can_focus">False</property>
<property name="can-focus">True</property>
<property name="title" translatable="yes">Select destination</property>
<property name="modal">True</property>
<property name="window_position">center-on-parent</property>
<property name="type_hint">dialog</property>
<property name="window-position">center-on-parent</property>
<property name="type-hint">dialog</property>
<property name="action">save</property>
<property name="do_overwrite_confirmation">True</property>
<property name="preview_widget_active">False</property>
<property name="use_preview_label">False</property>
<property name="extra_widget">allsamefolder</property>
<property name="do-overwrite-confirmation">True</property>
<property name="extra-widget">allsamefolder</property>
<property name="preview-widget-active">False</property>
<property name="use-preview-label">False</property>
<child internal-child="vbox">
<object class="GtkBox">
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can_focus">False</property>
<property name="layout_style">end</property>
<property name="can-focus">False</property>
<property name="layout-style">end</property>
<child>
<object class="GtkButton" id="btnOK">
<property name="label">gtk-save</property>
<object class="GtkButton" id="btnCancel">
<property name="label" translatable="yes">_Cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_default">True</property>
<property name="has_default">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_btnOK_clicked" swapped="no"/>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="use-underline">True</property>
<signal name="clicked" handler="on_btnCancel_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
@ -39,13 +44,15 @@
</packing>
</child>
<child>
<object class="GtkButton" id="btnCancel">
<property name="label">gtk-cancel</property>
<object class="GtkButton" id="btnOK">
<property name="label" translatable="yes">_Save</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_stock">True</property>
<signal name="clicked" handler="on_btnCancel_clicked" swapped="no"/>
<property name="can-focus">True</property>
<property name="can-default">True</property>
<property name="has-default">True</property>
<property name="receives-default">True</property>
<property name="use-underline">True</property>
<signal name="clicked" handler="on_btnOK_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
@ -62,16 +69,9 @@
</child>
</object>
</child>
<!-- to be recognized by the embedded GtkFileChooser -->
<action-widgets>
<action-widget response="-3">btnOK</action-widget>
<action-widget response="-6">btnCancel</action-widget>
<action-widget response="-3">btnOK</action-widget>
</action-widgets>
</object>
<object class="GtkCheckButton" id="allsamefolder">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
</object>
</interface>

File diff suppressed because it is too large Load Diff

View File

@ -1,93 +1,189 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<!--*- mode: xml -*-->
<interface>
<requires lib="gtk+" version="3.16"/>
<object class="GtkDialog" id="gPodderWelcome">
<property name="default_height">230</property>
<property name="default_width">340</property>
<property name="modal">True</property>
<property name="transient-for">parent_widget</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Getting started</property>
<property name="modal">True</property>
<property name="default-width">340</property>
<property name="default-height">230</property>
<property name="type-hint">dialog</property>
<child internal-child="vbox">
<object class="GtkBox" id="dialog1-vbox">
<property name="border_width">2</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">2</property>
<property name="margin-end">2</property>
<property name="margin-top">2</property>
<property name="margin-bottom">2</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="vbox1">
<property name="border_width">12</property>
<property name="spacing">12</property>
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="label1">
<property name="label" translatable="yes">&lt;big&gt;Welcome to gPodder&lt;/big&gt;</property>
<property name="use_markup">True</property>
<property name="visible">True</property>
<property name="xalign">0.0</property>
<property name="yalign">1.0</property>
</object>
</child>
<child>
<object class="GtkLabel" id="label2">
<property name="label" translatable="yes">Your podcast list is empty.</property>
<property name="visible">True</property>
<property name="xalign">0.0</property>
<property name="yalign">0.0</property>
</object>
</child>
<child>
<object class="GtkBox" id="vbox_buttons">
<property name="spacing">6</property>
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkButton" id="btnOPML">
<property name="is_focus">True</property>
<property name="label" translatable="yes">Choose from a list of example podcasts</property>
<property name="visible">True</property>
<signal handler="on_show_example_podcasts" name="clicked"/>
</object>
</child>
<child>
<object class="GtkButton" id="btnAddURL">
<property name="is_focus">True</property>
<property name="label" translatable="yes">Add a podcast by entering its URL</property>
<property name="visible">True</property>
<signal handler="on_add_podcast_via_url" name="clicked"/>
</object>
</child>
<child>
<object class="GtkButton" id="btnMygPodder">
<property name="label" translatable="yes">Restore my subscriptions from gpodder.net</property>
<property name="visible">True</property>
<signal handler="on_setup_my_gpodder" name="clicked"/>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="position">2</property>
</packing>
</child>
<child internal-child="action_area">
<object class="GtkHButtonBox" id="dialog1-action_area">
<property name="border_width">5</property>
<property name="layout_style">end</property>
<property name="spacing">6</property>
<object class="GtkButtonBox" id="dialog1-action_area">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="layout-style">end</property>
<child>
<object class="GtkButton" id="btnCancel">
<property name="label">gtk-cancel</property>
<property name="use_stock">True</property>
<property name="label" translatable="yes">_Cancel</property>
<property name="visible">True</property>
<signal handler="on_btnCancel_clicked" name="clicked"/>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="use-underline">True</property>
<signal name="clicked" handler="on_btnCancel_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="pack_type">end</property>
<property name="fill">False</property>
<property name="pack-type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="vbox1">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">&lt;big&gt;Welcome to gPodder&lt;/big&gt;</property>
<property name="use-markup">True</property>
<property name="xalign">0</property>
<property name="yalign">1</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Your podcast list is empty.</property>
<property name="xalign">0</property>
<property name="yalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="vbox_buttons">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkButton" id="btnOPML">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="has-focus">True</property>
<property name="is-focus">True</property>
<property name="receives-default">False</property>
<signal name="clicked" handler="on_show_example_podcasts" swapped="no"/>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
<property name="label" translatable="yes">Choose from a list of example podcasts</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="btnAddURL">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="is-focus">True</property>
<property name="receives-default">False</property>
<signal name="clicked" handler="on_add_podcast_via_url" swapped="no"/>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
<property name="label" translatable="yes">Add a podcast by entering its URL</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="btnMygPodder">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="is-focus">True</property>
<property name="receives-default">False</property>
<signal name="clicked" handler="on_setup_my_gpodder" swapped="no"/>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
<property name="label" translatable="yes">Restore my subscriptions from gpodder.net</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>

View File

@ -453,9 +453,11 @@ class DownloadQueueManager(object):
self.__spawn_threads()
def force_start_task(self, task):
if self.tasks.set_downloading(task):
worker = ForceDownloadWorker(task)
util.run_in_background(worker.run)
with task:
if task.status in (task.QUEUED, task.PAUSED, task.CANCELLED, task.FAILED):
task.status = task.DOWNLOADING
worker = ForceDownloadWorker(task)
util.run_in_background(worker.run)
def queue_task(self, task):
"""Marks a task as queued
@ -609,9 +611,25 @@ class DownloadTask(object):
downloader = property(fget=__get_downloader, fset=__set_downloader)
def pause(self):
with self:
# Pause a queued download
if self.status == self.QUEUED:
self.status = self.PAUSED
# Request pause of a running download
elif self.status == self.DOWNLOADING:
self.status = self.PAUSING
def cancel(self):
with self:
if self.status in (self.DOWNLOADING, self.QUEUED):
# Cancelling directly is allowed if the task isn't currently downloading
if self.status in (self.QUEUED, self.PAUSED, self.FAILED):
self.status = self.CANCELLED
# Call run, so the partial file gets deleted
self.run()
self.recycle()
# Otherwise request cancellation
elif self.status == self.DOWNLOADING:
self.status = self.CANCELLING
def removed_from_list(self):
@ -758,7 +776,12 @@ class DownloadTask(object):
time.sleep(delay)
def recycle(self):
self.episode.download_task = None
if self.status not in (self.FAILED, self.PAUSED):
self.episode.download_task = None
def set_episode_download_task(self):
if not self.episode.download_task:
self.episode.download_task = self
def run(self):
# Speed calculation (re-)starts here
@ -779,19 +802,17 @@ class DownloadTask(object):
self.status = DownloadTask.PAUSED
return False
# We only start this download if its status is queued
if self.status != DownloadTask.QUEUED:
# We only start this download if its status is downloading
if self.status != DownloadTask.DOWNLOADING:
return False
# We are downloading this file right now
self.status = DownloadTask.DOWNLOADING
self._notification_shown = False
# Restore a reference to this task in the episode
# when running a recycled task following a pause or failed
# see #649
if not self.episode.download_task:
self.episode.download_task = self
self.set_episode_download_task()
url = self.__episode.url
result = DownloadTask.DOWNLOADING
@ -916,14 +937,6 @@ class DownloadTask(object):
self.error_message = _('Error: %s') % (str(e),)
with self:
if result == DownloadTask.FAILED:
self.status = DownloadTask.FAILED
self.__episode._download_error = self.error_message
# Delete empty partial files, they prevent streaming after a download failure (live stream)
if util.calculate_size(self.filename) == 0:
util.delete_file(self.tempname)
if result == DownloadTask.DOWNLOADING:
# Everything went well - we're done (even if the task was cancelled/paused,
# since it's finished we might as well mark it done)
@ -937,8 +950,16 @@ class DownloadTask(object):
self.speed = 0.0
# cancelled -- update state to mark it as safe to manipulate this task again
if self.status == DownloadTask.PAUSING:
if result == DownloadTask.FAILED:
self.status = DownloadTask.FAILED
self.__episode._download_error = self.error_message
# Delete empty partial files, they prevent streaming after a download failure (live stream)
if util.calculate_size(self.filename) == 0:
util.delete_file(self.tempname)
# cancelled/paused -- update state to mark it as safe to manipulate this task again
elif self.status == DownloadTask.PAUSING:
self.status = DownloadTask.PAUSED
elif self.status == DownloadTask.CANCELLING:
self.status = DownloadTask.CANCELLED

View File

@ -31,6 +31,7 @@ N_ = gpodder.ngettext
class gPodderExportToLocalFolder(BuilderWidget):
""" Export to Local Folder UI: file dialog + checkbox to save all to same folder """
def new(self):
self.gPodderExportToLocalFolder.set_transient_for(self.parent_widget)
self._config.connect_gtk_window(self.gPodderExportToLocalFolder,
'export_to_local_folder', True)
self._ok = False

View File

@ -99,6 +99,8 @@ class DirectoryProvidersModel(Gtk.ListStore):
class gPodderPodcastDirectory(BuilderWidget):
def new(self):
self.gPodderPodcastDirectory.set_transient_for(self.parent_widget)
self.flap_show_image.set_from_file(os.path.join(
gpodder.icons_folder, 'actions', 'view-sidebar-end-symbolic.svg'))

View File

@ -185,6 +185,7 @@ class gPodderPreferences(BuilderWidget):
C_TOGGLE, C_LABEL, C_EXTENSION, C_SHOW_TOGGLE = list(range(4))
def new(self):
self.gPodderPreferences.set_transient_for(self.parent_widget)
for cb in (self.combo_audio_player_app, self.combo_video_player_app):
cellrenderer = Gtk.CellRendererPixbuf()
cb.pack_start(cellrenderer, False)

View File

@ -26,16 +26,9 @@ _ = gpodder.gettext
class gPodderWelcome(BuilderWidget):
PADDING = 10
def new(self):
for widget in self.vbox_buttons.get_children():
for child in widget.get_children():
if isinstance(child, Gtk.Alignment):
child.set_padding(self.PADDING, self.PADDING,
self.PADDING, self.PADDING)
else:
child.set_padding(self.PADDING, self.PADDING)
self.gPodderWelcome.set_transient_for(self.parent_widget)
def on_btnCancel_clicked(self, button):
self.main_window.response(Gtk.ResponseType.CANCEL)

View File

@ -35,76 +35,43 @@ from gpodder import download, util
_ = gpodder.gettext
class TaskQueue:
class DequeueRequest:
def __init__(self):
self.lock = threading.Lock()
self.tasks = list()
self.cv = threading.Condition()
self.value = None
self.resolved = False
def __len__(self):
with self.lock:
return len(self.tasks)
def dequeue(self):
with self.cv:
self.cv.wait_for(lambda: self.resolved)
return self.value
def add_task(self, task):
with self.lock:
if task not in self.tasks:
self.tasks.append(task)
def remove_task(self, task):
with self.lock:
try:
self.tasks.remove(task)
return True
except ValueError:
# already dequeued
return False
def pop(self):
with self.lock:
if len(self.tasks) == 0:
return None
task = self.tasks.pop(0)
return task
def move_after(self, task, after):
with self.lock:
try:
index = self.tasks.index(after)
self.tasks.remove(task)
self.tasks.insert(index + 1, task)
except ValueError:
pass
def move_before(self, task, before):
with self.lock:
try:
index = self.tasks.index(before)
self.tasks.remove(task)
self.tasks.insert(index, task)
except ValueError:
pass
def resolve(self, value):
self.value = value
self.resolved = True
with self.cv:
self.cv.notify()
class DownloadStatusModel:
class DownloadStatusModel(Gtk.ListStore):
# Symbolic names for our columns, so we know what we're up to
C_TASK, C_NAME, C_URL, C_PROGRESS, C_PROGRESS_TEXT, C_ICON_NAME = list(range(6))
SEARCH_COLUMNS = (C_NAME, C_URL)
def __init__(self):
self.list = Gtk.ListStore(object, str, str, int, str, str)
self.work_queue = TaskQueue()
Gtk.ListStore.__init__(self, object, str, str, int, str, str)
# Set up stock icon IDs for tasks
self._status_ids = collections.defaultdict(lambda: None)
self._status_ids[download.DownloadTask.DOWNLOADING] = 'go-down'
self._status_ids[download.DownloadTask.DONE] = Gtk.STOCK_APPLY
self._status_ids[download.DownloadTask.FAILED] = 'dialog-error'
self._status_ids[download.DownloadTask.CANCELLING] = 'media-playback-stop'
self._status_ids[download.DownloadTask.CANCELLED] = 'media-playback-stop'
self._status_ids[download.DownloadTask.PAUSING] = 'media-playback-pause'
self._status_ids[download.DownloadTask.PAUSED] = 'media-playback-pause'
def get_model(self):
return self.list
def _format_message(self, episode, message, podcast):
episode = html.escape(episode)
podcast = html.escape(podcast)
@ -114,10 +81,10 @@ class DownloadStatusModel:
def request_update(self, iter, task=None):
if task is None:
# Ongoing update request from UI - get task from model
task = self.list.get_value(iter, self.C_TASK)
task = self.get_value(iter, self.C_TASK)
else:
# Initial update request - update non-changing fields
self.list.set(iter,
self.set(iter,
self.C_TASK, task,
self.C_URL, task.url)
@ -151,7 +118,7 @@ class DownloadStatusModel:
else:
progress_message = ('unknown size')
self.list.set(iter,
self.set(iter,
self.C_NAME, self._format_message(task.episode.title,
status_message, task.episode.channel.title),
self.C_PROGRESS, 100. * task.progress,
@ -159,30 +126,36 @@ class DownloadStatusModel:
self.C_ICON_NAME, self._status_ids[task.status])
def __add_new_task(self, task):
iter = self.list.append()
iter = self.append()
self.request_update(iter, task)
def register_task(self, task):
# self.work_queue.add_task(task)
util.idle_add(self.__add_new_task, task)
def register_task(self, task, background=True):
if background:
util.idle_add(self.__add_new_task, task)
else:
self.__add_new_task(task)
def queue_task(self, task):
with task:
if task.status in (task.NEW, task.FAILED, task.CANCELLED, task.PAUSED):
task.status = task.QUEUED
self.work_queue.add_task(task)
task.set_episode_download_task()
def tell_all_tasks_to_quit(self):
for row in self.list:
for row in self:
task = row[DownloadStatusModel.C_TASK]
if task is not None:
with task:
# Pause currently-running (and queued) downloads
if task.status in (task.QUEUED, task.DOWNLOADING):
# Pause currently queued downloads
if task.status == task.QUEUED:
task.status = task.PAUSED
# Request pause of currently running downloads
elif task.status == task.DOWNLOADING:
task.status = task.PAUSING
# Delete cancelled and failed downloads
if task.status in (task.CANCELLED, task.FAILED):
elif task.status in (task.CANCELLED, task.FAILED):
task.removed_from_list()
def are_downloads_in_progress(self):
@ -190,7 +163,7 @@ class DownloadStatusModel:
Returns True if there are any downloads in the
QUEUED or DOWNLOADING status, False otherwise.
"""
for row in self.list:
for row in self:
task = row[DownloadStatusModel.C_TASK]
if task is not None and \
task.status in (task.DOWNLOADING,
@ -199,30 +172,34 @@ class DownloadStatusModel:
return False
def move_after(self, iter, position):
self.list.move_after(iter, position)
iter_task = self.list.get_value(iter, DownloadStatusModel.C_TASK)
pos_task = self.list.get_value(position, DownloadStatusModel.C_TASK)
self.work_queue.move_after(iter_task, pos_task)
def move_before(self, iter, position):
self.list.move_before(iter, position)
iter_task = self.list.get_value(iter, DownloadStatusModel.C_TASK)
pos_task = self.list.get_value(position, DownloadStatusModel.C_TASK)
self.work_queue.move_before(iter_task, pos_task)
def has_work(self):
return len(self.work_queue) > 0
return any(self._work_gen())
def available_work_count(self):
return len(self.work_queue)
return len(list(self._work_gen()))
def __get_next(self, dqr):
try:
task = next(self._work_gen())
# this is the only thread accessing the list store, so it's safe
# to assume a) the task is still queued and b) we can transition to downloading
task.status = task.DOWNLOADING
except StopIteration as e:
task = None
# hand the task off to the worker thread
dqr.resolve(task)
# get the next task to download. this proxies the request to the main thread,
# as only the main thread is allowed to manipulate the list store.
def get_next(self):
return self.work_queue.pop()
dqr = DequeueRequest()
util.idle_add(self.__get_next, dqr)
return dqr.dequeue()
def set_downloading(self, task):
# return False if Task was already dequeued by get_next
return self.work_queue.remove_task(task)
def _work_gen(self):
return (task for task in
(row[DownloadStatusModel.C_TASK] for row in self)
if task.status == task.QUEUED)
class DownloadTaskMonitor(object):

View File

@ -28,6 +28,7 @@ _ = gpodder.gettext
class gPodderAddPodcast(BuilderWidget):
def new(self):
self.gPodderAddPodcast.set_transient_for(self.parent_widget)
if not hasattr(self, 'add_podcast_list'):
self.add_podcast_list = None
if hasattr(self, 'custom_label'):

View File

@ -30,6 +30,7 @@ _ = gpodder.gettext
class gPodderConfigEditor(BuilderWidget):
def new(self):
self.gPodderConfigEditor.set_transient_for(self.parent_widget)
name_column = Gtk.TreeViewColumn(_('Setting'))
name_renderer = Gtk.CellRendererText()
name_column.pack_start(name_renderer, True)

View File

@ -1318,7 +1318,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
column.set_property('min-width', 150)
column.set_property('max-width', 150)
self.treeDownloads.set_model(self.download_status_model.get_model())
self.treeDownloads.set_model(self.download_status_model)
TreeViewHelper.set(self.treeDownloads, TreeViewHelper.ROLE_DOWNLOADS)
# Set up context menu
@ -1387,7 +1387,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
self.download_list_update_enabled = True
def cleanup_downloads(self):
model = self.download_status_model.get_model()
model = self.download_status_model
all_tasks = [(Gtk.TreeRowReference.new(model, row.path), row[0]) for row in model]
changed_episode_urls = set()
@ -1434,7 +1434,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
def update_downloads_list(self, can_call_cleanup=True):
try:
model = self.download_status_model.get_model()
model = self.download_status_model
downloading, synchronizing, failed, finished, queued, paused, others = 0, 0, 0, 0, 0, 0, 0
total_speed, total_size, done_size = 0, 0, 0
@ -1451,7 +1451,6 @@ class gPodder(BuilderWidget, dbus.service.Object):
task = row[self.download_status_model.C_TASK]
speed, size, status, progress, activity = task.speed, task.total_size, task.status, task.progress, task.activity
logger.info("%s: %f", task.episode.title, progress)
# Let the download task monitors know of changes
for monitor in self.download_task_monitors:
@ -1486,7 +1485,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
self.download_tasks_seen = download_tasks_seen
text = [_('Progress')]
if downloading + failed + queued + synchronizing > 0:
if downloading + synchronizing + failed + queued + paused > 0:
s = []
if downloading > 0:
s.append(N_('%(count)d active', '%(count)d active', downloading) % {'count': downloading})
@ -1496,6 +1495,8 @@ class gPodder(BuilderWidget, dbus.service.Object):
s.append(N_('%(count)d failed', '%(count)d failed', failed) % {'count': failed})
if queued > 0:
s.append(N_('%(count)d queued', '%(count)d queued', queued) % {'count': queued})
if paused > 0:
s.append(N_('%(count)d paused', '%(count)d paused', paused) % {'count': paused})
text.append(' (' + ', '.join(s) + ')')
self.labelDownloads.set_text(''.join(text))
self.transfer_revealer.set_reveal_child(True)
@ -1838,36 +1839,39 @@ class gPodder(BuilderWidget, dbus.service.Object):
return (''.join(result)).strip()
def queue_task(self, task, force_start):
if force_start:
self.download_queue_manager.force_start_task(task)
else:
self.download_queue_manager.queue_task(task)
def _for_each_task_set_status(self, tasks, status, force_start=False):
episode_urls = set()
model = self.treeDownloads.get_model()
for row_reference, task in tasks:
with task:
if status == download.DownloadTask.QUEUED:
# Only queue task when its paused/failed/cancelled (or forced)
# Only queue task when it's paused/failed/cancelled (or forced)
if task.status in (download.DownloadTask.PAUSED,
download.DownloadTask.FAILED,
download.DownloadTask.CANCELLED) or force_start:
if force_start:
self.download_queue_manager.force_start_task(task)
else:
self.download_queue_manager.queue_task(task)
# add the task back in if it was already cleaned up
# (to trigger this cancel one downloads in the active list, cancel all
# other downloads, quickly right click on the cancelled on one to get
# the context menu, wait until the active list is cleared, and then
# then choose download)
if task not in self.download_tasks_seen:
self.download_status_model.register_task(task, False)
self.download_tasks_seen.add(task)
self.queue_task(task, force_start)
self.set_download_list_state(gPodderSyncUI.DL_ONEOFF)
elif status == download.DownloadTask.CANCELLING:
logger.info(("cancelling task %s" % task.status))
# Cancelling a download allowed when downloading/queued
if task.status in (download.DownloadTask.QUEUED, download.DownloadTask.DOWNLOADING):
task.status = status
# Cancelling paused/failed downloads requires a call to .run()
elif task.status in (download.DownloadTask.PAUSED, download.DownloadTask.FAILED):
task.status = status
# Call run, so the partial file gets deleted
task.run()
task.recycle()
task.cancel()
elif status == download.DownloadTask.PAUSING:
# Pausing a download only when queued/downloading
if task.status in (download.DownloadTask.QUEUED, download.DownloadTask.DOWNLOADING):
task.status = status
task.pause()
elif status is None:
# Remove the selected task - cancel downloading/queued tasks
if task.status in (download.DownloadTask.QUEUED, download.DownloadTask.DOWNLOADING):
@ -2397,7 +2401,6 @@ class gPodder(BuilderWidget, dbus.service.Object):
def play_or_download(self, current_page=None):
(can_play, can_download, can_cancel, can_delete) = (False,) * 4
(is_played, is_locked) = (False,) * 2
open_instead_of_play = False
@ -2421,8 +2424,6 @@ class gPodder(BuilderWidget, dbus.service.Object):
if episode.was_downloaded():
can_play = episode.was_downloaded(and_exists=True)
is_played = not episode.is_new
is_locked = episode.archive
if not can_play:
can_download = True
else:
@ -3294,10 +3295,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
task.status = task.PAUSED
else:
self.mygpo_client.on_download([task.episode])
if force_start:
self.download_queue_manager.force_start_task(task)
else:
self.download_queue_manager.queue_task(task)
self.queue_task(task, force_start)
if tasks or queued_existing_task:
self.set_download_list_state(gPodderSyncUI.DL_ONEOFF)
# Flush updated episode status
@ -3323,10 +3321,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
if downloader:
# replace existing task's download with forced one
task.downloader = downloader
if force_start:
self.download_queue_manager.force_start_task(task)
else:
self.download_queue_manager.queue_task(task)
self.queue_task(task, force_start)
queued_existing_task = True
continue
@ -3355,16 +3350,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
return
for task in tasks:
with task:
if task.status in (task.NEW, task.QUEUED, task.DOWNLOADING):
task.status = task.CANCELLING
elif task.status == task.PAUSED:
task.status = task.CANCELLED
# Call run, so the partial file gets deleted
task.run()
task.recycle()
elif force:
task.status = task.CANCELLED
task.cancel()
self.update_episode_list_icons([task.url for task in tasks])
self.play_or_download()
@ -3957,7 +3943,7 @@ class gPodder(BuilderWidget, dbus.service.Object):
for tree_row_reference, task in selected_tasks:
with task:
if task.status in (task.DOWNLOADING, task.QUEUED):
task.status = task.PAUSED
task.pause()
elif task.status in (task.CANCELLED, task.PAUSED, task.FAILED):
self.download_queue_manager.queue_task(task)
self.set_download_list_state(gPodderSyncUI.DL_ONEOFF)

View File

@ -380,11 +380,21 @@ class EpisodeListModel(Gtk.ListStore):
view_show_unplayed = False
icon_theme = Gtk.IconTheme.get_default()
if episode.downloading:
tooltip.append('%s %d%%' % (_('Downloading'),
int(episode.download_task.progress * 100)))
task = episode.download_task
index = int(self.PROGRESS_STEPS * episode.download_task.progress)
if task is not None and task.status in (task.PAUSING, task.PAUSED):
tooltip.append('%s %d%%' % (_('Paused'),
int(task.progress * 100)))
status_icon = 'media-playback-pause'
view_show_downloaded = True
view_show_unplayed = True
elif episode.downloading:
tooltip.append('%s %d%%' % (_('Downloading'),
int(task.progress * 100)))
index = int(self.PROGRESS_STEPS * task.progress)
status_icon = 'gpodder-progress-%d' % index
view_show_downloaded = True

View File

@ -30,6 +30,7 @@ import os.path
import threading
import time
from enum import Enum
from os import sync
from re import S
from urllib.parse import urlparse
@ -210,6 +211,15 @@ class Device(services.ObservableService):
logger.warning('Not syncing disks. Unmount your device before unplugging.')
return True
def create_task(self, track):
return SyncTask(track)
def cancel_task(self, task):
pass
def cleanup_task(self, task):
pass
def add_sync_tasks(self, tracklist, force_played=False, done_callback=None):
for track in list(tracklist):
# Filter tracks that are not meant to be synchronized
@ -228,7 +238,7 @@ class Device(services.ObservableService):
break
# XXX: need to check if track is added properly?
sync_task = SyncTask(track)
sync_task = self.create_task(track)
sync_task.status = sync_task.NEW
sync_task.device = self
@ -567,7 +577,22 @@ class MP3PlayerDevice(Device):
def get_episode_file_on_device(self, episode):
return episode_filename_on_device(self._config, episode)
def add_track(self, episode, reporthook=None):
def create_task(self, track):
return GioSyncTask(track)
def cancel_task(self, task):
task.cancellable.cancel()
# called by the sync task when it is removed and needs partial files cleaning up
def cleanup_task(self, task):
episode = task.episode
folder = self.get_episode_folder_on_device(episode)
file = self.get_episode_file_on_device(episode)
file = folder.get_child(file)
self.remove_track_file(file)
def add_track(self, task, reporthook=None):
episode = task.episode
self.notify('status', _('Adding %s') % episode.title)
# get the folder on the device
@ -604,8 +629,10 @@ class MP3PlayerDevice(Device):
try:
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)
from_file.copy(to_file, Gio.FileCopyFlags.OVERWRITE, task.cancellable, hookconvert, None)
except GLib.Error as err:
if err.matches(Gio.io_error_quark(), Gio.IOErrorEnum.CANCELLED):
raise SyncCancelledException()
logger.error('Error copying %s to %s: %s', from_file.get_uri(), to_file.get_uri(), err.message)
d = {'from_file': from_file.get_uri(), 'to_file': to_file.get_uri(), 'message': err.message}
self.errors.append(_('Error copying %(from_file)s to %(to_file)s: %(message)s') % d)
@ -656,11 +683,7 @@ class MP3PlayerDevice(Device):
self._config.device_sync.max_filename_length)
return self._track_on_device(e)
def remove_track(self, track):
self.notify('status', _('Removing %s') % track.title)
# get the folder on the device
file = Gio.File.new_for_uri(track.filename)
def remove_track_file(self, file):
folder = file.get_parent()
if file.query_exists():
try:
@ -681,6 +704,13 @@ class MP3PlayerDevice(Device):
if not err.matches(Gio.io_error_quark(), Gio.IOErrorEnum.NOT_FOUND):
logger.error('deleting folder %s failed: %s', folder.get_uri(), err.message)
def remove_track(self, track):
self.notify('status', _('Removing %s') % track.title)
# get the folder on the device
file = Gio.File.new_for_uri(track.filename)
self.remove_track_file(file)
def directory_is_empty(self, directory):
for child in directory.enumerate_children(Gio.FILE_ATTRIBUTE_STANDARD_NAME, Gio.FileQueryInfoFlags.NONE, None):
return False
@ -750,14 +780,31 @@ class SyncTask(download.DownloadTask):
episode = property(fget=__get_episode)
def pause(self):
with self:
# Pause a queued download
if self.status == self.QUEUED:
self.status = self.PAUSED
# Request pause of a running download
elif self.status == self.DOWNLOADING:
self.status = self.PAUSING
def cancel(self):
with self:
if self.status in (self.DOWNLOADING, self.QUEUED):
# Cancelling directly is allowed if the task isn't currently downloading
if self.status in (self.QUEUED, self.PAUSED, self.FAILED):
self.status = self.CANCELLED
# Call run, so the partial file gets deleted
self.run()
self.recycle()
# Otherwise request cancellation
elif self.status == self.DOWNLOADING:
self.status = self.CANCELLING
self.device.cancel(self)
def removed_from_list(self):
# XXX: Should we delete temporary/incomplete files here?
pass
if self.status != self.DONE:
self.device.cleanup_task(self)
def __init__(self, episode):
self.__lock = threading.RLock()
@ -768,7 +815,6 @@ class SyncTask(download.DownloadTask):
# Create the target filename and save it in the database
self.filename = self.__episode.local_filename(create=False)
self.tempname = self.filename + '.partial'
self.total_size = self.__episode.file_size
self.speed = 0.0
@ -826,11 +872,12 @@ class SyncTask(download.DownloadTask):
self.progress = max(0.0, min(1.0, (count * blockSize) / self.total_size))
self._progress_updated(self.progress)
if self.status == SyncTask.CANCELLING:
raise SyncCancelledException()
if self.status in (SyncTask.CANCELLING, SyncTask.PAUSING):
self._signal_cancel_from_status()
if self.status == SyncTask.PAUSING:
raise SyncCancelledException()
# default implementation
def _signal_cancel_from_status(self):
raise SyncCancelledException()
def recycle(self):
self.episode.download_task = None
@ -841,31 +888,28 @@ class SyncTask(download.DownloadTask):
self.__start_blocks = 0
# If the download has already been cancelled/paused, skip it
if self.status == SyncTask.CANCELLING:
util.delete_file(self.tempname)
self.progress = 0.0
self.speed = 0.0
self.status = SyncTask.CANCELLED
return False
if self.status == SyncTask.PAUSING:
self.status = SyncTask.PAUSED
return False
with self:
# We only start this download if its status is "queued"
if self.status != SyncTask.QUEUED:
if self.status in (SyncTask.CANCELLING, SyncTask.CANCELLED):
self.progress = 0.0
self.speed = 0.0
self.status = SyncTask.CANCELLED
return False
if self.status == SyncTask.PAUSING:
self.status = SyncTask.PAUSED
return False
# We only start this download if its status is downloading
if self.status != SyncTask.DOWNLOADING:
return False
# We are synching this file right now
self.status = SyncTask.DOWNLOADING
self._notification_shown = False
self._notification_shown = False
sync_result = SyncTask.DONE
sync_result = SyncTask.DOWNLOADING
try:
logger.info('Starting SyncTask')
self.device.add_track(self.episode, reporthook=self.status_updated)
self.device.add_track(self, reporthook=self.status_updated)
except SyncCancelledException as e:
sync_result = SyncTask.CANCELLED
except Exception as e:
@ -874,12 +918,7 @@ class SyncTask(download.DownloadTask):
self.error_message = _('Error: %s') % (str(e),)
with self:
if sync_result == SyncTask.CANCELLED:
if self.status == SyncTask.CANCELLING:
self.status = SyncTask.CANCELLED
else:
self.status = SyncTask.PAUSED
elif sync_result == SyncTask.DONE:
if sync_result == SyncTask.DOWNLOADING:
# Everything went well - we're done
self.status = SyncTask.DONE
if self.total_size <= 0:
@ -889,7 +928,26 @@ class SyncTask(download.DownloadTask):
gpodder.user_extensions.on_episode_synced(self.device, self.__episode)
return True
self.speed = 0.0
self.speed = 0.0
if sync_result == SyncTask.FAILED:
self.status = SyncTask.FAILED
# cancelled/paused -- update state to mark it as safe to manipulate this task again
elif self.status == SyncTask.PAUSING:
self.status = SyncTask.PAUSED
elif self.status == SyncTask.CANCELLING:
self.status = SyncTask.CANCELLED
# We finished, but not successfully (at least not really)
return False
class GioSyncTask(SyncTask):
def __init__(self, episode):
super().__init__(episode)
# For cancelling the copy
self.cancellable = Gio.Cancellable()
def _signal_cancel_from_status(self):
self.cancellable.cancel()