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
23 logger
= logging
.getLogger(__name__
)
25 # Use Django's File Storage API to access podcast logos. This could be swapped
26 # out for another storage implementation (eg for storing to Amazon S3)
27 # https://docs.djangoproject.com/en/1.11/ref/files/storage/
28 LOGO_STORAGE
= FileSystemStorage(location
=settings
.MEDIA_ROOT
)
31 def _last_modified(request
, size
, prefix
, filename
):
33 target
= os
.path
.join("logo", str(size
), prefix
, filename
)
36 return LOGO_STORAGE
.get_modified_time(target
)
38 except (FileNotFoundError
, NotImplementedError):
44 self
.storage
= LOGO_STORAGE
46 @method_decorator(last_modified(_last_modified
))
47 def get(self
, request
, size
, prefix
, filename
):
51 prefix
= get_prefix(filename
)
52 target
= self
.get_thumbnail_path(size
, prefix
, filename
)
53 original
= self
.get_original_path(prefix
, filename
)
55 if self
.storage
.exists(target
):
56 return self
.send_file(target
)
58 if not self
.storage
.exists(original
):
59 logger
.warning("Original cover {} not found".format(original
))
60 raise Http404("Cover Art not available" + original
)
62 target_dir
= self
.get_dir(filename
)
64 fp
= self
.storage
.open(original
, "rb")
66 if im
.mode
not in ("RGB", "RGBA"):
67 im
= im
.convert("RGBA")
68 except IOError as ioe
:
69 logger
.warning("Cover file {} cannot be opened: {}".format(original
, ioe
))
70 raise Http404("Cannot open cover file") from ioe
73 im
.thumbnail((size
, size
), Image
.ANTIALIAS
)
75 except (struct
.error
, IOError, IndexError) as ex
:
76 # raised when trying to read an interlaced PNG;
77 logger
.warning("Could not create thumbnail: %s", str(ex
))
79 # we use the original instead
80 return self
.send_file(original
)
87 "JPEG" if im
.mode
== "RGB" else "PNG",
93 return self
.send_file(original
)
97 self
.storage
.save(target
, sio
)
99 return self
.send_file(target
)
102 def get_thumbnail_path(size
, prefix
, filename
):
103 return os
.path
.join("logo", str(size
), prefix
, filename
)
106 def get_dir(filename
):
107 return os
.path
.dirname(filename
)
110 def remove_existing_thumbnails(prefix
, filename
):
111 dirs
, _files
= LOGO_STORAGE
.listdir("logo") # TODO: cache list of sizes
113 if size
== "original":
116 path
= os
.path
.join("logo", size
, prefix
, filename
)
117 logger
.info("Removing {}".format(path
))
118 LOGO_STORAGE
.delete(path
)
121 def get_original_path(prefix
, filename
):
122 return os
.path
.join("logo", "original", prefix
, filename
)
124 def send_file(self
, filename
):
125 return HttpResponseRedirect(LOGO_STORAGE
.url(filename
))
128 def save_podcast_logo(cls
, cover_art_url
):
129 if not cover_art_url
:
133 image_sha1
= hashlib
.sha1(cover_art_url
.encode("utf-8")).hexdigest()
134 prefix
= get_prefix(image_sha1
)
136 filename
= cls
.get_original_path(prefix
, image_sha1
)
137 dirname
= cls
.get_dir(filename
)
139 # get hash of existing file
140 if LOGO_STORAGE
.exists(filename
):
141 with LOGO_STORAGE
.open(filename
, "rb") as f
:
142 old_hash
= file_hash(f
).digest()
146 logger
.info("Logo {}, saving to {}".format(cover_art_url
, filename
))
149 LOGO_STORAGE
.delete(filename
)
150 source
= io
.BytesIO(requests
.get(cover_art_url
).content
)
151 LOGO_STORAGE
.save(filename
, source
)
153 # get hash of new file
154 with LOGO_STORAGE
.open(filename
, "rb") as f
:
155 new_hash
= file_hash(f
).digest()
157 # remove thumbnails if cover changed
158 if old_hash
!= new_hash
:
159 logger
.info("Removing thumbnails")
160 thumbnails
= cls
.remove_existing_thumbnails(prefix
, filename
)
166 requests
.exceptions
.RequestException
,
170 logger
.warning("Exception while updating podcast logo: %s", str(e
))
173 def get_prefix(filename
):
177 def get_logo_url(podcast
, size
):
178 """Return the logo URL for the podcast
180 The logo either comes from the media storage (see CoverArt) or from the
181 default logos in the static storage.
185 filename
= hashlib
.sha1(podcast
.logo_url
.encode("utf-8")).hexdigest()
186 return reverse("logo", args
=[size
, get_prefix(filename
), filename
])
189 filename
= "podcast-%d.png" % (hash(podcast
.title
) % 5,)
190 return staticfiles_storage
.url("logo/{0}".format(filename
))