diff --git a/share/man/man1/gpo.1 b/share/man/man1/gpo.1 index 1e51169..ad1770c 100644 --- a/share/man/man1/gpo.1 +++ b/share/man/man1/gpo.1 @@ -1,4 +1,4 @@ -.TH GPO "1" "March 2020" "gpodder 4.10.0" "User Commands" +.TH GPO "1" "March 2020" "gpodder 4.11.0" "User Commands" .SH NAME gpo \- gPodder command-line interface .SH SYNOPSIS diff --git a/src/gpodder/__init__.py b/src/gpodder/__init__.py index f27a2fd..94a33fc 100644 --- a/src/gpodder/__init__.py +++ b/src/gpodder/__init__.py @@ -50,9 +50,9 @@ # This metadata block gets parsed by setup.py - use single quotes only __tagline__ = 'Media and podcast aggregator' __author__ = 'Thomas Perl ' -__version__ = '4.10.0' -__date__ = '2020-03-03' -__relname__ = 'Matar' +__version__ = '4.11.0' +__date__ = '2020-03-31' +__relname__ = 'Geshem' __copyright__ = '© 2005-2020 Thomas Perl and the gPodder Team' __license__ = 'ISC / GPLv3 or later' __url__ = 'http://gpodder.org/' diff --git a/src/gpodder/coverart.py b/src/gpodder/coverart.py index e7a05b9..bfb1dfe 100644 --- a/src/gpodder/coverart.py +++ b/src/gpodder/coverart.py @@ -44,9 +44,21 @@ class CoverDownloader(object): def __init__(self, core): self.core = core - def get_cover(self, podcast, download=False): - filename = podcast.cover_file - cover_url = podcast.cover_url + def get_cover(self, podcast, download=False, episode=None): + if episode: + # Get episode art. + filename = episode.art_file + cover_url = episode.episode_art_url + else: + # Get podcast cover. + filename = podcast.cover_file + cover_url = podcast.cover_url + + if not cover_url: + return None + + username = podcast.auth_username + password = podcast.auth_password # Return already existing files for extension in self.EXTENSIONS: @@ -62,8 +74,7 @@ def get_cover(self, podcast, download=False): # We have to add username/password, because password-protected # feeds might keep their cover art also protected (bug 1521) - cover_url = util.url_add_authentication(cover_url, podcast.auth_username, - podcast.auth_password) + cover_url = util.url_add_authentication(cover_url, username, password) try: logger.info('Downloading cover art: %s', cover_url) @@ -80,7 +91,7 @@ def get_cover(self, podcast, download=False): extension = filetype break - if extension is None: + if not extension: msg = 'Unknown file type: %s (%r)' % (cover_url, data[:6]) raise ValueError(msg) diff --git a/src/gpodder/model.py b/src/gpodder/model.py index e0b715f..62d4439 100644 --- a/src/gpodder/model.py +++ b/src/gpodder/model.py @@ -94,6 +94,7 @@ class EpisodeModelFields(minidb.Model): chapters = minidb.JSON subtitle = str description_html = str + episode_art_url = str class PodcastModelFields(minidb.Model): @@ -148,6 +149,7 @@ class __minidb_defaults__: current_position = 0 current_position_updated = 0 last_playback = 0 + episode_art_url = '' def __init__(self, channel): self._parent = channel @@ -462,6 +464,17 @@ def update_from_dict(self, episode_dict): if k in episode_dict: setattr(self, k, episode_dict[k]) + @property + def art_file(self): + if self.episode_art_url: + filename, extension = util.filename_from_url(self.episode_art_url) + + if not filename: + filename = hashlib.sha512(self.episode_art_url.encode('utf-8')).hexdigest() + + return os.path.join(self.podcast.save_dir, filename) + return None + class PodcastChannel(PodcastModelFields, PodcastModelMixin): _common_prefix = str @@ -553,9 +566,15 @@ def check_download_folder(self): known_files.add(filename) - known_files.update(os.path.join(self.save_dir, 'folder' + ext) + known_files.update(os.path.join(self.cover_file + ext) for ext in coverart.CoverDownloader.EXTENSIONS) + for episode in self.episodes: + filename = episode.art_file + if filename: + known_files.update(os.path.join(episode.art_file + ext) + for ext in coverart.CoverDownloader.EXTENSIONS) + existing_files = {filename for filename in glob.glob(os.path.join(self.save_dir, '*')) if not filename.endswith('.partial')} @@ -737,6 +756,10 @@ def _consume_custom_feed(self, custom_feed): # Add new episodes to episodes self.episodes.extend(new_episodes) + # Verify that all episode art is up-to-date + for episode in self.episodes: + self.model.core.cover_downloader.get_cover(self, download=True, episode=episode) + # Sort episodes by pubdate, descending self.episodes.sort(key=lambda e: e.published, reverse=True) @@ -753,8 +776,9 @@ def update(self): logger.info('URL updated: {} -> {}'.format(old_url, self.url)) self._consume_custom_feed(result) - # Download the cover art if it's not yet available - self.model.core.cover_downloader.get_cover(self, download=True) + # Download the cover art if it's not yet available, don't run if no save_dir was created yet. + if self.save_dir: + self.model.core.cover_downloader.get_cover(self, download=True) self.save() @@ -920,7 +944,12 @@ def remove_downloaded(self): @property def cover_file(self): - return os.path.join(self.save_dir, 'folder') + if self.cover_url: + filename, extension = util.filename_from_url(self.cover_url) + if not filename: + filename = hashlib.sha512(self.cover_url.encode('utf-8')).hexdigest() + return os.path.join(self.save_dir, filename) + return None class Model(object): diff --git a/src/gpodder/plugins/soundcloud.py b/src/gpodder/plugins/soundcloud.py index 2492e53..9d06460 100644 --- a/src/gpodder/plugins/soundcloud.py +++ b/src/gpodder/plugins/soundcloud.py @@ -107,7 +107,12 @@ def get_user_info(self): def get_coverart(self): user_info = self.get_user_info() - return user_info.get('avatar_url', None) + avatar_url = user_info.get('avatar_url', None) + if avatar_url: + # Soundcloud API by default returns the URL to "large" artwork - 100x100 + # by replacing "-large" with "-original" in the URL we get unresized files. + return avatar_url.replace("-large", "-original") + return avatar_url def get_user_id(self): user_info = self.get_user_info() @@ -159,7 +164,7 @@ def get_tracks(self, feed, channel): for track in tracks: # Prefer stream URL (MP3), fallback to download URL base_url = track.get('stream_url') if track['streamable'] else track.get('download_url') - if base_url != None: + if base_url: url = base_url + '?consumer_key=%(consumer_key)s' % {'consumer_key': CONSUMER_KEY} else: logger.debug('Skipping track with no base_url') @@ -174,6 +179,10 @@ def get_tracks(self, feed, channel): filetype = self.cache['episodes'][track_guid]['filetype'] read_from_cache += 1 + artwork_url = track.get('artwork_url') + if artwork_url: + artwork_url = artwork_url.replace("-large", "-original") + yield { 'title': track.get('title', track.get('permalink')) or ('Unknown track'), 'link': track.get('permalink_url') or 'https://soundcloud.com/' + self.username, @@ -184,6 +193,7 @@ def get_tracks(self, feed, channel): 'guid': track_guid, 'published': soundcloud_parsedate(track.get('created_at', None)), 'total_time': int(track.get('duration') / 1000), + 'episode_art_url' : artwork_url, } logger.debug('Read %d episodes from %d cached episodes', read_from_cache, len(self.cache['episodes']))