1+ from pathlib import PurePath
2+ from urllib .parse import quote
3+
4+ import requests
15from django .contrib import admin
6+ from django .core .files .base import ContentFile
27from django .db import transaction
8+ from django .utils .html import format_html
39
410from program import pretalx , pretalx_sync
511from program .models import Room , Slot , Speaker , Talk , Utility , Workshop
@@ -140,7 +146,6 @@ class TalkAdmin(admin.ModelAdmin):
140146 "fields" : [
141147 "pretalx_code" ,
142148 "og_image" ,
143- "video_id" ,
144149 ],
145150 },
146151 ),
@@ -153,6 +158,15 @@ class TalkAdmin(admin.ModelAdmin):
153158 ],
154159 },
155160 ),
161+ (
162+ "Slides and Video" ,
163+ {
164+ "fields" : [
165+ "video_url" ,
166+ "video_image_html" ,
167+ ],
168+ },
169+ ),
156170 (
157171 "Talk info (edit in pretalx)" ,
158172 {
@@ -181,10 +195,29 @@ class TalkAdmin(admin.ModelAdmin):
181195 "minimum_python_knowledge" ,
182196 "minimum_topic_knowledge" ,
183197 "type" ,
198+ "video_image_html" ,
184199 ]
185200 actions = [make_public , make_not_public , talk_update_from_pretalx ]
186201 change_form_template = "program/admin/change_form_session.html"
187202
203+ @admin .display (description = "Video Image" )
204+ def video_image_html (self , obj : Talk ):
205+ if not obj .video_image :
206+ return "(no image)"
207+
208+ html = (
209+ '<a href="{image_url}" style="display: inline-block">'
210+ '<img src="{image_url}" height="180"/><br>'
211+ '<span style="display: inline-block; margin-top: 1ex;">{image_name}</span>'
212+ "</a>"
213+ )
214+
215+ return format_html (
216+ html ,
217+ image_url = obj .video_image .url ,
218+ image_name = obj .video_image .name ,
219+ )
220+
188221 def get_queryset (self , request ):
189222 qs = super ().get_queryset (request )
190223 qs = qs .prefetch_related ("talk_speakers" )
@@ -201,12 +234,52 @@ def get_readonly_fields(self, request, obj=None):
201234 return ro_fields
202235
203236 def save_model (self , request , obj : Talk , form , change : bool ) -> None :
237+ if change :
238+ self ._update_video_image (obj )
239+
204240 obj .save ()
205241
206242 if not change and obj .pretalx_code :
207243 sync = create_pretalx_sync ()
208244 sync .update_talks ([obj ])
209245
246+ def _update_video_image (self , talk : Talk ):
247+ video_id = talk .video_id
248+ if not video_id :
249+ # Delete the existing image, if any.
250+ if talk .video_image :
251+ talk .video_image .delete (save = False )
252+ return
253+
254+ # Get the video ID of the current image:
255+ # the image is always named <video_id>.jpg
256+ image_video_id : str | None = None
257+ if talk .video_image :
258+ image_path = PurePath (talk .video_image .name )
259+ image_video_id = image_path .stem
260+
261+ # Check if the video ID has changed and download a new image when necessary.
262+ if video_id != image_video_id :
263+ image_data = self ._download_youtube_video_image (video_id )
264+ talk .video_image .save (
265+ name = image_data .name ,
266+ content = image_data ,
267+ save = False ,
268+ )
269+
270+ def _download_youtube_video_image (self , video_id : str ) -> ContentFile :
271+ image_url = self ._format_youtube_video_image_url (video_id )
272+
273+ with requests .get (image_url , timeout = 30 ) as image_response :
274+ image_response .raise_for_status ()
275+ return ContentFile (
276+ content = image_response .content ,
277+ name = f"{ video_id } .jpg" ,
278+ )
279+
280+ def _format_youtube_video_image_url (self , video_id ):
281+ return f"https://img.youtube.com/vi/{ quote (video_id )} /maxresdefault.jpg"
282+
210283
211284@admin .action (description = "Update from pretalx" )
212285def workshop_update_from_pretalx (modeladmin , request , queryset ):
0 commit comments