readonly type, source_sha1 in media admin
[wolnelektury.git] / apps / catalogue / models.py
index ab480d7..a0ebf05 100644 (file)
@@ -17,9 +17,11 @@ from django.conf import settings
 
 from newtagging.models import TagBase, tags_updated
 from newtagging import managers
 
 from newtagging.models import TagBase, tags_updated
 from newtagging import managers
-from catalogue.fields import JSONField
+from catalogue.fields import JSONField, OverwritingFileField
+from catalogue.utils import ExistingFile
 
 from librarian import dcparser, html, epub, NoDublinCore
 
 from librarian import dcparser, html, epub, NoDublinCore
+import mutagen
 from mutagen import id3
 from slughifi import slughifi
 
 from mutagen import id3
 from slughifi import slughifi
 
@@ -64,6 +66,9 @@ class Tag(TagBase):
     gazeta_link = models.CharField(blank=True, max_length=240)
     wiki_link = models.CharField(blank=True, max_length=240)
 
     gazeta_link = models.CharField(blank=True, max_length=240)
     wiki_link = models.CharField(blank=True, max_length=240)
 
+    class UrlDeprecationWarning(DeprecationWarning):
+        pass
+
     categories_rev = {
         'autor': 'author',
         'epoka': 'epoch',
     categories_rev = {
         'autor': 'author',
         'epoka': 'epoch',
@@ -122,19 +127,20 @@ class Tag(TagBase):
             real_tags = []
             ambiguous_slugs = []
             category = None
             real_tags = []
             ambiguous_slugs = []
             category = None
+            deprecated = False
             tags_splitted = tags.split('/')
             tags_splitted = tags.split('/')
-            for index, name in enumerate(tags_splitted):
-                if name in Tag.categories_rev:
+            for name in tags_splitted:
+                if category:
+                    real_tags.append(Tag.objects.get(slug=name, category=category))
+                    category = None
+                elif name in Tag.categories_rev:
                     category = Tag.categories_rev[name]
                 else:
                     category = Tag.categories_rev[name]
                 else:
-                    if category:
-                        real_tags.append(Tag.objects.get(slug=name, category=category))
-                        category = None
-                    else:
-                        try:
-                            real_tags.append(Tag.objects.exclude(category='book').get(slug=name))
-                        except Tag.MultipleObjectsReturned, e:
-                            ambiguous_slugs.append(name)
+                    try:
+                        real_tags.append(Tag.objects.exclude(category='book').get(slug=name))
+                        deprecated = True 
+                    except Tag.MultipleObjectsReturned, e:
+                        ambiguous_slugs.append(name)
 
             if category:
                 # something strange left off
 
             if category:
                 # something strange left off
@@ -145,8 +151,11 @@ class Tag(TagBase):
                 e.tags = real_tags
                 e.ambiguous_slugs = ambiguous_slugs
                 raise e
                 e.tags = real_tags
                 e.ambiguous_slugs = ambiguous_slugs
                 raise e
-            else:
-                return real_tags
+            if deprecated:
+                e = Tag.UrlDeprecationWarning()
+                e.tags = real_tags
+                raise e
+            return real_tags
         else:
             return TagBase.get_tag_list(tags)
 
         else:
             return TagBase.get_tag_list(tags)
 
@@ -156,25 +165,30 @@ class Tag(TagBase):
 
 
 # TODO: why is this hard-coded ?
 
 
 # TODO: why is this hard-coded ?
-def book_upload_path(ext=None):
+def book_upload_path(ext=None, maxlen=100):
     def get_dynamic_path(media, filename, ext=ext):
         # how to put related book's slug here?
         if not ext:
     def get_dynamic_path(media, filename, ext=ext):
         # how to put related book's slug here?
         if not ext:
-            ext = media.type
+            if media.type == 'daisy':
+                ext = 'daisy.zip'
+            else:
+                ext = media.type
         if not media.name:
             name = slughifi(filename.split(".")[0])
         else:
             name = slughifi(media.name)
         if not media.name:
             name = slughifi(filename.split(".")[0])
         else:
             name = slughifi(media.name)
-        return 'lektura/%s.%s' % (name, ext)
+        return 'book/%s/%s.%s' % (ext, name[:maxlen-len('book/%s/.%s' % (ext, ext))-4], ext)
     return get_dynamic_path
 
 
 class BookMedia(models.Model):
     return get_dynamic_path
 
 
 class BookMedia(models.Model):
-    type        = models.CharField(_('type'), choices=MEDIA_FORMATS, max_length="100")
-    name        = models.CharField(_('name'), max_length="100", blank=True)
-    file        = models.FileField(_('file'), upload_to=book_upload_path(), blank=True)
+    type        = models.CharField(_('type'), choices=MEDIA_FORMATS, max_length="100", editable=False)
+    name        = models.CharField(_('name'), max_length="100")
+    file        = OverwritingFileField(_('file'), upload_to=book_upload_path())
     uploaded_at = models.DateTimeField(_('creation date'), auto_now_add=True, editable=False)
     uploaded_at = models.DateTimeField(_('creation date'), auto_now_add=True, editable=False)
-    extra_info  = JSONField(_('extra information'), default='{}')
+    extra_info  = JSONField(_('extra information'), default='{}', editable=False)
+    book = models.ForeignKey('Book', related_name='media')
+    source_sha1 = models.CharField(null=True, blank=True, max_length=40, editable=False)
 
     def __unicode__(self):
         return "%s (%s)" % (self.name, self.file.name.split("/")[-1])
 
     def __unicode__(self):
         return "%s (%s)" % (self.name, self.file.name.split("/")[-1])
@@ -184,25 +198,75 @@ class BookMedia(models.Model):
         verbose_name        = _('book media')
         verbose_name_plural = _('book media')
 
         verbose_name        = _('book media')
         verbose_name_plural = _('book media')
 
-    def save(self, force_insert=False, force_update=False, **kwargs):
-        media = super(BookMedia, self).save(force_insert, force_update, **kwargs)
-        if self.type == 'mp3':
-            file = self.file
-            extra_info = self.get_extra_info_value()
-            extra_info.update(self.get_mp3_info())
-            self.set_extra_info_value(extra_info)
-            media = super(BookMedia, self).save(force_insert, force_update, **kwargs)
-        return media
-
-    def get_mp3_info(self):
-        """Retrieves artist and director names from audio ID3 tags."""
+    def save(self, *args, **kwargs):
         try:
         try:
-            audio = id3.ID3(self.file.path)
-            artist_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE1'))
-            director_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE3'))
-        except:
-            artist_name = director_name = ''
-        return {'artist_name': artist_name, 'director_name': director_name}
+            old = BookMedia.objects.get(pk=self.pk)
+        except BookMedia.DoesNotExist, e:
+            pass
+        else:
+            # if name changed, change the file name, too
+            if self.name != old.name:
+                self.file.save(None, ExistingFile(self.file.path), save=False, leave=True)
+
+        super(BookMedia, self).save(*args, **kwargs)
+        extra_info = self.get_extra_info_value()
+        extra_info.update(self.read_meta())
+        self.set_extra_info_value(extra_info)
+        self.source_sha1 = self.read_source_sha1(self.file.path, self.type)
+        return super(BookMedia, self).save(*args, **kwargs)
+
+    def read_meta(self):
+        """
+            Reads some metadata from the audiobook.
+        """
+
+        artist_name = director_name = project = funded_by = ''
+        if self.type == 'mp3':
+            try:
+                audio = id3.ID3(self.file.path)
+                artist_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE1'))
+                director_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE3'))
+                project = ", ".join([t.data for t in audio.getall('PRIV') 
+                        if t.owner=='wolnelektury.pl?project'])
+                funded_by = ", ".join([t.data for t in audio.getall('PRIV') 
+                        if t.owner=='wolnelektury.pl?funded_by'])
+            except:
+                pass
+        elif self.type == 'ogg':
+            try:
+                audio = mutagen.File(self.file.path)
+                artist_name = ', '.join(audio.get('artist', []))
+                director_name = ', '.join(audio.get('conductor', []))
+                project = ", ".join(audio.get('project', []))
+                funded_by = ", ".join(audio.get('funded_by', []))
+            except:
+                pass
+        else:
+            return {}
+        return {'artist_name': artist_name, 'director_name': director_name,
+                'project': project, 'funded_by': funded_by}
+
+    @staticmethod
+    def read_source_sha1(filepath, filetype):
+        """
+            Reads source file SHA1 from audiobok metadata.
+        """
+
+        if filetype == 'mp3':
+            try:
+                audio = id3.ID3(filepath)
+                return [t.data for t in audio.getall('PRIV') 
+                        if t.owner=='wolnelektury.pl?flac_sha1'][0]
+            except:
+                return None
+        elif filetype == 'ogg':
+            try:
+                audio = mutagen.File(filepath)
+                return audio.get('flac_sha1', [None])[0] 
+            except:
+                return None
+        else:
+            return None
 
 
 class Book(models.Model):
 
 
 class Book(models.Model):
@@ -221,9 +285,7 @@ class Book(models.Model):
     pdf_file      = models.FileField(_('PDF file'), upload_to=book_upload_path('pdf'), blank=True)
     epub_file     = models.FileField(_('EPUB file'), upload_to=book_upload_path('epub'), blank=True)    
     txt_file      = models.FileField(_('TXT file'), upload_to=book_upload_path('txt'), blank=True)        
     pdf_file      = models.FileField(_('PDF file'), upload_to=book_upload_path('pdf'), blank=True)
     epub_file     = models.FileField(_('EPUB file'), upload_to=book_upload_path('epub'), blank=True)    
     txt_file      = models.FileField(_('TXT file'), upload_to=book_upload_path('txt'), blank=True)        
-    # other files
-    medias        = models.ManyToManyField(BookMedia, blank=True)
-    
+
     parent        = models.ForeignKey('self', blank=True, null=True, related_name='children')
     objects  = models.Manager()
     tagged   = managers.ModelTaggedItemManager(Tag)
     parent        = models.ForeignKey('self', blank=True, null=True, related_name='children')
     objects  = models.Manager()
     tagged   = managers.ModelTaggedItemManager(Tag)
@@ -302,7 +364,7 @@ class Book(models.Model):
             else:
                 return False                          
         else:
             else:
                 return False                          
         else:
-            if self.medias.filter(book=self, type=type).count() > 0:
+            if self.media.filter(type=type).exists():
                 return True
             else:
                 return False
                 return True
             else:
                 return False
@@ -320,7 +382,7 @@ class Book(models.Model):
             elif type == "pdf":
                 return self.pdf_file
             else:                                             
             elif type == "pdf":
                 return self.pdf_file
             else:                                             
-                return self.medias.filter(book=self, type=type)
+                return self.media.filter(type=type)
         else:
             return None
 
         else:
             return None
 
@@ -354,7 +416,7 @@ class Book(models.Model):
             if self.has_media("txt"):
                 formats.append(u'<a href="%s">TXT</a>' % self.get_media('txt').url)
             # other files
             if self.has_media("txt"):
                 formats.append(u'<a href="%s">TXT</a>' % self.get_media('txt').url)
             # other files
-            for m in self.medias.order_by('type'):
+            for m in self.media.order_by('type'):
                 formats.append(u'<a href="%s">%s</a>' % (m.file.url, m.type.upper()))
 
             formats = [mark_safe(format) for format in formats]
                 formats.append(u'<a href="%s">%s</a>' % (m.file.url, m.type.upper()))
 
             formats = [mark_safe(format) for format in formats]
@@ -547,7 +609,7 @@ class Book(models.Model):
                     tag.save()
                 book_tags.append(tag)
 
                     tag.save()
                 book_tags.append(tag)
 
-        book.tags = book_tags + book_shelves
+        book.tags = set(book_tags + book_shelves)
 
         book_tag = book.book_tag()
 
 
         book_tag = book.book_tag()
 
@@ -603,8 +665,8 @@ class Book(models.Model):
                 new_fragment.save()
                 new_fragment.tags = set(book_tags + themes + [book_tag] + ancestor_tags)
 
                 new_fragment.save()
                 new_fragment.tags = set(book_tags + themes + [book_tag] + ancestor_tags)
 
-        if not settings.NO_BUILD_TXT and build_txt:
-            book.build_txt()
+            if not settings.NO_BUILD_TXT and build_txt:
+                book.build_txt()
 
         if not settings.NO_BUILD_EPUB and build_epub:
             book.root_ancestor.build_epub()
 
         if not settings.NO_BUILD_EPUB and build_epub:
             book.root_ancestor.build_epub()
@@ -759,23 +821,14 @@ def _tags_updated_handler(sender, affected_tags, **kwargs):
 tags_updated.connect(_tags_updated_handler)
 
 
 tags_updated.connect(_tags_updated_handler)
 
 
-def _m2m_changed_handler(sender, instance, action, reverse, pk_set, **kwargs):
-    """ refresh all the short_html stuff on BookMedia delete """
-    if sender == Book.medias.through and reverse and action == 'pre_clear':
-        for book in instance.book_set.all():
-            book.save()
-m2m_changed.connect(_m2m_changed_handler)
-
 def _pre_delete_handler(sender, instance, **kwargs):
 def _pre_delete_handler(sender, instance, **kwargs):
-    """ explicitly clear m2m, so that Books can be refreshed """
+    """ refresh Book on BookMedia delete """
     if sender == BookMedia:
     if sender == BookMedia:
-        instance.book_set.clear()
+        instance.book.save()
 pre_delete.connect(_pre_delete_handler)
 
 def _post_save_handler(sender, instance, **kwargs):
     """ refresh all the short_html stuff on BookMedia update """
     if sender == BookMedia:
 pre_delete.connect(_pre_delete_handler)
 
 def _post_save_handler(sender, instance, **kwargs):
     """ refresh all the short_html stuff on BookMedia update """
     if sender == BookMedia:
-        for book in instance.book_set.all():
-            book.save()
+        instance.book.save()
 post_save.connect(_post_save_handler)
 post_save.connect(_post_save_handler)
-