Skip to content

Commit 345956f

Browse files
committed
Fix gpodder#1405 Group subscriptions in exported OPML files
also implement sections in OPML import. Adapted from the same feature by Keeper-of-the-Keys in gpodder-core, thanks! Now any DirectoryProvider can set the channel['section'] to have it added as is.
1 parent 0f55098 commit 345956f

File tree

7 files changed

+52
-18
lines changed

7 files changed

+52
-18
lines changed

bin/gpo

+3-3
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ class gPodderCli(object):
313313

314314
def import_(self, url):
315315
for channel in opml.Importer(url).items:
316-
self.subscribe(channel['url'], channel.get('title'))
316+
self.subscribe(channel['url'], channel.get('title'), channel.get('section'))
317317

318318
def export(self, filename):
319319
podcasts = self._model.get_podcasts()
@@ -369,7 +369,7 @@ class gPodderCli(object):
369369
self._error(_('You are not subscribed to %s.') % url)
370370
return None
371371

372-
def subscribe(self, url, title=None):
372+
def subscribe(self, url, title=None, section=None):
373373
existing = self.get_podcast(url, check_only=True)
374374
if existing is not None:
375375
self._error(_('Already subscribed to %s.') % existing.url)
@@ -814,7 +814,7 @@ class gPodderCli(object):
814814

815815
title, url = results[index - 1]
816816
self._info(_('Adding %s...') % title)
817-
self.subscribe(url)
817+
self.subscribe(url, title=title)
818818
if not multiple:
819819
break
820820

src/gpodder/directory.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,13 @@ def __init__(self, warning):
4444

4545

4646
class DirectoryEntry(object):
47-
def __init__(self, title, url, image=None, subscribers=-1, description=None):
47+
def __init__(self, title, url, image=None, subscribers=-1, description=None, section=None):
4848
self.title = title
4949
self.url = url
5050
self.image = image
5151
self.subscribers = subscribers
5252
self.description = description
53+
self.section = section
5354

5455

5556
class DirectoryTag(object):
@@ -92,7 +93,7 @@ def get_tags(self):
9293

9394

9495
def directory_entry_from_opml(url):
95-
return [DirectoryEntry(d['title'], d['url'], description=d['description']) for d in opml.Importer(url).items]
96+
return [DirectoryEntry(d['title'], d['url'], description=d['description'], section=d['section']) for d in opml.Importer(url).items]
9697

9798

9899
def directory_entry_from_mygpo_json(url):

src/gpodder/gtkui/desktop/podcastdirectory.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -43,21 +43,21 @@
4343

4444

4545
class DirectoryPodcastsModel(Gtk.ListStore):
46-
C_SELECTED, C_MARKUP, C_TITLE, C_URL = list(range(4))
46+
C_SELECTED, C_MARKUP, C_TITLE, C_URL, C_SECTION = list(range(5))
4747

4848
def __init__(self, callback_can_subscribe):
49-
Gtk.ListStore.__init__(self, bool, str, str, str)
49+
Gtk.ListStore.__init__(self, bool, str, str, str, str)
5050
self.callback_can_subscribe = callback_can_subscribe
5151

5252
def load(self, directory_entries):
5353
self.clear()
5454
for entry in directory_entries:
5555
if entry.subscribers != -1:
5656
self.append((False, '%s (%d)\n<small>%s</small>' % (html.escape(entry.title),
57-
entry.subscribers, html.escape(entry.url)), entry.title, entry.url))
57+
entry.subscribers, html.escape(entry.url)), entry.title, entry.url, entry.section))
5858
else:
5959
self.append((False, '%s\n<small>%s</small>' % (html.escape(entry.title),
60-
html.escape(entry.url)), entry.title, entry.url))
60+
html.escape(entry.url)), entry.title, entry.url, entry.section))
6161
self.callback_can_subscribe(len(self.get_selected_podcasts()) > 0)
6262

6363
def toggle(self, path):
@@ -70,7 +70,7 @@ def set_selection_to(self, selected):
7070
self.callback_can_subscribe(len(self.get_selected_podcasts()) > 0)
7171

7272
def get_selected_podcasts(self):
73-
return [(row[self.C_TITLE], row[self.C_URL]) for row in self if row[self.C_SELECTED]]
73+
return [(row[self.C_TITLE], row[self.C_URL], row[self.C_SECTION]) for row in self if row[self.C_SELECTED]]
7474

7575

7676
class DirectoryProvidersModel(Gtk.ListStore):
@@ -159,7 +159,7 @@ def setup_podcasts_treeview(self):
159159
self.tv_podcasts.append_column(column)
160160

161161
self.tv_podcasts.set_model(self.podcasts_model)
162-
self.podcasts_model.append((False, 'a', 'b', 'c'))
162+
self.podcasts_model.append((False, 'a', 'b', 'c', 'd'))
163163

164164
def setup_providers_treeview(self):
165165
column = Gtk.TreeViewColumn('')

src/gpodder/gtkui/interface/addpodcast.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,4 @@ def on_btn_add_clicked(self, widget):
9292
self.on_btn_close_clicked(widget)
9393
if self.add_podcast_list is not None:
9494
title = None # FIXME: Add title GUI element
95-
self.add_podcast_list([(title, url)])
95+
self.add_podcast_list([(title, url, None)])

src/gpodder/gtkui/main.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,8 @@ def execute_podcast_actions(selected):
610610
# In the future, we might retrieve the title from gpodder.net here,
611611
# but for now, we just use "None" to use the feed-provided title
612612
title = None
613-
add_list = [(title, c.action.url)
613+
section = None
614+
add_list = [(title, c.action.url, section)
614615
for c in selected if c.action.is_add]
615616
remove_list = [c.podcast for c in selected if c.action.is_remove]
616617

@@ -2561,9 +2562,12 @@ def add_podcast_list(self, podcasts, auth_tokens=None):
25612562
# For a given URL, the desired title (or None)
25622563
title_for_url = {}
25632564

2565+
# For a given URL, the desired section (or None)
2566+
section_for_url = {}
2567+
25642568
# Sort and split the URL list into five buckets
25652569
queued, failed, existing, worked, authreq = [], [], [], [], []
2566-
for input_title, input_url in podcasts:
2570+
for input_title, input_url, input_section in podcasts:
25672571
url = util.normalize_feed_url(input_url)
25682572

25692573
# Check if it's a YouTube channel, user, or playlist and resolves it to its feed if that's the case
@@ -2580,6 +2584,7 @@ def add_podcast_list(self, podcasts, auth_tokens=None):
25802584
else:
25812585
# This URL has survived the first round - queue for add
25822586
title_for_url[url] = input_title
2587+
section_for_url[url] = input_section
25832588
queued.append(url)
25842589
if url != input_url and input_url in auth_tokens:
25852590
auth_tokens[url] = auth_tokens[input_url]
@@ -2654,7 +2659,7 @@ def on_after_update():
26542659

26552660
# If we have authentication data to retry, do so here
26562661
if retry_podcasts:
2657-
podcasts = [(title_for_url.get(url), url)
2662+
podcasts = [(title_for_url.get(url), url, section_for_url.get(url))
26582663
for url in list(retry_podcasts.keys())]
26592664
self.add_podcast_list(podcasts, retry_podcasts)
26602665
# This will NOT show new episodes for podcasts that have
@@ -2680,6 +2685,7 @@ def thread_proc():
26802685
length = len(queued)
26812686
for index, url in enumerate(queued):
26822687
title = title_for_url.get(url)
2688+
section = section_for_url.get(url)
26832689
progress.on_progress(float(index) / float(length))
26842690
progress.on_message(title or url)
26852691
try:
@@ -2696,6 +2702,8 @@ def thread_proc():
26962702
if title is not None:
26972703
# Prefer title from subscription source (bug 1711)
26982704
channel.title = title
2705+
if section is not None:
2706+
channel.section = section
26992707

27002708
if username is not None and channel.auth_username is None and \
27012709
password is not None and channel.auth_password is None:

src/gpodder/my.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -657,11 +657,11 @@ def __init__(self):
657657
self.client = public.PublicClient()
658658

659659
def toplist(self):
660-
return [(p.title or p.url, p.url)
660+
return [(p.title or p.url, p.url, None)
661661
for p in self.client.get_toplist()
662662
if p.url]
663663

664664
def search(self, query):
665-
return [(p.title or p.url, p.url)
665+
return [(p.title or p.url, p.url, None)
666666
for p in self.client.search_podcasts(query)
667667
if p.url]

src/gpodder/opml.py

+26-1
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,17 @@ def __init__(self, url):
7070
else:
7171
doc = xml.dom.minidom.parse(io.BytesIO(util.urlopen(url).content))
7272

73+
section = None
7374
for outline in doc.getElementsByTagName('outline'):
7475
# Make sure we are dealing with a valid link type (ignore case)
7576
otl_type = outline.getAttribute('type')
7677
if otl_type is None or otl_type.lower() not in self.VALID_TYPES:
78+
breakpoint()
79+
otl_title = outline.getAttribute('title')
80+
otl_text = outline.getAttribute('text')
81+
# gPodder sections will have name == text, if OPML accepts it type=section
82+
if otl_title is not None and otl_title == otl_text:
83+
section = otl_title
7784
continue
7885

7986
if outline.getAttribute('xmlUrl') or outline.getAttribute('url'):
@@ -90,6 +97,7 @@ def __init__(self, url):
9097
outline.getAttribute('text')
9198
or outline.getAttribute('xmlUrl')
9299
or outline.getAttribute('url'),
100+
'section': section or 'audio',
93101
}
94102

95103
if channel['description'] == channel['title']:
@@ -143,6 +151,15 @@ def create_outline(self, doc, channel):
143151
outline.setAttribute('type', self.FEED_TYPE)
144152
return outline
145153

154+
def create_section(self, doc, name):
155+
"""
156+
Creates an empty OPML outline element used to divide sections.
157+
"""
158+
section = doc.createElement('outline')
159+
section.setAttribute('title', name)
160+
section.setAttribute('text', name)
161+
return section
162+
146163
def write(self, channels):
147164
"""
148165
Creates a XML document containing metadata for each
@@ -169,8 +186,16 @@ def write(self, channels):
169186
opml.appendChild(head)
170187

171188
body = doc.createElement('body')
189+
190+
sections = {}
172191
for channel in channels:
173-
body.appendChild(self.create_outline(doc, channel))
192+
if channel.section not in sections.keys():
193+
sections[channel.section] = self.create_section(doc, channel.section)
194+
sections[channel.section].appendChild(self.create_outline(doc, channel))
195+
196+
for section in sections.values():
197+
body.appendChild(section)
198+
174199
opml.appendChild(body)
175200

176201
try:

0 commit comments

Comments
 (0)