celery tasks and some other changes
authorRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Fri, 5 Aug 2011 09:06:16 +0000 (11:06 +0200)
committerRadek Czajka <radoslaw.czajka@nowoczesnapolska.org.pl>
Fri, 5 Aug 2011 09:06:16 +0000 (11:06 +0200)
22 files changed:
apps/archive/constants.py [new file with mode: 0755]
apps/archive/cover.png [new file with mode: 0644]
apps/archive/forms.py
apps/archive/migrations/0002_auto__del_field_audiobook_publishing_tags__del_field_audiobook_publish.py [new file with mode: 0644]
apps/archive/migrations/0003_auto__add_field_audiobook_source_sha1__add_field_audiobook_translator.py [new file with mode: 0644]
apps/archive/models.py
apps/archive/settings.py
apps/archive/static/style.css
apps/archive/tasks.py [new file with mode: 0755]
apps/archive/templates/archive/base.html
apps/archive/templates/archive/file_managed.html
apps/archive/templates/archive/list_published.html
apps/archive/templates/archive/tags/multiple_tags_table.html
apps/archive/templates/archive/tags/tags_table.html [new file with mode: 0755]
apps/archive/templates/base.html [new file with mode: 0755]
apps/archive/templates/registration/login.html [new file with mode: 0755]
apps/archive/templatetags/tags.py
apps/archive/utils.py
apps/archive/views.py
audiobooks/settings.py
audiobooks/urls.py
requirements.txt

diff --git a/apps/archive/constants.py b/apps/archive/constants.py
new file mode 100755 (executable)
index 0000000..17f4e78
--- /dev/null
@@ -0,0 +1,14 @@
+from django.utils.translation import ugettext_lazy as _
+
+class status:
+    WAITING = 1
+    ENCODING = 2
+    TAGGING = 3
+    SENDING = 4
+
+    choices = [
+        (WAITING, _('Waiting')),
+        (ENCODING, _('Encoding')),
+        (TAGGING, _('Tagging')),
+        (SENDING, _('Sending')),
+    ]
diff --git a/apps/archive/cover.png b/apps/archive/cover.png
new file mode 100644 (file)
index 0000000..1cdf8e1
Binary files /dev/null and b/apps/archive/cover.png differ
index 922bd28..c551cf8 100755 (executable)
@@ -1,6 +1,6 @@
+from datetime import datetime
 import os
 import os.path
-from datetime import datetime
 
 from django import forms
 from django.utils.translation import ugettext_lazy as _
@@ -8,7 +8,7 @@ import mutagen
 
 from archive.models import Audiobook
 from archive.settings import FILES_PATH
-from archive.utils import ExistingFile
+from archive.utils import ExistingFile, sha1_file
 
 class AudiobookForm(forms.ModelForm):
     class Meta:
@@ -25,9 +25,12 @@ class AudiobookForm(forms.ModelForm):
             # save the file in model
 
             m.source_file.save(
-                os.path.join(FILES_PATH, os.path.basename(path)),
+                os.path.basename(path),
                 ExistingFile(path))
 
+            f = open(m.source_file.path)
+            m.source_sha1 = sha1_file(f)
+            f.close()
+
         if commit:
             m.save()
-
diff --git a/apps/archive/migrations/0002_auto__del_field_audiobook_publishing_tags__del_field_audiobook_publish.py b/apps/archive/migrations/0002_auto__del_field_audiobook_publishing_tags__del_field_audiobook_publish.py
new file mode 100644 (file)
index 0000000..03cb130
--- /dev/null
@@ -0,0 +1,139 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        
+        # Deleting field 'Audiobook.publishing_tags'
+        db.delete_column('archive_audiobook', 'publishing_tags')
+
+        # Deleting field 'Audiobook.published_tags'
+        db.delete_column('archive_audiobook', 'published_tags')
+
+        # Deleting field 'Audiobook.publish_wait'
+        db.delete_column('archive_audiobook', 'publish_wait')
+
+        # Deleting field 'Audiobook.published'
+        db.delete_column('archive_audiobook', 'published')
+
+        # Deleting field 'Audiobook.publishing'
+        db.delete_column('archive_audiobook', 'publishing')
+
+        # Adding field 'Audiobook.mp3_status'
+        db.add_column('archive_audiobook', 'mp3_status', self.gf('django.db.models.fields.SmallIntegerField')(null=True), keep_default=False)
+
+        # Adding field 'Audiobook.mp3_task'
+        db.add_column('archive_audiobook', 'mp3_task', self.gf('django.db.models.fields.CharField')(max_length=64, null=True), keep_default=False)
+
+        # Adding field 'Audiobook.mp3_tags'
+        db.add_column('archive_audiobook', 'mp3_tags', self.gf('jsonfield.fields.JSONField')(null=True), keep_default=False)
+
+        # Adding field 'Audiobook.mp3_published_tags'
+        db.add_column('archive_audiobook', 'mp3_published_tags', self.gf('jsonfield.fields.JSONField')(null=True), keep_default=False)
+
+        # Adding field 'Audiobook.mp3_published'
+        db.add_column('archive_audiobook', 'mp3_published', self.gf('django.db.models.fields.DateTimeField')(null=True), keep_default=False)
+
+        # Adding field 'Audiobook.ogg_status'
+        db.add_column('archive_audiobook', 'ogg_status', self.gf('django.db.models.fields.SmallIntegerField')(null=True), keep_default=False)
+
+        # Adding field 'Audiobook.ogg_task'
+        db.add_column('archive_audiobook', 'ogg_task', self.gf('django.db.models.fields.CharField')(max_length=64, null=True), keep_default=False)
+
+        # Adding field 'Audiobook.ogg_tags'
+        db.add_column('archive_audiobook', 'ogg_tags', self.gf('jsonfield.fields.JSONField')(null=True), keep_default=False)
+
+        # Adding field 'Audiobook.ogg_published_tags'
+        db.add_column('archive_audiobook', 'ogg_published_tags', self.gf('jsonfield.fields.JSONField')(null=True), keep_default=False)
+
+        # Adding field 'Audiobook.ogg_published'
+        db.add_column('archive_audiobook', 'ogg_published', self.gf('django.db.models.fields.DateTimeField')(null=True), keep_default=False)
+
+
+    def backwards(self, orm):
+        
+        # Adding field 'Audiobook.publishing_tags'
+        db.add_column('archive_audiobook', 'publishing_tags', self.gf('jsonfield.fields.JSONField')(null=True), keep_default=False)
+
+        # Adding field 'Audiobook.published_tags'
+        db.add_column('archive_audiobook', 'published_tags', self.gf('jsonfield.fields.JSONField')(null=True), keep_default=False)
+
+        # Adding field 'Audiobook.publish_wait'
+        db.add_column('archive_audiobook', 'publish_wait', self.gf('django.db.models.fields.DateTimeField')(null=True), keep_default=False)
+
+        # Adding field 'Audiobook.published'
+        db.add_column('archive_audiobook', 'published', self.gf('django.db.models.fields.DateTimeField')(null=True), keep_default=False)
+
+        # Adding field 'Audiobook.publishing'
+        db.add_column('archive_audiobook', 'publishing', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False)
+
+        # Deleting field 'Audiobook.mp3_status'
+        db.delete_column('archive_audiobook', 'mp3_status')
+
+        # Deleting field 'Audiobook.mp3_task'
+        db.delete_column('archive_audiobook', 'mp3_task')
+
+        # Deleting field 'Audiobook.mp3_tags'
+        db.delete_column('archive_audiobook', 'mp3_tags')
+
+        # Deleting field 'Audiobook.mp3_published_tags'
+        db.delete_column('archive_audiobook', 'mp3_published_tags')
+
+        # Deleting field 'Audiobook.mp3_published'
+        db.delete_column('archive_audiobook', 'mp3_published')
+
+        # Deleting field 'Audiobook.ogg_status'
+        db.delete_column('archive_audiobook', 'ogg_status')
+
+        # Deleting field 'Audiobook.ogg_task'
+        db.delete_column('archive_audiobook', 'ogg_task')
+
+        # Deleting field 'Audiobook.ogg_tags'
+        db.delete_column('archive_audiobook', 'ogg_tags')
+
+        # Deleting field 'Audiobook.ogg_published_tags'
+        db.delete_column('archive_audiobook', 'ogg_published_tags')
+
+        # Deleting field 'Audiobook.ogg_published'
+        db.delete_column('archive_audiobook', 'ogg_published')
+
+
+    models = {
+        'archive.audiobook': {
+            'Meta': {'ordering': "('title',)", 'object_name': 'Audiobook'},
+            'artist': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'conductor': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'encoded_by': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'modified': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+            'mp3_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True'}),
+            'mp3_published': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+            'mp3_published_tags': ('jsonfield.fields.JSONField', [], {'null': 'True'}),
+            'mp3_status': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True'}),
+            'mp3_tags': ('jsonfield.fields.JSONField', [], {'null': 'True'}),
+            'mp3_task': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}),
+            'ogg_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True'}),
+            'ogg_published': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+            'ogg_published_tags': ('jsonfield.fields.JSONField', [], {'null': 'True'}),
+            'ogg_status': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True'}),
+            'ogg_tags': ('jsonfield.fields.JSONField', [], {'null': 'True'}),
+            'ogg_task': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['archive.Project']"}),
+            'source_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'url': ('django.db.models.fields.URLField', [], {'max_length': '255'})
+        },
+        'archive.project': {
+            'Meta': {'ordering': "('name',)", 'object_name': 'Project'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
+            'sponsors': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        }
+    }
+
+    complete_apps = ['archive']
diff --git a/apps/archive/migrations/0003_auto__add_field_audiobook_source_sha1__add_field_audiobook_translator.py b/apps/archive/migrations/0003_auto__add_field_audiobook_source_sha1__add_field_audiobook_translator.py
new file mode 100644 (file)
index 0000000..9711b72
--- /dev/null
@@ -0,0 +1,84 @@
+# encoding: utf-8
+import datetime
+from hashlib import sha1
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+def sha1_file(f):
+    sha = sha1()
+    for piece in iter(lambda: f.read(1024*1024), ''):
+        sha.update(piece)
+    return sha.hexdigest()
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        
+        # Adding field 'Audiobook.source_sha1'
+        db.add_column('archive_audiobook', 'source_sha1', self.gf('django.db.models.fields.CharField')(default='', max_length=40), keep_default=False)
+
+        # Adding field 'Audiobook.translator'
+        db.add_column('archive_audiobook', 'translator', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True), keep_default=False)
+
+        if not db.dry_run:
+            for obj in orm.Audiobook.objects.all():
+                try:
+                    f = open(obj.source_file.path)
+                except ValueError, e:
+                    print "Audiobook has no source file"
+                    pass
+                else:
+                    obj.source_sha1 = sha1_file(f)
+                    f.close()
+                    obj.save()
+
+
+    def backwards(self, orm):
+        
+        # Deleting field 'Audiobook.source_sha1'
+        db.delete_column('archive_audiobook', 'source_sha1')
+
+        # Deleting field 'Audiobook.translator'
+        db.delete_column('archive_audiobook', 'translator')
+
+
+    models = {
+        'archive.audiobook': {
+            'Meta': {'ordering': "('title',)", 'object_name': 'Audiobook'},
+            'artist': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'conductor': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'date': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'encoded_by': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'modified': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+            'mp3_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True'}),
+            'mp3_published': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+            'mp3_published_tags': ('jsonfield.fields.JSONField', [], {'null': 'True'}),
+            'mp3_status': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True'}),
+            'mp3_tags': ('jsonfield.fields.JSONField', [], {'null': 'True'}),
+            'mp3_task': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}),
+            'ogg_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100', 'null': 'True'}),
+            'ogg_published': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+            'ogg_published_tags': ('jsonfield.fields.JSONField', [], {'null': 'True'}),
+            'ogg_status': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True'}),
+            'ogg_tags': ('jsonfield.fields.JSONField', [], {'null': 'True'}),
+            'ogg_task': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True'}),
+            'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['archive.Project']"}),
+            'source_file': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
+            'source_sha1': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'translator': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'url': ('django.db.models.fields.URLField', [], {'max_length': '255'})
+        },
+        'archive.project': {
+            'Meta': {'ordering': "('name',)", 'object_name': 'Project'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'}),
+            'sponsors': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
+        }
+    }
+
+    complete_apps = ['archive']
index 78c5ab3..cf3829b 100644 (file)
@@ -1,7 +1,10 @@
+# -*- coding: utf-8 -*-
+
 from django.db import models
 from jsonfield.fields import JSONField
 from django.utils.translation import ugettext_lazy as _
-from archive.settings import FILES_PATH
+from archive.constants import status
+from archive.settings import FILES_PATH, ADVERT, LICENSE, ORGANIZATION, PROJECT
 
 # Create your models here.
 
@@ -22,7 +25,8 @@ class Project(models.Model):
 
 
 class Audiobook(models.Model):
-    source_file = models.FileField(upload_to=FILES_PATH, verbose_name=_('source file'), editable=False)
+    source_file = models.FileField(upload_to='archive/files', verbose_name=_('source file'), editable=False)
+    source_sha1 = models.CharField(max_length=40, editable=False)
 
     title = models.CharField(max_length=255, verbose_name=_('title'))
     artist = models.CharField(max_length=255, verbose_name=_('artist'))
@@ -31,16 +35,24 @@ class Audiobook(models.Model):
     date = models.CharField(max_length=255, verbose_name=_('date'))
     project = models.ForeignKey(Project, verbose_name=_('project'))
     url = models.URLField(max_length=255, verbose_name=_('book url'))
+    translator = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('translator'))
     modified = models.DateTimeField(null=True, editable=False)
 
-    published_tags = JSONField(null=True, editable=False)
+    # publishing process
+    mp3_status = models.SmallIntegerField(null=True, editable=False, choices=status.choices)
+    mp3_task = models.CharField(max_length=64, null=True, editable=False)
+    mp3_tags = JSONField(null=True, editable=False)
     mp3_file = models.FileField(null=True, upload_to='archive/final', editable=False)
+    mp3_published_tags = JSONField(null=True, editable=False)
+    mp3_published = models.DateTimeField(null=True, editable=False)
+
+    ogg_status = models.SmallIntegerField(null=True, editable=False, choices=status.choices)
+    ogg_task = models.CharField(max_length=64, null=True, editable=False)
+    ogg_tags = JSONField(null=True, editable=False)
     ogg_file = models.FileField(null=True, upload_to='archive/final', editable=False)
-    publishing_tags = JSONField(null=True, editable=False)
+    ogg_published_tags = JSONField(null=True, editable=False)
+    ogg_published = models.DateTimeField(null=True, editable=False)
 
-    publish_wait = models.DateTimeField(null=True, editable=False) # somebody hit "publish"
-    publishing = models.BooleanField(default=False, editable=False)
-    published = models.DateTimeField(null=True, editable=False)
 
     class Meta:
         verbose_name = _("audiobook")
@@ -50,8 +62,35 @@ class Audiobook(models.Model):
     def __unicode__(self):
         return self.title
 
+    def published(self):
+        return self.mp3_published and self.ogg_published
+
     def new_publish_tags(self):
+        title = self.title
+        if self.translator:
+            title += u' (tÅ‚um. %s)' % self.translator
+
+        copyright = u"%s %s. Licensed to the public under %s verify at %s" % (
+                self.date, ORGANIZATION, LICENSE, self.url)
+
+        comment = u"Audiobook nagrany w ramach projektu %s%s.\n%s" % (
+                    self.project.name,
+                    u" finansowanego przez %s" % self.project.sponsors if self.project.sponsors else "",
+                    ADVERT)
+
         return {
-            'title': self.title,
-            'copyright': 'Fundacja Nowoczesna Polska',
+            'album': PROJECT,
+            'albumartist': ORGANIZATION,
+            'artist': self.artist,
+            'comment': comment,
+            'conductor': self.conductor,
+            'contact': self.url,
+            'copyright': copyright,
+            'date': self.date,
+            'genre': u'Speech',
+            'language': u'pol',
+            'license': LICENSE,
+            'organization': ORGANIZATION,
+            'title': title,
+            'flac_sha1': self.source_sha1,
         }
index a0db355..9c05b70 100755 (executable)
@@ -1,3 +1,5 @@
+# -*- coding: utf-8 -*-
+
 import os.path
 from django.conf import settings
 
@@ -30,3 +32,69 @@ try:
 except AttributeError:
     FINAL_PATH = os.path.abspath(os.path.join(settings.MEDIA_ROOT,
                         "archive/final"))
+
+
+# here the app keeps temporary build files
+try:
+    BUILD_PATH = settings.ARCHIVE_BUILD_PATH
+except AttributeError:
+    BUILD_PATH = os.path.abspath(os.path.join(settings.MEDIA_ROOT,
+                        "archive/build"))
+
+# upload conf
+try:
+    UPLOAD_HOST = settings.ARCHIVE_UPLOAD_HOST
+except AttributeError:
+    UPLOAD_HOST = 'wolnelektury.pl'
+
+try:
+    UPLOAD_USER = settings.ARCHIVE_UPLOAD_USER
+except AttributeError:
+    UPLOAD_USER = 'username'
+
+try:
+    UPLOAD_PATH = settings.ARCHIVE_UPLOAD_PATH
+except AttributeError:
+    UPLOAD_PATH = ''
+
+try:
+    UPLOAD_CMD = settings.ARCHIVE_UPLOAD_CMD
+except AttributeError:
+    UPLOAD_CMD = '/path/to/manage.py savemedia'
+
+try:
+    UPLOAD_SUDO = settings.ARCHIVE_UPLOAD_SUDO
+except AttributeError:
+    UPLOAD_SUDO = None
+
+
+try:
+    PROJECT = settings.ARCHIVE_PROJECT
+except AttributeError:
+    PROJECT = u'Wolne Lektury'
+
+try:
+    LICENSE = settings.ARCHIVE_LICENSE
+except AttributeError:
+    LICENSE = u'http://creativecommons.org/licenses/by-sa/3.0/deed.pl'
+
+try:
+    ORGANIZATION = settings.ARCHIVE_ORGANIZATION
+except AttributeError:
+    ORGANIZATION = u'Fundacja Nowoczesna Polska'
+
+try:
+    ADVERT = settings.ARCHIVE_ADVERT
+except AttributeError:
+    ADVERT = u"""
+Przekaż 1% podatku na rozwój Wolnych Lektur:
+Fundacja Nowoczesna Polska
+KRS 0000070056
+http://nowoczesnapolska.org.pl/wesprzyj_nas/
+"""
+
+try:
+    COVER_IMAGE = settings.ARCHIVE_COVER_IMAGE
+except AttributeError:
+    COVER_IMAGE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'cover.png')
+
index 0d30ad6..2e7dbb3 100755 (executable)
@@ -47,3 +47,13 @@ a {
 .errorlist li {
     color: red;
 }
+
+
+.tags, .multiple_tags {
+    border-collapse: collapse;
+}
+
+.tags td, .tags th, .multiple_tags td, .multiple_tags th {
+    border: 1px solid #ddd;
+    padding: 3px;
+}
diff --git a/apps/archive/tasks.py b/apps/archive/tasks.py
new file mode 100755 (executable)
index 0000000..b90b15e
--- /dev/null
@@ -0,0 +1,178 @@
+from datetime import datetime
+import errno
+import mimetypes
+import os
+import os.path
+import pipes
+import subprocess
+from tempfile import NamedTemporaryFile
+from time import sleep
+
+#from celery.decorators import task
+from celery.task import Task
+from fabric import api
+from mutagen import File
+from mutagen import id3
+
+import mutagen
+
+from archive.constants import status
+from archive.models import Audiobook
+from archive.settings import (BUILD_PATH, COVER_IMAGE,
+    UPLOAD_HOST, UPLOAD_USER, UPLOAD_PATH, UPLOAD_CMD, UPLOAD_SUDO)
+from archive.utils import ExistingFile
+
+api.env.host_string = UPLOAD_HOST
+api.env.user = UPLOAD_USER
+
+class AudioFormatTask(Task):
+    abstract = True
+
+    @classmethod
+    def set_status(cls, audiobook, status):
+        setattr(audiobook, '%s_status' % cls.ext, status)
+        audiobook.save()
+
+    @staticmethod
+    def encode(in_path, out_path):
+        pass
+
+    @classmethod
+    def set_tags(cls, audiobook, file_name):
+        audio = File(file_name)
+        for k, v in getattr(audiobook, "%s_tags" % cls.ext)['tags'].items():
+            audio[k] = v
+        audio.save()
+
+    @classmethod
+    def save(cls, audiobook, file_name):
+        getattr(audiobook, "%s_file" % cls.ext).save(
+            "%d.%s" % (audiobook.pk, cls.ext),
+            ExistingFile(file_name)
+            )
+
+    @classmethod
+    def published(cls, audiobook):
+        setattr(audiobook, "%s_published_tags" % cls.ext,
+            getattr(audiobook, "%s_tags" % cls.ext))
+        setattr(audiobook, "%s_tags" % cls.ext, None)
+        setattr(audiobook, "%s_published" % cls.ext, datetime.now())
+        cls.set_status(audiobook, None)
+
+    @classmethod
+    def put(cls, audiobook):
+        tags = getattr(audiobook, "%s_tags" % cls.ext)
+        prefix, slug = tags['url'].rstrip('/').rsplit('/', 1)
+        name = tags['name']
+        path = getattr(audiobook, "%s_file" % cls.ext).path
+        api.put(path, UPLOAD_PATH)
+        command = UPLOAD_CMD + (u' %s %s %s > output.txt' % (
+            pipes.quote(os.path.join(UPLOAD_PATH, os.path.basename(path))),
+            pipes.quote(slug),
+            pipes.quote(name)
+            )).encode('utf-8')
+        print command
+        if UPLOAD_SUDO:
+            api.sudo(command, user=UPLOAD_SUDO, shell=False)
+        else:
+            api.run(command)
+
+    def run(self, aid):
+        audiobook = Audiobook.objects.get(id=aid)
+        self.set_status(audiobook, status.ENCODING)
+
+        try:
+            os.makedirs(BUILD_PATH)
+        except OSError as e:
+            if e.errno == errno.EEXIST:
+                pass
+            else:
+                raise
+
+        out_file = NamedTemporaryFile(delete=False, prefix='audiobook-', suffix='.%s' % self.ext, dir=BUILD_PATH)
+        out_file.close()
+        self.encode(audiobook.source_file.path, out_file.name)
+        self.set_status(audiobook, status.TAGGING)
+        self.set_tags(audiobook, out_file.name)
+        self.save(audiobook, out_file.name)
+        self.set_status(audiobook, status.SENDING)
+
+        #self.put(audiobook)
+
+        self.published(audiobook)
+        audiobook.save()
+
+
+class Mp3Task(AudioFormatTask):
+    ext = 'mp3'
+
+    # these shouldn't be staticmethods
+    def id3_text(tag, text):
+        return tag(encoding=1, text=text)
+    def id3_url(tag, text):
+        return tag(url=text)
+    def id3_comment(tag, text, lang=u'pol'):
+        return tag(encoding=1, lang=lang, desc=u'', text=text)
+    def id3_sha1(tag, text, what=u''):
+        return tag(owner='http://wolnelektury.pl?%s' % what, data=text)
+
+    TAG_MAP = {
+        'album': (id3_text, id3.TALB),
+        'albumartist': (id3_text, id3.TPE2),
+        'artist': (id3_text, id3.TPE1),
+        'conductor': (id3_text, id3.TPE3),
+        'copyright': (id3_text, id3.TCOP),
+        'date': (id3_text, id3.TDRC),
+        'genre': (id3_text, id3.TCON),
+        'language': (id3_text, id3.TLAN),
+        'organization': (id3_text, id3.TPUB),
+        'title': (id3_text, id3.TIT2),
+        'comment': (id3_comment, id3.COMM, 'pol'),
+        'contact': (id3_url, id3.WOAF),
+        'license': (id3_url, id3.WCOP),
+        'flac_sha1': (id3_sha1, id3.PRIV, 'flac_sha1'),
+    }
+
+    @staticmethod
+    def encode(in_path, out_path):
+        # 44.1kHz 64kbps mono MP3
+        subprocess.check_call(['ffmpeg', 
+            '-i', in_path,
+            '-ar', '44100',
+            '-ab', '64k',
+            '-ac', '1',
+            '-y',
+            out_path
+            ])
+
+    @classmethod
+    def set_tags(cls, audiobook, file_name):
+        audio = id3.ID3(file_name)
+        for k, v in audiobook.mp3_tags['tags'].items():
+            factory_tuple = cls.TAG_MAP[k]
+            factory, tagtype = factory_tuple[:2]
+            audio.add(factory(tagtype, v, *factory_tuple[2:]))
+
+        if COVER_IMAGE:
+            mime = mimetypes.guess_type(COVER_IMAGE)
+            f = open(COVER_IMAGE)
+            audio.add(id3.APIC(encoding=0, mime=mime, type=3, desc=u'', data=f.read()))
+            f.close()
+
+        audio.save()
+
+
+class OggTask(AudioFormatTask):
+    ext = 'ogg'
+
+    @staticmethod
+    def encode(in_path, out_path):
+        # 44.1kHz 64kbps mono Ogg Vorbis
+        subprocess.check_call(['oggenc', 
+            in_path,
+            '--discard-comments',
+            '--resample', '44100',
+            '--downmix',
+            '-b', '64',
+            '-o', out_path
+            ])
index 807227b..fbc5a18 100644 (file)
@@ -1,28 +1,11 @@
+{% extends "base.html" %}
 {% load i18n %}
-<!DOCTYPE html>
-<html>
-<head>
-    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
-    <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}style.css" />
-    <title>{% trans "Audiobook repository" %}</title>
-</head>
-<body>
 
-<div id="repo-zones-nav">
+{% block repo-zones-nav %}
     <a {% if division = "new" %}class="active" {% endif %}href="{% url list_new %}">{% trans "New" %}</a>
     <a {% if division = "unpublished" %}class="active" {% endif %}href="{% url list_unpublished %}">{% trans "Unpublished" %}</a>
     <a {% if division = "published" %}class="active" {% endif %}href="{% url list_published %}">{% trans "Published" %}</a>
     <a {% if division = "unmanaged" %}class="active" {% endif %}href="{% url list_unmanaged %}">{% trans "Archive" %}</a>
+    <a href="{% url logout %}" style='float: right;'>{% trans "Logout" %}</a>
     <div class='clr' ></div>
-</div>
-
-<div id="messages">
-{% block messages %}{% endblock %}
-</div>
-
-<div id="content">
-{% block content %}{% endblock %}
-</div>
-
-</body>
-</html>
+{% endblock %}
index 897aae2..c67edea 100755 (executable)
@@ -1,37 +1,46 @@
 {% extends "archive/base.html" %}
 {% load i18n %}
+{% load tags %}
 
 {% block content %}
 
 <h2>{% trans "Publishing" %}</h2>
 
-{% if audiobook.publish_wait %}
-    <p>{% trans "Audiobook marked for publishing with tags:" %}</p>
+{% if audiobook.mp3_status or audiobook.ogg_status %}
 
-    <table>
-        {% for t, v in audiobook.publishing_tags.items %}
-            <tr><th>{{ t }}</th><td>{{ v }}</td></tr>
-        {% endfor %}
-    </table>
+<h2>{% trans "Publishing pending" %}</h2>
 
-    {% if audiobook.publishing %}
-        <p>{% trans "Publishing already in progress." %}</p>
-    {% else %}
-        <form method="post" action="{% url cancel_publishing audiobook.id %}">
-            {% csrf_token %}
-            <input type="submit" value="{% trans "Cancel publishing" %}" />
-        </form>
-    {% endif %}
-{% else %}
-    {% if audiobook.published %}
-        Here be currently published version, for comparison.
-    {% endif %}
+<form method="post" action="{% url cancel_publishing audiobook.id %}">
+    {% csrf_token %}
+    <input type="submit" value="{% trans "Cancel publishing" %}" />
+</form>
+
+
+{% if audiobook.mp3_status %}
+    <hr/>
+    <h2>MP3</h2>
+
+    {% tags_table audiobook.mp3_tags.tags %}
+
+    <p>Status: <b>{{ audiobook.get_mp3_status_display }}</b></p>
+{% endif %}
+
+{% if audiobook.ogg_status %}
+    <hr/>
+    <h2>Ogg Vorbis</h2>
+
+    {% tags_table audiobook.ogg_tags.tags %}
 
+    <p>Status: <b>{{ audiobook.get_ogg_status_display }}</b></p>
+{% endif %}
+
+
+
+
+{% else %}
     <form method="post" action="{% url publish audiobook.id %}">
-    <table>
-        {% for k, v in audiobook.new_publish_tags.items %}
-            <tr><th>{{ k }}</th><td>{{ v }}</td></tr>
-        {% endfor %}
+    <table class='tags'>
+        {% tags_table audiobook.new_publish_tags 0 %}
 
         {% csrf_token %}
         <tr><th></th><td><input type="submit" value="{% trans "Publish" %}" /></td></tr>
     </form>
 {% endif %}
 
+<hr/>
+{% if audiobook.mp3_published %}
+    <h2>Published MP3</h2>
+    <a href="{{ audiobook.mp3_file.url }}">{{ audiobook.mp3_published }}</a>
+    {% tags_table audiobook.mp3_published_tags.tags %}
+{% else %}<p>MP3 file hasn't been published yet.</p>
+{% endif %}
+
+<hr/>
+{% if audiobook.ogg_published %}
+    <h2>Published Ogg Vorbis</h2>
+    <a href="{{ audiobook.ogg_file.url }}">{{ audiobook.ogg_published }}</a>
+    {% tags_table audiobook.ogg_published_tags.tags %}
+{% else %}Ogg Vorbis file hasn't been published yet.
+{% endif %}
+
 
 
 
 
 <h2>{% trans "Update tags" %}</h2>
 
+Last modified: {{ audiobook.modified }}
 
-<table class="file_tags">
-    {% for t, v in tags.items %}
-        <tr><th>{{t}}</th><td>
-        {% for x in v %}
-            {{x}}<br />
-        {% endfor %}
-        </td></tr>
-    {% endfor %}
-</table>
+{% multiple_tags_table tags %}
 
 
 
index c51cace..49544b4 100755 (executable)
@@ -14,7 +14,7 @@
 {% block file-list %}
     {% for file in objects %}
         <li>
-            <a href='{% url file file %}'>{{ file }}</a></form>
+            <a href='{% url file file.id %}'>{{ file }}</a></form>
         </li>
     {% endfor %}
 {% endblock %}
index 4a39808..4609278 100755 (executable)
@@ -1,9 +1,9 @@
-<table>
+{% if table %}<table class='multiple_tags'>{% endif %}
     {% for t, v in tags.items %}
-        <tr><th>{{t}}</th><td>
+        <tr><th>{{ t }}</th><td>
         {% for x in v %}
-            <div>{{x}}</div>
+            <div>{{ x|linebreaksbr }}</div>
         {% endfor %}
         </td></tr>
     {% endfor %}
-</table>
+{% if table %}</table>{% endif %}
diff --git a/apps/archive/templates/archive/tags/tags_table.html b/apps/archive/templates/archive/tags/tags_table.html
new file mode 100755 (executable)
index 0000000..70b9d48
--- /dev/null
@@ -0,0 +1,7 @@
+{% if table %}<table class='tags'>{% endif %}
+    {% for t, v in tags.items %}
+        <tr><th>{{ t }}</th><td>
+            {{ v|linebreaksbr }}
+        </td></tr>
+    {% endfor %}
+{% if table %}</table>{% endif %}
diff --git a/apps/archive/templates/base.html b/apps/archive/templates/base.html
new file mode 100755 (executable)
index 0000000..c6b7ee6
--- /dev/null
@@ -0,0 +1,24 @@
+{% load i18n %}
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
+    <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}style.css" />
+    <title>{% trans "Audiobook repository" %}</title>
+</head>
+<body>
+
+<div id="repo-zones-nav">
+    {% block repo-zones-nav %}&nbsp;{% endblock %}
+</div>
+
+<div id="messages">
+{% block messages %}{% endblock %}
+</div>
+
+<div id="content">
+{% block content %}{% endblock %}
+</div>
+
+</body>
+</html>
diff --git a/apps/archive/templates/registration/login.html b/apps/archive/templates/registration/login.html
new file mode 100755 (executable)
index 0000000..115cdf1
--- /dev/null
@@ -0,0 +1,12 @@
+{% extends "base.html" %}
+{% load i18n %}
+
+{% block content %}
+
+<form method='post'>
+{% csrf_token %}
+{{ form.as_p }}
+<p><input type='submit' value='{% trans "Login" %}' /></p>
+</form>
+
+{% endblock %}
index 68a6a9b..25389ef 100755 (executable)
@@ -2,18 +2,11 @@ from django import template
 
 register = template.Library()
 
-@register.simple_tag
-def multiple_tags_table(tags):
-    return template.loader.render_to_string(
-            "archive/tags/multiple_tags_table.html",
-            {"tags": tags}
-        )
+@register.inclusion_tag('archive/tags/multiple_tags_table.html')
+def multiple_tags_table(tags, table=True):
+    return locals()
 
 
-#@register.simple_tag
-#def multiple_tags_table(tags):
-#    return template.loader.render_to_string(
-#            "archive/tags/multiple_tags_table.html",
-#            {"tags": tags}
-#        )
-
+@register.inclusion_tag('archive/tags/tags_table.html')
+def tags_table(tags, table=True):
+    return locals()
index 4710c20..6e0a8d1 100755 (executable)
@@ -1,5 +1,7 @@
+from hashlib import sha1
 from django.core.files.uploadedfile import UploadedFile
 
+
 class ExistingFile(UploadedFile):
 
     def __init__(self, path, *args, **kwargs):
@@ -11,3 +13,10 @@ class ExistingFile(UploadedFile):
 
     def close(self):
         pass
+
+
+def sha1_file(f):
+    sha = sha1()
+    for piece in iter(lambda: f.read(1024*1024), ''):
+        sha.update(piece)
+    return sha.hexdigest()
index 3ee87cb..575f5d9 100644 (file)
@@ -5,17 +5,23 @@ import os
 import os.path
 
 from archive import settings
+from django.contrib.auth import logout
+from django.contrib.auth.decorators import login_required
 from django.core.urlresolvers import reverse
+from django.db.models import Q
 from django.http import Http404
 from django.shortcuts import render, redirect, get_object_or_404
 from django.views.decorators.http import require_POST
 
 import mutagen
 
+from archive.constants import status
 from archive import models
 from archive.forms import AudiobookForm
+from archive import tasks
 
 
+@login_required
 def list_new(request):
     division = 'new'
 
@@ -24,6 +30,7 @@ def list_new(request):
     return render(request, "archive/list_new.html", locals())
 
 
+@login_required
 def file_new(request, filename):
     division = 'new'
 
@@ -60,6 +67,7 @@ def file_new(request, filename):
 
 
 @require_POST
+@login_required
 def move_to_archive(request, filename):
     """ move a new file to the unmanaged files dir """
 
@@ -84,6 +92,7 @@ def move_to_archive(request, filename):
 
 
 @require_POST
+@login_required
 def move_to_new(request, filename):
     """ move a unmanaged file to new files dir """
 
@@ -106,37 +115,70 @@ def move_to_new(request, filename):
 
     return redirect(list_unmanaged)
 
+
 @require_POST
-def publish(request, id):
+@login_required
+def publish(request, aid):
     """ mark file for publishing """
-    audiobook = get_object_or_404(models.Audiobook, id=id)
-    audiobook.publish_wait = datetime.now()
-    audiobook.publishing_tags = audiobook.new_publish_tags()
+    audiobook = get_object_or_404(models.Audiobook, id=aid)
+    tags = {
+        'name': audiobook.title,
+        'url': audiobook.url,
+        'tags': audiobook.new_publish_tags(),
+        }
+    audiobook.mp3_tags = tags
+    audiobook.ogg_tags = tags
+    audiobook.mp3_status = audiobook.ogg_status = status.WAITING
     audiobook.save()
-    return redirect(file_managed, id)
+    # isn't there a race here?
+    audiobook.mp3_task = tasks.Mp3Task.delay(aid).task_id
+    audiobook.ogg_task = tasks.OggTask.delay(aid).task_id
+    audiobook.save()
+
+    return redirect(file_managed, aid)
+
 
 @require_POST
-def cancel_publishing(request, id):
+@login_required
+def cancel_publishing(request, aid):
     """ cancel scheduled publishing """
-    audiobook = get_object_or_404(models.Audiobook, id=id)
-    if not audiobook.publishing:
-        audiobook.publish_wait = None
-        audiobook.publishing_tags = None
-        audiobook.save()
-    return redirect(file_managed, id)
+    audiobook = get_object_or_404(models.Audiobook, id=aid)
+    # TODO: cancel tasks
+    audiobook.mp3_status = None
+    audiobook.ogg_status = None
+    audiobook.save()
+    return redirect(file_managed, aid)
 
 
+@login_required
 def list_unpublished(request):
     division = 'unpublished'
 
-    objects = models.Audiobook.objects.filter(published=None)
+    objects = models.Audiobook.objects.filter(Q(mp3_published=None) | Q(ogg_published=None))
     return render(request, "archive/list_unpublished.html", locals())
 
 
+@login_required
+def list_published(request):
+    division = 'published'
+
+    objects = models.Audiobook.objects.exclude(Q(mp3_published=None) | Q(ogg_published=None))
+    return render(request, "archive/list_published.html", locals())
+
 
+@login_required
 def file_managed(request, id):
     audiobook = get_object_or_404(models.Audiobook, id=id)
-    division = 'published' if audiobook.published else 'unpublished'
+
+    if request.POST:
+        form = AudiobookForm(request.POST, instance=audiobook)
+        if form.is_valid():
+            try:
+                form.save()
+            except IOError:
+                raise Http404
+
+    division = 'published' if audiobook.published() else 'unpublished'
 
     # for tags update
     tags = mutagen.File(audiobook.source_file.path)
@@ -145,16 +187,7 @@ def file_managed(request, id):
     return render(request, "archive/file_managed.html", locals())
 
 
-
-def list_published(request):
-    division = 'published'
-
-    objects = models.Audiobook.objects.exclude(published=None)
-    return render(request, "archive/list_published.html", locals())
-
-
-
-
+@login_required
 def list_unmanaged(request):
     division = 'unmanaged'
 
@@ -162,6 +195,7 @@ def list_unmanaged(request):
     return render(request, "archive/list_unmanaged.html", locals())
 
 
+@login_required
 def file_unmanaged(request, filename):
     division = 'unmanaged'
 
@@ -169,3 +203,8 @@ def file_unmanaged(request, filename):
     err_exists = request.GET.get('exists')
     return render(request, "archive/file_unmanaged.html", locals())
 
+
+@login_required
+def logout_view(request):
+    logout(request)
+    return redirect(list_new)
index 9f0731a..dc55333 100644 (file)
@@ -58,7 +58,7 @@ MEDIA_ROOT = os.path.join(PROJECT_ROOT, '../media')
 # URL that handles the media served from MEDIA_ROOT. Make sure to use a
 # trailing slash.
 # Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
-MEDIA_URL = ''
+MEDIA_URL = '/media/'
 
 # Absolute path to the directory static files should be collected to.
 # Don't put anything in this directory yourself; store your static files
@@ -128,6 +128,9 @@ INSTALLED_APPS = (
     # Uncomment the next line to enable admin documentation:
     # 'django.contrib.admindocs',
 
+    'djcelery',
+    'djkombu',
+
     'south',
     'archive',
 )
@@ -157,6 +160,18 @@ LOGGING = {
 
 
 
+
+import djcelery
+djcelery.setup_loader()
+
+BROKER_BACKEND = "djkombu.transport.DatabaseTransport"
+BROKER_HOST = "localhost"
+BROKER_PORT = 5672
+BROKER_USER = "guest"
+BROKER_PASSWORD = "guest"
+BROKER_VHOST = "/"
+
+
 try:
     from localsettings import *
 except:
index 2cb91e0..76efbb3 100644 (file)
@@ -1,3 +1,4 @@
+from django.conf import settings
 from django.conf.urls.defaults import patterns, include, url
 
 # Uncomment the next two lines to enable the admin:
@@ -9,4 +10,11 @@ urlpatterns = patterns('',
     url(r'^archive/', include('archive.urls')),
 
     url(r'^admin/', include(admin.site.urls)),
+    url(r'^accounts/login/', 'django.contrib.auth.views.login'),
+    url(r'^accounts/logout/', 'archive.views.logout_view', name='logout'),
+)
+
+if settings.DEBUG:
+    urlpatterns += patterns('',
+        (r'^media/(?P<path>.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT, 'show_indexes':True}),
 )
index 7b29e99..899e65b 100644 (file)
@@ -2,4 +2,9 @@ django>=1.3
 django-jsonfield
 South>=0.7
 
+django-celery
+django-kombu
+
+paramiko>=1.7.7.1 # fabric dependency with http://code.fabfile.org/issues/show/214 fixed
+fabric
 mutagen