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