8 from PIL
import Image
, ImageDraw
10 from django
.urls
import reverse
11 from django
.conf
import settings
12 from django
.http
import Http404
, HttpResponseRedirect
13 from django
.views
import View
14 from django
.utils
.decorators
import method_decorator
15 from django
.views
.decorators
.http
import last_modified
16 from django
.contrib
.staticfiles
.storage
import staticfiles_storage
17 from django
.core
.files
.storage
import FileSystemStorage
19 from mygpo
.utils
import file_hash
22 logger
= logging
.getLogger(__name__
)
24 # Use Django's File Storage API to access podcast logos. This could be swapped
25 # out for another storage implementation (eg for storing to Amazon S3)
26 # https://docs.djangoproject.com/en/1.11/ref/files/storage/
27 LOGO_STORAGE
= FileSystemStorage(
28 location
=settings
.MEDIA_ROOT
,
32 def _last_modified(request
, size
, prefix
, filename
):
34 target
= os
.path
.join('logo', str(size
), prefix
, filename
)
37 return LOGO_STORAGE
.get_modified_time(target
)
39 except (FileNotFoundError
, NotImplementedError):
46 self
.storage
= LOGO_STORAGE
48 @method_decorator(last_modified(_last_modified
))
49 def get(self
, request
, size
, prefix
, filename
):
53 prefix
= get_prefix(filename
)
54 target
= self
.get_thumbnail_path(size
, prefix
, filename
)
55 original
= self
.get_original_path(prefix
, filename
)
57 if self
.storage
.exists(target
):
58 return self
.send_file(target
)
60 if not self
.storage
.exists(original
):
61 logger
.warning('Original cover {} not found'.format(original
))
62 raise Http404('Cover Art not available' + original
)
64 target_dir
= self
.get_dir(filename
)
66 fp
= self
.storage
.open(original
, 'rb')
68 if im
.mode
not in ('RGB', 'RGBA'):
69 im
= im
.convert('RGBA')
70 except IOError as ioe
:
71 logger
.warning('Cover file {} cannot be opened: {}'.format(
73 raise Http404('Cannot open cover file') from ioe
76 im
.thumbnail((size
, size
), Image
.ANTIALIAS
)
78 except (struct
.error
, IOError, IndexError) as ex
:
79 # raised when trying to read an interlaced PNG;
80 logger
.warning('Could not create thumbnail: %s', str(ex
))
82 # we use the original instead
83 return self
.send_file(original
)
88 resized
.save(sio
, 'JPEG', optimize
=True, progression
=True,
91 return self
.send_file(original
)
95 self
.storage
.save(target
, sio
)
97 return self
.send_file(target
)
100 def get_thumbnail_path(size
, prefix
, filename
):
101 return os
.path
.join('logo', str(size
), prefix
, filename
)
104 def get_dir(filename
):
105 return os
.path
.dirname(filename
)
108 def remove_existing_thumbnails(prefix
, filename
):
109 dirs
, _files
= LOGO_STORAGE
.listdir('logo') # TODO: cache list of sizes
111 if size
== 'original':
114 path
= os
.path
.join('logo', size
, prefix
, filename
)
115 logger
.info('Removing {}'.format(path
))
116 LOGO_STORAGE
.delete(path
)
119 def get_original_path(prefix
, filename
):
120 return os
.path
.join('logo', 'original', prefix
, filename
)
122 def send_file(self
, filename
):
123 return HttpResponseRedirect(LOGO_STORAGE
.url(filename
))
126 def save_podcast_logo(cls
, cover_art_url
):
127 if not cover_art_url
:
131 image_sha1
= hashlib
.sha1(cover_art_url
.encode('utf-8')).hexdigest()
132 prefix
= get_prefix(image_sha1
)
134 filename
= cls
.get_original_path(prefix
, image_sha1
)
135 dirname
= cls
.get_dir(filename
)
137 # get hash of existing file
138 if LOGO_STORAGE
.exists(filename
):
139 with LOGO_STORAGE
.open(filename
, 'rb') as f
:
140 old_hash
= file_hash(f
).digest()
144 logger
.info('Logo {}, saving to {}'.format(cover_art_url
, filename
))
147 LOGO_STORAGE
.delete(filename
)
148 source
= io
.BytesIO(requests
.get(cover_art_url
).content
)
149 LOGO_STORAGE
.save(filename
, source
)
151 # get hash of new file
152 with LOGO_STORAGE
.open(filename
, 'rb') as f
:
153 new_hash
= file_hash(f
).digest()
155 # remove thumbnails if cover changed
156 if old_hash
!= new_hash
:
157 logger
.info('Removing thumbnails')
158 thumbnails
= cls
.remove_existing_thumbnails(prefix
, filename
)
162 except (ValueError, requests
.exceptions
.RequestException
,
163 socket
.error
, IOError) as e
:
164 logger
.warning('Exception while updating podcast logo: %s', str(e
))
167 def get_prefix(filename
):
171 def get_logo_url(podcast
, size
):
172 """ Return the logo URL for the podcast
174 The logo either comes from the media storage (see CoverArt) or from the
175 default logos in the static storage.
179 filename
= hashlib
.sha1(podcast
.logo_url
.encode('utf-8')).hexdigest()
180 return reverse('logo', args
=[size
, get_prefix(filename
), filename
])
183 filename
= 'podcast-%d.png' % (hash(podcast
.title
) % 5, )
184 return staticfiles_storage
.url('logo/{0}'.format(filename
))