Merge tag 'merge5' into dev-adaptive
This commit is contained in:
commit
4453316aa8
|
@ -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
|
||||
|
|
2
bin/gpo
2
bin/gpo
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
@ -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"><big>Welcome to gPodder</big></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"><big>Welcome to gPodder</big></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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'))
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue