root/trunk/djedna/catalog2/models.py

Revision 499, 45.8 kB (checked in by thomas, 2 months ago)

Added active queries and tests

Line 
1 # (c) Copyright 2008 Thomas Bohmbach, Jr.
2 #
3 # This file is part of DJ Edna.
4 #
5 # DJ Edna is free software: you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
8 # any later version.
9 #
10 # DJ Edna is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
13 # more details.
14 #
15 # You should have received a copy of the GNU General Public License along with
16 # DJ Edna.  If not, see <http://www.gnu.org/licenses/>.
17
18 import datetime
19 import logging as log
20 import mimetypes
21 import os
22 import shutil
23 import StringIO
24 import time
25 import wave
26 import zipfile
27
28 from django.conf import settings
29 from django.contrib.auth.models import User
30 from django.contrib.sites.models import Site
31 from django.contrib.sites.managers import CurrentSiteManager
32 from django.core.urlresolvers import reverse
33 from django.db import models
34 from django.dispatch import dispatcher
35 from django.template import defaultfilters
36 from django.utils.encoding import smart_unicode, smart_str, force_unicode
37 from django.utils.encoding import iri_to_uri
38 from django.utils.http import urlquote
39
40 from mutagen.mp3 import MP3
41
42 from PIL import Image
43
44 from sorl.thumbnail.main import DjangoThumbnail
45
46 from djedna import mediaserver
47 from djedna.catalog2.signals import trackfileupload_update
48 from djedna.catalog2.signals import imagefileupload_update
49 from djedna.catalog2.signals import trackfile_update
50 from djedna.catalog2.signals import track_update, album_update
51
52
53 mimetypes.init()
54 MP3_EXT = '.mp3'
55 WAV_EXT = '.wav'
56 MP3_MIMETYPE = mimetypes.types_map[MP3_EXT]
57 WAV_MIMETYPE = mimetypes.types_map[WAV_EXT]
58 TRACKFILE_REGEX = '^.+(\%s|\%s)$' % (MP3_EXT, WAV_EXT)
59 PNG_EXT = '.png'
60 JPG_EXT = '.jpg'
61 GIF_EXT = '.gif'
62 PNG_MIMETYPE = mimetypes.types_map[PNG_EXT]
63 JPG_MIMETYPE = mimetypes.types_map[JPG_EXT]
64 GIF_MIMETYPE = mimetypes.types_map[GIF_EXT]
65 IMAGEFILE_REGEX = '^.+(\%s|\%s|\%s)$' % (PNG_EXT, JPG_EXT, GIF_EXT)
66
67 def pathwalk(path):
68     if os.path.isdir(path):
69         for f in os.listdir(path):
70             newpath = os.path.join(path, f)
71             for x in pathwalk(newpath):
72                 yield x
73     else:
74         yield path
75
76 def is_mimetype(path, mimetype):
77     dirpath, filename = os.path.split(path)
78     if not filename or filename[0] == '.':
79         return False
80     path_mimetype, encoding = mimetypes.guess_type(path)
81     return mimetype == path_mimetype
82
83 def get_group_hash():
84     count = 1
85     while count:
86         group_hash = User.objects.make_random_password(
87             allowed_chars='BCDFGHJKLMNPQRSTVWXYZ123456789'
88         )
89         count = TrackFile.objects.filter(group_hash=group_hash).count()
90     return group_hash
91
92 def slugify(value):
93     return str(defaultfilters.slugify(value))
94
95
96 class PublishableManager(models.Manager):
97     def get_published_q(self, now=None):
98         if not now:
99             now = datetime.datetime.now()
100         published_q = \
101             models.Q(publish__isnull=True) | \
102             models.Q(publish__lte=now)
103         not_expired_q = \
104             models.Q(expire__isnull=True) | \
105             models.Q(expire__gte=now)
106         return published_q & not_expired_q
107    
108     def get_active_q(self, now=None):
109         return models.Q(active=True) & self.get_published_q(now=now)
110    
111     def get_site_q(self, site_id=None):
112         if site_id == None:
113             site_id = settings.SITE_ID
114         return models.Q(sites__id__exact=site_id)
115    
116     def get_active_site_q(self, now=None, site_id=None):
117         return self.get_active_q(now=now) & self.get_site_q(site_id=site_id)
118    
119     def get_active_query_set(self, now=None):
120         return self.get_query_set().filter(self.get_active_q(now=now))
121     active = property(get_active_query_set)
122    
123     def get_inactive_query_set(self, now=None):
124         return self.get_query_set().exclude(self.get_active_q(now=now))
125     inactive = property(get_inactive_query_set)
126    
127     def get_site_query_set(self, site_id=None):
128         return self.get_query_set().filter(self.get_site_q(site_id=site_id))
129     on_site = property(get_site_query_set)
130    
131     def get_active_site_query_set(self, now=None, site_id=None):
132         return self.get_query_set().filter(
133             self.get_active_site_q(now=now, site_id=site_id)
134         )
135     active_on_site = property(get_active_site_query_set)
136    
137     def get_inactive_site_query_set(self, now=None, site_id=None):
138         return self.get_query_set().exclude(
139             self.get_active_site_q(now=now, site_id=site_id)
140         )
141     inactive_on_site = property(get_inactive_site_query_set)
142    
143
144 class Publishable(models.Model):
145     active = models.BooleanField(default=True, blank=True)
146     publish = models.DateTimeField(null=True, blank=True)
147     expire = models.DateTimeField(null=True, blank=True)
148     added = models.DateTimeField(auto_now_add=True)
149     updated = models.DateTimeField(auto_now=True)
150     sites = models.ManyToManyField(Site)
151    
152     class Meta:
153         abstract = True
154    
155     def is_active(self, now=None):
156         active = self.active
157         if active:
158             if not now:
159                 now = datetime.datetime.now()
160             published = True
161             if self.publish and self.publish > now:
162                 published = False
163             if published and self.expire and self.expire < now:
164                 published = False
165             active = published
166         return active
167    
168
169
170 class MediaFileManager(PublishableManager):
171     def get_or_create_from_path(
172             self, path, sites=None, import_path=None, defaults={}
173         ):
174         media_relative_path = None
175         relative_path, absolute_path = mediaserver.media_paths(path)
176         if not relative_path:
177             import_path = import_path or os.path.join(
178                 'imported', '%Y', '%m', '%d'
179             )
180             filename = os.path.basename(path)
181             media_relative_path = os.path.join(import_path, filename)
182             media_relative_path = os.path.normpath(
183                 force_unicode(datetime.datetime.now().strftime(
184                     smart_str(media_relative_path))
185                 )
186             )
187             relative_path = mediaserver.put_file(path, media_relative_path)
188         mediafile, created = self.get_or_create(
189             relative_path=relative_path,
190             defaults=defaults
191         )
192         if created:
193             if not media_relative_path:
194                 mediaserver.put_file(relative_path)
195             if sites:
196                 mediafile.sites.add(*sites)
197         return (mediafile, created)
198    
199     def get_in_path(self, path):
200         relative_path, absolute_path = mediaserver.media_paths(path)
201         if not (relative_path and os.path.exists(absolute_path)):
202             return self.none()
203         return self.filter(relative_path__startswith=relative_path)
204    
205     def by_mimetype(self, mimetype):
206         return self.get_query_set().filter(mimetype=mimetype)
207    
208
209 class MediaFile(Publishable):
210     relative_path = models.CharField(max_length=1000, core=True, unique=True)
211     mimetype = models.CharField(max_length=256, default='', blank=True)
212     file_modified = models.DateTimeField(null=True, blank=True)
213     group_hash = models.CharField(max_length=256, default='', blank=True)
214    
215     class Meta:
216         abstract = True
217    
218     def __unicode__(self):
219         return u"%s: %s" % (self.mimetype, self.relative_path)
220    
221     def get_media_url(self, user=None, query_auth=None, expires_in=None):
222         return mediaserver.get_url(
223             self.relative_path,
224             query_auth=query_auth,
225             expires_in=expires_in,
226             user=user
227         )
228    
229     def _get_path(self):
230         relative_path, absolute_path = mediaserver.media_paths(
231             self.relative_path
232         )
233         return absolute_path
234     path = property(_get_path)
235    
236     def _get_relative_dirpath(self):
237         dirpath, filename = os.path.split(self.relative_path)
238         return dirpath
239     relative_dirpath = property(_get_relative_dirpath)
240    
241     def _get_filename(self):
242         dirpath, filename = os.path.split(self.relative_path)
243         return filename
244     filename = property(_get_filename)
245    
246     def _get_filename_root(self):
247         file_root, ext = os.path.splitext(self.filename)
248         return file_root
249     filename_root = property(_get_filename_root)
250    
251     def _get_file_modified_datetime(self):
252         file_mod_date = None
253         if self.file_exists():
254             file_mod = os.path.getmtime(self.path)
255             file_mod_date = datetime.datetime.fromtimestamp(file_mod)
256         return file_mod_date
257    
258     def _get_file_mimetype(self):
259         mimetype = None
260         if self.file_exists():
261             mimetype, encoding = mimetypes.guess_type(self.path)
262         return mimetype
263     file_mimetype = property(_get_file_mimetype)
264    
265     def file_exists(self):
266         return mediaserver.media_exists(self.relative_path)
267    
268     def update(self, group_hash=None, force_update=False):
269         needs_update = force_update
270         if self.active and not self.file_exists():
271             needs_update = True
272         file_modified = self._get_file_modified_datetime()
273         if file_modified:
274             if not self.file_modified or file_modified > self.file_modified:
275                 needs_update = True
276         if needs_update:
277             if group_hash:
278                 self.group_hash = group_hash
279             self.save()
280         return needs_update
281    
282     def save(self):
283         create = not self.id
284         self.file_modified = self._get_file_modified_datetime()
285         self.mimetype = self.file_mimetype or ''
286         if not self.file_exists():
287             self.active = False
288         super(MediaFile, self).save()
289         if create and not self.sites.count():
290             self.sites.add(Site.objects.get_current())
291    
292     def delete(self, remove_file=False):
293         relative_path = self.relative_path
294         super(MediaFile, self).delete()
295         if remove_file and relative_path:
296             mediaserver.remove(relative_path)
297    
298
299 class MediaFileUpload(models.Model):
300     """
301     Subclasses should specify a FileField or ImageField called
302         'uploaded_file'.  For example:
303     
304     uploaded_file = models.FileField(
305         upload_to=os.path.join('uploaded', '%Y', '%m', '%d')
306     )
307     
308         or
309     
310     uploaded_file = models.ImageField(
311         upload_to=os.path.join('uploaded', '%Y', '%m', '%d')
312     )
313     """
314     user = models.ForeignKey(
315         User,
316         blank=True,
317         null=True,
318         related_name='%(class)ss'
319     )
320     added = models.DateTimeField(auto_now_add=True)
321     updated = models.DateTimeField(auto_now=True)
322    
323     class Meta:
324         abstract = True
325    
326     def __unicode__(self):
327         return u"%s" % self.get_uploaded_file_filename()
328    
329     def delete(self):
330         path = self.get_uploaded_file_filename()
331         media_path = path[len(settings.MEDIA_ROOT):]
332         super(MediaFileUpload, self).delete()
333         mediaserver.remove(media_path)
334    
335
336
337 class TrackFileManager(MediaFileManager):
338     def is_mp3(self, path):
339         return is_mimetype(path, MP3_MIMETYPE)
340    
341     def is_wav(self, path):
342         return is_mimetype(path, WAV_MIMETYPE)
343    
344     def is_music_file(self, path):
345         return self.is_mp3(path) or self.is_wav(path)
346    
347     def get_or_create_from_path(
348             self, path, create_track=True, sites=None, import_path=None
349         ):
350         import_path = import_path or os.path.join(
351             settings.FILE_DIRNAME, 'imported', '%Y', '%m', '%d'
352         )
353         trackfile, created = \
354             super(TrackFileManager, self).get_or_create_from_path(
355                 path,
356                 sites=sites,
357                 import_path=import_path
358             )
359         if created and create_track:
360             track, track_created = \
361                 Track.objects.get_or_create_from_trackfile(trackfile)
362             trackfile.track = track
363             trackfile.save()
364         return (trackfile, created)
365    
366     def add_from_path(
367             self, path, create_track=True, sites=None, import_path=None
368         ):
369         added_hash = None
370         group_hash = get_group_hash()
371         relative_path, absolute_path = mediaserver.media_paths(path)
372         if absolute_path:
373             for f in pathwalk(absolute_path):
374                 if self.is_music_file(f):
375                     trackfile, created = self.get_or_create_from_path(
376                         f,
377                         create_track=create_track,
378                         sites=sites,
379                         import_path=import_path
380                     )
381                     if created:
382                         added_hash = group_hash
383                         trackfile.group_hash = group_hash
384                         trackfile.save()
385         return added_hash
386    
387     def update_in_path(self, path):
388         updated_hash = None
389         group_hash = get_group_hash()
390         trackfiles = self.get_in_path(path)
391         for trackfile in trackfiles:
392             if trackfile.update(group_hash):
393                 updated_hash = group_hash
394         return updated_hash
395    
396     def _get_mp3s(self):
397         return self.by_mimetype(mimetype=MP3_MIMETYPE)
398     mp3s = property(_get_mp3s)
399    
400     def _get_wavs(self):
401         return self.by_mimetype(mimetype=WAV_MIMETYPE)
402     wavs = property(_get_wavs)
403    
404
405 class TrackFileCurrentSiteManager(CurrentSiteManager, TrackFileManager):
406     pass
407
408 class TrackFile(MediaFile):
409     quality = models.CharField(max_length=256, default='', blank=True)
410     extracted_image = models.ForeignKey(
411         'ImageFile', null=True, blank=True,
412         related_name='trackfile_extracted', editable=False
413     )
414     image_override = models.ForeignKey(
415         'ImageFile', null=True, blank=True,
416         related_name='trackfile_overrides', edit_inline=models.TABULAR
417     )
418     track = models.ForeignKey(
419         'Track', null=True, blank=True, related_name='trackfiles',
420         edit_inline=models.TABULAR
421     )
422    
423     objects = TrackFileManager()
424     on_site = TrackFileCurrentSiteManager(field_name='sites')
425    
426     class Meta:
427         ordering = ['relative_path',]
428    
429     class Admin:
430         search_fields = ['relative_path',]
431         list_display = ['id',
432                         'relative_path',
433                         'mimetype',
434                         'file_modified',
435                         'added',
436                         'updated',]
437         list_filter = ['file_modified',]
438    
439     def __unicode__(self):
440         info = self.info
441         return "%s / %s / %s / %s / %s" % (
442             info['title'],
443             info['album_title'],
444             info['artist_name'],
445             info['quality'],
446             self.filename
447         )
448    
449     def get_absolute_url(self):
450         return reverse(
451             'djedna_trackfile_detail',
452             kwargs={'trackfile_id' : self.id}
453         )
454    
455     def is_mp3(self):
456         return self.mimetype == MP3_MIMETYPE
457    
458     def is_wav(self):
459         return self.mimetype == WAV_MIMETYPE
460    
461     def _get_image(self):
462         if self.image_override:
463             return self.image_override
464         else:
465             return self.extracted_image
466     image = property(_get_image)
467    
468     def _get_info_mp3(self):
469         title = None
470         artist_name = None
471         album_title = None
472         year = None
473         number = None
474         length = None
475         quality = None
476         cover_image = None
477         try:
478             mp3 = MP3(self.path)
479         except:
480             log.exception("Error getting info from '%s'" % self.path)
481         else:
482             titles = mp3.tags.getall('TIT2')
483             if not titles:
484                 titles = mp3.tags.getall('TT2') #v2.2
485             if titles:
486                 title = titles[0].text[0]
487             artist_names = mp3.tags.getall('TPE1')
488             if not artist_names:
489                 artist_names = mp3.tags.getall('TP1') #v2.2
490             if artist_names:
491                 artist_name = artist_names[0].text[0]
492             album_titles = mp3.tags.getall('TALB')
493             if not album_titles:
494                 album_titles = mp3.tags.getall('TAL') #v2.2
495             if album_titles:
496                 album_title = album_titles[0].text[0]
497             try:
498                 years = mp3.tags.getall('TDRC')
499                 year = years[0].text[0].text
500             except:
501                 try:
502                     years = mp3.tags.getall('TYER')
503                     year = years[0].text[0]
504                 except:
505                     try:
506                         years = mp3.tags.getall('TYE') #v2.2
507                         year = years[0].text
508                     except:
509                         year = None
510             tracks = mp3.tags.getall('TRCK')
511             if not tracks:
512                 tracks = mp3.tags.getall('TT2') #v2.2
513             if tracks:
514                 track = tracks[0].text[0].strip()
515                 try:
516                     number = int(track)
517                 except:
518                     try:
519                         number = int(track[0:track.find('/')].strip())
520                     except:
521                         log.exception(
522                             "Error trying to parse track '%s' from '%s'" %
523                             (track, self.path)
524                         )
525             try:
526                 length = int(mp3.info.length)
527             except:
528                 log.exception(
529                     "Error getting length from '%s'" %
530                     self.path
531                 )
532             try:
533                 quality = "%skbps" % (mp3.info.bitrate / 1000)
534             except:
535                 log.exception(
536                     "Error getting quality (bitrate) from '%s'" %
537                     self.path
538                 )
539             pictures = mp3.tags.getall('APIC')
540             if not pictures:
541                 pictures = mp3.tags.getall('PIC')
542             if pictures:
543                 try:
544                     picture_data = pictures[0].data
545                     cover_image = Image.open(StringIO.StringIO(picture_data))
546                 except:
547                     log.exception(
548                         "Error getting cover image from '%s'" %
549                         self.path
550                     )
551         return {'title': title,
552                 'artist_name' : artist_name,
553                 'album_title' : album_title,
554                 'year' : year,
555                 'number' : number,
556                 'length' : length,
557                 'quality' : quality,
558                 'cover_image' : cover_image}
559    
560     def _get_info_wav(self):
561         length = None
562         try:
563             wav = wave.open(self.path)
564             length = round(float(wav.getnframes()) /
565                            float(wav.getframerate()))
566         except:
567             pass
568         return {'title': None,
569                 'artist_name' : None,
570                 'album_title' : None,
571                 'year' : None,
572                 'number' : None,
573                 'length' : length,
574                 'quality' : 'lossless',
575                 'cover_image' : None}
576    
577     def _get_info(self):
578         if self.file_exists():
579             if self.is_mp3():
580                 return self._get_info_mp3()
581             if self.is_wav():
582                 return self._get_info_wav()
583         return {'title': None,
584                 'artist_name' : None,
585                 'album_title' : None,
586                 'year' : None,
587                 'number' : None,
588                 'length' : None,
589                 'quality' : None,
590                 'cover_image' : None}
591     info = property(_get_info)
592    
593     def update(self, group_hash=None, force_update=False):
594         needs_update = force_update
595         try:
596             relative_path, absolute_path = mediaserver.media_paths(
597                 self.trackfileupload.get_uploaded_file_filename()
598             )
599             if self.relative_path != relative_path:
600                 self.relative_path = relative_path
601                 needs_update = True
602         except TrackFileUpload.DoesNotExist:
603             pass
604         return super(TrackFile, self).update(
605             group_hash=group_hash,
606             force_update=needs_update
607         )
608    
609     def save(self):
610         self.quality = self.info['quality'] or ''
611         if not self.extracted_image:
612             self.extracted_image, created = \
613                 ImageFile.objects.get_or_create_from_trackfile(self)
614         super(TrackFile, self).save()
615    
616     def delete(self, remove_file=False):
617         if self.extracted_image:
618             self.extracted_image.delete(remove_file=True)
619             self.extracted_image = None
620         super(TrackFile, self).delete(remove_file=remove_file)
621    
622 # Call track.update() (via trackfileupload_update()) after save
623 dispatcher.connect(
624     trackfile_update,
625     signal=models.signals.post_save,
626     sender=TrackFile
627 )
628
629 class TrackFileUpload(MediaFileUpload):
630     uploaded_file = models.FileField(
631         upload_to=os.path.join(
632             settings.FILE_DIRNAME, 'uploaded', '%Y', '%m', '%d'
633         ),
634         max_length=1000
635     )
636     trackfile = models.OneToOneField(
637         'TrackFile', blank=True, null=True, related_name='trackfileupload'
638     )
639    
640     def save(self):
641         if not self.trackfile:
642             path = self.get_uploaded_file_filename()
643             group_hash = TrackFile.objects.add_from_path(
644                 path, create_track=False
645             )
646             if group_hash:
647                 trackfiles = TrackFile.objects.filter(group_hash=group_hash)
648                 self.trackfile = trackfiles[0]
649         super(TrackFileUpload, self).save()
650    
651    
652     class Admin:
653         search_fields = ['trackfile__relative_path']
654         list_display = ['id', 'trackfile', 'user', 'added', 'updated']
655    
656 # Call trackfile.update() (via trackfileupload_update()) after save
657 dispatcher.connect(
658     trackfileupload_update,
659     signal=models.signals.post_save,
660     sender=TrackFileUpload
661 )
662
663
664 class ImageFileManager(MediaFileManager):
665     def is_png(self, path):
666         return is_mimetype(path, PNG_MIMETYPE)
667    
668     def is_jpg(self, path):
669         return is_mimetype(path, JPG_MIMETYPE)
670    
671     def is_gif(self, path):
672         return is_mimetype(path, GIF_MIMETYPE)
673    
674     def is_image_file(self, path):
675         return self.is_png(path) or self.is_jpg(path) or self.is_gif(path)
676    
677     def get_in_path(self, path):
678         return super(ImageFileManager, self).get_in_path(path).filter(
679             master=None
680         )
681    
682     def get_or_create_from_path(
683             self, path, master=None, sites=None, import_path=None
684         ):
685         import_path = import_path or os.path.join(
686             settings.IMAGE_DIRNAME, 'imported', '%Y', '%m', '%d'
687         )
688         return super(ImageFileManager, self).get_or_create_from_path(
689             path,
690             sites=sites,
691             import_path=import_path,
692             defaults={'master' : master}
693         )
694    
695     def get_or_create_from_trackfile(
696             self, trackfile, filename='cover.png', base_path=None
697         ):
698         base_path = base_path or os.path.join(
699             settings.IMAGE_DIRNAME, 'trackfile'
700         )
701         relative_path = os.path.join(
702             base_path,
703             smart_str(trackfile.id),
704             filename
705         )
706         imagefile = None
707         created = False
708         try:
709             imagefile = ImageFile.objects.get(relative_path=relative_path)
710         except ImageFile.DoesNotExist:
711             relative_path, absolute_path = mediaserver.media_paths(
712                 relative_path
713             )
714             image = trackfile.info['cover_image']
715             if image:
716                 mediaserver.remove(relative_path)
717                 try:
718                     os.makedirs(os.path.split(absolute_path)[0])
719                 except:
720                     pass
721                 image.save(absolute_path)
722                 imagefile, created = self.get_or_create_from_path(
723                     relative_path, sites=trackfile.sites.all()
724                 )
725         return (imagefile, created)
726    
727
728 class ImageFileCurrentSiteManager(CurrentSiteManager, ImageFileManager):
729     pass
730
731 class ImageFile(MediaFile):
732     width = models.PositiveIntegerField(blank=True, null=True)
733     height = models.PositiveIntegerField(blank=True, null=True)
734     master = models.ForeignKey('self', null=True, related_name="scaled")
735    
736     objects = ImageFileManager()
737     on_site = ImageFileCurrentSiteManager(field_name='sites')
738    
739     def get_absolute_url(self):
740         return reverse(
741             'djedna_imagefile_detail',
742             kwargs={'imagefile_id' : self.id}
743         )
744    
745     def get_scaled(self, requested_size=None, scale=0.0):
746         imagefile = self
747         if not requested_size and scale:
748             requested_size = (
749                 int(float(self.width) * float(scale)),
750                 int(float(self.height) * float(scale))
751             )
752         # Did we request a specific width and height
753         if requested_size:
754             width, height = requested_size
755             # Is this cover_file not the correct size?
756             if self.width != width or self.height != height:
757                 scaled_imagefiles = self.scaled.filter(
758                     width=width, height=height
759                 )
760                 # Is there a scaled cover_file that's the right size?
761                 try:
762                     imagefile = scaled_imagefiles[0]
763                 except:
764                     thumbnail = DjangoThumbnail(
765                         self.relative_path, requested_size
766                     )
767                     imagefile, created = \
768                         ImageFile.objects.get_or_create_from_path(
769                             thumbnail.relative_dest,
770                             master=self,
771                             sites=self.sites.all()
772                         )
773         return imagefile
774    
775     def is_png(self):
776         return self.mimetype == PNG_MIMETYPE
777    
778     def is_jpg(self):
779         return self.mimetype == JPG_MIMETYPE
780    
781     def is_gif(self):
782         return self.mimetype == GIF_MIMETYPE
783    
784     def update(self, group_hash=None, force_update=False):
785         needs_update = force_update
786         try:
787             relative_path, absolute_path = mediaserver.media_paths(
788                 self.imagefileupload.get_uploaded_file_filename()
789             )
790             if self.relative_path != relative_path:
791                 self.relative_path = relative_path
792                 needs_update = True
793         except ImageFileUpload.DoesNotExist:
794             pass
795         return super(ImageFile, self).update(
796             group_hash=group_hash,
797             force_update=needs_update
798         )
799    
800     def save(self):
801         if not (self.width and self.height) and self.file_exists():
802             image = Image.open(
803                 mediaserver.get_media_path(self.relative_path)
804             )
805             size = image.size
806             self.width = size[0]
807             self.height = size[1]
808         super(ImageFile, self).save()
809    
810
811 class ImageFileUpload(MediaFileUpload):
812     uploaded_file = models.ImageField(
813         upload_to=os.path.join(
814             settings.IMAGE_DIRNAME, 'uploaded', '%Y', '%m', '%d'
815         ),
816         max_length=1000
817     )
818     imagefile = models.OneToOneField(
819         'ImageFile', blank=True, null=True, related_name='imagefileupload'
820     )
821    
822     def save(self):
823         if not self.imagefile:
824             path = self.get_uploaded_file_filename()
825             self.imagefile, created = \
826                 ImageFile.objects.get_or_create_from_path(path)
827             if self.imagefile and created:
828                 self.imagefile.group_hash = get_group_hash()
829                 self.imagefile.save()
830         super(ImageFileUpload, self).save()
831    
832    
833     class Admin:
834         search_fields = ['imagefile__relative_path']
835         list_display = ['id', 'imagefile', 'user', 'added', 'updated']
836    
837 # Call trackfile.update() (via trackfileupload_update()) after save
838 dispatcher.connect(
839     imagefileupload_update,
840     signal=models.signals.post_save,
841     sender=ImageFileUpload
842 )
843
844
845 class ArchiveFile(MediaFile):
846     trackfiles = models.ManyToManyField(TrackFile)
847     imagefiles = models.ManyToManyField(ImageFile)
848    
849     def get_absolute_url(self):
850         return reverse(
851             'djedna_downloadfile_detail',
852             kwargs={'downloadfile_id' : self.id}
853         )
854    
855     def get_models(self):
856         tracks = []
857         artists = []
858         albums = []
859         for trackfile in self.trackfiles.all():
860             track = trackfile.track
861             artist = trackfile.track.artist
862             album = trackfile.track.album
863             if track and track not in tracks:
864                 tracks.append(track)
865             if artist and artist not in artists:
866                 artists.append(artist)
867             if album and album not in albums:
868                 albums.append(album)
869         return tracks, albums, artists
870         trackfiles = self.trackfiles.filter(TrackFile.objects.get_active_q())
871         tracks = Track.objects.filter(
872             id__in=[trackfile.track.id for trackfile in trackfiles]
873         )
874    
875     def remove_archive(self):
876         if self.file_exists():
877             mediaserver.remove_media(self.relative_path)