Ebooks are now named after the book's slug and overwrite old versions.
[wolnelektury.git] / apps / catalogue / models / bookmedia.py
1 # -*- coding: utf-8 -*-
2 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
3 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
4 #
5 import json
6 from collections import namedtuple
7 from django.db import models
8 from django.utils.translation import ugettext_lazy as _
9 from django.utils.datastructures import SortedDict
10 import jsonfield
11 from fnpdjango.utils.text.slughifi import slughifi
12 from catalogue.fields import OverwritingFileField
13
14
15 class BookMedia(models.Model):
16     """Represents media attached to a book."""
17     FileFormat = namedtuple("FileFormat", "name ext")
18     formats = SortedDict([
19         ('mp3', FileFormat(name='MP3', ext='mp3')),
20         ('ogg', FileFormat(name='Ogg Vorbis', ext='ogg')),
21         ('daisy', FileFormat(name='DAISY', ext='daisy.zip')),
22     ])
23     format_choices = [(k, _('%s file') % t.name)
24             for k, t in formats.items()]
25
26     type = models.CharField(_('type'), db_index=True, choices=format_choices, max_length=20)
27     name = models.CharField(_('name'), max_length=512)
28     file = OverwritingFileField(_('file'), max_length=600,
29         upload_to=lambda i, _n: 'book/%(ext)s/%(name)s.%(ext)s' % {
30                     'ext': i.ext(), 'name': slughifi(i.name)})
31     uploaded_at = models.DateTimeField(_('creation date'), auto_now_add=True, editable=False, db_index=True)
32     extra_info = jsonfield.JSONField(_('extra information'), default={}, editable=False)
33     book = models.ForeignKey('Book', related_name='media')
34     source_sha1 = models.CharField(null=True, blank=True, max_length=40, editable=False)
35
36     def __unicode__(self):
37         return "%s (%s)" % (self.name, self.file.name.split("/")[-1])
38
39     class Meta:
40         ordering            = ('type', 'name')
41         verbose_name        = _('book media')
42         verbose_name_plural = _('book media')
43         app_label = 'catalogue'
44
45     def save(self, *args, **kwargs):
46         from fnpdjango.utils.text.slughifi import slughifi
47         from catalogue.utils import ExistingFile, remove_zip
48
49         try:
50             old = BookMedia.objects.get(pk=self.pk)
51         except BookMedia.DoesNotExist:
52             old = None
53         else:
54             # if name changed, change the file name, too
55             if slughifi(self.name) != slughifi(old.name):
56                 self.file.save(None, ExistingFile(self.file.path), save=False, leave=True)
57
58         super(BookMedia, self).save(*args, **kwargs)
59
60         # remove the zip package for book with modified media
61         if old:
62             remove_zip("%s_%s" % (old.book.slug, old.type))
63         remove_zip("%s_%s" % (self.book.slug, self.type))
64
65         extra_info = self.extra_info
66         if isinstance(extra_info, basestring):
67             # Walkaround for weird jsonfield 'no-decode' optimization.
68             extra_info = json.loads(extra_info)
69         extra_info.update(self.read_meta())
70         self.extra_info = extra_info
71         self.source_sha1 = self.read_source_sha1(self.file.path, self.type)
72         return super(BookMedia, self).save(*args, **kwargs)
73
74     def read_meta(self):
75         """
76             Reads some metadata from the audiobook.
77         """
78         import mutagen
79         from mutagen import id3
80
81         artist_name = director_name = project = funded_by = ''
82         if self.type == 'mp3':
83             try:
84                 audio = id3.ID3(self.file.path)
85                 artist_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE1'))
86                 director_name = ', '.join(', '.join(tag.text) for tag in audio.getall('TPE3'))
87                 project = ", ".join([t.data for t in audio.getall('PRIV')
88                         if t.owner == 'wolnelektury.pl?project'])
89                 funded_by = ", ".join([t.data for t in audio.getall('PRIV')
90                         if t.owner == 'wolnelektury.pl?funded_by'])
91             except:
92                 pass
93         elif self.type == 'ogg':
94             try:
95                 audio = mutagen.File(self.file.path)
96                 artist_name = ', '.join(audio.get('artist', []))
97                 director_name = ', '.join(audio.get('conductor', []))
98                 project = ", ".join(audio.get('project', []))
99                 funded_by = ", ".join(audio.get('funded_by', []))
100             except:
101                 pass
102         else:
103             return {}
104         return {'artist_name': artist_name, 'director_name': director_name,
105                 'project': project, 'funded_by': funded_by}
106
107     def ext(self):
108         return self.formats[self.type].ext
109
110     @staticmethod
111     def read_source_sha1(filepath, filetype):
112         """
113             Reads source file SHA1 from audiobok metadata.
114         """
115         import mutagen
116         from mutagen import id3
117
118         if filetype == 'mp3':
119             try:
120                 audio = id3.ID3(filepath)
121                 return [t.data for t in audio.getall('PRIV')
122                         if t.owner == 'wolnelektury.pl?flac_sha1'][0]
123             except:
124                 return None
125         elif filetype == 'ogg':
126             try:
127                 audio = mutagen.File(filepath)
128                 return audio.get('flac_sha1', [None])[0]
129             except:
130                 return None
131         else:
132             return None