From bfb0ab26a5786c8e337f91282dde034adce26b5c Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Wed, 16 Apr 2025 15:37:49 +0300 Subject: [PATCH 1/5] Switch itunes plugin to f-strings. Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- src/gpodder/plugins/itunes.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/gpodder/plugins/itunes.py b/src/gpodder/plugins/itunes.py index 27754cd..473bda6 100644 --- a/src/gpodder/plugins/itunes.py +++ b/src/gpodder/plugins/itunes.py @@ -39,26 +39,26 @@ def itunes_feed_handler(channel, max_episodes, config): logger.debug('Detected iTunes feed.') - itunes_lookup_url = 'https://itunes.apple.com/lookup?entity=podcast&id=' + m.group('podcast_id') + itunes_lookup_url = f'https://itunes.apple.com/lookup?entity=podcast&id={m.group("podcast_id")}' try: json_data = util.read_json(itunes_lookup_url) if len(json_data['results']) != 1: - raise ITunesFeedException('Unsupported number of results: ' + str(len(json_data['results']))) + raise ITunesFeedException(f'Unsupported number of results: {str(len(json_data["results"]))}') feed_url = util.normalize_feed_url(json_data['results'][0]['feedUrl']) if not feed_url: - raise ITunesFeedException('Could not resolve real feed URL from iTunes feed.\nDetected URL: ' + json_data['results'][0]['feedUrl']) + raise ITunesFeedException(f'Could not resolve real feed URL from iTunes feed.\nDetected URL: {json_data["results"][0]["feedUrl"]}') - logger.info('Resolved iTunes feed URL: {} -> {}'.format(channel.url, feed_url)) + logger.info(f'Resolved iTunes feed URL: {channel.url} -> {feed_url}') channel.url = feed_url # Delegate further processing of the feed to the normal podcast parser # by returning None (will try the next handler in the resolver chain) return None except Exception as ex: - logger.warn('Cannot resolve iTunes feed: {}'.format(str(ex))) + logger.warn(f'Cannot resolve iTunes feed: {str(ex)}') raise @registry.directory.register_instance @@ -69,6 +69,6 @@ def __init__(self): self.priority = directory.Provider.PRIORITY_SECONDARY_SEARCH def on_search(self, query): - json_url = 'https://itunes.apple.com/search?media=podcast&term={}'.format(urllib.parse.quote(query)) + json_url = f'https://itunes.apple.com/search?media=podcast&term={urllib.parse.quote(query)}' return [directory.DirectoryEntry(entry['collectionName'], entry['feedUrl'], entry['artworkUrl100']) for entry in util.read_json(json_url)['results']] From 602e9dfc6ea172d2dd454ae27255afa3d6d7760a Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Fri, 25 Apr 2025 15:53:47 +0300 Subject: [PATCH 2/5] Add pagination support to itunes plugin Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- src/gpodder/plugins/itunes.py | 36 ++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/gpodder/plugins/itunes.py b/src/gpodder/plugins/itunes.py index 473bda6..9c67478 100644 --- a/src/gpodder/plugins/itunes.py +++ b/src/gpodder/plugins/itunes.py @@ -1,7 +1,6 @@ - # # gpodder.plugins.itunes: Resolve iTunes feed URLs (based on a gist by Yepoleb, 2014-03-09) -# Copyright (c) 2014, Thomas Perl +# Copyright (c) 2014-2025, Thomas Perl . E.S. Rosenberg (keeper-of-the-keys) # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above @@ -26,6 +25,9 @@ import urllib.parse logger = logging.getLogger(__name__) +# 200 is the upper limit according to +# https://performance-partners.apple.com/search-api +PAGE_SIZE = 200 class ITunesFeedException(Exception): pass @@ -69,6 +71,30 @@ def __init__(self): self.priority = directory.Provider.PRIORITY_SECONDARY_SEARCH def on_search(self, query): - json_url = f'https://itunes.apple.com/search?media=podcast&term={urllib.parse.quote(query)}' - - return [directory.DirectoryEntry(entry['collectionName'], entry['feedUrl'], entry['artworkUrl100']) for entry in util.read_json(json_url)['results']] + offset = 0 + + while True: + json_url = f'https://itunes.apple.com/search?media=podcast&term={urllib.parse.quote(query)}&limit={PAGE_SIZE}&offset={offset}' + json_data = util.read_json(json_url) + + if json_data['resultCount'] > 0: + for entry in json_data['results']: + if entry.get('feedUrl') is None: + continue + + title = entry['collectionName'] + url = entry['feedUrl'] + image = entry['artworkUrl100'] + + yield(directory.DirectoryEntry(title, url, image)) + returned_res += 1 + + offset = offset + json_data['resultCount'] + else: + ''' + Unlike the podverse stop condition where we detect a resultCount smaller than the page size for apple we can only stop when 0 results + are returned because the API seems to consistently return more than the page size and does this in an inconsistent fasion, most often + returning 210 results but based on my observartion any number between page size and page size + 10 is possible. + With an API that does not obey its own rules the only valid stop condition is no results. + ''' + break From 03d50c5191f498fd0e230f4475202230c82ba5a8 Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Sun, 11 May 2025 15:09:51 +0300 Subject: [PATCH 3/5] @thp requested changes Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- src/gpodder/plugins/itunes.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/gpodder/plugins/itunes.py b/src/gpodder/plugins/itunes.py index 9c67478..1e90ba5 100644 --- a/src/gpodder/plugins/itunes.py +++ b/src/gpodder/plugins/itunes.py @@ -1,6 +1,7 @@ # # gpodder.plugins.itunes: Resolve iTunes feed URLs (based on a gist by Yepoleb, 2014-03-09) -# Copyright (c) 2014-2025, Thomas Perl . E.S. Rosenberg (keeper-of-the-keys) +# Copyright (c) 2014, Thomas Perl . +# Copyright (c) 2025, E.S. Rosenberg (Keeper-of-the-Keys). # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above @@ -46,7 +47,7 @@ def itunes_feed_handler(channel, max_episodes, config): json_data = util.read_json(itunes_lookup_url) if len(json_data['results']) != 1: - raise ITunesFeedException(f'Unsupported number of results: {str(len(json_data["results"]))}') + raise ITunesFeedException(f'Unsupported number of results: {len(json_data["results"])}') feed_url = util.normalize_feed_url(json_data['results'][0]['feedUrl']) @@ -60,7 +61,7 @@ def itunes_feed_handler(channel, max_episodes, config): # by returning None (will try the next handler in the resolver chain) return None except Exception as ex: - logger.warn(f'Cannot resolve iTunes feed: {str(ex)}') + logger.warn(f'Cannot resolve iTunes feed: {ex}') raise @registry.directory.register_instance @@ -86,10 +87,10 @@ def on_search(self, query): url = entry['feedUrl'] image = entry['artworkUrl100'] - yield(directory.DirectoryEntry(title, url, image)) + yield directory.DirectoryEntry(title, url, image) returned_res += 1 - offset = offset + json_data['resultCount'] + offset += json_data['resultCount'] else: ''' Unlike the podverse stop condition where we detect a resultCount smaller than the page size for apple we can only stop when 0 results From d61ba71ad9594916fb9cb73ff34d362ee405eeb6 Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Sun, 11 May 2025 15:50:05 +0300 Subject: [PATCH 4/5] PEP-8 Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- src/gpodder/plugins/itunes.py | 62 ++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/src/gpodder/plugins/itunes.py b/src/gpodder/plugins/itunes.py index 1e90ba5..482a7e6 100644 --- a/src/gpodder/plugins/itunes.py +++ b/src/gpodder/plugins/itunes.py @@ -1,5 +1,6 @@ # -# gpodder.plugins.itunes: Resolve iTunes feed URLs (based on a gist by Yepoleb, 2014-03-09) +# gpodder.plugins.itunes: Resolve iTunes feed URLs +# (initially based on a gist by Yepoleb, 2014-03-09) # Copyright (c) 2014, Thomas Perl . # Copyright (c) 2025, E.S. Rosenberg (Keeper-of-the-Keys). # @@ -7,13 +8,13 @@ # purpose with or without fee is hereby granted, provided that the above # copyright notice and this permission notice appear in all copies. # -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -# PERFORMANCE OF THIS SOFTWARE. +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER +# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF +# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # @@ -26,7 +27,7 @@ import urllib.parse logger = logging.getLogger(__name__) -# 200 is the upper limit according to +# As of 2025-05-11 200 is the upper limit according to # https://performance-partners.apple.com/search-api PAGE_SIZE = 200 @@ -36,23 +37,35 @@ class ITunesFeedException(Exception): @registry.feed_handler.register def itunes_feed_handler(channel, max_episodes, config): - m = re.match(r'https?://(podcasts|itunes)\.apple\.com/(?:[^/]*/)?podcast/.*id(?P[0-9]+).*$', channel.url, re.I) + expression = ( + r'https?://(podcasts|itunes)\.apple\.com/(?:[^/]*/)?' + r'podcast/.*id(?P[0-9]+).*$' + ) + m = re.match(expression, channel.url, re.I) if m is None: return None logger.debug('Detected iTunes feed.') - itunes_lookup_url = f'https://itunes.apple.com/lookup?entity=podcast&id={m.group("podcast_id")}' + itunes_lookup_url = ( + f'https://itunes.apple.com/lookup?entity=podcast&id=' + f'{m.group("podcast_id")}' + ) try: json_data = util.read_json(itunes_lookup_url) if len(json_data['results']) != 1: - raise ITunesFeedException(f'Unsupported number of results: {len(json_data["results"])}') + raise ITunesFeedException( + f'Unsupported number of results: {len(json_data["results"])}' + ) feed_url = util.normalize_feed_url(json_data['results'][0]['feedUrl']) if not feed_url: - raise ITunesFeedException(f'Could not resolve real feed URL from iTunes feed.\nDetected URL: {json_data["results"][0]["feedUrl"]}') + raise ITunesFeedException( + f'Could not resolve real feed URL from iTunes feed.\n' + f'Detected URL: {json_data["results"][0]["feedUrl"]}' + ) logger.info(f'Resolved iTunes feed URL: {channel.url} -> {feed_url}') channel.url = feed_url @@ -75,7 +88,11 @@ def on_search(self, query): offset = 0 while True: - json_url = f'https://itunes.apple.com/search?media=podcast&term={urllib.parse.quote(query)}&limit={PAGE_SIZE}&offset={offset}' + json_url = ( + f'https://itunes.apple.com/search?media=podcast&term=' + f'{urllib.parse.quote(query)}&limit={PAGE_SIZE}&offset=' + f'{offset}' + ) json_data = util.read_json(json_url) if json_data['resultCount'] > 0: @@ -92,10 +109,15 @@ def on_search(self, query): offset += json_data['resultCount'] else: - ''' - Unlike the podverse stop condition where we detect a resultCount smaller than the page size for apple we can only stop when 0 results - are returned because the API seems to consistently return more than the page size and does this in an inconsistent fasion, most often - returning 210 results but based on my observartion any number between page size and page size + 10 is possible. - With an API that does not obey its own rules the only valid stop condition is no results. - ''' + # Unlike the podverse stop condition where we detect a resultCount + # smaller than the page size for apple we can only stop when 0 + # results are returned because the API seems to consistently + # return more than the page size and does this in an inconsistent + # fasion, most often returning 210 results but based on my + # observartion any number between page size and page size + 10 is + # possible. + # + # With an API that does not obey its own rules the only valid stop + # condition is no results. + break From 2e344832b2d7eb89e4c14df1c1834d1174b50b32 Mon Sep 17 00:00:00 2001 From: "E.S. Rosenberg a.k.a. Keeper of the Keys" Date: Sun, 11 May 2025 16:00:57 +0300 Subject: [PATCH 5/5] @thp requested change Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys --- src/gpodder/plugins/itunes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gpodder/plugins/itunes.py b/src/gpodder/plugins/itunes.py index 482a7e6..dc24484 100644 --- a/src/gpodder/plugins/itunes.py +++ b/src/gpodder/plugins/itunes.py @@ -97,7 +97,7 @@ def on_search(self, query): if json_data['resultCount'] > 0: for entry in json_data['results']: - if entry.get('feedUrl') is None: + if 'feedUrl' not in entry: continue title = entry['collectionName']