1# -*- coding: utf-8 -*- 2# 3# gPodder - A media aggregator and podcast client 4# Copyright (c) 2005-2018 The gPodder Team 5# 6# gPodder is free software; you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation; either version 3 of the License, or 9# (at your option) any later version. 10# 11# gPodder is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU General Public License for more details. 15# 16# You should have received a copy of the GNU General Public License 17# along with this program. If not, see <http://www.gnu.org/licenses/>. 18# 19 20 21# 22# gpodder.coverart - Unified cover art downloading module (2012-03-04) 23# 24 25 26import logging 27import os 28 29import gpodder 30from gpodder import util, youtube 31 32_ = gpodder.gettext 33 34logger = logging.getLogger(__name__) 35 36 37class CoverDownloader(object): 38 # File name extension dict, lists supported cover art extensions 39 # Values: functions that check if some data is of that file type 40 SUPPORTED_EXTENSIONS = { 41 '.png': lambda d: d.startswith(b'\x89PNG\r\n\x1a\n\x00'), 42 '.jpg': lambda d: d.startswith(b'\xff\xd8'), 43 '.gif': lambda d: d.startswith(b'GIF89a') or d.startswith(b'GIF87a'), 44 '.ico': lambda d: d.startswith(b'\0\0\1\0'), 45 } 46 47 EXTENSIONS = list(SUPPORTED_EXTENSIONS.keys()) 48 ALL_EPISODES_ID = ':gpodder:all-episodes:' 49 50 # Low timeout to avoid unnecessary hangs of GUIs 51 TIMEOUT = 5 52 53 def __init__(self): 54 pass 55 56 def get_cover_all_episodes(self): 57 return self._default_filename('podcast-all.png') 58 59 def get_cover(self, filename, cover_url, feed_url, title, 60 username=None, password=None, download=False): 61 # Detection of "all episodes" podcast 62 if filename == self.ALL_EPISODES_ID: 63 return self.get_cover_all_episodes() 64 65 # Return already existing files 66 for extension in self.EXTENSIONS: 67 if os.path.exists(filename + extension): 68 return filename + extension 69 70 # If allowed to download files, do so here 71 if download: 72 # YouTube-specific cover art image resolver 73 youtube_cover_url = youtube.get_cover(feed_url) 74 if youtube_cover_url is not None: 75 cover_url = youtube_cover_url 76 77 if not cover_url: 78 return self._fallback_filename(title) 79 80 # We have to add username/password, because password-protected 81 # feeds might keep their cover art also protected (bug 1521) 82 if username is not None and password is not None: 83 cover_url = util.url_add_authentication(cover_url, 84 username, password) 85 86 try: 87 logger.info('Downloading cover art: %s', cover_url) 88 data = util.urlopen(cover_url, timeout=self.TIMEOUT).read() 89 except Exception as e: 90 logger.warn('Cover art download failed: %s', e) 91 return self._fallback_filename(title) 92 93 try: 94 extension = None 95 96 for filetype, check in list(self.SUPPORTED_EXTENSIONS.items()): 97 if check(data): 98 extension = filetype 99 break 100 101 if extension is None: 102 msg = 'Unknown file type: %s (%r)' % (cover_url, data[:6]) 103 raise ValueError(msg) 104 105 # Successfully downloaded the cover art - save it! 106 fp = open(filename + extension, 'wb') 107 fp.write(data) 108 fp.close() 109 110 return filename + extension 111 except Exception as e: 112 logger.warn('Cannot save cover art', exc_info=True) 113 114 # Fallback to cover art based on the podcast title 115 return self._fallback_filename(title) 116 117 def _default_filename(self, basename): 118 return os.path.join(gpodder.images_folder, basename) 119 120 def _fallback_filename(self, title): 121 return self._default_filename('podcast-%d.png' % (hash(title) % 5)) 122