Almost ready.
authorRadek Czajka <rczajka@rczajka.pl>
Fri, 11 Mar 2022 14:51:30 +0000 (15:51 +0100)
committerRadek Czajka <rczajka@rczajka.pl>
Fri, 11 Mar 2022 14:51:30 +0000 (15:51 +0100)
13 files changed:
src/catalogue/fields.py
src/catalogue/migrations/0034_auto_20220310_1251.py [new file with mode: 0644]
src/catalogue/migrations/0035_bookmedia_duration.py [new file with mode: 0644]
src/catalogue/migrations/0036_book_toc.py [new file with mode: 0644]
src/catalogue/models/book.py
src/catalogue/models/bookmedia.py
src/catalogue/models/edition.py [new file with mode: 0644]
src/catalogue/templates/catalogue/2021/book_detail.html
src/catalogue/templates/catalogue/snippets/2022_jplayer.html
src/catalogue/templates/catalogue/snippets/2022_jplayer_link.html [new file with mode: 0644]
src/wolnelektury/static/2022/styles/components/_player.scss
src/wolnelektury/static/2022/styles/layout/_article.scss
src/wolnelektury/static/js/current.js [new file with mode: 0644]

index 581bf38..49fd46e 100644 (file)
@@ -342,6 +342,15 @@ class BuildCover(BuildEbook):
         pass
 
 
+@BuildEbook.register('cover_clean')
+@task(ignore_result=True)
+class BuildCoverClean(BuildCover):
+    @classmethod
+    def transform(cls, wldoc, fieldfile):
+        from librarian.cover import WLCover
+        return WLCover(wldoc.book_info, width=240).output_file()
+
+
 @BuildEbook.register('cover_thumb')
 @task(ignore_result=True)
 class BuildCoverThumb(BuildCover):
diff --git a/src/catalogue/migrations/0034_auto_20220310_1251.py b/src/catalogue/migrations/0034_auto_20220310_1251.py
new file mode 100644 (file)
index 0000000..758106a
--- /dev/null
@@ -0,0 +1,25 @@
+# Generated by Django 2.2.25 on 2022-03-10 11:51
+
+import catalogue.fields
+import catalogue.models.book
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('catalogue', '0033_auto_20220128_1409'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='book',
+            name='cover_clean',
+            field=catalogue.fields.EbookField('cover_clean', blank=True, max_length=255, null=True, upload_to=catalogue.models.book.UploadToPath('book/cover_clean/%s.jpg'), verbose_name='clean cover'),
+        ),
+        migrations.AddField(
+            model_name='book',
+            name='cover_clean_etag',
+            field=models.CharField(db_index=True, default='', editable=False, max_length=255),
+        ),
+    ]
diff --git a/src/catalogue/migrations/0035_bookmedia_duration.py b/src/catalogue/migrations/0035_bookmedia_duration.py
new file mode 100644 (file)
index 0000000..9412528
--- /dev/null
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.25 on 2022-03-10 12:49
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('catalogue', '0034_auto_20220310_1251'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='bookmedia',
+            name='duration',
+            field=models.IntegerField(blank=True, null=True),
+        ),
+    ]
diff --git a/src/catalogue/migrations/0036_book_toc.py b/src/catalogue/migrations/0036_book_toc.py
new file mode 100644 (file)
index 0000000..4a9dc87
--- /dev/null
@@ -0,0 +1,18 @@
+# Generated by Django 2.2.25 on 2022-03-10 14:18
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('catalogue', '0035_bookmedia_duration'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='book',
+            name='toc',
+            field=models.TextField(blank=True, verbose_name='toc'),
+        ),
+    ]
index ee85955..01f0281 100644 (file)
@@ -8,6 +8,7 @@ from random import randint
 import os.path
 import re
 from urllib.request import urlretrieve
+from django.apps import apps
 from django.conf import settings
 from django.db import connection, models, transaction
 import django.dispatch
@@ -17,7 +18,7 @@ from django.urls import reverse
 from django.utils.translation import ugettext_lazy as _, get_language
 from django.utils.deconstruct import deconstructible
 from fnpdjango.storage import BofhFileSystemStorage
-
+from lxml import html
 from librarian.cover import WLCover
 from librarian.html import transform_abstrakt
 from newtagging import managers
@@ -43,6 +44,7 @@ class UploadToPath(object):
 
 
 _cover_upload_to = UploadToPath('book/cover/%s.jpg')
+_cover_clean_upload_to = UploadToPath('book/cover_clean/%s.jpg')
 _cover_thumb_upload_to = UploadToPath('book/cover_thumb/%s.jpg')
 _cover_api_thumb_upload_to = UploadToPath('book/cover_api_thumb/%s.jpg')
 _simple_cover_upload_to = UploadToPath('book/cover_simple/%s.jpg')
@@ -64,6 +66,7 @@ class Book(models.Model):
     language = models.CharField(_('language code'), max_length=3, db_index=True, default=app_settings.DEFAULT_LANGUAGE)
     description = models.TextField(_('description'), blank=True)
     abstract = models.TextField(_('abstract'), blank=True)
+    toc = models.TextField(_('toc'), blank=True)
     created_at = models.DateTimeField(_('creation date'), auto_now_add=True, db_index=True)
     changed_at = models.DateTimeField(_('change date'), auto_now=True, db_index=True)
     parent_number = models.IntegerField(_('parent number'), default=0)
@@ -86,6 +89,13 @@ class Book(models.Model):
         storage=bofh_storage, max_length=255)
     cover_etag = models.CharField(max_length=255, editable=False, default='', db_index=True)
     # Cleaner version of cover for thumbs
+    cover_clean = EbookField(
+        'cover_clean', _('clean cover'),
+        null=True, blank=True,
+        upload_to=_cover_clean_upload_to,
+        max_length=255
+    )
+    cover_clean_etag = models.CharField(max_length=255, editable=False, default='', db_index=True)
     cover_thumb = EbookField(
         'cover_thumb', _('cover thumbnail'),
         null=True, blank=True,
@@ -286,6 +296,17 @@ class Book(models.Model):
             return sibling.get_first_text()
         return self.parent.get_next_text()
 
+    def get_child_audiobook(self):
+        BookMedia = apps.get_model('catalogue', 'BookMedia')
+        if not BookMedia.objects.filter(book__ancestor=self).exists():
+            return None
+        for child in self.children.all():
+            if child.has_mp3_file():
+                return child
+            child_sub = child.get_child_audiobook()
+            if child_sub is not None:
+                return child_sub
+
     def get_siblings(self):
         if not self.parent:
             return []
@@ -429,6 +450,7 @@ class Book(models.Model):
 
         audiobooks = []
         projects = set()
+        total_duration = 0
         for mp3 in self.media.filter(type='mp3').iterator():
             # ogg files are always from the same project
             meta = mp3.get_extra_info_json()
@@ -438,6 +460,7 @@ class Book(models.Model):
                 project = 'CzytamySłuchając'
 
             projects.add((project, meta.get('funded_by', '')))
+            total_duration += mp3.duration or 0
 
             media = {'mp3': mp3}
 
@@ -447,7 +470,11 @@ class Book(models.Model):
             audiobooks.append(media)
 
         projects = sorted(projects)
-        return audiobooks, projects
+        total_duration = '%d:%02d' % (
+            total_duration // 60,
+            total_duration % 60
+        )
+        return audiobooks, projects, total_duration
 
     def wldocument(self, parse_dublincore=True, inherit=True):
         from catalogue.import_utils import ORMDocProvider
@@ -542,6 +569,20 @@ class Book(models.Model):
         else:
             self.abstract = ''
 
+    def load_toc(self):
+        self.toc = ''
+        if self.html_file:
+            parser = html.HTMLParser(encoding='utf-8')
+            tree = html.parse(self.html_file.path, parser=parser)
+            toc = tree.find('//div[@id="toc"]/ol')
+            if toc is None or not len(toc):
+                return
+            html_link = reverse('book_text', args=[self.slug])
+            for a in toc.findall('.//a'):
+                a.attrib['href'] = html_link + a.attrib['href']
+            self.toc = html.tostring(toc, encoding='unicode')
+            # div#toc
+            
     @classmethod
     def from_xml_file(cls, xml_file, **kwargs):
         from django.core.files import File
@@ -607,6 +648,7 @@ class Book(models.Model):
             book.common_slug = book.slug
         book.extra_info = json.dumps(book_info.to_dict())
         book.load_abstract()
+        book.load_toc()
         book.save()
 
         meta_tags = Tag.tags_from_info(book_info)
@@ -649,6 +691,7 @@ class Book(models.Model):
         # Build cover.
         if 'cover' not in dont_build:
             book.cover.build_delay()
+            book.cover_clean.build_delay()
             book.cover_thumb.build_delay()
             book.cover_api_thumb.build_delay()
             book.simple_cover.build_delay()
@@ -793,9 +836,11 @@ class Book(models.Model):
         if not self.cover_info(inherit=False):
             if 'cover' not in app_settings.DONT_BUILD:
                 self.cover.build_delay()
+                self.cover_clean.build_delay()
                 self.cover_thumb.build_delay()
                 self.cover_api_thumb.build_delay()
                 self.simple_cover.build_delay()
+                self.cover_ebookpoint.build_delay()
             for format_ in constants.EBOOK_FORMATS_WITH_COVERS:
                 if format_ not in app_settings.DONT_BUILD:
                     getattr(self, '%s_file' % format_).build_delay()
index e2fc343..c08ed11 100644 (file)
@@ -7,7 +7,8 @@ from collections import namedtuple
 from django.db import models
 from django.utils.translation import ugettext_lazy as _
 from slugify import slugify
-from mutagen import MutagenError
+import mutagen
+from mutagen import id3
 
 from catalogue.fields import OverwriteStorage
 
@@ -37,6 +38,7 @@ class BookMedia(models.Model):
     part_name = models.CharField(_('part name'), default='', blank=True, max_length=512)
     index = models.IntegerField(_('index'), default=0)
     file = models.FileField(_('file'), max_length=600, upload_to=_file_upload_to, storage=OverwriteStorage())
+    duration = models.IntegerField(null=True, blank=True)
     uploaded_at = models.DateTimeField(_('creation date'), auto_now_add=True, editable=False, db_index=True)
     project_description = models.CharField(max_length=2048, blank=True)
     project_icon = models.CharField(max_length=2048, blank=True)
@@ -96,15 +98,19 @@ class BookMedia(models.Model):
         extra_info.update(self.read_meta())
         self.extra_info = json.dumps(extra_info)
         self.source_sha1 = self.read_source_sha1(self.file.path, self.type)
+        self.duration = self.read_duration()
         return super(BookMedia, self).save(*args, **kwargs)
 
+    def read_duration(self):
+        try:
+            return mutagen.File(self.file.path).info.length
+        except:
+            return None
+
     def read_meta(self):
         """
             Reads some metadata from the audiobook.
         """
-        import mutagen
-        from mutagen import id3
-
         artist_name = director_name = project = funded_by = license = ''
         if self.type == 'mp3':
             try:
@@ -118,7 +124,7 @@ class BookMedia(models.Model):
                 funded_by = ", ".join([
                     t.data.decode('utf-8') for t in audio.getall('PRIV')
                     if t.owner == 'wolnelektury.pl?funded_by'])
-            except MutagenError:
+            except mutagen.MutagenError:
                 pass
         elif self.type == 'ogg':
             try:
@@ -128,7 +134,7 @@ class BookMedia(models.Model):
                 license = ', '.join(audio.get('license', []))
                 project = ", ".join(audio.get('project', []))
                 funded_by = ", ".join(audio.get('funded_by', []))
-            except (MutagenError, AttributeError):
+            except (mutagen.MutagenError, AttributeError):
                 pass
         else:
             return {}
@@ -143,21 +149,18 @@ class BookMedia(models.Model):
         """
             Reads source file SHA1 from audiobok metadata.
         """
-        import mutagen
-        from mutagen import id3
-
         if filetype == 'mp3':
             try:
                 audio = id3.ID3(filepath)
                 return [t.data.decode('utf-8') for t in audio.getall('PRIV')
                         if t.owner == 'wolnelektury.pl?flac_sha1'][0]
-            except (MutagenError, IndexError):
+            except (mutagen.MutagenError, IndexError):
                 return None
         elif filetype == 'ogg':
             try:
                 audio = mutagen.File(filepath)
                 return audio.get('flac_sha1', [None])[0]
-            except (MutagenError, AttributeError, IndexError):
+            except (mutagen.MutagenError, AttributeError, IndexError):
                 return None
         else:
             return None
diff --git a/src/catalogue/models/edition.py b/src/catalogue/models/edition.py
new file mode 100644 (file)
index 0000000..d04dd0a
--- /dev/null
@@ -0,0 +1,10 @@
+from django.db import models
+
+
+class Edition(models.Model):
+    book = models.ForeignKey('Book', models.CASCADE)
+    identifier = models.CharField(max_length=32)
+    created_at = models.DateTimeField(auto_now_add=True)
+
+
+
index 36515fa..5b6c1f8 100644 (file)
@@ -72,7 +72,7 @@
       <section class="l-section">
         <aside class="l-aside">
           <figure>
-            <img src="{% if book.cover_thumb %}{{ book.cover_thumb.url }}{% endif %}" alt="{{ book.pretty_title }}" width="238">
+            <img src="{% if book.cover_clean %}{{ book.cover_clean.url }}{% endif %}" alt="{{ book.pretty_title }}" width="240">
           </figure>
           <ul class="l-aside__info">
             <li><span>Epoka:</span> {% for tag in book.epochs %}<a href="{{ tag.get_absolute_url }}">{{ tag.name|lower }}</a> {% endfor %}</li>
 
               {% if book.has_mp3_file %}
                 {% include 'catalogue/snippets/2022_jplayer.html' %}
+              {% else %}
+                {% with ch=book.get_child_audiobook %}
+                  {% if ch %}
+                    {% include 'catalogue/snippets/2022_jplayer_link.html' with book=ch %}
+                  {% endif %}
+                {% endwith %}
+
               {% endif %}
 
 
               <h3>Opis</h3>
               {{ book.abstract|safe }}
 
-              <h4>Spis treści:</h4>
-              <ul>
-                <li>Dziady. Poema</li>
-                <li>Przedmowa</li>
-                <li>Upiór</li>
-                <li>Dziady, część II</li>
-                <li>Dziady, część IV</li>
-                <li>Dziady, część III</li>
-                <li>Dziady. Widowisko, część I</li>
-              </ul>
+              {% if book.toc %}
+                <h4>Spis treści:</h4>
+                {{ book.toc|safe }}
+              {% endif %}
             </div>
             <button class="l-article__read-more" aria-label="Kliknij aby rozwinąć" data-label="Czytaj więcej" data-action="Zwiń tekst">Czytaj więcej</button>
           </article>
-        </div>
-      </section>
-      <section class="l-section">
-        <div class="c-support">
-          <h2>Ta książka jest dostępna dla tysięcy dzieciaków dzięki darowiznom od osób takich jak Ty!</h2>
-          <figure>
-            <img src="{% static '2022/images/img-1.jpg' %}" alt="Dorzuć się!">
-            <a href="/towarzystwo/">Dorzuć się!</a>
-          </figure>
+          <div class="c-support">
+            <h2>Ta książka jest dostępna dla tysięcy dzieciaków dzięki darowiznom od osób takich jak Ty!</h2>
+            <figure>
+              <img src="{% static '2022/images/img-1.jpg' %}" alt="Dorzuć się!">
+              <a href="/towarzystwo/">Dorzuć się!</a>
+            </figure>
+          </div>
         </div>
       </section>
 
               </div>
             </div>
 
-            <div class="row">
-              <div class="l-author__quotes">
-                <div class="l-author__quotes__slider">
-                  {% choose_cites book 3 as cites %}
-                  {% for fragment in cites %}
-                    <div class="l-author__quotes__slider__item">
-                      <em>
-                        {{ fragment.short_text|safe }}
-                      </em>
-                      <p>{{ fragment.book.pretty_title }}</p>
-                    </div>
-                  {% endfor %}
+            {% choose_cites book 3 as cites %}
+            {% if cites %}
+              <div class="row">
+                <div class="l-author__quotes">
+                  <div class="l-author__quotes__slider">
+                    {% for fragment in cites %}
+                      <div class="l-author__quotes__slider__item">
+                        <em>
+                          {{ fragment.short_text|safe }}
+                        </em>
+                        <p>{{ fragment.book.pretty_title }}</p>
+                      </div>
+                    {% endfor %}
+                  </div>
                 </div>
               </div>
-            </div>
+            {% endif %}
           </div>
       </section>
       {% endfor %}
     </div>
 
     <script src="{% static '2022/scripts/vendor.js' %}"></script>
-    <script src="{% static '2022/scripts/main.js' %}"></script>
+    <script src="{% static '2021/scripts/main.js' %}"></script>
 
     {% javascript '2022' %}
     {% javascript '2022_player' %}
index 9201e72..2f7caf6 100644 (file)
@@ -1,70 +1,75 @@
 {% load i18n catalogue_tags %}
 
-<div class="c-media__player" id="jp_container_{{ book.pk }}">
-  <div class="jp-jplayer" data-player="jp_container_{{ book.pk }}"
-       data-supplied="oga,mp3"></div>
+{% with audiobooks=book.get_audiobooks %}
+  <div class="c-media__player" id="jp_container_{{ book.pk }}">
+    <div class="jp-jplayer" data-player="jp_container_{{ book.pk }}"
+         data-supplied="oga,mp3"></div>
 
-  <div class="c-player">
-    <button class="c-player__btn jp-play">
-      <i class="icon icon-play"></i>
-    </button>
-    <button class="c-player__btn jp-pause">
-      <i class="icon icon-play"></i>
-    </button>
-    <div class="c-player__timeline">
-      <div class="c-player__title">
-      </div>
-      <div class="c-player__info"></div>
-      <span>
-        <span class="jp-seek-bar">
-          <span class="jp-play-bar"></span>
-        </span>
-      </span>
-      <time class="c-player__length">
-        <span class="jp-current-time"></span>
+    <div class="c-player">
+      <button class="c-player__btn jp-play">
+        <i class="icon icon-play"></i>
+      </button>
+      <button class="c-player__btn jp-pause">
+        <i class="icon icon-pause"></i>
+      </button>
+      <div class="c-player__timeline">
+        <div class="c-player__title">
+        </div>
+        <div class="c-player__info"></div>
         <span>
-          <span class="jp-duration"></span>
+          <span class="jp-seek-bar">
+            <span class="jp-play-bar"></span>
+          </span>
         </span>
-      </time>
+        <time class="c-player__length">
+          <span class="jp-current-time"></span>
+          <span>
+            <span class="jp-duration"></span>
+            {% if audiobooks.0|length > 1 %}
+              / {{ audiobooks.2 }}
+            {% endif %}
+          </span>
+        </time>
+      </div>
+    </div>
+    <div class="c-media__caption">
+      <div class="license"></div>
+      <div class="project-logo"></div>
+      <div class="content"></div>
     </div>
-  </div>
-  <div class="c-media__caption">
-    <div class="license"></div>
-    <div class="project-logo"></div>
-    <div class="content"></div>
-  </div>
 
-  <ul class="jp-playlist" style="display: none;">
-    {% for i in book.get_audiobooks.0 %}
-      <li
-          data-mp3='{{ i.mp3.file.url }}'
-          data-ogg='{{ i.ogg.file.url }}'
-          data-media-id="{{ i.mp3.id }}"
-      >
-        {% with extra_info=i.mp3.get_extra_info_json %}
-          <span class="title">
-            {{ i.mp3.part_name }}
-          </span>
-          <span class="attribution">
-            {% trans "Artist:" %}&nbsp;<span class='artist'>{{ extra_info.artist_name }}</span>,
-            {% trans "director:" %}&nbsp;<span class='director'>{{ extra_info.director_name }}</span>
-          </span>
-          <span class="license">
-            {% if extra_info.license %}{% license_icon extra_info.license %}{% endif %}
-          </span>
-          <span class="project-icon">
-            {% if i.mp3.project_icon %}<img class="project-icon" src="{{ i.mp3.project_icon }}">{% endif %}
-          </span>
-          <span class="project-description">
-            {% if i.mp3.project_description %}{{ i.mp3.project_description }}{% else %}
-              {% with fb=extra_info.funded_by %}
-                {% if fb %}Dofinansowano ze środków: {{ fb }}.{% endif %}
-              {% endwith %}
-           {% endif %}
-          </span>
-       {% endwith %}
-      </li>
-    {% endfor %}
-  </ul>
+    <ul class="jp-playlist" style="display: none;">
+      {% for i in audiobooks.0 %}
+        <li
+            data-mp3='{{ i.mp3.file.url }}'
+            data-ogg='{{ i.ogg.file.url }}'
+            data-media-id="{{ i.mp3.id }}"
+        >
+          {% with extra_info=i.mp3.get_extra_info_json %}
+            <span class="title">
+              {{ i.mp3.part_name }}
+            </span>
+            <span class="attribution">
+              {% trans "Artist:" %}&nbsp;<span class='artist'>{{ extra_info.artist_name }}</span>,
+              {% trans "director:" %}&nbsp;<span class='director'>{{ extra_info.director_name }}</span>
+            </span>
+            <span class="license">
+              {% if extra_info.license %}{% license_icon extra_info.license %}{% endif %}
+            </span>
+            <span class="project-icon">
+              {% if i.mp3.project_icon %}<img class="project-icon" src="{{ i.mp3.project_icon }}">{% endif %}
+            </span>
+            <span class="project-description">
+              {% if i.mp3.project_description %}{{ i.mp3.project_description }}{% else %}
+                {% with fb=extra_info.funded_by %}
+                  {% if fb %}Dofinansowano ze środków: {{ fb }}.{% endif %}
+                {% endwith %}
+             {% endif %}
+            </span>
+         {% endwith %}
+        </li>
+      {% endfor %}
+    </ul>
 
-</div>
+  </div>
+{% endwith %}
diff --git a/src/catalogue/templates/catalogue/snippets/2022_jplayer_link.html b/src/catalogue/templates/catalogue/snippets/2022_jplayer_link.html
new file mode 100644 (file)
index 0000000..b49175a
--- /dev/null
@@ -0,0 +1,21 @@
+{% load i18n catalogue_tags %}
+
+<div class="c-media__player" id="jp_container_{{ book.pk }}">
+  <div class="jp-jplayer"></div>
+
+  <div class="c-player">
+    <a class="c-player__btn" href="{{ book.get_absolute_url }}">
+      <i class="icon icon-next">⏭</i>
+    </a>
+      <div class="c-player__timeline">
+        <div class="c-player__title">
+          {{ book.title }}
+        </div>
+        <span>
+          <span class="jp-seek-bar">
+            <span class="jp-play-bar"></span>
+          </span>
+        </span>
+      </div>
+    </div>
+  </div>
index 0604298..9e40950 100644 (file)
     height: 10px;
     width: 100%;
     display: block;
-    background: #FFFFFF;
+    background: #F2F2F2;
     border-radius: 5px;
     position: relative;
     overflow: hidden;
 
-    span {
-      width: 40%;
+    span.jp-seek-bar {
+      height: 100%;
+      position: absolute;
+      border-radius: 5px;
+      background-color: #FFFFFF;
+      top: 0; left: 0;
+    }
+    
+    span.jp-play-bar {
       height: 100%;
       position: absolute;
       border-radius: 5px;
   }
 }
 
+
+.c-player__title {
+  top: -27px;
+  left: 0;
+  position: absolute;
+  font-style: normal;
+  font-weight: normal;
+  font-size: 12px;
+  line-height: 140%;
+  letter-spacing: 0.05em;
+  color: #083F4D;
+}
+
+
 .c-player__info {
   top: -27px;
   right: 0;
   display: block;
   position: relative;
 
-  span {
+  span {
     position: absolute;
     padding-top: 10px;
     font-style: normal;
index 66e4381..a40be7b 100644 (file)
@@ -25,7 +25,7 @@
     font-weight: bolder;
   }
 
-  ul {
+  ol {
     padding: 0;
     list-style: none;
     margin-top: 0.25rem;
diff --git a/src/wolnelektury/static/js/current.js b/src/wolnelektury/static/js/current.js
new file mode 100644 (file)
index 0000000..e963dd2
--- /dev/null
@@ -0,0 +1,35 @@
+(function($){
+    $(function() {
+
+
+        if (Modernizr.localstorage) {
+            try {
+                audiobooks = JSON.parse(localStorage["audiobook-history"]);
+            } catch {
+                audiobooks = {};
+            }
+
+            latest = [];
+            Object.keys(audiobooks).forEach(function(slug) {
+                [ts, media_id, time] = audiobooks[slug];
+                latest.push([ts, slug]);
+            });
+            latest.sort().reverse().forEach(function(item) {
+                [ts, slug] = item;
+                $newitem = $('<div style="display:inline-block;"></div>');  // remove from history
+                $("#last-audiobooks").append($newitem);
+                (function($box) {
+                    $.get(
+                        "/katalog/lektura/" + slug + "/mini_box.html",
+                        function(data) {
+                            console.log(data);
+                            $box.html(data);
+                            $("#personal-history").slideDown("slow");
+                        }).fail(function() {
+                            $box.remove();
+                        });
+                })($newitem);
+            });
+        }
+    });
+})(jQuery);