Gtk UI: New podcast directory UI
Before Width: | Height: | Size: 916 B After Width: | Height: | Size: 916 B |
After Width: | Height: | Size: 780 B |
After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 410 B After Width: | Height: | Size: 410 B |
Before Width: | Height: | Size: 743 B After Width: | Height: | Size: 743 B |
Before Width: | Height: | Size: 908 B After Width: | Height: | Size: 908 B |
Before Width: | Height: | Size: 441 B After Width: | Height: | Size: 441 B |
|
@ -1,326 +1,233 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--*- mode: xml -*-->
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<!-- interface-requires gtk+ 2.16 -->
|
||||
<!-- interface-naming-policy toplevel-contextual -->
|
||||
<object class="GtkDialog" id="gPodderPodcastDirectory">
|
||||
<property name="visible">True</property>
|
||||
<property name="has_separator">False</property>
|
||||
<property name="visible">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="border_width">6</property>
|
||||
<property name="title" translatable="yes">Find new podcasts</property>
|
||||
<property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="window_position">center-on-parent</property>
|
||||
<property name="default_width">600</property>
|
||||
<property name="default_height">400</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>
|
||||
<property name="border-width">6</property>
|
||||
<signal handler="on_gPodderPodcastDirectory_destroy" name="destroy"/>
|
||||
<property name="type_hint">dialog</property>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkVBox" id="vboxOPML">
|
||||
<object class="GtkVBox" id="vb_directory">
|
||||
<property name="visible">True</property>
|
||||
<property name="homogeneous">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">6</property>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkHButtonBox" id="hboxBottomButtons">
|
||||
<property name="visible">True</property>
|
||||
<property name="layout_style">GTK_BUTTONBOX_END</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="layout_style">end</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="btnSelectAll">
|
||||
<property name="label" translatable="yes">Select All</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="label" translatable="yes">Select All</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="focus_on_click">True</property>
|
||||
<signal handler="on_btnSelectAll_clicked" name="clicked"/>
|
||||
<signal name="clicked" handler="on_btnSelectAll_clicked" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="btnSelectNone">
|
||||
<property name="label" translatable="yes">Select None</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="label" translatable="yes">Select None</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="focus_on_click">True</property>
|
||||
<signal handler="on_btnSelectNone_clicked" name="clicked"/>
|
||||
<signal name="clicked" handler="on_btnSelectNone_clicked" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="btnCancel">
|
||||
<property name="label">gtk-cancel</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="has_focus">True</property>
|
||||
<property name="label">gtk-cancel</property>
|
||||
<property name="can_default">True</property>
|
||||
<property name="has_default">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="use_stock">True</property>
|
||||
<property name="focus_on_click">True</property>
|
||||
<signal handler="on_btnCancel_clicked" name="clicked"/>
|
||||
<signal name="clicked" handler="on_btnCancel_clicked" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="btnOK">
|
||||
<property name="label">gtk-add</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="label">gtk-add</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="use_stock">True</property>
|
||||
<property name="focus_on_click">True</property>
|
||||
<signal handler="on_btnOK_clicked" name="clicked"/>
|
||||
<signal name="clicked" handler="on_btnOK_clicked" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">3</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="GtkNotebook" id="notebookChannelAdder">
|
||||
<object class="GtkHPaned" id="hpaned">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="show_tabs">True</property>
|
||||
<property name="show_border">True</property>
|
||||
<property name="tab_pos">GTK_POS_TOP</property>
|
||||
<property name="scrollable">False</property>
|
||||
<property name="enable_popup">False</property>
|
||||
<child>
|
||||
<object class="GtkVBox" id="vboxOpmlImport">
|
||||
<property name="visible">True</property>
|
||||
<property name="homogeneous">False</property>
|
||||
<child>
|
||||
<object class="GtkHBox" id="hboxOpmlUrlEntry">
|
||||
<property name="border_width">5</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="homogeneous">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="entryURL">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_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_btnDownloadOpml_clicked" name="activate"/>
|
||||
<signal handler="on_entryURL_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="btnDownloadOpml">
|
||||
<property name="label" translatable="yes">Download</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="focus_on_click">True</property>
|
||||
<signal handler="on_btnDownloadOpml_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">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="scrolledwindow5">
|
||||
<property name="border_width">5</property>
|
||||
<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="treeviewChannelChooser">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="headers_visible">False</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="tab_expand">False</property>
|
||||
<property name="tab_fill">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child type="tab">
|
||||
<object class="GtkLabel" id="label138">
|
||||
<property name="visible">True</property>
|
||||
<property name="label" translatable="yes">_OPML/Search</property>
|
||||
<property name="use_underline">True</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>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="scrolledwindow9">
|
||||
<property name="border_width">5</property>
|
||||
<object class="GtkScrolledWindow" id="sw_providers">
|
||||
<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>
|
||||
<property name="hscrollbar_policy">never</property>
|
||||
<property name="vscrollbar_policy">automatic</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="treeviewTopPodcastsChooser">
|
||||
<object class="GtkTreeView" id="tv_providers">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="headers_visible">False</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>
|
||||
<property name="enable_search">False</property>
|
||||
<signal name="row-activated" handler="on_tv_providers_row_activated" swapped="no"/>
|
||||
<signal name="cursor-changed" handler="on_tv_providers_cursor_changed" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="tab_expand">False</property>
|
||||
<property name="tab_fill">True</property>
|
||||
<property name="resize">False</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child type="tab">
|
||||
<object class="GtkLabel" id="label139">
|
||||
<property name="visible">True</property>
|
||||
<property name="label" translatable="yes">Top _podcasts</property>
|
||||
<property name="use_underline">True</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>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkVBox" id="vbox46">
|
||||
<object class="GtkVBox" id="vb_podcasts">
|
||||
<property name="visible">True</property>
|
||||
<property name="homogeneous">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkHBox" id="hbox43">
|
||||
<property name="border_width">5</property>
|
||||
<object class="GtkHBox" id="hb_text_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="homogeneous">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="entryYoutubeSearch">
|
||||
<object class="GtkLabel" id="lb_search">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_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_entryYoutubeSearch_key_press_event" name="key_press_event"/>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">label</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="padding">0</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="btnSearchYouTube">
|
||||
<object class="GtkEntry" id="en_query">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="label" translatable="true">Search</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="focus_on_click">True</property>
|
||||
<signal handler="on_btnSearchYouTube_clicked" name="clicked"/>
|
||||
<property name="invisible_char">•</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_activatable">False</property>
|
||||
<property name="primary_icon_sensitive">True</property>
|
||||
<property name="secondary_icon_sensitive">True</property>
|
||||
<signal name="activate" handler="on_bt_search_clicked" 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="bt_search">
|
||||
<property name="label" translatable="yes">...</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<signal name="clicked" handler="on_bt_search_clicked" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="padding">0</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="fill">True</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">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="scrolledwindow10">
|
||||
<property name="border_width">5</property>
|
||||
<object class="GtkScrolledWindow" id="sw_tagcloud">
|
||||
<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>
|
||||
<property name="hscrollbar_policy">automatic</property>
|
||||
<property name="vscrollbar_policy">automatic</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="treeviewYouTubeChooser">
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="sw_podcasts">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="hscrollbar_policy">never</property>
|
||||
<property name="vscrollbar_policy">automatic</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="tv_podcasts">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="headers_visible">False</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>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="tab_expand">False</property>
|
||||
<property name="tab_fill">True</property>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child type="tab">
|
||||
<object class="GtkLabel" id="label140">
|
||||
<property name="visible">True</property>
|
||||
<property name="label" translatable="yes">_YouTube</property>
|
||||
<property name="use_underline">True</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>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="padding">0</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
|
|
|
@ -0,0 +1,208 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# gPodder - A media aggregator and podcast client
|
||||
# Copyright (c) 2005-2014 Thomas Perl and the gPodder Team
|
||||
#
|
||||
# gPodder is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# gPodder is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
|
||||
#
|
||||
# gpodder.directory - Podcast directory and search providers
|
||||
# Thomas Perl <thp@gpodder.org>; 2014-10-22
|
||||
#
|
||||
|
||||
import gpodder
|
||||
|
||||
_ = gpodder.gettext
|
||||
|
||||
import urllib
|
||||
import json
|
||||
import os
|
||||
|
||||
from gpodder import opml
|
||||
|
||||
class DirectoryEntry(object):
|
||||
def __init__(self, title, url, image=None, subscribers=-1, description=None):
|
||||
self.title = title
|
||||
self.url = url
|
||||
self.image = image
|
||||
self.subscribers = subscribers
|
||||
self.description = description
|
||||
|
||||
class DirectoryTag(object):
|
||||
def __init__(self, tag, weight):
|
||||
self.tag = tag
|
||||
self.weight = weight
|
||||
|
||||
|
||||
class Provider(object):
|
||||
PROVIDER_SEARCH, PROVIDER_URL, PROVIDER_FILE, PROVIDER_TAGCLOUD, PROVIDER_STATIC = range(5)
|
||||
|
||||
def __init__(self):
|
||||
self.name = ''
|
||||
self.kind = self.PROVIDER_SEARCH
|
||||
self.icon = None
|
||||
|
||||
def on_search(self, query):
|
||||
# Should return a list of DirectoryEntry objects
|
||||
raise NotImplemented()
|
||||
|
||||
def on_url(self, url):
|
||||
# Should return a list of DirectoryEntry objects
|
||||
raise NotImplemented()
|
||||
|
||||
def on_file(self, filename):
|
||||
# Should return a list of DirectoryEntry objects
|
||||
raise NotImplemented()
|
||||
|
||||
def on_tag(self, tag):
|
||||
# Should return a list of DirectoryEntry objects
|
||||
raise NotImplemented()
|
||||
|
||||
def on_static(self):
|
||||
# Should return a list of DirectoryEntry objects
|
||||
raise NotImplemented()
|
||||
|
||||
def get_tags(self):
|
||||
# Should return a list of DirectoryTag objects
|
||||
raise NotImplemented()
|
||||
|
||||
|
||||
|
||||
def directory_entry_from_opml(url):
|
||||
return [DirectoryEntry(d['title'], d['url'], description=d['description']) for d in opml.Importer(url).items]
|
||||
|
||||
def directory_entry_from_mygpo_json(url):
|
||||
return [DirectoryEntry(d['title'], d['url'], d['logo_url'], d['subscribers'], d['description'])
|
||||
for d in json.load(urllib.urlopen(url))]
|
||||
|
||||
|
||||
class GPodderNetSearchProvider(Provider):
|
||||
def __init__(self):
|
||||
self.name = _('gpodder.net search')
|
||||
self.kind = Provider.PROVIDER_SEARCH
|
||||
self.icon = 'directory-gpodder.png'
|
||||
|
||||
def on_search(self, query):
|
||||
return directory_entry_from_mygpo_json('http://gpodder.net/search.json?q=' + urllib.quote(query))
|
||||
|
||||
class OpmlWebImportProvider(Provider):
|
||||
def __init__(self):
|
||||
self.name = _('OPML from web')
|
||||
self.kind = Provider.PROVIDER_URL
|
||||
self.icon = 'directory-opml.png'
|
||||
|
||||
def on_url(self, url):
|
||||
return directory_entry_from_opml(url)
|
||||
|
||||
class OpmlFileImportProvider(Provider):
|
||||
def __init__(self):
|
||||
self.name = _('OPML file')
|
||||
self.kind = Provider.PROVIDER_FILE
|
||||
self.icon = 'directory-opml.png'
|
||||
|
||||
def on_file(self, filename):
|
||||
return directory_entry_from_opml(filename)
|
||||
|
||||
class GPodderRecommendationsProvider(Provider):
|
||||
def __init__(self):
|
||||
self.name = _('Getting started')
|
||||
self.kind = Provider.PROVIDER_STATIC
|
||||
self.icon = 'directory-examples.png'
|
||||
|
||||
def on_static(self):
|
||||
return directory_entry_from_opml('http://gpodder.org/directory.opml')
|
||||
|
||||
class GPodderNetToplistProvider(Provider):
|
||||
def __init__(self):
|
||||
self.name = _('gpodder.net Top 50')
|
||||
self.kind = Provider.PROVIDER_STATIC
|
||||
self.icon = 'directory-toplist.png'
|
||||
|
||||
def on_static(self):
|
||||
return directory_entry_from_mygpo_json('http://gpodder.net/toplist/50.json')
|
||||
|
||||
class GPodderNetTagsProvider(Provider):
|
||||
def __init__(self):
|
||||
self.name = _('gpodder.net Tags')
|
||||
self.kind = Provider.PROVIDER_TAGCLOUD
|
||||
self.icon = 'directory-tags.png'
|
||||
|
||||
def on_tag(self, tag):
|
||||
return directory_entry_from_mygpo_json('http://gpodder.net/api/2/tag/%s/50.json' % urllib.quote(tag))
|
||||
|
||||
def get_tags(self):
|
||||
return [DirectoryTag(d['tag'], d['usage']) for d in json.load(urllib.urlopen('http://gpodder.net/api/2/tags/40.json'))]
|
||||
|
||||
class YouTubeSearchProvider(Provider):
|
||||
def __init__(self):
|
||||
self.name = _('YouTube search')
|
||||
self.kind = Provider.PROVIDER_SEARCH
|
||||
self.icon = 'directory-youtube.png'
|
||||
|
||||
def on_search(self, query):
|
||||
url = 'http://gdata.youtube.com/feeds/api/videos?alt=json&q=%s' % urllib.quote(query)
|
||||
data = json.load(urllib.urlopen(url))
|
||||
|
||||
result = []
|
||||
|
||||
seen_users = set()
|
||||
for entry in data['feed']['entry']:
|
||||
user = os.path.basename(entry['author'][0]['uri']['$t'])
|
||||
title = entry['title']['$t']
|
||||
url = 'http://www.youtube.com/rss/user/%s/videos.rss' % user
|
||||
if user not in seen_users:
|
||||
result.append(DirectoryEntry(user, url))
|
||||
seen_users.add(user)
|
||||
|
||||
return result
|
||||
|
||||
class SoundcloudSearchProvider(Provider):
|
||||
def __init__(self):
|
||||
self.name = _('Soundcloud search')
|
||||
self.kind = Provider.PROVIDER_SEARCH
|
||||
self.icon = 'directory-soundcloud.png'
|
||||
|
||||
def on_search(self, query):
|
||||
# XXX: This cross-import of the plugin here is bad, but it
|
||||
# works for now (no proper plugin architecture...)
|
||||
from gpodder.plugins.soundcloud import search_for_user
|
||||
|
||||
return [DirectoryEntry(entry['username'], entry['permalink_url']) for entry in search_for_user(query)]
|
||||
|
||||
class FixedOpmlFileProvider(Provider):
|
||||
def __init__(self, filename):
|
||||
self.name = _('Imported OPML file')
|
||||
self.kind = Provider.PROVIDER_STATIC
|
||||
self.icon = 'directory-opml.png'
|
||||
|
||||
self.filename = filename
|
||||
|
||||
def on_static(self):
|
||||
return directory_entry_from_opml(self.filename)
|
||||
|
||||
PROVIDERS = [
|
||||
GPodderRecommendationsProvider,
|
||||
None,
|
||||
GPodderNetSearchProvider,
|
||||
GPodderNetToplistProvider,
|
||||
#GPodderNetTagsProvider,
|
||||
None,
|
||||
OpmlWebImportProvider,
|
||||
#OpmlFileImportProvider,
|
||||
None,
|
||||
YouTubeSearchProvider,
|
||||
SoundcloudSearchProvider,
|
||||
]
|
|
@ -17,184 +17,289 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
|
||||
#
|
||||
# gpodder.gtkui.desktop.podcastdirectory - Podcast directory Gtk UI
|
||||
# Thomas Perl <thp@gpodder.org>; 2014-10-22
|
||||
#
|
||||
|
||||
|
||||
import gtk
|
||||
import pango
|
||||
import urllib
|
||||
import os.path
|
||||
import cgi
|
||||
import os
|
||||
|
||||
import gpodder
|
||||
|
||||
_ = gpodder.gettext
|
||||
|
||||
from gpodder import util
|
||||
from gpodder import opml
|
||||
from gpodder import youtube
|
||||
from gpodder import my
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from gpodder.gtkui.opml import OpmlListModel
|
||||
from gpodder import util
|
||||
from gpodder import directory
|
||||
|
||||
from gpodder.gtkui.interface.common import BuilderWidget
|
||||
from gpodder.gtkui.interface.progress import ProgressIndicator
|
||||
from gpodder.gtkui.interface.tagcloud import TagCloud
|
||||
|
||||
class DirectoryPodcastsModel(gtk.ListStore):
|
||||
C_SELECTED, C_MARKUP, C_TITLE, C_URL = range(4)
|
||||
|
||||
def __init__(self, callback_can_subscribe):
|
||||
gtk.ListStore.__init__(self, bool, str, str, str)
|
||||
self.callback_can_subscribe = callback_can_subscribe
|
||||
|
||||
def load(self, directory_entries):
|
||||
self.clear()
|
||||
for entry in directory_entries:
|
||||
if entry.subscribers != -1:
|
||||
self.append((False, '%s (%d)\n<small>%s</small>' % (cgi.escape(entry.title),
|
||||
entry.subscribers, cgi.escape(entry.url)), entry.title, entry.url))
|
||||
else:
|
||||
self.append((False, '%s\n<small>%s</small>' % (cgi.escape(entry.title),
|
||||
cgi.escape(entry.url)), entry.title, entry.url))
|
||||
self.callback_can_subscribe(len(self.get_selected_podcasts()) > 0)
|
||||
|
||||
def toggle(self, path):
|
||||
self[path][self.C_SELECTED] = not self[path][self.C_SELECTED]
|
||||
self.callback_can_subscribe(len(self.get_selected_podcasts()) > 0)
|
||||
|
||||
def set_selection_to(self, selected):
|
||||
for row in self:
|
||||
row[self.C_SELECTED] = selected
|
||||
self.callback_can_subscribe(len(self.get_selected_podcasts()) > 0)
|
||||
|
||||
def get_selected_podcasts(self):
|
||||
return [(row[self.C_TITLE], row[self.C_URL]) for row in self if row[self.C_SELECTED]]
|
||||
|
||||
|
||||
class DirectoryProvidersModel(gtk.ListStore):
|
||||
C_WEIGHT, C_TEXT, C_ICON, C_PROVIDER = range(4)
|
||||
|
||||
SEPARATOR = (pango.WEIGHT_NORMAL, '', None, None)
|
||||
|
||||
def __init__(self, providers):
|
||||
gtk.ListStore.__init__(self, int, str, gtk.gdk.Pixbuf, object)
|
||||
for provider in providers:
|
||||
self.add_provider(provider() if provider else None)
|
||||
|
||||
def add_provider(self, provider):
|
||||
if provider is None:
|
||||
self.append(self.SEPARATOR)
|
||||
else:
|
||||
try:
|
||||
pixbuf = gtk.gdk.pixbuf_new_from_file(os.path.join(gpodder.images_folder, provider.icon)) if provider.icon else None
|
||||
except Exception as e:
|
||||
logger.warn('Could not load icon: %s (%s)', provider.icon or '-', e)
|
||||
pixbuf = None
|
||||
self.append((pango.WEIGHT_NORMAL, provider.name, pixbuf, provider))
|
||||
|
||||
def is_row_separator(self, model, it):
|
||||
return self.get_value(it, self.C_PROVIDER) is None
|
||||
|
||||
|
||||
class gPodderPodcastDirectory(BuilderWidget):
|
||||
def new(self):
|
||||
if hasattr(self, 'custom_title'):
|
||||
self.gPodderPodcastDirectory.set_title(self.custom_title)
|
||||
|
||||
if hasattr(self, 'hide_url_entry'):
|
||||
self.hboxOpmlUrlEntry.hide_all()
|
||||
new_parent = self.notebookChannelAdder.get_parent()
|
||||
new_parent.remove(self.notebookChannelAdder)
|
||||
self.vboxOpmlImport.reparent(new_parent)
|
||||
self.main_window.set_title(self.custom_title)
|
||||
|
||||
if not hasattr(self, 'add_podcast_list'):
|
||||
self.add_podcast_list = None
|
||||
|
||||
self.setup_treeview(self.treeviewChannelChooser)
|
||||
self.setup_treeview(self.treeviewTopPodcastsChooser)
|
||||
self.setup_treeview(self.treeviewYouTubeChooser)
|
||||
self.providers_model = DirectoryProvidersModel(directory.PROVIDERS)
|
||||
self.podcasts_model = DirectoryPodcastsModel(self.on_can_subscribe_changed)
|
||||
self.current_provider = None
|
||||
self.podcasts_progress_indicator = None
|
||||
|
||||
self.notebookChannelAdder.connect('switch-page', lambda a, b, c: self.on_change_tab(c))
|
||||
self.setup_providers_treeview()
|
||||
self.setup_podcasts_treeview()
|
||||
self.setup_tag_cloud()
|
||||
|
||||
def on_entryURL_changed(self, entry):
|
||||
url = entry.get_text()
|
||||
if self.is_search_term(url):
|
||||
self.btnDownloadOpml.set_label(_('Search'))
|
||||
selection = self.tv_providers.get_selection()
|
||||
selection.select_path((0,))
|
||||
self.on_tv_providers_row_activated(self.tv_providers, (0,), None)
|
||||
|
||||
self.main_window.show()
|
||||
|
||||
def download_opml_file(self, filename):
|
||||
self.providers_model.add_provider(directory.FixedOpmlFileProvider(filename))
|
||||
self.tv_providers.set_cursor(len(self.providers_model)-1)
|
||||
|
||||
def setup_podcasts_treeview(self):
|
||||
column = gtk.TreeViewColumn('')
|
||||
cell = gtk.CellRendererToggle()
|
||||
column.pack_start(cell, False)
|
||||
cell.set_activatable(True)
|
||||
column.add_attribute(cell, 'active', DirectoryPodcastsModel.C_SELECTED)
|
||||
cell.connect('toggled', lambda cell, path: self.podcasts_model.toggle(path))
|
||||
self.tv_podcasts.append_column(column)
|
||||
|
||||
column = gtk.TreeViewColumn('')
|
||||
cell = gtk.CellRendererText()
|
||||
cell.set_property('ellipsize', pango.ELLIPSIZE_END)
|
||||
column.pack_start(cell)
|
||||
column.add_attribute(cell, 'markup', DirectoryPodcastsModel.C_MARKUP)
|
||||
self.tv_podcasts.append_column(column)
|
||||
|
||||
self.tv_podcasts.set_model(self.podcasts_model)
|
||||
self.podcasts_model.append((False, 'a', 'b', 'c'))
|
||||
|
||||
def setup_providers_treeview(self):
|
||||
column = gtk.TreeViewColumn('')
|
||||
cell = gtk.CellRendererPixbuf()
|
||||
column.pack_start(cell, False)
|
||||
column.add_attribute(cell, 'pixbuf', DirectoryProvidersModel.C_ICON)
|
||||
cell = gtk.CellRendererText()
|
||||
#cell.set_property('ellipsize', pango.ELLIPSIZE_END)
|
||||
column.pack_start(cell)
|
||||
column.add_attribute(cell, 'text', DirectoryProvidersModel.C_TEXT)
|
||||
column.add_attribute(cell, 'weight', DirectoryProvidersModel.C_WEIGHT)
|
||||
self.tv_providers.append_column(column)
|
||||
|
||||
self.tv_providers.set_row_separator_func(self.providers_model.is_row_separator)
|
||||
|
||||
self.tv_providers.set_model(self.providers_model)
|
||||
|
||||
def setup_tag_cloud(self):
|
||||
self.tag_cloud = TagCloud()
|
||||
self.tag_cloud.set_size_request(-1, 130)
|
||||
self.tag_cloud.show_all()
|
||||
self.sw_tagcloud.add(self.tag_cloud)
|
||||
|
||||
self.tag_cloud.connect('selected', self.on_tag_selected)
|
||||
|
||||
def on_tag_selected(self, tag_cloud, tag):
|
||||
self.obtain_podcasts_with(lambda: self.current_provider.on_tag(tag))
|
||||
|
||||
def on_tv_providers_row_activated(self, treeview, path, column):
|
||||
it = self.providers_model.get_iter(path)
|
||||
|
||||
for row in self.providers_model:
|
||||
row[DirectoryProvidersModel.C_WEIGHT] = pango.WEIGHT_NORMAL
|
||||
|
||||
if it:
|
||||
self.providers_model.set_value(it, DirectoryProvidersModel.C_WEIGHT, pango.WEIGHT_BOLD)
|
||||
provider = self.providers_model.get_value(it, DirectoryProvidersModel.C_PROVIDER)
|
||||
self.use_provider(provider)
|
||||
|
||||
def use_provider(self, provider):
|
||||
self.podcasts_model.clear()
|
||||
self.current_provider = provider
|
||||
|
||||
if provider.kind == directory.Provider.PROVIDER_SEARCH:
|
||||
self.lb_search.set_text('Search:')
|
||||
self.bt_search.set_label('Search')
|
||||
elif provider.kind == directory.Provider.PROVIDER_URL:
|
||||
self.lb_search.set_text('URL:')
|
||||
self.bt_search.set_label('Download')
|
||||
elif provider.kind == directory.Provider.PROVIDER_FILE:
|
||||
self.lb_search.set_text('Filename:')
|
||||
self.bt_search.set_label('Open')
|
||||
elif provider.kind == directory.Provider.PROVIDER_TAGCLOUD:
|
||||
self.tag_cloud.clear_tags()
|
||||
|
||||
@util.run_in_background
|
||||
def load_tags():
|
||||
try:
|
||||
tags = [(t.tag, t.weight) for t in provider.get_tags()]
|
||||
except Exception as e:
|
||||
logger.warn('Got exception while loading tags: %s', e)
|
||||
tags = []
|
||||
|
||||
@util.idle_add
|
||||
def update_ui():
|
||||
self.tag_cloud.set_tags(tags)
|
||||
elif provider.kind == directory.Provider.PROVIDER_STATIC:
|
||||
self.obtain_podcasts_with(provider.on_static)
|
||||
|
||||
if provider.kind in (directory.Provider.PROVIDER_SEARCH,
|
||||
directory.Provider.PROVIDER_URL,
|
||||
directory.Provider.PROVIDER_FILE):
|
||||
self.en_query.set_text('')
|
||||
self.hb_text_entry.show()
|
||||
util.idle_add(self.en_query.grab_focus)
|
||||
else:
|
||||
self.btnDownloadOpml.set_label(_('Download'))
|
||||
self.hb_text_entry.hide()
|
||||
|
||||
def setup_treeview(self, tv):
|
||||
togglecell = gtk.CellRendererToggle()
|
||||
togglecell.set_property( 'activatable', True)
|
||||
togglecell.connect( 'toggled', self.callback_edited)
|
||||
togglecolumn = gtk.TreeViewColumn( '', togglecell, active=OpmlListModel.C_SELECTED)
|
||||
togglecolumn.set_min_width(40)
|
||||
self.sw_tagcloud.set_visible(provider.kind == directory.Provider.PROVIDER_TAGCLOUD)
|
||||
|
||||
titlecell = gtk.CellRendererText()
|
||||
titlecell.set_property('ellipsize', pango.ELLIPSIZE_END)
|
||||
titlecolumn = gtk.TreeViewColumn(_('Podcast'), titlecell, markup=OpmlListModel.C_DESCRIPTION_MARKUP)
|
||||
|
||||
for itemcolumn in (togglecolumn, titlecolumn):
|
||||
tv.append_column(itemcolumn)
|
||||
def on_tv_providers_cursor_changed(self, treeview):
|
||||
path, column = treeview.get_cursor()
|
||||
self.on_tv_providers_row_activated(treeview, path, column)
|
||||
|
||||
def callback_edited( self, cell, path):
|
||||
model = self.get_treeview().get_model()
|
||||
model[path][OpmlListModel.C_SELECTED] = not model[path][OpmlListModel.C_SELECTED]
|
||||
self.btnOK.set_sensitive(bool(len(self.get_selected_channels())))
|
||||
def obtain_podcasts_with(self, callback):
|
||||
if self.podcasts_progress_indicator is not None:
|
||||
self.podcasts_progress_indicator.on_finished()
|
||||
|
||||
def get_selected_channels(self, tab=None):
|
||||
channels = []
|
||||
self.podcasts_progress_indicator = ProgressIndicator(_('Loading podcasts'),
|
||||
_('Please wait while the podcast list is downloaded'),
|
||||
parent=self.main_window)
|
||||
|
||||
model = self.get_treeview(tab).get_model()
|
||||
if model is not None:
|
||||
for row in model:
|
||||
if row[OpmlListModel.C_SELECTED]:
|
||||
channels.append((row[OpmlListModel.C_TITLE],
|
||||
row[OpmlListModel.C_URL]))
|
||||
original_provider = self.current_provider
|
||||
|
||||
return channels
|
||||
self.en_query.set_sensitive(False)
|
||||
self.bt_search.set_sensitive(False)
|
||||
self.tag_cloud.set_sensitive(False)
|
||||
self.podcasts_model.clear()
|
||||
|
||||
def on_change_tab(self, tab):
|
||||
self.btnOK.set_sensitive( bool(len(self.get_selected_channels(tab))))
|
||||
@util.run_in_background
|
||||
def download_data():
|
||||
try:
|
||||
podcasts = callback()
|
||||
except Exception as e:
|
||||
logger.warn('Got exception while loading podcasts: %s', e)
|
||||
podcasts = []
|
||||
|
||||
def thread_finished(self, model, tab=0):
|
||||
if tab == 1:
|
||||
tv = self.treeviewTopPodcastsChooser
|
||||
elif tab == 2:
|
||||
tv = self.treeviewYouTubeChooser
|
||||
self.entryYoutubeSearch.set_sensitive(True)
|
||||
self.btnSearchYouTube.set_sensitive(True)
|
||||
self.btnOK.set_sensitive(False)
|
||||
else:
|
||||
tv = self.treeviewChannelChooser
|
||||
self.btnDownloadOpml.set_sensitive(True)
|
||||
self.entryURL.set_sensitive(True)
|
||||
@util.idle_add
|
||||
def update_ui():
|
||||
if self.podcasts_progress_indicator is not None:
|
||||
self.podcasts_progress_indicator.on_finished()
|
||||
self.podcasts_progress_indicator = None
|
||||
|
||||
tv.set_model(model)
|
||||
tv.set_sensitive(True)
|
||||
if original_provider != self.current_provider:
|
||||
logger.warn('Ignoring update from old thread')
|
||||
return
|
||||
|
||||
def is_search_term(self, url):
|
||||
return ('://' not in url and not os.path.exists(url))
|
||||
self.podcasts_model.load(podcasts or [])
|
||||
self.en_query.set_sensitive(True)
|
||||
self.bt_search.set_sensitive(True)
|
||||
self.tag_cloud.set_sensitive(True)
|
||||
self.en_query.grab_focus()
|
||||
|
||||
def thread_func(self, tab=0):
|
||||
if tab == 1:
|
||||
model = OpmlListModel(opml.Importer(my.TOPLIST_OPML))
|
||||
if len(model) == 0:
|
||||
self.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
|
||||
elif tab == 2:
|
||||
model = OpmlListModel(youtube.find_youtube_channels(self.entryYoutubeSearch.get_text()))
|
||||
if len(model) == 0:
|
||||
self.notification(_('There are no YouTube channels that would match this query.'), _('No channels found'))
|
||||
else:
|
||||
url = self.entryURL.get_text()
|
||||
if self.is_search_term(url):
|
||||
url = 'http://gpodder.net/search.opml?q=' + urllib.quote(url)
|
||||
model = OpmlListModel(opml.Importer(url))
|
||||
if len(model) == 0:
|
||||
self.notification(_('The specified URL does not provide any valid OPML podcast items.'), _('No feeds found'))
|
||||
def on_bt_search_clicked(self, widget):
|
||||
if self.current_provider is None:
|
||||
return
|
||||
|
||||
util.idle_add(self.thread_finished, model, tab)
|
||||
|
||||
def download_opml_file(self, url):
|
||||
self.entryURL.set_text(url)
|
||||
self.btnDownloadOpml.set_sensitive(False)
|
||||
self.entryURL.set_sensitive(False)
|
||||
self.btnOK.set_sensitive(False)
|
||||
self.treeviewChannelChooser.set_sensitive(False)
|
||||
util.run_in_background(self.thread_func)
|
||||
util.run_in_background(lambda: self.thread_func(1))
|
||||
query = self.en_query.get_text()
|
||||
|
||||
def select_all( self, value ):
|
||||
enabled = False
|
||||
model = self.get_treeview().get_model()
|
||||
if model is not None:
|
||||
for row in model:
|
||||
row[OpmlListModel.C_SELECTED] = value
|
||||
if value:
|
||||
enabled = True
|
||||
self.btnOK.set_sensitive(enabled)
|
||||
@self.obtain_podcasts_with
|
||||
def load_data():
|
||||
if self.current_provider.kind == directory.Provider.PROVIDER_SEARCH:
|
||||
return self.current_provider.on_search(query)
|
||||
elif self.current_provider.kind == directory.Provider.PROVIDER_URL:
|
||||
return self.current_provider.on_url(query)
|
||||
elif self.current_provider.kind == directory.Provider.PROVIDER_FILE:
|
||||
return self.current_provider.on_file(query)
|
||||
|
||||
def on_gPodderPodcastDirectory_destroy(self, widget, *args):
|
||||
pass
|
||||
|
||||
def on_btnDownloadOpml_clicked(self, widget, *args):
|
||||
self.download_opml_file(self.entryURL.get_text())
|
||||
|
||||
def on_btnSearchYouTube_clicked(self, widget, *args):
|
||||
self.entryYoutubeSearch.set_sensitive(False)
|
||||
self.treeviewYouTubeChooser.set_sensitive(False)
|
||||
self.btnSearchYouTube.set_sensitive(False)
|
||||
util.run_in_background(lambda: self.thread_func(2))
|
||||
def on_can_subscribe_changed(self, can_subscribe):
|
||||
self.btnOK.set_sensitive(can_subscribe)
|
||||
|
||||
def on_btnSelectAll_clicked(self, widget, *args):
|
||||
self.select_all(True)
|
||||
|
||||
self.podcasts_model.set_selection_to(True)
|
||||
|
||||
def on_btnSelectNone_clicked(self, widget, *args):
|
||||
self.select_all(False)
|
||||
self.podcasts_model.set_selection_to(False)
|
||||
|
||||
def on_btnOK_clicked(self, widget, *args):
|
||||
channels = self.get_selected_channels()
|
||||
self.gPodderPodcastDirectory.destroy()
|
||||
urls = self.podcasts_model.get_selected_podcasts()
|
||||
|
||||
self.main_window.destroy()
|
||||
|
||||
# add channels that have been selected
|
||||
if self.add_podcast_list is not None:
|
||||
self.add_podcast_list(channels)
|
||||
self.add_podcast_list(urls)
|
||||
|
||||
def on_btnCancel_clicked(self, widget, *args):
|
||||
self.gPodderPodcastDirectory.destroy()
|
||||
|
||||
def on_entryYoutubeSearch_key_press_event(self, widget, event):
|
||||
if event.keyval == gtk.keysyms.Return:
|
||||
self.on_btnSearchYouTube_clicked(widget)
|
||||
|
||||
def get_treeview(self, tab=None):
|
||||
if tab is None:
|
||||
tab = self.notebookChannelAdder.get_current_page()
|
||||
|
||||
if tab == 0:
|
||||
return self.treeviewChannelChooser
|
||||
elif tab == 1:
|
||||
return self.treeviewTopPodcastsChooser
|
||||
else:
|
||||
return self.treeviewYouTubeChooser
|
||||
|
||||
self.main_window.destroy()
|
||||
|
|
|
@ -1,80 +1,69 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# gPodder - A media aggregator and podcast client
|
||||
# Copyright (c) 2005-2014 Thomas Perl and the gPodder Team
|
||||
#
|
||||
# gPodder is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# gPodder is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
|
||||
import gtk
|
||||
import gobject
|
||||
import cgi
|
||||
|
||||
tags = (
|
||||
('Electronica', 5),
|
||||
('Reggae', 5),
|
||||
('Electro', 20),
|
||||
('Detroit Techno', 4),
|
||||
('Funk', 14),
|
||||
('Jazz', 4),
|
||||
('Minimal', 20),
|
||||
('Soulful Drum and Bass', 6),
|
||||
('Dub', 7),
|
||||
('Drum and Bass', 23),
|
||||
('Deep Techno', 7),
|
||||
('Deephouse', 27),
|
||||
('Soulful', 9),
|
||||
('Minimal Techno', 30),
|
||||
('Downtempo', 17),
|
||||
('House', 29),
|
||||
('Dubstep', 14),
|
||||
('Techno', 32),
|
||||
('Electrotech', 8),
|
||||
('Techhouse', 28),
|
||||
('Disco', 15),
|
||||
('Downbeat', 28),
|
||||
('Electrohouse', 14),
|
||||
('Hiphop', 25),
|
||||
('Trance', 6),
|
||||
('Freestyle', 14),
|
||||
('Funky House', 3),
|
||||
('Minimal House', 4),
|
||||
('Nu Jazz', 11),
|
||||
('Chill-Out', 6),
|
||||
('Breaks', 10),
|
||||
('UK Garage', 4),
|
||||
('Soul', 10),
|
||||
('Progressive House', 3),
|
||||
('Lounge', 6),
|
||||
)
|
||||
|
||||
|
||||
class TagCloud(gtk.Layout):
|
||||
__gsignals__ = {
|
||||
'selected': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
|
||||
(gobject.TYPE_STRING,))
|
||||
}
|
||||
|
||||
def __init__(self, tags, min_size=20, max_size=36):
|
||||
def __init__(self, min_size=20, max_size=36):
|
||||
self.__gobject_init__()
|
||||
gtk.Layout.__init__(self)
|
||||
self._tags = tags
|
||||
self._min_weight = 0
|
||||
self._max_weight = 0
|
||||
self._min_size = min_size
|
||||
self._max_size = max_size
|
||||
self._min_weight = min(weight for tag, weight in self._tags)
|
||||
self._max_weight = max(weight for tag, weight in self._tags)
|
||||
self._size = 0, 0
|
||||
self._alloc_id = self.connect('size-allocate', self._on_size_allocate)
|
||||
self._init_tags()
|
||||
self._in_relayout = False
|
||||
|
||||
def clear_tags(self):
|
||||
for child in self.get_children():
|
||||
self.remove(child)
|
||||
|
||||
def set_tags(self, tags):
|
||||
tags = list(tags)
|
||||
self._min_weight = min(weight for tag, weight in tags)
|
||||
self._max_weight = max(weight for tag, weight in tags)
|
||||
|
||||
for tag, weight in tags:
|
||||
label = gtk.Label()
|
||||
markup = '<span size="%d">%s</span>' % (1000*self._scale(weight), cgi.escape(tag))
|
||||
label.set_markup(markup)
|
||||
button = gtk.ToolButton(label)
|
||||
button.connect('clicked', lambda b, t: self.emit('selected', t), tag)
|
||||
self.put(button, 1, 1)
|
||||
button.show_all()
|
||||
|
||||
self.relayout()
|
||||
|
||||
def _on_size_allocate(self, widget, allocation):
|
||||
self._size = (allocation.width, allocation.height)
|
||||
if not self._in_relayout:
|
||||
self.relayout()
|
||||
|
||||
def _init_tags(self):
|
||||
for tag, weight in self._tags:
|
||||
label = gtk.Label()
|
||||
markup = '<span size="%d">%s</span>' % (1000*self._scale(weight), cgi.escape(tag))
|
||||
label.set_markup(markup)
|
||||
button = gtk.ToolButton(label)
|
||||
button.connect('clicked', lambda b: self.emit('selected', tag))
|
||||
self.put(button, 1, 1)
|
||||
|
||||
def _scale(self, weight):
|
||||
weight_range = float(self._max_weight-self._min_weight)
|
||||
ratio = float(weight-self._min_weight)/weight_range
|
||||
|
@ -110,32 +99,5 @@ class TagCloud(gtk.Layout):
|
|||
self._in_relayout = False
|
||||
return False
|
||||
gobject.idle_add(unrelayout)
|
||||
|
||||
gobject.type_register(TagCloud)
|
||||
|
||||
if __name__ == '__main__':
|
||||
l = TagCloud(tags)
|
||||
|
||||
try:
|
||||
import hildon
|
||||
w = hildon.StackableWindow()
|
||||
sw = hildon.PannableArea()
|
||||
except:
|
||||
w = gtk.Window()
|
||||
w.set_default_size(600, 300)
|
||||
sw = gtk.ScrolledWindow()
|
||||
sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
|
||||
|
||||
w.set_title('Tag cloud Demo')
|
||||
w.add(sw)
|
||||
sw.add(l)
|
||||
|
||||
def on_tag_selected(cloud, tag):
|
||||
print 'tag selected:', tag
|
||||
|
||||
l.connect('selected', on_tag_selected)
|
||||
|
||||
w.show_all()
|
||||
w.connect('destroy', gtk.main_quit)
|
||||
|
||||
gtk.main()
|
||||
|
|
@ -3123,9 +3123,9 @@ class gPodder(BuilderWidget, dbus.service.Object):
|
|||
dlg.destroy()
|
||||
|
||||
def on_itemImportChannels_activate(self, widget, *args):
|
||||
dir = gPodderPodcastDirectory(self.main_window, _config=self.config, \
|
||||
self._podcast_directory = gPodderPodcastDirectory(self.main_window,
|
||||
_config=self.config,
|
||||
add_podcast_list=self.add_podcast_list)
|
||||
util.idle_add(dir.download_opml_file, my.EXAMPLES_OPML)
|
||||
|
||||
def on_homepage_activate(self, widget, *args):
|
||||
util.open_website(gpodder.__url__)
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# gPodder - A media aggregator and podcast client
|
||||
# Copyright (c) 2005-2014 Thomas Perl and the gPodder Team
|
||||
#
|
||||
# gPodder is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# gPodder is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
|
||||
#
|
||||
# gpodder.gtkui.opml - Module for displaying OPML feeds (2009-08-13)
|
||||
#
|
||||
|
||||
|
||||
import gtk
|
||||
|
||||
import cgi
|
||||
import urllib
|
||||
|
||||
class OpmlListModel(gtk.ListStore):
|
||||
C_SELECTED, C_TITLE, C_DESCRIPTION_MARKUP, C_URL = range(4)
|
||||
|
||||
def __init__(self, importer):
|
||||
gtk.ListStore.__init__(self, bool, str, str, str)
|
||||
for channel in importer.items:
|
||||
self.append([False, channel['title'],
|
||||
self._format_channel(channel), channel['url']])
|
||||
|
||||
def _format_channel(self, channel):
|
||||
title = cgi.escape(urllib.unquote_plus(channel['title']))
|
||||
description = cgi.escape(channel['description'])
|
||||
return '<b>%s</b>\n%s' % (title, description)
|
||||
|
|
@ -72,8 +72,6 @@ from mygpoclient import public
|
|||
|
||||
from mygpoclient import util as mygpoutil
|
||||
|
||||
EXAMPLES_OPML = 'http://gpodder.org/directory.opml'
|
||||
TOPLIST_OPML = 'http://gpodder.org/toplist.opml'
|
||||
EPISODE_ACTIONS_BATCH_SIZE=100
|
||||
|
||||
# Database model classes
|
||||
|
|
|
@ -40,6 +40,7 @@ import time
|
|||
|
||||
import re
|
||||
import email
|
||||
import urllib
|
||||
|
||||
|
||||
# gPodder's consumer key for the Soundcloud API
|
||||
|
@ -227,3 +228,7 @@ class SoundcloudFavFeed(SoundcloudFeed):
|
|||
# Register our URL handlers
|
||||
model.register_custom_handler(SoundcloudFeed)
|
||||
model.register_custom_handler(SoundcloudFavFeed)
|
||||
|
||||
def search_for_user(query):
|
||||
json_url = 'http://api.soundcloud.com/users.json?q=%s&consumer_key=%s' % (urllib.quote(query), CONSUMER_KEY)
|
||||
return json.load(util.urlopen(json_url))
|
||||
|
|
|
@ -219,29 +219,3 @@ def get_real_cover(url):
|
|||
return None
|
||||
|
||||
return for_each_feed_pattern(return_user_cover, url, None)
|
||||
|
||||
def find_youtube_channels(string):
|
||||
url = 'http://gdata.youtube.com/feeds/api/videos?alt=json&q=%s' % urllib.quote(string, '')
|
||||
data = json.load(util.urlopen(url))
|
||||
|
||||
class FakeImporter(object):
|
||||
def __init__(self):
|
||||
self.items = []
|
||||
|
||||
result = FakeImporter()
|
||||
|
||||
seen_users = set()
|
||||
for entry in data['feed']['entry']:
|
||||
user = os.path.basename(entry['author'][0]['uri']['$t'])
|
||||
title = entry['title']['$t']
|
||||
url = 'http://www.youtube.com/rss/user/%s/videos.rss' % user
|
||||
if user not in seen_users:
|
||||
result.items.append({
|
||||
'title': user,
|
||||
'url': url,
|
||||
'description': title
|
||||
})
|
||||
seen_users.add(user)
|
||||
|
||||
return result
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
gPodder "tres" draft implementation / prototype code
|
|
@ -1,201 +0,0 @@
|
|||
|
||||
import gtk
|
||||
import gobject
|
||||
import pango
|
||||
import tagcloud
|
||||
import json
|
||||
|
||||
w = gtk.Dialog()
|
||||
w.set_title('Discover new podcasts')
|
||||
w.set_default_size(650, 450)
|
||||
|
||||
tv = gtk.TreeView()
|
||||
tv.set_headers_visible(False)
|
||||
tv.set_size_request(160, -1)
|
||||
|
||||
class OpmlEdit(object): pass
|
||||
class Search(object): pass
|
||||
class OpmlFixed(object): pass
|
||||
class TagCloud(object): pass
|
||||
|
||||
search_providers = (
|
||||
('gpodder.net', 'search_gpodder.png', Search),
|
||||
('YouTube', 'search_youtube.png', Search),
|
||||
('SoundCloud', 'search_soundcloud.png', Search),
|
||||
('Miro Guide', 'search_miro.png', Search),
|
||||
)
|
||||
|
||||
directory_providers = (
|
||||
('Toplist', 'directory_toplist.png', OpmlFixed),
|
||||
('Examples', 'directory_example.png', OpmlFixed),
|
||||
('Tag cloud', 'directory_tags.png', TagCloud),
|
||||
)
|
||||
|
||||
SEPARATOR = (True, pango.WEIGHT_NORMAL, '', None, None)
|
||||
C_SEPARATOR, C_WEIGHT, C_TEXT, C_ICON, C_PROVIDER = range(5)
|
||||
store = gtk.ListStore(bool, int, str, gtk.gdk.Pixbuf, object)
|
||||
|
||||
opml_pixbuf = gtk.gdk.pixbuf_new_from_file('directory_opml.png')
|
||||
store.append((False, pango.WEIGHT_NORMAL, 'OPML', opml_pixbuf, OpmlEdit))
|
||||
|
||||
store.append(SEPARATOR)
|
||||
|
||||
for name, icon, provider in search_providers:
|
||||
pixbuf = gtk.gdk.pixbuf_new_from_file(icon)
|
||||
store.append((False, pango.WEIGHT_NORMAL, name, pixbuf, provider))
|
||||
|
||||
store.append(SEPARATOR)
|
||||
|
||||
for name, icon, provider in directory_providers:
|
||||
pixbuf = gtk.gdk.pixbuf_new_from_file(icon)
|
||||
store.append((False, pango.WEIGHT_NORMAL, name, pixbuf, provider))
|
||||
|
||||
store.append(SEPARATOR)
|
||||
|
||||
for i in range(1, 5):
|
||||
store.append((False, pango.WEIGHT_NORMAL, 'Bookmark %d' % i, None, None))
|
||||
|
||||
tv.set_model(store)
|
||||
|
||||
def is_row_separator(model, iter):
|
||||
return model.get_value(iter, C_SEPARATOR)
|
||||
|
||||
tv.set_row_separator_func(is_row_separator)
|
||||
|
||||
column = gtk.TreeViewColumn('')
|
||||
cell = gtk.CellRendererPixbuf()
|
||||
column.pack_start(cell, False)
|
||||
column.add_attribute(cell, 'pixbuf', C_ICON)
|
||||
cell = gtk.CellRendererText()
|
||||
column.pack_start(cell)
|
||||
column.add_attribute(cell, 'text', C_TEXT)
|
||||
column.add_attribute(cell, 'weight', C_WEIGHT)
|
||||
tv.append_column(column)
|
||||
|
||||
def on_row_activated(treeview, path, column):
|
||||
model = treeview.get_model()
|
||||
iter = model.get_iter(path)
|
||||
|
||||
for row in model:
|
||||
row[C_WEIGHT] = pango.WEIGHT_NORMAL
|
||||
|
||||
if iter:
|
||||
model.set_value(iter, C_WEIGHT, pango.WEIGHT_BOLD)
|
||||
provider = model.get_value(iter, C_PROVIDER)
|
||||
use_provider(provider)
|
||||
|
||||
def on_cursor_changed(treeview):
|
||||
path, column = treeview.get_cursor()
|
||||
on_row_activated(treeview, path, column)
|
||||
|
||||
tv.connect('row-activated', on_row_activated)
|
||||
tv.connect('cursor-changed', on_cursor_changed)
|
||||
|
||||
sw = gtk.ScrolledWindow()
|
||||
sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
|
||||
sw.set_shadow_type(gtk.SHADOW_IN)
|
||||
sw.add(tv)
|
||||
|
||||
sidebar = gtk.VBox()
|
||||
sidebar.set_spacing(6)
|
||||
|
||||
sidebar.pack_start(sw, True, True)
|
||||
sidebar.pack_start(gtk.Button('Add bookmark'), False, False)
|
||||
|
||||
vb = gtk.VBox()
|
||||
vb.set_spacing(6)
|
||||
|
||||
title_label = gtk.Label('Title')
|
||||
title_label.set_alignment(0, 0)
|
||||
vb.pack_start(title_label, False, False)
|
||||
|
||||
search_hbox = gtk.HBox()
|
||||
search_hbox.set_spacing(6)
|
||||
search_label = gtk.Label('')
|
||||
search_hbox.pack_start(search_label, False, False)
|
||||
search_entry = gtk.Entry()
|
||||
search_hbox.pack_start(search_entry, True, True)
|
||||
search_button = gtk.Button('')
|
||||
search_hbox.pack_start(search_button, False, False)
|
||||
|
||||
vb.pack_start(search_hbox, False, False)
|
||||
|
||||
tagcloud_sw = gtk.ScrolledWindow()
|
||||
tagcloud_sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
|
||||
tagcloud_sw.set_shadow_type(gtk.SHADOW_IN)
|
||||
podcast_tags = json.loads("""
|
||||
[
|
||||
{"tag": "Technology",
|
||||
"usage": 530 },
|
||||
{"tag": "Society & Culture",
|
||||
"usage": 420 },
|
||||
{"tag": "Arts",
|
||||
"usage": 400},
|
||||
{"tag": "News & Politics",
|
||||
"usage": 320}
|
||||
]
|
||||
""")
|
||||
tagcloudw = tagcloud.TagCloud(list((x['tag'], x['usage']) for x in podcast_tags), 10, 14)
|
||||
tagcloud_sw.set_size_request(-1, 130)
|
||||
tagcloud_sw.add(tagcloudw)
|
||||
vb.pack_start(tagcloud_sw, False, False)
|
||||
|
||||
podcasts_sw = gtk.ScrolledWindow()
|
||||
podcasts_sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
|
||||
podcasts_sw.set_shadow_type(gtk.SHADOW_IN)
|
||||
podcasts_tv = gtk.TreeView()
|
||||
podcasts_sw.add(podcasts_tv)
|
||||
vb.pack_start(podcasts_sw, True, True)
|
||||
|
||||
|
||||
hb = gtk.HBox()
|
||||
hb.set_spacing(12)
|
||||
hb.set_border_width(12)
|
||||
hb.pack_start(sidebar, False, True)
|
||||
hb.pack_start(vb, True, True)
|
||||
|
||||
w.vbox.add(hb)
|
||||
w.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
|
||||
w.add_button('Subscribe', gtk.RESPONSE_OK)
|
||||
w.set_response_sensitive(gtk.RESPONSE_OK, False)
|
||||
|
||||
def use_provider(provider):
|
||||
if provider == OpmlEdit:
|
||||
search_label.set_text('URL:')
|
||||
search_button.set_label('Download')
|
||||
else:
|
||||
search_label.set_text('Search:')
|
||||
search_button.set_label('Search')
|
||||
|
||||
if provider in (OpmlEdit, Search):
|
||||
title_label.hide()
|
||||
search_hbox.show()
|
||||
search_entry.set_text('')
|
||||
def later():
|
||||
search_entry.grab_focus()
|
||||
return False
|
||||
gobject.idle_add(later)
|
||||
elif provider == TagCloud:
|
||||
title_label.hide()
|
||||
search_hbox.hide()
|
||||
else:
|
||||
if provider == OpmlFixed:
|
||||
title_label.set_text('Example stuff')
|
||||
elif provider == TagCloud:
|
||||
title_label.set_text('Tag cloud')
|
||||
title_label.show()
|
||||
search_hbox.hide()
|
||||
|
||||
tagcloud_sw.set_visible(provider == TagCloud)
|
||||
|
||||
print 'using provider:', provider
|
||||
|
||||
#w.connect('destroy', gtk.main_quit)
|
||||
w.show_all()
|
||||
|
||||
on_row_activated(tv, (0,), None)
|
||||
|
||||
w.run()
|
||||
|
||||
#gtk.main()
|
||||
|
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 973 B |
Before Width: | Height: | Size: 766 B |