--- /dev/null
+FROM python:3.9-trixie AS base
+
+ARG UID=1000
+ARG GID=1000
+
+RUN apt-get update && apt-get install -y \
+ git \
+ calibre \
+ texlive-xetex texlive-lang-polish \
+ texlive-extra-utils \
+ texlive-lang-greek \
+ texlive-lang-other \
+ texlive-luatex \
+ texlive-fonts-extra \
+ texlive-fonts-extra-links \
+ fonts-noto-core fonts-noto-extra
+
+
+COPY requirements/requirements.txt requirements.txt
+
+RUN pip install --no-cache-dir -r requirements.txt
+RUN pip install --no-cache-dir \
+ psycopg2-binary \
+ django-debug-toolbar==3.2.2
+
+RUN addgroup --gid $GID app && \
+ adduser --gid $GID --home /app --uid $UID app
+
+
+# fonts
+RUN cp -a /usr/local/lib/python*/site-packages/librarian/fonts /usr/share/fonts
+RUN fc-cache
+
+USER app
+
+WORKDIR /app/src
+
+RUN mkdir /app/.ipython
+
+FROM base AS dev
+
+#RUN pip install --no-cache-dir coverage
+
+
+FROM base AS prod
+
+RUN pip install --no-cache-dir gunicorn
+
+COPY src /app/src
-.PHONY: deploy test
+.PHONY: deploy test up down build shell logs restart
+
+
+UID := $(shell id -u)
+GID := $(shell id -g)
deploy: src/wolnelektury/localsettings.py
mv ../htmlcov.new ../htmlcov
coverage report
rm .coverage
+
+
+up:
+ UID=$(UID) GID=$(GID) docker compose up --build -d
+
+down:
+ docker compose down
+
+build:
+ UID=$(UID) GID=$(GID) docker compose build
+
+shell:
+ UID=$(UID) GID=$(GID) docker compose run --rm web bash
+
+logs:
+ docker compose logs -f
+
+restart:
+ docker compose restart
--- /dev/null
+x-app-base: &app-base
+ build:
+ context: .
+ target: dev
+ args:
+ UID: ${UID:-1000}
+ GID: ${GID:-1000}
+ env_file:
+ - .env
+ volumes:
+ - ./src:/app/src
+ - ./var/media:/app/var/media
+ - ./var/static:/app/var/static
+ - ./var/counters/:/app/var/counters
+ - ipython:/app/.ipython
+ depends_on:
+ - db
+ - redis
+ - memcached
+
+services:
+ web:
+ <<: *app-base
+ ports:
+ - "8000:8000"
+ command: ./manage.py runserver 0.0.0.0:8000
+
+ celery:
+ <<: *app-base
+ command: celery -A wolnelektury worker -l info
+
+ db:
+ image: postgres:18
+ container_name: db
+ env_file:
+ - .env
+ volumes:
+ - ./var/postgresql-data/:/var/lib/postgresql/
+
+ redis:
+ image: redis:8-alpine
+
+ memcached:
+ image: memcached:1.6-alpine
+
+
+volumes:
+ ipython:
\ No newline at end of file
fnpdjango==0.6
docutils==0.20
+python-memcached==1.59
+
django-pipeline==3.1.0
libsasscompiler==0.2.0
jsmin==3.0.1
django-allauth==0.51
django-extensions==3.2.3
djangorestframework==3.15.1
+django-filter==23.5
djangorestframework-xml==2.0.0
django-admin-ordering==0.18
django-countries==7.6.1
Feedparser==6.0.11
-Pillow==10.4
+Pillow==9.5.0
mutagen==1.47
sorl-thumbnail==12.10.0
# home-brewed & dependencies
-librarian==24.5.4
+librarian==26.2
# celery tasks
celery[redis]==5.4.0
firebase-admin==6.6.0
Wikidata==0.8.1
+ipython
from . import models
+admin.site.register(models.Campaign)
+
+
class BannerAdmin(TranslationAdmin):
list_display = [
'place', 'text',
--- /dev/null
+# Generated by Django 4.0.8 on 2025-11-25 15:13
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('catalogue', '0051_book_has_audio'),
+ ('annoy', '0018_alter_banner_style'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Campaign',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(help_text='Dla zespołu', max_length=255)),
+ ('image', models.FileField(blank=True, upload_to='annoy/banners/', verbose_name='obraz')),
+ ],
+ ),
+ migrations.AddField(
+ model_name='banner',
+ name='books',
+ field=models.ManyToManyField(blank=True, to='catalogue.book'),
+ ),
+ migrations.AlterField(
+ model_name='banner',
+ name='place',
+ field=models.SlugField(choices=[('top', 'U góry wszystkich stron'), ('book-page', 'Strona książki'), ('book-page-center', 'Strona książki, środek'), ('book-text-intermission', 'Przerwa w treści książki'), ('book-fragment-list', 'Obok listy fragmentów książki'), ('blackout', 'Blackout'), ('crisis', 'Kryzysowa')], verbose_name='miejsce'),
+ ),
+ migrations.AddField(
+ model_name='banner',
+ name='campaign',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='annoy.campaign'),
+ ),
+ ]
from .places import PLACES, PLACE_CHOICES, STYLES
+class Campaign(models.Model):
+ name = models.CharField(max_length=255, help_text='Dla zespołu')
+ image = models.FileField('obraz', upload_to='annoy/banners/', blank=True)
+
+ def __str__(self):
+ return self.name
+
+
class Banner(models.Model):
place = models.SlugField('miejsce', choices=PLACE_CHOICES)
+ campaign = models.ForeignKey(Campaign, models.PROTECT, null=True, blank=True)
+
style = models.CharField(
'styl', max_length=255, blank=True,
choices=STYLES,
help_text='Bannery z wyższym priorytetem mają pierwszeństwo.')
since = models.DateTimeField('od', null=True, blank=True)
until = models.DateTimeField('do', null=True, blank=True)
+ books = models.ManyToManyField('catalogue.Book', blank=True)
+
target = models.IntegerField('cel', null=True, blank=True)
progress = models.IntegerField('postęp', null=True, blank=True)
show_members = models.BooleanField('widoczny dla członków klubu', default=False)
def get_text(self):
return Template(self.text).render(Context())
+ def get_image(self):
+ if self.campaign and self.campaign.image:
+ return self.campaign.image
+ else:
+ return self.image
+
+ def is_external(self):
+ return (self.url and
+ not self.url.startswith('/') and
+ not self.url.startswith('https://wolnelektury.pl/')
+ )
+
@classmethod
- def choice(cls, place, request, exemptions=True):
+ def choice(cls, place, request, exemptions=True, book=None):
Membership = apps.get_model('club', 'Membership')
if exemptions and hasattr(request, 'annoy_banner_exempt'):
until__lt=n
).order_by('-priority', '?')
+ if book is None:
+ banners = banners.filter(books=None)
+ else:
+ banners = banners.filter(models.Q(books=None) | models.Q(books=book))
+
+
if not request.user.is_authenticated:
banners = banners.filter(only_authenticated=False)
PLACE_DEFINITIONS = [
('top', 'U góry wszystkich stron', True),
('book-page', 'Strona książki', False),
+ ('book-page-center', 'Strona książki, środek', False),
('book-text-intermission', 'Przerwa w treści książki', False),
('book-fragment-list', 'Obok listy fragmentów książki', False),
('blackout', 'Blackout', True, (
annoy-banner
annoy-banner_{{ banner.place }}
annoy-banner-style_{{ banner.style }}
- {% if banner.image %}with-image{% endif %}
+ {% if banner.get_image %}with-image{% endif %}
{% if banner.smallfont %}banner-smallfont{% endif %}
"
id="annoy-banner-{{ banner.id }}"
{% if banner.background_color %}background-color: {{ banner.background_color }};{% endif %}
">
{% if not banner.action_label %}
- <a href="{{ banner.url }}">
+ <a
+ {% if banner.is_external %}target="_blank"{% endif %}
+ href="{{ banner.url }}">
{% endif %}
<div class="annoy-banner-inner">
- {% if banner.image %}
- <img src="{{ banner.image.url }}">
+ {% if banner.get_image %}
+ <div>
+ <img src="{{ banner.get_image.url }}">
+ </div>
{% endif %}
<div class="text">
{{ banner.get_text|safe|linebreaks }}
</div>
{% if banner.action_label %}
- <a class="action" href="{{ banner.url }}">
+ <a class="action"
+ {% if banner.is_external %}target="_blank"{% endif %}
+ href="{{ banner.url }}">
{{ banner.action_label }}
</a>
{% endif %}
--- /dev/null
+{% load l10n %}
+
+{% if banner %}
+<div class="
+ annoy-banner_{{ banner.place }}-container
+ annoy-banner-style_{{ banner.style }}
+ ">
+ <div class="
+ annoy-banner
+ annoy-banner_{{ banner.place }}
+ {% if banner.image %}with-image{% endif %}
+ {% if banner.smallfont %}banner-smallfont{% endif %}
+ "
+ id="annoy-banner-{{ banner.id }}"
+ style="
+ {% if banner.text_color %}color: {{ banner.text_color }};{% endif %}
+ {% if banner.background_color %}background-color: {{ banner.background_color }};{% endif %}
+
+ ">
+ <div class="annoy-banner-inner">
+
+ <div class="image-box">
+ {% if banner.image %}
+ <img src="{{ banner.image.url }}">
+ {% endif %}
+ </div>
+
+ <div class="text-box">
+ <div class="text">
+ {{ banner.get_text|safe|linebreaks }}
+ </div>
+
+ <div class="state-box">
+ <div class="action-box">
+ {% if banner.action_label %}
+ <a class="action" href="{{ banner.url }}">
+ {{ banner.action_label }}
+ </a>
+ {% endif %}
+ </div>
+ </div>
+ </div>
+
+
+
+ </div>
+ </div>
+ </div>
+
+{% endif %}
+
@register.inclusion_tag('annoy/banner.html', takes_context=True)
-def annoy_banner(context, place):
- banners = Banner.choice(place, request=context['request'])
+def annoy_banner(context, place, **kwargs):
+ banners = Banner.choice(place, request=context['request'], **kwargs)
return {
'banner': banners.first(),
'closable': PLACES.get(place, False),
'closable': True,
}
+@register.inclusion_tag('annoy/banner_top.html', takes_context=True)
+def annoy_banner_top(context):
+ banners = Banner.choice('top', request=context['request'])
+ return {
+ 'banner': banners.first(),
+ 'closable': True,
+ }
@register.inclusion_tag('annoy/banners.html', takes_context=True)
def annoy_banners(context, place):
--- /dev/null
+# Generated by Django 4.0.8 on 2025-02-24 15:19
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0007_alter_token_consumer'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='token',
+ name='token_type',
+ field=models.IntegerField(choices=[(1, 'Request'), (2, 'Access'), (3, 'Refresh')]),
+ ),
+ ]
--- /dev/null
+# Generated by Django 4.0.8 on 2025-11-19 13:00
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('api', '0008_alter_token_token_type'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='SessionTransferToken',
+ fields=[
+ ('token', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('expires_at', models.DateTimeField()),
+ ('used', models.BooleanField(default=False)),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ ]
# This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
#
+from datetime import timedelta
+import uuid
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models.signals import pre_delete
-
+from django.utils import timezone
from catalogue.models import Book, Tag
def __str__(self):
return "%s Token %s for %s" % (self.get_token_type_display(), self.key, self.consumer)
+
+
+class SessionTransferToken(models.Model):
+ token = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False)
+ user = models.ForeignKey(
+ settings.AUTH_USER_MODEL, models.CASCADE)
+ created_at = models.DateTimeField(auto_now_add=True)
+ expires_at = models.DateTimeField()
+ used = models.BooleanField(default=False)
+
+ @classmethod
+ def create_for_user(cls, user, lifetime_seconds=30):
+ return cls.objects.create(
+ user=user,
+ expires_at=timezone.now() + timedelta(seconds=lifetime_seconds)
+ )
+
+ def is_valid(self):
+ if self.used:
+ return False
+ if timezone.now() > self.expires_at:
+ return False
+ return True
class WLLimitOffsetPagination(LimitOffsetPagination):
+ def get_results(self, data):
+ return data['member']
+
def get_paginated_response(self, data):
return Response({
"member": data,
class RegisterSerializer(serializers.Serializer):
email = serializers.CharField()
password = serializers.CharField(style={'input_type': 'password'})
- options = serializers.ListField(child=serializers.IntegerField())
+ options = serializers.ListField(child=serializers.IntegerField(), required=False)
class RefreshTokenSerializer(serializers.Serializer):
refresh_token = serializers.CharField(style={'input_type': 'password'})
+
+
+class RequestConfirmSerializer(serializers.Serializer):
+ email = serializers.CharField()
+
+
+class DeleteAccountSerializer(serializers.Serializer):
+ password =serializers.CharField(
+ style={'input_type': 'password'}
+ )
+
+ def validate_password(self, value):
+ u = self.context['user']
+ if not u.check_password(value):
+ raise serializers.ValidationError("Password incorrect.")
+ return value
+
+
+class PasswordSerializer(serializers.Serializer):
+ old_password = serializers.CharField(
+ style={'input_type': 'password'}
+ )
+ new_password = serializers.CharField(
+ style={'input_type': 'password'}
+ )
+
+ def validate_old_password(self, value):
+ u = self.context['user']
+ if not u.check_password(value):
+ raise serializers.ValidationError("Password incorrect.")
+ return value
+
+
+class ResetPasswordSerializer(serializers.Serializer):
+ email = serializers.EmailField()
path('requestConfirm/', csrf_exempt(views.RequestConfirmView.as_view())),
path('login/', csrf_exempt(views.Login2View.as_view())),
path('me/', views.UserView.as_view()),
+ path('deleteAccount/', views.DeleteAccountView.as_view()),
+ path('password/', views.PasswordView.as_view()),
+ path('password-reset/', views.ResetPasswordView.as_view()),
+ path("session-transfer/", views.SessionTransferTokenView.as_view()),
+ path("session-transfer/continue/", views.ConsumeSessionTransferTokenView.as_view(), name="api-session-continue"),
+
path('', include('catalogue.api.urls2')),
+ path('', include('social.api.urls2')),
+ path('', include('bookmarks.api.urls')),
+ path('', include('search.api.urls')),
+ path('', include('push.api.urls')),
]
from django.utils.decorators import method_decorator
from django.utils.encoding import iri_to_uri
from django.views.decorators.vary import vary_on_headers
+import django.views.decorators.cache
def oauthlib_request(request):
vary_on_auth = method_decorator(vary_on_headers('Authorization'), 'dispatch')
+never_cache = method_decorator(django.views.decorators.cache.never_cache, 'dispatch')
class HttpResponseAppRedirect(HttpResponseRedirect):
# Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
#
from time import time
-from django.contrib.auth import authenticate
+from allauth.account.forms import ResetPasswordForm
+from allauth.account.utils import filter_users_by_email
+from django.conf import settings
+from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import login_required
+from django.contrib.auth.models import User
from django import forms
-from django.http import HttpResponse
+from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
from django.http import Http404
-from django.shortcuts import render
+from django.shortcuts import redirect, render
from django.views.generic.base import View
from oauthlib.common import urlencode, generate_token
from oauthlib.oauth1 import RequestTokenEndpoint, AccessTokenEndpoint
from rest_framework.views import APIView
from rest_framework.generics import GenericAPIView, RetrieveAPIView, get_object_or_404
from catalogue.models import Book
-from .models import BookUserData, KEY_SIZE, SECRET_SIZE, Token
+from .models import BookUserData, KEY_SIZE, SECRET_SIZE, Token, SessionTransferToken
+from social.models import UserConfirmation
from . import serializers
from .request_validator import PistonRequestValidator
from .utils import oauthlib_request, oauthlib_response, vary_on_auth
-class RegisterView(APIView):
+class RegisterView(GenericAPIView):
serializer_class = serializers.RegisterSerializer
def get(self, request):
})
def post(self, request):
- pass
-
+ if not settings.FEATURE_API_REGISTER:
+ return Response(
+ {
+ "detail": "Rejestracja aktualnie niedostępna."
+ },
+ status=400
+ )
+ serializer = self.get_serializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ d = serializer.validated_data
+
+ email = d['email']
+
+ user = User(
+ username=email,
+ email=email,
+ is_active=True
+ )
+ user.set_password(d['password'])
+
+ if settings.FEATURE_CONFIRM_USER:
+ user.is_active = False
+
+ try:
+ assert not filter_users_by_email(email)
+ user.save()
+ except:
+ return Response(
+ {
+ "detail": "Nie można utworzyć konta.",
+ },
+ status=400
+ )
+
+ if settings.FEATURE_CONFIRM_USER:
+ UserConfirmation.request(user)
+ return Response({
+ 'emailConfirmationRequired': settings.FEATURE_CONFIRM_USER,
+ })
+
class RefreshTokenView(APIView):
serializer_class = serializers.RefreshTokenSerializer
class RequestConfirmView(APIView):
- pass
+ serializer_class = serializers.RequestConfirmSerializer
+
+ def post(self, request):
+ serializer = self.get_serializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ d = serializer.validated_data
+
+ try:
+ user = User.objects.get(
+ username=d['email'],
+ is_active=False
+ )
+ except User.DoesNotExist:
+ raise Http404
+
+ UserConfirmation.request(user)
+ return Response({})
+
+
+class DeleteAccountView(GenericAPIView):
+ permission_classes = [IsAuthenticated]
+ serializer_class = serializers.DeleteAccountSerializer
+
+ def post(self, request):
+ u = request.user
+ serializer = self.get_serializer(
+ data=request.data,
+ context={'user': u}
+ )
+ serializer.is_valid(raise_exception=True)
+ d = serializer.validated_data
+ u.is_active = False
+ u.save()
+ return Response({})
+
+
+class PasswordView(GenericAPIView):
+ permission_classes = [IsAuthenticated]
+ serializer_class = serializers.PasswordSerializer
+
+ def post(self, request):
+ u = request.user
+ serializer = self.get_serializer(
+ data=request.data,
+ context={'user': u}
+ )
+ serializer.is_valid(raise_exception=True)
+ d = serializer.validated_data
+ u.set_password(d['new_password'])
+ u.save()
+ return Response({})
+
+
+class ResetPasswordView(GenericAPIView):
+ serializer_class = serializers.ResetPasswordSerializer
+
+ def post(self, request):
+ serializer = serializers.ResetPasswordSerializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ form = ResetPasswordForm({"email": serializer.validated_data['email']})
+ form.is_valid()
+ form.save(request)
+ return Response({})
+
+
+class SessionTransferTokenView(APIView):
+ permission_classes = [IsAuthenticated]
+
+ def post(self, request):
+ ott = SessionTransferToken.create_for_user(request.user)
+ return Response({
+ "token": str(ott.token)
+ })
+
+
+class ConsumeSessionTransferTokenView(View):
+ def get(self, request):
+ token_str = request.GET.get("token")
+ next_url = request.GET.get("next", "/") #TODO: validate
+
+ if not token_str:
+ return HttpResponseBadRequest("Missing token")
+
+ try:
+ ott = SessionTransferToken.objects.get(token=token_str)
+ except SessionTransferToken.DoesNotExist:
+ return HttpResponseBadRequest("Invalid token")
+
+ if not ott.is_valid():
+ return HttpResponseForbidden("Token expired or already used")
+
+ # Mark token as used
+ ott.used = True
+ ott.save(update_fields=["used"])
+
+ # Log in the user via Django session
+ login(request, ott.user)
+
+ return redirect(next_url)
--- /dev/null
+from django.urls import path
+from . import views
+
+
+urlpatterns = [
+ path('bookmarks/', views.BookmarksView.as_view()),
+ path('bookmarks/book/<slug:book>/', views.BookBookmarksView.as_view()),
+ path('bookmarks/<uuid:uuid>/', views.BookmarkView.as_view(), name='api_bookmark'),
+]
--- /dev/null
+from api.utils import never_cache
+
+from django.db.models import Q
+from django.http import Http404, JsonResponse
+from django.shortcuts import render, get_object_or_404
+from django.views.decorators import cache
+import catalogue.models
+from wolnelektury.utils import is_ajax
+from bookmarks import models
+from lxml import html
+import re
+from rest_framework.generics import ListAPIView, ListCreateAPIView, RetrieveUpdateDestroyAPIView
+from rest_framework import serializers
+from rest_framework.permissions import SAFE_METHODS, IsAuthenticated, IsAuthenticatedOrReadOnly
+from api.fields import AbsoluteURLField
+
+
+class BookmarkSerializer(serializers.ModelSerializer):
+ book = serializers.SlugRelatedField(
+ queryset=catalogue.models.Book.objects.all(), slug_field='slug',
+ required=False
+ )
+ href = AbsoluteURLField(view_name='api_bookmark', view_args=['uuid'])
+ timestamp = serializers.IntegerField(required=False)
+ location = serializers.CharField(required=False)
+
+ class Meta:
+ model = models.Bookmark
+ fields = ['book', 'anchor', 'audio_timestamp', 'mode', 'note', 'href', 'uuid', 'location', 'timestamp', 'deleted']
+ read_only_fields = ['uuid', 'mode']
+
+
+
+@never_cache
+class BookmarksView(ListCreateAPIView):
+ permission_classes = [IsAuthenticated]
+ serializer_class = BookmarkSerializer
+
+ def get_queryset(self):
+ return self.request.user.bookmark_set.all()
+
+ def perform_create(self, serializer):
+ serializer.save(user=self.request.user)
+
+
+@never_cache
+class BookBookmarksView(ListAPIView):
+ permission_classes = [IsAuthenticated]
+ serializer_class = BookmarkSerializer
+ pagination_class = None
+
+ def get_queryset(self):
+ return self.request.user.bookmark_set.filter(book__slug=self.kwargs['book'])
+
+
+@never_cache
+class BookmarkView(RetrieveUpdateDestroyAPIView):
+ permission_classes = [IsAuthenticatedOrReadOnly]
+ serializer_class = BookmarkSerializer
+ lookup_field = 'uuid'
+
+ def get_queryset(self):
+ if self.request.method in SAFE_METHODS:
+ q = Q(deleted=False)
+ if self.request.user.is_authenticated:
+ q |= Q(user=self.request.user)
+ return models.Bookmark.objects.filter(q)
+ else:
+ return self.request.user.bookmark_set.all()
--- /dev/null
+# Generated by Django 4.0.8 on 2025-08-01 14:35
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('bookmarks', '0002_quote'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='bookmark',
+ name='deleted',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name='bookmark',
+ name='reported_timestamp',
+ field=models.DateTimeField(default=django.utils.timezone.now),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='bookmark',
+ name='updated_at',
+ field=models.DateTimeField(auto_now=True),
+ ),
+ ]
--- /dev/null
+# Generated by Django 4.0.8 on 2025-08-22 14:52
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('bookmarks', '0003_bookmark_deleted_bookmark_reported_timestamp_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='bookmark',
+ name='audio_timestamp',
+ field=models.IntegerField(blank=True, null=True),
+ ),
+ migrations.AddField(
+ model_name='bookmark',
+ name='mode',
+ field=models.CharField(choices=[('text', 'text'), ('audio', 'audio')], default='text', max_length=64),
+ ),
+ migrations.AlterField(
+ model_name='bookmark',
+ name='reported_timestamp',
+ field=models.DateTimeField(default=django.utils.timezone.now),
+ ),
+ ]
import uuid
+from django.apps import apps
from django.db import models
+from django.utils.timezone import now
+from social.syncable import Syncable
-class Bookmark(models.Model):
+class Bookmark(Syncable, models.Model):
uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey('auth.User', models.CASCADE)
book = models.ForeignKey('catalogue.Book', models.CASCADE)
anchor = models.CharField(max_length=100, blank=True)
+ audio_timestamp = models.IntegerField(null=True, blank=True)
+ mode = models.CharField(max_length=64, choices=[
+ ('text', 'text'),
+ ('audio', 'audio'),
+ ], default='text')
created_at = models.DateTimeField(auto_now_add=True)
note = models.TextField(blank=True)
+ updated_at = models.DateTimeField(auto_now=True)
+ reported_timestamp = models.DateTimeField(default=now)
+ deleted = models.BooleanField(default=False)
+
+ syncable_fields = [
+ 'deleted', 'note',
+ ]
def __str__(self):
return str(self.uuid)
+
+ def save(self, *args, **kwargs):
+ # TODO: placeholder.
+ try:
+ audio_l = self.book.get_audio_length()
+ except:
+ audio_l = 60
+
+ if self.anchor:
+ self.mode = 'text'
+ self.audio_timestamp = self.book.sync_elid(self.anchor)
+ if self.audio_timestamp:
+ self.mode = 'audio'
+ self.anchor = self.book.sync_ts(self.audio_timestamp) or ''
+ return super().save(*args, **kwargs)
+
+ @classmethod
+ def create_from_data(cls, user, data):
+ if data.get('location'):
+ return cls.get_by_location(user, data['location'], create=True)
+ elif data.get('book') and data.get('anchor'):
+ return cls.objects.create(user=user, book=data['book'], anchor=data['anchor'])
+ elif data.get('book') and data.get('audio_timestamp'):
+ return cls.objects.create(user=user, book=data['book'], audio_timestamp=data['audio_timestamp'])
+
+ @property
+ def timestamp(self):
+ return self.updated_at.timestamp()
+
+ def location(self):
+ if self.mode == 'text':
+ return f'{self.book.slug}/{self.anchor}'
+ else:
+ return f'{self.book.slug}/audio/{self.audio_timestamp}'
+
+ @classmethod
+ def get_by_location(cls, user, location, create=False):
+ Book = apps.get_model('catalogue', 'Book')
+ try:
+ slug, anchor = location.split('/', 1)
+ except:
+ return None
+ if '/' in anchor:
+ try:
+ mode, audio_timestamp = anchor.split('/', 1)
+ assert mode == 'audio'
+ audio_timestamp = int(audio_timestamp)
+ except:
+ return None
+ anchor = ''
+ instance = cls.objects.filter(
+ user=user,
+ book__slug=slug,
+ mode=mode,
+ audio_timestamp=audio_timestamp,
+ ).first()
+ else:
+ mode = 'text'
+ audio_timestamp = None
+ instance = cls.objects.filter(
+ user=user,
+ book__slug=slug,
+ mode='text',
+ anchor=anchor,
+ ).first()
+ if instance is None and create:
+ try:
+ book = Book.objects.get(slug=slug)
+ except Book.DoesNotExist:
+ return None
+ instance = cls.objects.create(
+ user=user,
+ book=book,
+ mode=mode,
+ anchor=anchor,
+ audio_timestamp=audio_timestamp,
+ )
+ return instance
def get_for_json(self):
return {
@cache.never_cache
def quotes(request):
+ if not request.user.is_authenticated:
+ return JsonResponse({})
try:
slug = request.headers['Referer'].rsplit('.', 1)[0].rsplit('/', 1)[-1]
except:
]
+class AuthorItemSerializer(serializers.ModelSerializer):
+ url = AbsoluteURLField()
+ href = AbsoluteURLField(
+ view_name='catalogue_api_author',
+ view_args=('slug',)
+ )
+
+ class Meta:
+ model = Tag
+ fields = [
+ 'id', 'url', 'href', 'name', 'slug'
+ ]
+
+class AuthorSerializer(AuthorItemSerializer):
+ photo_thumb = ThumbnailField('139x193', source='photo')
+
+ class Meta:
+ model = Tag
+ fields = [
+ 'id', 'url', 'href', 'name', 'slug', 'sort_key', 'description',
+ 'genitive', 'photo', 'photo_thumb', 'photo_attribution',
+ ]
+
+class EpochItemSerializer(serializers.ModelSerializer):
+ url = AbsoluteURLField()
+ href = AbsoluteURLField(
+ view_name='catalogue_api_epoch',
+ view_args=('slug',)
+ )
+ class Meta:
+ model = Tag
+ fields = ['id', 'url', 'href', 'name', 'slug']
+
+class EpochSerializer(EpochItemSerializer):
+ class Meta:
+ model = Tag
+ fields = [
+ 'url', 'href', 'name', 'slug', 'sort_key', 'description',
+ 'adjective_feminine_singular', 'adjective_nonmasculine_plural',
+ ]
+
+class GenreItemSerializer(serializers.ModelSerializer):
+ url = AbsoluteURLField()
+ href = AbsoluteURLField(
+ view_name='catalogue_api_genre',
+ view_args=('slug',)
+ )
+ class Meta:
+ model = Tag
+ fields = ['id', 'url', 'href', 'name', 'slug']
+
+class GenreSerializer(GenreItemSerializer):
+ class Meta:
+ model = Tag
+ fields = [
+ 'url', 'href', 'name', 'slug', 'sort_key', 'description',
+ 'plural', 'genre_epoch_specific',
+ ]
+
+class KindItemSerializer(serializers.ModelSerializer):
+ url = AbsoluteURLField()
+ href = AbsoluteURLField(
+ view_name='catalogue_api_kind',
+ view_args=('slug',)
+ )
+ class Meta:
+ model = Tag
+ fields = ['id', 'url', 'href', 'name', 'slug']
+
+class KindSerializer(KindItemSerializer):
+ class Meta:
+ model = Tag
+ fields = [
+ 'url', 'href', 'name', 'slug', 'sort_key', 'description',
+ 'collective_noun',
+ ]
+
+class ThemeSerializer(serializers.ModelSerializer):
+ url = AbsoluteURLField()
+ href = AbsoluteURLField(
+ view_name='catalogue_api_theme',
+ view_args=('slug',)
+ )
+ class Meta:
+ model = Tag
+ fields = [
+ 'url', 'href', 'name', 'slug', 'sort_key', 'description',
+ ]
+
+
class TranslatorSerializer(serializers.Serializer):
name = serializers.CharField(source='*')
mobi = EmbargoURLField(source='mobi_url')
pdf = EmbargoURLField(source='pdf_url')
+ authors = AuthorItemSerializer(many=True)
+ translators = AuthorItemSerializer(many=True)
+ epochs = EpochItemSerializer(many=True)
+ genres = GenreItemSerializer(many=True)
+ kinds = KindItemSerializer(many=True)
+ parent = serializers.HyperlinkedRelatedField(
+ read_only=True,
+ view_name='catalogue_api_book',
+ lookup_field='slug'
+ )
+ children = serializers.SerializerMethodField()
+ audiences = serializers.ListField(source='audiences_pl')
+
class Meta:
model = Book
fields = [
- 'full_sort_key', 'title',
+ 'slug', 'title', 'full_sort_key',
'href', 'url', 'language',
- #'epochs', 'genres', 'kinds', 'authors', 'translators',
- #'children',
+ 'authors', 'translators',
+ 'epochs', 'genres', 'kinds',
+ 'children',
'parent', 'preview',
'epub', 'mobi', 'pdf', 'html', 'txt', 'fb2', 'xml',
'cover_thumb', 'cover',
'isbn_pdf', 'isbn_epub', 'isbn_mobi',
+ 'abstract',
+ 'has_mp3_file', 'has_sync_file',
+ 'elevenreader_link', 'content_warnings', 'audiences',
+ 'changed_at', 'read_time', 'pages', 'redakcja'
]
+ def get_children(self, obj):
+ return list(obj.get_children().values('slug', 'title'))
+
+class BookSerializer11Labs(serializers.ModelSerializer):
+ url = AbsoluteURLField()
+ href = AbsoluteURLField(view_name='catalogue_api_book', view_args=['slug'])
+ html = EmbargoURLField(source='html_nonotes_url')
+
+ authors = AuthorItemSerializer(many=True)
+ translators = AuthorItemSerializer(many=True)
+ epochs = EpochItemSerializer(many=True)
+ genres = GenreItemSerializer(many=True)
+ kinds = KindItemSerializer(many=True)
+ parent = serializers.HyperlinkedRelatedField(
+ read_only=True,
+ view_name='catalogue_api_book',
+ lookup_field='slug'
+ )
+
+ class Meta:
+ model = Book
+ fields = [
+ 'slug', 'title', 'full_sort_key',
+ 'href', 'url', 'language',
+ 'authors', 'translators',
+ 'epochs', 'genres', 'kinds',
+ #'children',
+ 'parent', 'preview',
+ 'html',
+ 'cover_thumb', 'cover',
+ 'isbn_pdf', 'isbn_epub', 'isbn_mobi',
+ ]
+
+
class BookSerializer(LegacyMixin, serializers.ModelSerializer):
author = serializers.CharField(source='author_unicode')
kind = serializers.CharField(source='kind_unicode')
legacy_non_null_fields = ['director', 'artist']
+class MediaSerializer2(MediaSerializer):
+ size = serializers.SerializerMethodField()
+
+ class Meta:
+ model = BookMedia
+ fields = ['url', 'director', 'type', 'name', 'part_name', 'artist', 'duration', 'size']
+
+ def get_size(self, obj):
+ return obj.file.size
+
+
class BookDetailSerializer(LegacyMixin, serializers.ModelSerializer):
url = AbsoluteURLField()
class Meta:
model = Fragment
fields = ['book', 'anchor', 'text', 'url', 'themes']
+
+
+class FragmentSerializer2(serializers.ModelSerializer):
+ url = AbsoluteURLField()
+ html = serializers.CharField(source='text')
+
+ class Meta:
+ model = Fragment
+ fields = ['anchor', 'html', 'url']
+
+
+class FilterTagSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Tag
+ fields = ['id', 'category', 'name', 'slug']
--- /dev/null
+from collections import defaultdict
+import json
+import re
+from sys import argv
+from lxml import etree
+
+tags = {
+ 'utwor': ('_pass', False, None, None, None),
+ '{http://www.w3.org/1999/02/22-rdf-syntax-ns#}RDF': ('_ignore', False, None, None, None),
+ 'abstrakt': ('_ignore', False, None, None, None),
+ 'uwaga': ('_ignore', False, None, None, None),
+ 'extra': ('_ignore', False, None, None, None),
+ 'nota_red': ('_ignore', False, None, None, None),
+ 'numeracja': ('_ignore', False, None, None, None),
+
+ 'powiesc': ('master', False, None, None, None),
+ 'opowiadanie': ('master', False, None, None, None),
+ 'liryka_lp': ('master', False, None, None, None),
+ 'liryka_l': ('master', False, None, None, None),
+ 'dramat_wspolczesny': ('master', False, None, None, None),
+ 'dramat_wierszowany_lp': ('master', False, None, None, None),
+ 'dramat_wierszowany_l': ('master', False, None, None, None),
+
+ 'dlugi_cytat': ('blockquote', False, None, None, None),
+ 'poezja_cyt': ('blockquote', False, None, None, None),
+ 'dlugi_cyt': ('blockquote', False, None, None, None),
+ 'ramka': ('blockquote', False, {'class': 'ramka'}, None, None),
+
+ 'blok': ('div', False, None, None, None),
+
+ 'strofa': ('div', True, {'class': 'stanza'}, None, None),
+ 'wers': ('div', True, {'class': 'verse'}, None, None),
+ 'wers_wciety': ('div', True, {'class': 'wers_wciety'}, None, None),
+ 'wers_cd': ('div', True, {'class': 'wers_cd'}, None, None),
+ 'wers_akap': ('div', True, {'class': 'wers_akap'}, None, None),
+ 'zastepnik_wersu': ('div', True, {'class': 'zastepnik_wersu'}, None, None),
+ 'wers_do_prawej': ('div', True, {'class': 'wers_do_prawej'}, None, None),
+ 'wers_srodek': ('div', True, {'class': 'wers_srodek'}, None, None),
+
+ 'autor_utworu': ('div', True, {'class': 'author'}, None, None),
+ 'dzielo_nadrzedne': ('div', True, {'class': 'dzielo_nadrzedne'}, None, None),
+ 'nazwa_utworu': ('div', True, {'class': 'title'}, None, None),
+ 'podtytul': ('div', True, {'class': 'podtytul'}, None, None),
+
+ 'motto': ('div', False, {'class': 'motto'}, None, None),
+ 'motto_podpis': ('div', True, {'class': 'motto_podpis'}, None, None),
+ 'dedykacja': ('div', True, {'class': 'dedykacja'}, None, None),
+ 'miejsce_czas': ('div', True, {'class': 'miejsce_czas'}, None, None),
+
+ 'lista_osob': ('div', False, {'class': 'lista_osob'}, None, None),
+ 'naglowek_listy': ('div', True, {'class': 'naglowek_listy'}, None, None),
+ 'lista_osoba': ('div', True, {'class': 'lista_osoba'}, None, None),
+ 'naglowek_osoba': ('div', True, {'class': 'naglowek_osoba'}, None, None),
+ 'osoba': ('em', True, {'class': 'osoba'}, None, None),
+ 'didaskalia': ('div', True, {'class': 'didaskalia'}, None, None),
+ 'kwestia': ('div', False, {'class': 'kwestia'}, None, None),
+ 'didask_tekst': ('em', True, {'class': 'didask_tekst'}, None, None),
+
+ 'naglowek_czesc': ('h2', True, None, None, None),
+ 'naglowek_akt': ('h2', True, None, None, None),
+ 'naglowek_scena': ('h3', True, None, None, None),
+ 'naglowek_rozdzial': ('h3', True, None, None, None),
+ 'naglowek_podrozdzial': ('h4', True, None, None, None),
+ 'srodtytul': ('h5', True, None, None, None),
+
+ 'nota': ('div', True, {'class': 'note'}, None, False),
+
+ 'akap': ('p', True, {'class': 'paragraph'}, None, True),
+ 'akap_dialog': ('p', True, {'class': 'paragraph'}, None, True),
+ 'akap_cd': ('p', True, {'class': 'paragraph'}, None, True),
+
+ 'sekcja_asterysk': ('p', True, {'class': 'spacer-asterisk'}, None, True),
+ 'sekcja_swiatlo': ('p', True, {'class': 'sekcja_swiatlo'}, None, True),
+ 'separator_linia': ('p', True, {'class': 'separator_linia'}, None, True),
+
+ 'tytul_dziela': ('em', True, {'class': 'book-title'}, None, False),
+ 'slowo_obce': ('em', True, {'class': 'foreign-word'}, None, False),
+ 'wyroznienie': ('em', True, {'class': 'author-emphasis'}, None, False),
+ 'wieksze_odstepy': ('em', True, {'class': 'wieksze_odstepy'}, None, False),
+
+ 'ref': ('a', True, {'class': 'reference'}, {'data-uri': 'href'}, False),
+
+ 'begin': ('_ignore', True, {'class': 'reference'}, {'data-uri': 'href'}, False),
+ 'end': ('_ignore', True, {'class': 'reference'}, {'data-uri': 'href'}, False),
+ 'motyw': ('_ignore', True, {'class': 'theme'}, None, False),
+
+ 'pa': ('a', True, {'class': 'footnote footnote-pa'}, None, False),
+ 'pe': ('a', True, {'class': 'footnote footnote-pe'}, None, False),
+ 'pr': ('a', True, {'class': 'footnote footnote-pr'}, None, False),
+ 'pt': ('a', True, {'class': 'footnote footnote-pt'}, None, False),
+ 'ptrad': ('a', True, {'class': 'footnote footnote-ptrad'}, None, False),
+
+ 'werset': ('p', True, {'class': 'werset'}, None, True),
+ 'br': ('br', False, None, None, None),
+ 'indeks_dolny': ('em', True, {'class': 'indeks_dolny'}, None, False),
+ 'mat': ('span', True, {'class': 'mat'}, None, False),
+
+ 'mfenced': ('math_mfenced', True, None, None, False),
+ 'mfrac': ('math_mfrac', True, None, None, False),
+ 'mrow': ('math_mrow', True, None, None, False),
+ 'mi': ('math_mi', True, None, None, False),
+ 'mn': ('math_mn', True, None, None, False),
+ 'mo': ('math_mo', True, None, None, False),
+ 'msup': ('math_msup', True, None, None, False),
+
+ 'list': ('blockquote', False, {'class': 'list'}, None, None),
+ 'wywiad_pyt': ('blockquote', False, {'class': 'wywiad_pyt'}, None, None),
+ 'wywiad_odp': ('blockquote', False, {'class': 'wywiad_odp'}, None, None),
+ 'rownolegle': ('blockquote', False, {'class': 'rownolegle'}, None, None),
+ 'animacja': ('div', False, {'class': 'animacja'}, None, None),
+ 'data': ('div', True, {'class': 'data'}, None, None),
+ 'podpis': ('div', True, {'class': 'podpis'}, None, None),
+ 'naglowek_listu': ('div', True, {'class': 'naglowek_listu'}, None, None),
+ 'pozdrowienie': ('div', True, {'class': 'pozdrowienie'}, None, None),
+ 'adresat': ('div', True, {'class': 'adresat'}, None, None),
+ 'tytul_oryg': ('div', True, {'class': 'tytul_oryg'}, None, None),
+ 'miejsce_data': ('div', True, {'class': 'miejsce_data'}, None, None),
+ 'audio': ('_ignore', False, None, None, None),
+ 'www': ('a', True, {'class': 'www'}, {'href': '.text'}, False),
+
+ 'tabela': ('table', False, None, None, None),
+ 'tabelka': ('table', False, None, None, None),
+ 'wiersz': ('tr', False, None, None, None),
+ 'kol': ('td', True, None, None, None),
+
+ 'ilustr': ('img', False, None, {'src': 'src'}, False),
+ 'tab': ('span', False, {'class': 'tab'}, {'szer': 'szer'}, False),
+
+}
+
+id_prefixes = {
+ 'pa': 'fn',
+ 'pe': 'fn',
+ 'pr': 'fn',
+ 'pt': 'fn',
+ 'ptrad': 'fn',
+ 'wers': 'f',
+ 'wers_wciety': 'f',
+ 'wers_cd': 'f',
+ 'wers_akap': 'f',
+ 'zastepnik_wersu': 'f',
+ 'wers_do_prawej': 'f',
+ 'wers_srodek': 'f',
+ 'akap': 'f',
+ 'akap_cd': 'f',
+ 'akap_dialog': 'f',
+}
+
+
+#tree = etree.parse(argv[1])
+
+front1 = set([
+ 'dzielo_nadrzedne',
+ 'nazwa_utworu',
+ 'podtytul',
+ ])
+front2 = set(['autor_utworu'])
+
+
+def norm(text):
+ text = text.replace('---', '—').replace('--', '–').replace('...', '…').replace(',,', '„').replace('"', '”').replace('\n', ' ')
+ return text
+
+
+def toj(elem, S):
+ if elem.tag is etree.Comment: return []
+ tag, hastext, attrs, attr_map, num = tags[elem.tag]
+ contents = []
+ if tag == '_pass':
+ output = contents
+ elif tag == '_ignore':
+ return []
+ else:
+ output = {
+ 'tag': tag,
+ }
+ if num:
+ S['index'] += 1
+ output['paragraphIndex'] = S['index']
+ if 'dlugi_cytat' not in S['stack'] and 'poezja_cyt' not in S['stack']:
+ S['vindex'] += 1
+ output['visibleNumber'] = S['vindex']
+ id_prefix = id_prefixes.get(elem.tag, 'i')
+ S['id'][id_prefix] += 1
+ output['id'] = id_prefix + str(S['id'][id_prefix])
+ if elem.attrib.get('id'):
+ output['id'] = 'wl-' + elem.attrib.get('id')
+ if attrs:
+ output['attr'] = attrs.copy()
+ if attr_map:
+ output.setdefault('attr', {})
+ for k, v in attr_map.items():
+ if v == '.text':
+ val = elem.text
+ else:
+ val = elem.attrib[v]
+ output['attr'][k] = val
+ output['contents'] = contents
+ output = [output]
+ if elem.tag == 'strofa':
+ verses = [etree.Element('wers')]
+ if elem.text:
+ vparts = re.split(r'/\s+', elem.text)
+ for i, v in enumerate(vparts):
+ if i:
+ verses.append(etree.Element('wers'))
+ verses[-1].text = (verses[-1].text or '') + v
+ for child in elem:
+ vparts = re.split(r'/\s+', child.tail or '')
+ child.tail = vparts[0]
+ verses[-1].append(child)
+ for v in vparts[1:]:
+ verses.append(etree.Element('wers'))
+ verses[-1].text = v
+
+ if not(len(verses[-1]) or (verses[-1].text or '').strip()):
+ verses.pop()
+
+ elem.clear(keep_tail=True)
+ for verse in verses:
+ if len(verse) == 1 and (verse[0].tag.startswith('wers') or verse[0].tag == 'zastepnik_wersu') and not (verse[0].tail or '').strip():
+ elem.append(verse[0])
+ else:
+ elem.append(verse)
+
+ #if not len(elem):
+ # for v in re.split(r'/\s+', elem.text):
+ # etree.SubElement(elem, 'wers').text = v
+ # elem.text = None
+
+ if hastext and elem.text:
+ contents.append(norm(elem.text))
+ for c in elem:
+ S['stack'].append(elem.tag)
+ contents += toj(c, S)
+ if hastext and c.tail:
+ contents.append(norm(c.tail))
+ S['stack'].pop()
+
+ if elem.tag in front1:
+ S['front1'] += output
+ return []
+ if elem.tag in front2:
+ S['front2'] += output
+ return []
+ return output
+
+def conv(tree):
+ S = {
+ 'index': 0,
+ 'vindex': 0,
+ 'id': defaultdict(lambda: 0),
+ 'stack': [],
+ 'front1': [],
+ 'front2': [],
+ }
+ output = toj(tree.getroot(), S)
+ if not len(output): return {}
+ jt = output[0]
+ jt['front1'] = S['front1']
+ jt['front2'] = S['front2']
+ return jt
+
+#print(json.dumps(jt, indent=2, ensure_ascii=False))
piwik_track_view(views.BookList2.as_view()),
name='catalogue_api_book_list'
),
+ path('11labs/books/',
+ piwik_track_view(views.BookList11Labs.as_view()),
+ name='catalogue_api_book_list'
+ ),
path('books/<slug:slug>/',
piwik_track_view(views.BookDetail2.as_view()),
name='catalogue_api_book'
),
+ path('books/<slug:slug>/fragment/',
+ piwik_track_view(views.BookFragmentView.as_view()),
+ name='catalogue_api_book_fragment'
+ ),
+ path('books/<slug:slug>/children/',
+ views.BookChildrenView.as_view()
+ ),
+ path('books/<slug:slug>/media/<slug:type>/',
+ views.BookMediaView.as_view()
+ ),
+ path('books/<slug:slug>.json',
+ views.BookJsonView.as_view()),
+ path('books/<slug:slug>/sync/',
+ views.BookSyncView.as_view()
+ ),
+ path('books/<slug:slug>/recommended/',
+ views.BookRecommendationsView.as_view()
+ ),
+
+ path('suggested-tags/',
+ piwik_track_view(views.SuggestedTags.as_view()),
+ name='catalogue_api_suggested_tags'
+ ),
+
+ path('authors/',
+ piwik_track_view(views.AuthorList.as_view()),
+ name="catalogue_api_author_list"),
+ path('authors/<slug:slug>/',
+ piwik_track_view(views.AuthorView.as_view()),
+ name='catalogue_api_author'),
+ path('epochs/',
+ piwik_track_view(views.EpochList.as_view()),
+ name="catalogue_api_epoch_list"),
+ path('epochs/<slug:slug>/',
+ piwik_track_view(views.EpochView.as_view()),
+ name='catalogue_api_epoch'),
+ path('kinds/',
+ piwik_track_view(views.KindList.as_view()),
+ name="catalogue_api_kind_list"),
+ path('kinds/<slug:slug>/',
+ piwik_track_view(views.KindView.as_view()),
+ name='catalogue_api_kind'),
+ path('genres/',
+ piwik_track_view(views.GenreList.as_view()),
+ name="catalogue_api_genre_list"),
+ path('genres/<slug:slug>/',
+ piwik_track_view(views.GenreView.as_view()),
+ name='catalogue_api_genre'),
+ path('themes/',
+ piwik_track_view(views.ThemeList.as_view()),
+ name="catalogue_api_theme_list"),
+ path('themes/<slug:slug>/',
+ piwik_track_view(views.ThemeView.as_view()),
+ name='catalogue_api_theme'),
]
from urllib.request import urlopen
from django.conf import settings
from django.core.files.base import ContentFile
-from django.http import Http404, HttpResponse
+from django.http import Http404, HttpResponse, JsonResponse
from django.utils.decorators import method_decorator
from django.views.decorators.cache import never_cache
+from django_filters import rest_framework as dfilters
+from rest_framework import filters
from rest_framework.generics import (ListAPIView, RetrieveAPIView,
RetrieveUpdateAPIView, get_object_or_404)
from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly
from api.handlers import read_tags
from api.utils import vary_on_auth
from catalogue.forms import BookImportForm
+from catalogue.helpers import get_top_level_related_tags
from catalogue.models import Book, Collection, Tag, Fragment, BookMedia
from catalogue.models.tag import prefetch_relations
from club.models import Membership
return Response({}, status=status.HTTP_201_CREATED)
+class BookFilter(dfilters.FilterSet):
+ sort = dfilters.OrderingFilter(
+ fields=(
+ ('sort_key_author', 'alpha'),
+ ('popularity', 'popularity'),
+ )
+ )
+ tag = dfilters.ModelMultipleChoiceFilter(
+ field_name='tag_relations__tag',
+ queryset=Tag.objects.filter(category__in=('author', 'epoch', 'genre', 'kind')),
+ conjoined=True,
+ )
+ translator = dfilters.ModelMultipleChoiceFilter(
+ field_name='translators',
+ queryset=Tag.objects.filter(category='author'),
+ conjoined=True,
+ )
+ has_audio = dfilters.BooleanFilter()
+
+
class BookList2(ListAPIView):
permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
queryset = Book.objects.none() # Required for DjangoModelPermissions
serializer_class = serializers.BookSerializer2
+ filter_backends = (
+ dfilters.DjangoFilterBackend,
+ filters.SearchFilter,
+ )
+ filterset_class = BookFilter
+ search_fields = [
+ 'title',
+ ]
def get_queryset(self):
books = Book.objects.all()
return books
+class BookRecommendationsView(ListAPIView):
+ serializer_class = serializers.BookSerializer2
+ pagination_class = None
+
+ def get_queryset(self):
+ book = get_object_or_404(
+ Book,
+ slug=self.kwargs['slug']
+ )
+ return book.get_recommended(limit=3)
+
+
+class BookList11Labs(BookList2):
+ serializer_class = serializers.BookSerializer11Labs
+
+ def get_queryset(self):
+ books = Book.objects.all()
+ books = books.filter(findable=True)
+ books = books.filter(license='')
+ books = order_books(books, True)
+
+ return books
+
+
@vary_on_auth # Because of 'liked'.
class BookDetail(RetrieveAPIView):
queryset = Book.objects.all()
serializer_class = serializers.BookSerializer2
+class BookSyncView(RetrieveAPIView):
+ queryset = Book.objects.all()
+ lookup_field = 'slug'
+
+ def retrieve(self, request, *args, **kwargs):
+ instance = self.get_object()
+ return Response([
+ {"id": i, "timestamp": ts}
+ for (i, ts) in instance.get_sync()
+ ])
+
+
@vary_on_auth # Because of embargo links.
class EbookList(BookList):
serializer_class = serializers.EbookSerializer
return tags
+class AuthorList(ListAPIView):
+ serializer_class = serializers.AuthorSerializer
+ queryset = Tag.objects.filter(category='author')
+
+class AuthorView(RetrieveAPIView):
+ serializer_class = serializers.AuthorSerializer
+ queryset = Tag.objects.filter(category='author')
+ lookup_field = 'slug'
+
+class EpochList(ListAPIView):
+ serializer_class = serializers.EpochSerializer
+ queryset = Tag.objects.filter(category='epoch')
+
+class EpochView(RetrieveAPIView):
+ serializer_class = serializers.EpochSerializer
+ queryset = Tag.objects.filter(category='epoch')
+ lookup_field = 'slug'
+
+class GenreList(ListAPIView):
+ serializer_class = serializers.GenreSerializer
+ queryset = Tag.objects.filter(category='genre')
+
+class GenreView(RetrieveAPIView):
+ serializer_class = serializers.GenreSerializer
+ queryset = Tag.objects.filter(category='genre')
+ lookup_field = 'slug'
+
+class KindList(ListAPIView):
+ serializer_class = serializers.KindSerializer
+ queryset = Tag.objects.filter(category='kind')
+
+class KindView(RetrieveAPIView):
+ serializer_class = serializers.KindSerializer
+ queryset = Tag.objects.filter(category='kind')
+ lookup_field = 'slug'
+
+class ThemeList(ListAPIView):
+ serializer_class = serializers.ThemeSerializer
+ queryset = Tag.objects.filter(category='theme')
+
+class ThemeView(RetrieveAPIView):
+ serializer_class = serializers.ThemeSerializer
+ queryset = Tag.objects.filter(category='theme')
+ lookup_field = 'slug'
+
class TagView(RetrieveAPIView):
permission_classes = [DjangoModelPermissionsOrAnonReadOnly]
book__slug=self.kwargs['book'],
anchor=self.kwargs['anchor']
)
+
+
+class SuggestedTags(ListAPIView):
+ serializer_class = serializers.FilterTagSerializer
+
+ def get_queryset(self):
+ tag_ids = self.request.GET.getlist('tag', [])
+ search = self.request.GET.get('search')
+ tags = [get_object_or_404(Tag, id=tid) for tid in tag_ids]
+ related_tags = list(t.id for t in get_top_level_related_tags(tags))
+ tags = Tag.objects.filter(id__in=related_tags)
+ if search:
+ tags = tags.filter(name__icontains=search)
+ return tags
+
+
+class BookFragmentView(RetrieveAPIView):
+ serializer_class = serializers.FragmentSerializer2
+
+ def get_object(self):
+ book = get_object_or_404(Book, slug=self.kwargs['slug'])
+ return book.choose_fragment()
+
+
+class BookChildrenView(ListAPIView):
+ serializer_class = serializers.BookSerializer2
+ pagination_class = None
+
+ def get_queryset(self):
+ book = get_object_or_404(Book, slug=self.kwargs['slug'])
+ return book.get_children()
+
+
+class BookMediaView(ListAPIView):
+ serializer_class = serializers.MediaSerializer2
+ pagination_class = None
+
+ def get_queryset(self):
+ return BookMedia.objects.filter(
+ book__slug=self.kwargs['slug'],
+ type=self.kwargs['type']
+ ).order_by('index')
+
+
+from .tojson import conv
+from lxml import etree
+from rest_framework.views import APIView
+
+class BookJsonView(APIView):
+ def get(self, request, slug):
+ book = get_object_or_404(Book, slug=slug)
+ js = conv(etree.parse(book.xml_file.path))
+ return JsonResponse(js, json_dumps_params={'ensure_ascii': False})
return wldoc.as_html(gallery_path=gal_path, gallery_url=gal_url, base_url=absolute_url(gal_url))
+class HtmlNonotesField(EbookField):
+ ext = 'html'
+ for_parents = False
+ directory = 'html_nonotes'
+
+ @staticmethod
+ def transform(wldoc, book):
+ # ugly, but we can't use wldoc.book_info here
+ from librarian import DCNS
+ url_elem = wldoc.edoc.getroot().find('.//' + DCNS('identifier.url'))
+ if url_elem is None:
+ gal_url = ''
+ gal_path = ''
+ else:
+ slug = url_elem.text.rstrip('/').rsplit('/', 1)[1]
+ gal_url = gallery_url(slug=slug)
+ gal_path = gallery_path(slug=slug)
+ return wldoc.as_html(gallery_path=gal_path, gallery_url=gal_url, base_url=absolute_url(gal_url), flags=['nonotes'])
+
+
class CoverField(EbookField):
ext = 'jpg'
directory = 'cover'
--- /dev/null
+# Generated by Django 4.0.8 on 2025-02-26 14:46
+
+import catalogue.fields
+from django.db import migrations, models
+import fnpdjango.storage
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('catalogue', '0048_remove_collection_kind_remove_tag_for_books_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='book',
+ name='html_nonotes_file',
+ field=catalogue.fields.HtmlNonotesField(etag_field_name='html_nonotes_file_etag', storage=fnpdjango.storage.BofhFileSystemStorage()),
+ ),
+ migrations.AddField(
+ model_name='book',
+ name='html_nonotes_file_etag',
+ field=models.CharField(db_index=True, default='', editable=False, max_length=255),
+ ),
+ migrations.AddField(
+ model_name='book',
+ name='license',
+ field=models.CharField(blank=True, db_index=True, max_length=255, verbose_name='licencja'),
+ ),
+ ]
--- /dev/null
+# Generated by Django 4.0.8 on 2025-08-12 09:37
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('catalogue', '0049_book_html_nonotes_file_book_html_nonotes_file_etag_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='book',
+ name='narrators',
+ field=models.ManyToManyField(blank=True, related_name='narrated', to='catalogue.tag'),
+ ),
+ ]
--- /dev/null
+# Generated by Django 4.0.8 on 2025-08-12 10:28
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('catalogue', '0050_book_narrators'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='book',
+ name='has_audio',
+ field=models.BooleanField(default=False),
+ ),
+ ]
--- /dev/null
+# Generated by Django 4.0.8 on 2026-02-17 14:24
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('catalogue', '0051_book_has_audio'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='book',
+ name='pages',
+ field=models.FloatField(blank=True, null=True),
+ ),
+ migrations.AddField(
+ model_name='book',
+ name='read_time',
+ field=models.FloatField(blank=True, null=True),
+ ),
+ ]
--- /dev/null
+# Generated by Django 4.0.8 on 2026-02-17 14:38
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('catalogue', '0052_book_pages_book_read_time'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='book',
+ name='pages',
+ field=models.IntegerField(blank=True, null=True),
+ ),
+ migrations.AlterField(
+ model_name='book',
+ name='read_time',
+ field=models.IntegerField(blank=True, null=True),
+ ),
+ ]
--- /dev/null
+# Generated by Django 4.0.8 on 2026-02-18 12:56
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('catalogue', '0053_alter_book_pages_alter_book_read_time'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='book',
+ name='sort_key',
+ field=models.CharField(db_collation='C', db_index=True, editable=False, max_length=120, verbose_name='klucz sortowania'),
+ ),
+ migrations.AlterField(
+ model_name='book',
+ name='sort_key_author',
+ field=models.CharField(db_collation='C', db_index=True, default='', editable=False, max_length=120, verbose_name='klucz sortowania wg autora'),
+ ),
+ migrations.AlterField(
+ model_name='tag',
+ name='sort_key',
+ field=models.CharField(db_collation='C', db_index=True, max_length=120, verbose_name='klucz sortowania'),
+ ),
+ ]
from random import randint
import os.path
import re
+from slugify import slugify
+from sortify import sortify
from urllib.request import urlretrieve
from django.apps import apps
from django.conf import settings
class Book(models.Model):
"""Represents a book imported from WL-XML."""
title = models.CharField('tytuł', max_length=32767)
- sort_key = models.CharField('klucz sortowania', max_length=120, db_index=True, editable=False)
+ sort_key = models.CharField('klucz sortowania', max_length=120, db_index=True, db_collation='C', editable=False)
sort_key_author = models.CharField(
- 'klucz sortowania wg autora', max_length=120, db_index=True, editable=False, default='')
+ 'klucz sortowania wg autora', max_length=120, db_index=True, db_collation='C', editable=False, default='')
slug = models.SlugField('slug', max_length=120, db_index=True, unique=True)
common_slug = models.SlugField('wspólny slug', max_length=120, db_index=True)
language = models.CharField('kod języka', max_length=3, db_index=True, default=app_settings.DEFAULT_LANGUAGE)
description = models.TextField('opis', blank=True)
+ license = models.CharField('licencja', max_length=255, blank=True, db_index=True)
abstract = models.TextField('abstrakt', blank=True)
toc = models.TextField('spis treści', blank=True)
created_at = models.DateTimeField('data utworzenia', auto_now_add=True, db_index=True)
# files generated during publication
xml_file = fields.XmlField(storage=bofh_storage, with_etag=False)
html_file = fields.HtmlField(storage=bofh_storage)
+ html_nonotes_file = fields.HtmlNonotesField(storage=bofh_storage)
fb2_file = fields.Fb2Field(storage=bofh_storage)
txt_file = fields.TxtField(storage=bofh_storage)
epub_file = fields.EpubField(storage=bofh_storage)
'okładka dla Ebookpoint')
ebook_formats = constants.EBOOK_FORMATS
- formats = ebook_formats + ['html', 'xml']
+ formats = ebook_formats + ['html', 'xml', 'html_nonotes']
parent = models.ForeignKey('self', models.CASCADE, blank=True, null=True, related_name='children')
ancestor = models.ManyToManyField('self', blank=True, editable=False, related_name='descendant', symmetrical=False)
objects = models.Manager()
tagged = managers.ModelTaggedItemManager(Tag)
tags = managers.TagDescriptor(Tag)
- tag_relations = GenericRelation(Tag.intermediary_table_model)
+ tag_relations = GenericRelation(Tag.intermediary_table_model, related_query_name='tagged_book')
translators = models.ManyToManyField(Tag, blank=True)
-
+ narrators = models.ManyToManyField(Tag, blank=True, related_name='narrated')
+ has_audio = models.BooleanField(default=False)
+ read_time = models.IntegerField(blank=True, null=True)
+ pages = models.IntegerField(blank=True, null=True)
+
html_built = django.dispatch.Signal()
published = django.dispatch.Signal()
def isbn_mobi(self):
return self.get_extra_info_json().get('isbn_mobi')
+ @property
+ def redakcja(self):
+ return self.get_extra_info_json().get('about')
+
def is_accessible_to(self, user):
if not self.preview:
return True
return sibling.get_first_text()
return self.parent.get_next_text(inside=False)
- 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.order_by('parent_number').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 []
else:
return self.media.filter(type=type_).exists()
- def has_audio(self):
- return self.has_media('mp3')
-
def get_media(self, type_):
if self.has_media(type_):
if type_ in Book.formats:
def html_url(self):
return self.media_url('html')
+ def html_nonotes_url(self):
+ return self.media_url('html_nonotes')
+
def pdf_url(self):
return self.media_url('pdf')
def has_sync_file(self):
return settings.FEATURE_SYNCHRO and self.has_media("sync")
+ def build_sync_file(self):
+ from lxml import html
+ from django.core.files.base import ContentFile
+ with self.html_file.open('rb') as f:
+ h = html.fragment_fromstring(f.read().decode('utf-8'))
+
+ durations = [
+ m['mp3'].duration
+ for m in self.get_audiobooks()[0]
+ ]
+ if settings.MOCK_DURATIONS:
+ durations = settings.MOCK_DURATIONS
+
+ sync = []
+ ts = None
+ sid = 1
+ dirty = False
+ for elem in h.iter():
+ if elem.get('data-audio-ts'):
+ part, ts = int(elem.get('data-audio-part')), float(elem.get('data-audio-ts'))
+ ts = str(round(sum(durations[:part - 1]) + ts, 3))
+ # check if inside verse
+ p = elem.getparent()
+ while p is not None:
+ # Workaround for missing ids.
+ if 'verse' in p.get('class', ''):
+ if not p.get('id'):
+ p.set('id', f'syn{sid}')
+ dirty = True
+ sid += 1
+ sync.append((ts, p.get('id')))
+ ts = None
+ break
+ p = p.getparent()
+ elif ts:
+ cls = elem.get('class', '')
+ # Workaround for missing ids.
+ if 'paragraph' in cls or 'verse' in cls or elem.tag in ('h1', 'h2', 'h3', 'h4'):
+ if not elem.get('id'):
+ elem.set('id', f'syn{sid}')
+ dirty = True
+ sid += 1
+ sync.append((ts, elem.get('id')))
+ ts = None
+ if dirty:
+ htext = html.tostring(h, encoding='utf-8')
+ with open(self.html_file.path, 'wb') as f:
+ f.write(htext)
+ try:
+ bm = self.media.get(type='sync')
+ except:
+ bm = BookMedia(book=self, type='sync')
+ sync = (
+ '27\n' + '\n'.join(
+ f'{s[0]}\t{sync[i+1][0]}\t{s[1]}' for i, s in enumerate(sync[:-1])
+ )).encode('latin1')
+ bm.file.save(
+ None, ContentFile(sync)
+ )
+
def get_sync(self):
+ if not self.has_sync_file():
+ return []
with self.get_media('sync').first().file.open('r') as f:
sync = f.read().split('\n')
offset = float(sync[0])
continue
start, end, elid = line.split()
items.append([elid, float(start) + offset])
- return json.dumps(items)
-
+ return items
+
+ def sync_ts(self, ts):
+ elid = None
+ for cur_id, t in self.get_sync():
+ if ts >= t:
+ elid = cur_id
+ else:
+ break
+ return elid
+
+ def sync_elid(self, elid):
+ for cur_id, t in self.get_sync():
+ if cur_id == elid:
+ return t
+
def has_audio_epub_file(self):
return self.has_media("audio.epub")
def media_audio_epub(self):
return self.get_media('audio.epub')
- def get_audiobooks(self):
+ def get_audiobooks(self, with_children=False, processing=False):
ogg_files = {}
for m in self.media.filter(type='ogg').order_by().iterator():
ogg_files[m.name] = m
media['ogg'] = ogg
audiobooks.append(media)
- projects = sorted(projects)
- total_duration = '%d:%02d' % (
- total_duration // 60,
- total_duration % 60
- )
+ if with_children:
+ for child in self.get_children():
+ ch_audiobooks, ch_projects, ch_duration = child.get_audiobooks(
+ with_children=True, processing=True)
+ audiobooks.append({'part': child})
+ audiobooks += ch_audiobooks
+ projects.update(ch_projects)
+ total_duration += ch_duration
+
+ if not processing:
+ projects = sorted(projects)
+ total_duration = '%d:%02d' % (
+ total_duration // 60,
+ total_duration % 60
+ )
+
return audiobooks, projects, total_duration
+ def get_audiobooks_with_children(self):
+ return self.get_audiobooks(with_children=True)
+
def wldocument(self, parse_dublincore=True, inherit=True):
from catalogue.import_utils import ORMDocProvider
from librarian.parser import WLDocument
book.findable = findable
book.language = book_info.language
book.title = book_info.title
+ book.license = book_info.license or ''
if book_info.variant_of:
book.common_slug = book_info.variant_of.slug
else:
book.load_toc()
book.save()
+ book.update_stats()
+
meta_tags = Tag.tags_from_info(book_info)
just_tags = [t for (t, rel) in meta_tags if not rel]
for format_ in constants.EBOOK_FORMATS_WITH_CHILDREN:
if format_ not in dont_build:
getattr(book, '%s_file' % format_).build_delay()
+ book.html_nonotes_file.build_delay()
if not settings.NO_SEARCH_INDEX and search_index and findable:
tasks.index_book.delay(book.id)
cls.published.send(sender=cls, instance=book)
return book
+ def update_stats(self):
+ stats = self.wldocument2().get_statistics()['total']
+ self.pages = round(
+ stats.get('verses_with_fn', 0) / 30 +
+ stats.get('chars_out_verse_with_fn', 0) / 1800)
+ self.read_time = round(self.get_time())
+ self.save(update_fields=['pages', 'read_time'])
+ if self.parent is not None:
+ self.parent.update_stats()
+
def update_references(self):
Entity = apps.get_model('references', 'Entity')
doc = self.wldocument2()
def references(self):
return self.reference_set.all().select_related('entity')
+ def update_has_audio(self):
+ self.has_audio = False
+ if self.media.filter(type='mp3').exists():
+ self.has_audio = True
+ if self.descendant.filter(has_audio=True).exists():
+ self.has_audio = True
+ self.save(update_fields=['has_audio'])
+ if self.parent is not None:
+ self.parent.update_has_audio()
+
+ def update_narrators(self):
+ narrator_names = set()
+ for bm in self.media.filter(type='mp3'):
+ narrator_names.update(set(
+ a.strip() for a in re.split(r',|\si\s', bm.artist)
+ ))
+ narrators = []
+
+ for name in narrator_names:
+ if not name: continue
+ slug = slugify(name)
+ try:
+ t = Tag.objects.get(category='author', slug=slug)
+ except Tag.DoesNotExist:
+ sort_key = sortify(
+ ' '.join(name.rsplit(' ', 1)[::-1]).lower()
+ )
+ t = Tag.objects.create(
+ category='author',
+ name_pl=name,
+ slug=slug,
+ sort_key=sort_key,
+ )
+ narrators.append(t)
+ self.narrators.set(narrators)
+
@classmethod
@transaction.atomic
def repopulate_ancestors(cls):
elif isinstance(publisher, list):
return ', '.join(publisher)
+ def get_recommended(self, limit=4):
+ books_qs = type(self).objects.filter(findable=True)
+ books_qs = books_qs.exclude(common_slug=self.common_slug).exclude(ancestor=self)
+ books = type(self).tagged.related_to(self, books_qs)[:limit]
+ return books
+
@classmethod
def tagged_top_level(cls, tags):
""" Returns top-level books tagged with `tags`.
return None
def update_popularity(self):
- count = self.tags.filter(category='set').values('user').order_by('user').distinct().count()
+ count = self.userlistitem_set.values('list__user').order_by('list__user').distinct().count()
try:
pop = self.popularity
pop.count = count
def ridero_link(self):
return 'https://ridero.eu/%s/books/wl_%s/' % (get_language(), self.slug.replace('-', '_'))
- def like(self, user):
- from social.utils import likes, get_set, set_sets
- if not likes(user, self):
- tag = get_set(user, '')
- set_sets(user, self, [tag])
+ def elevenreader_link(self):
+ first_text = self.get_first_text()
+ if first_text is None:
+ return None
+ return 'https://elevenreader.io/audiobooks/wolnelektury:' + first_text.slug
- def unlike(self, user):
- from social.utils import likes, set_sets
- if likes(user, self):
- set_sets(user, self, [])
+ def content_warnings(self):
+ warnings_def = {
+ 'wulgaryzmy': _('wulgaryzmy'),
+ }
+ warnings = self.get_extra_info_json().get('content_warnings', [])
+ warnings = [
+ warnings_def.get(w, w)
+ for w in warnings
+ ]
+ warnings.sort()
+ return warnings
def full_sort_key(self):
return self.SORT_KEY_SEP.join((self.sort_key_author, self.sort_key, str(self.id)))
return f'{name}.{ext}'
def save(self, parts_count=None, *args, **kwargs):
+ if self.type in ('daisy', 'audio.epub'):
+ return super().save(*args, **kwargs)
from catalogue.utils import ExistingFile, remove_zip
if not parts_count:
except BookMedia.DoesNotExist:
old = None
- super(BookMedia, self).save(*args, **kwargs)
+ #super(BookMedia, self).save(*args, **kwargs)
# remove the zip package for book with modified media
if old:
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)
+ super(BookMedia, self).save(*args, **kwargs)
+ self.book.update_narrators()
+ self.book.update_has_audio()
def read_duration(self):
try:
fragment themes (motifs) and some book hierarchy related kludges."""
name = models.CharField('nazwa', max_length=120, db_index=True)
slug = models.SlugField('slug', max_length=120, db_index=True)
- sort_key = models.CharField('klucz sortowania', max_length=120, db_index=True)
+ sort_key = models.CharField('klucz sortowania', max_length=120, db_index=True, db_collation='C')
category = models.CharField(
'kategoria', max_length=50, blank=False, null=False, db_index=True, choices=TAG_CATEGORIES)
description = models.TextField('opis', blank=True)
@staticmethod
def get_tag_list(tag_str):
+ from social.models import UserList
+
if not tag_str:
return []
tags = []
tags_splitted = tag_str.split('/')
for name in tags_splitted:
if category:
- tags.append(Tag.objects.get(slug=name, category=category))
+ if category == 'set':
+ tags.append(UserList.objects.get(slug=name, deleted=False))
+ else:
+ tags.append(Tag.objects.get(slug=name, category=category))
category = None
elif name in Tag.categories_rev:
category = Tag.categories_rev[name]
meta_tags.append((tag, relationship))
return meta_tags
+# def get_books(self):
+# """ Only useful for sets. """
+# return
+
+
TagRelation.tag_model = Tag
}
$("#paginator").on('click', 'a', function() {
- get_page_by_url(url=$(this).attr('href'));
+ get_page_by_url(url=$(this).attr('href'), () => {
+ $("html").animate({
+ scrollTop: $("#book-list").offset().top - 50
+ });
+ });
return false;
});
$(".c-media__caption .license", $root).html($(".license", elem).html());
$(".c-media__caption .project-logo", $root).html($(".project-icon", elem).html());
- console.log('sm 1');
doesUpdateSynchro = false;
if (!$currentMedia || $currentMedia[0] != elem[0]) {
- console.log('set', player.jPlayer("setMedia", media))
+ player.jPlayer("setMedia", media);
player.jPlayer("option", "playbackRate", speed);
}
doesUpdateSynchro = true;
player.jPlayer(cmd, time);
$currentMedia = elem;
- $(".play-next", $root).prop("disabled", !elem.next().length);
+ $(".play-next", $root).prop("disabled", !elem.nextAll('li').length);
let du = parseFloat(elem.data('duration'));
currentDuration = du;
- elem.nextAll().each(function() {
+ elem.nextAll('li').each(function() {
du += parseFloat($(this).data('duration'));
});
totalDurationLeft = du;
let pdu = 0;
- elem.prevAll().each(function() {
+ elem.prevAll('li').each(function() {
pdu += parseFloat($(this).data('duration'));
});
totalDurationBefore = pdu;
- console.log('sm 3', du, pdu);
return player;
};
// TODO: if snap then roll
locator.removeClass('up').removeClass('down');
if (locator.hasClass('snap')) {
- console.log('SCROLL!');
scrollTo();
} else {
if (y < miny) {
});
$('.play-next', $root).click(function() {
- let p = $currentMedia.next();
+ let p = $currentMedia.nextAll('li').first();
if (p.length) {
setMedia(p).jPlayer("play");
_paq.push(['trackEvent', 'audiobook', 'next']);
}
});
$('.play-prev', $root).click(function() {
- let p = $currentMedia.prev();
+ let p = $currentMedia.prevAll('li').first();
if (p.length) {
setMedia(p).jPlayer("play");
_paq.push(['trackEvent', 'audiobook', 'prev']);
_paq.push(['trackEvent', 'audiobook', 'chapter']);
});
- console.log('READY 3!');
var initialElem = $('.jp-playlist li', $root).first();
var initialTime = 0;
- console.log('READY 4!');
if (true || Modernizr.localstorage) {
try {
let speedStr = localStorage['audiobook-speed'];
initialTime = last[2];
}
}
- console.log('READY 5!', initialElem, initialTime);
setMedia($(initialElem), initialTime);
- console.log('READY 6!');
},
timeupdate: function(event) {
ended: function(event) {
- let p = $currentMedia.next();
+ let p = $currentMedia.nextAll('li');
if (p.length) {
setMedia(p).jPlayer("play");
}
{% if book.is_book %}
<span class="icon icon-book-alt" title="{% trans 'książka' %}"></span>
{% endif %}
- {% if book.has_mp3_file %}
+ {% if book.has_audio %}
<span class="icon icon-audio" title="{% trans 'audiobook' %}"></span>
{% endif %}
{% if book.is_picture %}
{% load choose_cites from social_tags %}
{% load catalogue_tags %}
{% load likes_book from social_tags %}
+{% load annoy %}
{% block global-content %}
+<div class="l-container">
+ {% annoy_banner 'book-page' %}
+ </div>
+
<div class="l-container">
<div class="l-breadcrumb">
<a href="/"><span>{% trans "Strona główna" %}</span></a>
<div class="c-media__btn">
<button class="l-button l-button--media" id="ebook"><i class="icon icon-book"></i> {% trans "pobierz książkę" %}</button>
</div>
+ <div class="c-media__btn">
+ {% if first_text %}
+ <a href="https://elevenreader.io/audiobooks/wolnelektury:{{ first_text.slug }}" target="_blank" class="l-button l-button--media"><img src="{% static 'img/elevenreader-21.png' %}" title="{% trans "Posłuchaj w ElevenReader" %}" alt="{% trans "Posłuchaj w ElevenReader" %}"></a>
+ {% endif %}
+ </div>
<div class="c-media__btn">
{% if first_text %}
<a href="{% url 'book_text' first_text.slug %}" class="l-button l-button--media l-button--media--full"><i class="icon icon-eye"></i> {% trans "czytaj online" %}</a>
</div>
-
- {% if book.has_mp3_file %}
+ {% if book.has_audio %}
{% include 'catalogue/snippets/jplayer.html' %}
- {% else %}
- {% with ch=book.get_child_audiobook %}
- {% if ch %}
- {% include 'catalogue/snippets/jplayer_link.html' with book=ch %}
- {% endif %}
- {% endwith %}
-
{% endif %}
-
-
<div class="c-media__popup" data-popup="ebook">
<div class="c-media__popup__box">
<div class="c-media__popup__box__lead">
<button class="l-article__read-more" aria-label="{% trans 'Kliknij aby rozwinąć' %}" data-label="{% trans 'Czytaj więcej' %}" data-action="{% trans 'Zwiń tekst' %}">{% trans 'Czytaj więcej' %}</button>
</article>
{% if accessible %}
- <div class="c-support">
- <div>
- <h2>
- {% blocktrans trimmed %}
- Ta książka jest dostępna dla tysięcy dzieciaków dzięki
- <span>darowiznom</span> od osób takich jak <span>Ty</span>!
- {% endblocktrans %}
- </h2>
- <a href="{% url 'club_join' %}?pk_campaign=layout">{% trans "Dorzuć się!" %}</a>
- </div>
- <div class="bg">
- <!-- img src="{% static '2022/images/dziecko.jpeg' %}" alt="Dorzuć się!" -->
- </div>
- </div>
+ {% annoy_banner 'book-page-center' book=book %}
{% endif %}
</div>
{% endwith %}
{% endif %}
- {% related_books_2022 book taken=book.other_versions|length as related_books %}
+ {% related_books book taken=book.other_versions|length as related_books %}
{% for rel in related_books %}
<article class="l-books__item">
<figure class="l-books__item__img">
</div>
{% endif %}
+ {% if narrated %}
+ <div class="l-section l-section--col">
+ <h2 class="header">Audiobooki</h2>
+ <div class="l-books__grid" id="book-list">
+ {% for book in narrated %}
+ {% include "catalogue/book_box.html" %}
+ {% endfor %}
+ </div>
+ </div>
+ {% endif %}
+
{% if main_tag %}
<section class="l-section">
<div class="l-author">
+++ /dev/null
-{% spaceless %}
- {% load i18n %}
- {% load thumbnail %}
- {% load cache %}
- {% load catalogue_tags %}
- {% load book_shelf_tags from social_tags %}
- {% load static %}
-
- {% with ga=book.get_audiobooks %}
- {% with audiobooks=ga.0 %}
- <div class="{% block box-class %}book-box{% if audiobooks %} audiobook-box{% endif %}{% endblock %}">
- <div class="book-box-inner">
-
- {% with book.tags_by_category as tags %}
- <div class="book-left-column">
- <div class="book-box-body">
- {% block book-box-body-pre %}
- {% endblock %}
-
- <div class="cover-area">
- {% if book.cover_clean %}
- <a href="{% block cover-link %}{{ book.get_absolute_url }}{% endblock %}">
- <img src="{% thumbnail book.cover_clean '139x193' as th %}{{ th.url }}{% endthumbnail %}" alt="Cover" class="cover" />
- </a>
- {% endif %}
- {% block cover-area-extra %}{% endblock %}
- </div>
-
- {% get_current_language as LANGUAGE_CODE %}
- {% cache 86400 book_box_head_tags book.pk LANGUAGE_CODE %}
- <div class="book-box-head">
- <div class="author">
- {% for tag in tags.author %}
- <a href="{{ tag.get_absolute_url }}">{{ tag.name }}</a>{% if not forloop.last %},
- {% endif %}{% endfor %}{% for parent in book.parents %},
- <a href="{{ parent.get_absolute_url }}">{{ parent.title }}</a>{% endfor %}
- </div>
- <div class="title">
- <a href="{{ book.get_absolute_url }}">{{ book.title }}</a>
- </div>
- {% if book.translator %}
- <div class="author">
- tłum. {{ book.translator }}
- </div>
- {% endif %}
- </div>
-
- <div class="tags">
- <span class="category">
- <span class="mono"> {% trans "Epoka" %}:</span> <span class="book-box-tag">
- {% for tag in tags.epoch %}
- <a href="{{ tag.get_absolute_url }}">{{ tag.name }}</a>
- {% if not forloop.last %}<span>, </span>{% endif %}
- {% endfor %}
- </span></span>
-
- <span class="category">
- <span class="mono"> {% trans "Rodzaj" %}:</span> <span class="book-box-tag">
- {% for tag in tags.kind %}
- <a href="{{ tag.get_absolute_url }}">{{ tag.name }}</a>
- {% if not forloop.last %}<span>, </span>{% endif %}
- {% endfor %}
- </span></span>
-
- <span class="category">
- <span class="mono"> {% trans "Gatunek" %}:</span> <span class="book-box-tag">
- {% for tag in tags.genre %}
- <a href="{{ tag.get_absolute_url }}">{{ tag.name }}</a>
- {% if not forloop.last %}<span>, </span>{% endif %}
- {% endfor %}
- </span></span>
-
- {% with extra_info=book.get_extra_info_json %}
- {% if extra_info.location %}
- <span class="category">
- <span class="mono"> {% trans "Region" %}:</span> <span class="book-box-tag">
- {{ extra_info.location }}
- </span></span>
- {% endif %}
- {% endwith %}
-
- {% if book.is_foreign %}
- <span class="category">
- <span class="mono"> {% trans "Język" %}:</span> <span class="book-box-tag">
- <a>{{ book.language_name }}</a>
- </span>
- </span>
- {% endif %}
-
- {% with stage_note=book.stage_note %}
- {% if stage_note.0 %}
- <br>
- <span class="category">
- <a{% if stage_note.1 %} href="{{ stage_note.1 }}"{% endif %}>{{ stage_note.0 }}</a>
- </span>
- {% endif %}
- {% endwith %}
- </div>
- {% endcache %}
- </div>
- {% book_shelf_tags book.pk %}
-
- {% cache 86400 book_box_tools book.pk book|status:request.user LANGUAGE_CODE %}
- {% if book|status:request.user != 'closed' %}
- <ul class="book-box-tools">
- <li class="book-box-read">
- {% if book.html_file %}
- <div>{% content_warning book %}</div>
- <a href="{% url 'book_text' book.slug %}" class="downarrow">{% trans "Czytaj online" %}</a>
- {% endif %}
- {% if book.print_on_demand %}
- <a href="{{ book.ridero_link }}" class="downarrow print tlite-tooltip" title="{% trans "Cena książki w druku cyfrowym jest zależna od liczby stron.<br>Przed zakupem upewnij się, że cena druku na żądanie jest dla Ciebie odpowiednia.<br>Wszystkie nasze zasoby w wersji elektronicznej są zawsze dostępne bezpłatnie." %}">{% trans "Druk na żądanie z" %}
- <img src="{% static 'img/ridero.png' %}" style="height: 0.8em;"/></a>
- {% endif %}
- </li>
- <li class="book-box-download">
- <div class="book-box-formats">
- {% trans "Pobierz ebook" %}:<br>
- {% if book.pdf_file %}
- <a href="{{ book.pdf_url}}">PDF</a>
- {% endif %}
- {% if book.epub_file %}
- <a href="{{ book.epub_url}}">EPUB</a>
- {% endif %}
- {% if book.mobi_file %}
- <a href="{{ book.mobi_url}}">MOBI</a>
- {% endif %}
- {% if book.fb2_file %}
- <a href="{{ book.fb2_url}}">FB2</a>
- {% endif %}
- {% if book.txt_file %}
- <a href="{{ book.txt_url}}">TXT</a>
- {% endif %}
- </div>
- {% if book.has_mp3_file %}
- <div class="book-box-formats">
- {% trans "Pobierz audiobook" %}:<br>
- {% download_audio book %}
- </div>
- {% endif %}
- <div class="book-box-formats">
- {% custom_pdf_link_li book %}
- </div>
- </li>
- </ul>
- {% else %}
- {% block preview-info %}
- <p class="book-box-tools book-box-tools-warn">
- Ten utwór jest na razie dostępny wyłącznie dla naszych Darczyńców.
- <a href="{% url 'club_join' %}">Wspieraj Wolne Lektury</a>
- </p>
- <div>{% content_warning book %}</div>
- {% endblock %}
- {% endif %}
- {% endcache %}
- {% block book-box-extra-info %}{% endblock %}
- {% block box-append %}{% endblock %}
- </div>
- {% endwith %}
-
- {% if book.abstract %}
- <div class="abstract more-expand">
- {{ book.abstract|safe }}
- </div>
- {% endif %}
-
- <div class="clearboth"></div>
- </div>
- </div>
- {% endwith %}
- {% endwith %}
-{% endspaceless %}
<article id="main-text" {% if book.has_sync_file %}class="has-sync"{% endif %}>
- {% with next=book.get_next_text prev=book.get_prev_text %}
+<div id="sidebar">
+ {% if book.other_versions.exists %}
+ <div class="box" id="other">
+ <h2>{% trans "Inne wersje tekstu" %}</h2>
+ <a class="other-text-close" href="#">{% trans "Zamknij drugą wersję" %}</a>
+ <ul>
+ {% spaceless %}
+ {% for other_version in book.other_versions %}
+ <li>
+ <a class="display-other"
+ data-other="{{ other_version.html_url }}"
+ href="{% url 'book_text' other_version.slug %}">
+ {{ other_version.mini_box_nolink }}
+ </a>
+ </li>
+ {% endfor %}
+ {% endspaceless %}
+ </ul>
+ </div>
+ {% endif %}
+</div>
+
+{% with next=book.get_next_text prev=book.get_prev_text %}
{% if next %}
<a class="text_next-book" href="{% url 'book_text' next.slug %}">{{ next.title }} →</a>
{% endif %}
<div class="pointer pointer-top"></div>
</div>
</div>
+</div>
- {% if book.other_versions.exists %}
- <div class="box" id="other">
- <h2>{% trans "Inne wersje utworu" %}</h2>
- <a class="other-text-close" href="#">{% trans "Zamknij drugą wersję" %}</a>
- <ul>
- {% spaceless %}
- {% for other_version in book.other_versions %}
- <li>
- <a class="display-other"
- data-other="{{ other_version.html_url }}"
- href="{% url 'book_text' other_version.slug %}">
- {{ other_version.mini_box_nolink }}
- </a>
- </li>
- {% endfor %}
- {% endspaceless %}
- </ul>
- </div>
- {% endif %}
-
<div id="annoy-stubs">
{% annoy_banners 'book-text-intermission' %}
<div id="player-bar">
{% include 'catalogue/snippets/jplayer_reader.html' %}
</div>
- <script type="application/json" id="smil">
- {{ book.get_sync|safe }}
- </script>
+ {{ book.get_sync|json_script:"smil" }}
{% endif %}
{% load i18n l10n catalogue_tags %}
-{% with audiobooks=book.get_audiobooks %}
+{% with audiobooks=book.get_audiobooks_with_children %}
<div class="c-media__player" id="jp_container_{{ book.pk }}" data-book-slug="{{ book.slug }}">
<div class="jp-jplayer" data-player="jp_container_{{ book.pk }}"
data-supplied="oga,mp3"></div>
<ul class="jp-playlist">
{% localize off %}
{% for i in audiobooks.0 %}
+ {% if i.part %}
+ <div class="title"><strong>{{ i.part.title }}</strong></div>
+ {% else %}
<li
data-mp3='{{ i.mp3.file.url }}'
data-ogg='{{ i.ogg.file.url }}'
</span>
{% endwith %}
</li>
+ {% endif %}
{% endfor %}
{% endlocalize %}
</ul>
+++ /dev/null
-{% load i18n catalogue_tags %}
-
-<div class="c-media__player" id="jp_container_{{ book.pk }}">
- <div class="c-player__head">
- <span> </span>
- </div>
-
- <div class="c-player">
- <div class="c-player__btns">
- <button disabled class="play-prev"><i class="icon icon-prev"></i></button>
- <button disabled class="c-player__btn--md"><i class="icon icon-play"></i></button>
- <form action='{{ book.get_absolute_url }}'>
- <button class="play-next"><i class="icon icon-next"></i></button>
- </form>
- </div>
-
- <div class="c-player__timeline">
- <div class="c-player__info">{{ book.pretty_title }}</div>
- </div>
-
- </div>
- <div class="c-media__caption">
- </div>
-</div>
from catalogue.models import Book, BookMedia, Fragment, Tag, Source
from catalogue.constants import LICENSES
from club.models import Membership
+from social.models import UserList
register = template.Library()
def split_tags(tags):
result = {}
for tag in tags:
- result.setdefault(tag.category, []).append(tag)
+ if isinstance(tag, UserList):
+ result.setdefault('userlist', []).append(tag)
+ else:
+ result.setdefault(tag.category, []).append(tag)
return result
self = split_tags(tags)
@register.simple_tag
-def related_books_2022(book=None, limit=4, taken=0):
- limit -= taken
- max_books = limit
-
- books_qs = Book.objects.filter(findable=True)
- if book is not None:
- books_qs = books_qs.exclude(common_slug=book.common_slug).exclude(ancestor=book)
- books = Book.tagged.related_to(book, books_qs)[:max_books]
-
- return books
+def related_books(book, limit=4, taken=0):
+ return book.get_recommended(limit=limit - taken)
@register.simple_tag
@register.inclusion_tag('catalogue/snippets/content_warning.html')
def content_warning(book):
- warnings_def = {
- 'wulgaryzmy': _('wulgaryzmy'),
- }
- warnings = book.get_extra_info_json().get('content_warnings', [])
- warnings = sorted(
- warnings_def.get(w, w)
- for w in warnings
- )
return {
- "warnings": warnings
+ "warnings": book.content_warnings(),
}
from club.models import Club
from annoy.models import DynamicTextInsert
from pdcounter import views as pdcounter_views
+from social.models import UserList
from wolnelektury.utils import is_ajax
from catalogue import constants
from catalogue import forms
class TaggedObjectList(BookList):
def analyse(self):
super().analyse()
+
self.ctx['tags'] = analyse_tags(self.request, self.kwargs['tags'])
- self.ctx['fragment_tags'] = [t for t in self.ctx['tags'] if t.category in ('theme', 'object')]
- self.ctx['work_tags'] = [t for t in self.ctx['tags'] if t not in self.ctx['fragment_tags']]
+ if len(self.ctx['tags']) > 4:
+ raise Http404
+ self.ctx.update({
+ 'fragment_tags': [],
+ 'work_tags': [],
+ 'user_lists': [],
+ })
+ for tag in self.ctx['tags']:
+ if isinstance(tag, UserList):
+ self.ctx['user_lists'].append(tag)
+ elif tag.category == 'theme':
+ self.ctx['fragment_tags'].append(tag)
+ else:
+ self.ctx['work_tags'].append(tag)
+
self.is_themed = self.ctx['has_theme'] = bool(self.ctx['fragment_tags'])
if self.is_themed:
self.ctx['main_tag'] = self.ctx['fragment_tags'][0]
- elif self.ctx['tags']:
- self.ctx['main_tag'] = self.ctx['tags'][0]
+ elif self.ctx['work_tags']:
+ self.ctx['main_tag'] = self.ctx['work_tags'][0]
else:
self.ctx['main_tag'] = None
self.ctx['filtering_tags'] = [
t for t in self.ctx['tags']
if t is not self.ctx['main_tag']
]
- if len(self.ctx['tags']) == 1 and self.ctx['main_tag'].category == 'author':
+ if len(self.ctx['tags']) == 1 and self.ctx['main_tag'] is not None and self.ctx['main_tag'].category == 'author':
self.ctx['translation_list'] = self.ctx['main_tag'].book_set.all()
+ self.ctx['narrated'] = self.ctx['main_tag'].narrated.all()
def get_queryset(self):
- qs = Book.tagged.with_all(self.ctx['work_tags']).filter(findable=True)
+ if self.ctx['work_tags']:
+ qs = Book.tagged.with_all(self.ctx['work_tags']).filter(findable=True)
+ else:
+ qs = Book.objects.filter(findable=True)
+ for ul in self.ctx['user_lists']:
+ qs = qs.filter(id__in=[i.id for i in ul.get_books()])
qs = qs.exclude(ancestor__in=qs)
if self.is_themed:
fqs = Fragment.tagged.with_all(self.ctx['fragment_tags'])
return qs
def get_suggested_tags(self, queryset):
+ if self.ctx['user_lists']:
+ # TODO
+ return []
tag_ids = [t.id for t in self.ctx['tags']]
if self.is_themed:
related_tags = []
containing_books,
).exclude(category='set').exclude(pk__in=tag_ids)
))
+ ### FIXME: These won't be tags
if self.request.user.is_authenticated:
related_tags.extend(list(
Tag.objects.usage_for_queryset(
Tag.objects.usage_for_queryset(
objects, counts=True
).exclude(category='set'))
+ ### FIXME: these won't be tags
if request.user.is_authenticated:
related_tag_lists.append(
Tag.objects.usage_for_queryset(
.only('name', 'sort_key', 'category', 'slug'))
if isinstance(objects, QuerySet):
objects = prefetch_relations(objects, 'author')
-
+
categories = split_tags(*related_tag_lists)
suggest = []
for c in ['set', 'author', 'epoch', 'kind', 'genre']:
}
template = 'catalogue/author_detail.html'
-
+
return render(
request, template, result,
)
from fnpdjango.actions import export_as_csv_action
from modeltranslation.admin import TranslationAdmin
import annoy.models
+from messaging.models import Contact, Level
from wolnelektury.utils import YesNoFilter
from . import models
)
+class OptOutFilter(YesNoFilter):
+ title = 'opt out'
+ parameter_name = 'optout'
+ q = Q(email__in=Contact.objects.filter(level=Level.OPT_OUT).values_list('email', flat=True))
+
class ScheduleAdmin(admin.ModelAdmin):
form = ScheduleForm
search_fields = ['email', 'source']
list_filter = [
'is_cancelled', 'monthly', 'yearly', 'method',
+ 'consent', OptOutFilter,
PayedFilter, ActiveFilter, ExpiredFilter,
SourceFilter, CrisisFilter
]
d = response.json()
return d
- def create_or_update_contact(self, email, key=None):
+ def create_or_update_contact(self, email, fields=None):
contact_id = self.get_contact_id(email)
if contact_id is None:
- contact_id = self.create_contact(email, key)
- elif key:
- self.update_contact(contact_id, key)
+ contact_id = self.create_contact(email, fields)
+ elif fields:
+ self.update_contact(contact_id, fields)
return contact_id
def get_contact_id(self, email):
if result:
return result[0]['id']
- def create_contact(self, email, key=None):
+ def create_contact(self, email, fields):
data = {
'values': {},
'chain': {
]
}
}
- if key:
- data['values']['WL.TPWL_key'] = key
+ if fields:
+ data['values'].update(fields)
result = self.request('Contact', 'create', data)
return result['values'][0]['id']
-
- def update_contact(self, contact_id, key):
+
+ def update_phone(self, contact_id, phone):
+ if self.request('Phone', 'get', {'where': [['phone', "=", phone], ['contact_id', "=", contact_id]]})['count']:
+ return
+ self.request('Phone', 'create', {'values': {'phone': phone, 'contact_id': contact_id}})
+
+ def update_contact(self, contact_id, fields):
return self.request(
'Contact',
'update',
{
- 'values': {
- 'WL.TPWL_key': key,
- },
+ 'values': fields,
'where': [
['id', '=', contact_id]
]
if not self.enabled:
return
- contact_id = self.create_or_update_contact(email, tpwl_key)
+ fields = {'WL.TPWL_key': tpwl_key}
+ contact_id = self.create_or_update_contact(email, fields)
activity_id = self.get_activity_id(key)
if activity_id is None:
self.referer = referer
super().__init__(*args, **kwargs)
club = models.Club.objects.first()
+ if self.instance.is_custom_amount():
+ self.fields['custom_amount'].initial = int(self.instance.amount)
if club is not None:
self.fields['custom_amount'].widget.attrs['min'] = club.min_amount
return state
def save(self, *args, **kwargs):
- self.instance.source = self.referer
+ if self.referer is not None:
+ self.instance.source = self.referer
return super().save(*args, **kwargs)
club = Club.objects.first()
return club.get_description_for_amount(self.amount, self.monthly)
+ def is_custom_amount(self):
+ club = Club.objects.first()
+ if not self.amount:
+ return False
+ if self.monthly:
+ return not club.monthlyamount_set.filter(amount=self.amount).exists()
+ else:
+ return not club.singleamount_set.filter(amount=self.amount).exists()
+
def initiate_payment(self, request):
return self.get_payment_method().initiate(request, self)
)
@classmethod
- def send_receipt(cls, email, year, resend=False):
+ def generate_receipt(cls, email, year):
+ # TODO: abstract out
Contact = apps.get_model('messaging', 'Contact')
Funding = apps.get_model('funding', 'Funding')
BillingAgreement = apps.get_model('paypal', 'BillingAgreement')
ctx = {
"email": email,
"year": year,
- "next_year": year + 1,
"total": sum(x['amount'] for x in payments),
"payments": payments,
- "optout": optout,
- "resend": resend,
}
temp = tempfile.NamedTemporaryFile(prefix='receipt-', suffix='.pdf', delete=False)
temp.close()
"wl.eps": os.path.join(settings.STATIC_ROOT, "img/wl.eps"),
})
+ with open(temp.name, 'rb') as f:
+ content = f.read()
+ os.unlink(f.name)
+ return content, optout, payments
+
+ @classmethod
+ def send_receipt(cls, email, year, resend=False):
+ receipt = cls.generate_receipt(email, year)
+ if receipt:
+ content, optout, payments = receipt
+ else:
+ return
+ ctx = {
+ "email": email,
+ "year": year,
+ "next_year": year + 1,
+ "total": sum(x['amount'] for x in payments),
+ "payments": payments,
+ "optout": optout,
+ "resend": resend,
+ }
message = EmailMessage(
'Odlicz darowiznę na Wolne Lektury od podatku',
template.loader.render_to_string('club/receipt_email.txt', ctx),
settings.CLUB_CONTACT_EMAIL, [email]
)
- with open(temp.name, 'rb') as f:
- message.attach('wolnelektury-darowizny.pdf', f.read(), 'application/pdf')
+ message.attach('wolnelektury-darowizny.pdf', content, 'application/pdf')
message.send()
- os.unlink(f.name)
class PayUCardToken(payu_models.CardToken):
{% load club %}
{% block content %}
+<div style="display: flex; gap:50px;">
<table class="table">
<tr>
<td>Aktywne miesięczne wpłaty cykliczne:</td>
<td>{% club_active_30day_sum %} zł.</td>
</tr>
</table>
+ <div>
+ <form method="post" action="{% url 'club_receipt' %}" style="display: flex; flex-direction: column;">
+ {% csrf_token %}
+ <span>Pobierz zestawienie roczne</span>
+ <input name="email" placeholder="email" required></input>
+ <input name="year" type="number" min="2013" max="2024" value="2024" placeholder="rok" required></input>
+ <button>pobierz zestawienie</button>
+ </form>
+ </div>
+ </div>
{{ block.super }}
{% endblock content %}
--- /dev/null
+<div class="checkout-infobar">
+ <div class="if-monthly">
+ Dziękujemy, że decydujesz się wspierać nas co miesiąc.<br/>
+ Jeśli to pomyłka, możesz zmienić darowiznę na <a class="donation-mod-monthly" data-url="{% url 'donation_set_monthly' schedule.key %}" data-monthly="false" href="{% url 'donation_step1' schedule.key %}">jednorazową</a>.
+ </div>
+ <div class="if-not-monthly">
+ Wolę wspierać co miesiąc!
+ <a class="donation-mod-monthly" data-url="{% url 'donation_set_monthly' schedule.key %}" data-monthly="true" href="{% url 'donation_step1' schedule.key %}">Zmień na comiesięczną wpłatę.</a>
+ </div>
+</div>
{% load static %}
{% load i18n %}
-<form method="post" action="{% url 'club_join' %}">
+<form method="post" action="{% if schedule %}{% url 'donation_step1' key=schedule.key %}{% else %}{% url 'club_join' %}{% endif %}">
{% csrf_token %}
{{ form.errors }}
<input type="radio" name="switch" id="switch-once" value="single" class="toggle-input" {% if schedule and not schedule.monthly %}checked{% endif %}>
{% with amounts=club.get_amounts %}
<div class="l-checkout__payments payments-once wide-spot-{{ amounts.single_wide_spot }}">
{% for amount in amounts.single %}
- <div class="l-checkout__payments__box once{% if not schedule.monthly and schedule.amount == amount.amount or not schedule and club.default_single_amount == amount.amount %} is-active{% endif %}{% if amount.wide %} l-checkout__payments__box--special{% endif %} l-checkout__payments__box--{{ amount.box_variant }}">
+ <div class="l-checkout__payments__box once{% if not schedule.monthly and schedule.amount == amount.amount or not schedule and club.default_single_amount == amount.amount %} is-active initial-active{% endif %}{% if amount.wide %} l-checkout__payments__box--special{% endif %} l-checkout__payments__box--{{ amount.box_variant }}">
- <h3>{{ amount.amount }} zł</h3>
<div class="l-checkout__payments__box__btn-wrp">
- {% if amount.description %}
- <p>{{ amount.description|safe }}</p>
- {% endif %}
- <button name="single_amount" value="{{ amount.amount }}">{% trans "Wybierz" %}</button>
+ <button name="single_amount" value="{{ amount.amount }}">{{ amount.amount }} zł</button>
</div>
</div>
{% endfor %}
<div class="l-checkout__payments payments-recurring wide-spot-{{ amounts.monthly_wide_spot }}">
{% for amount in amounts.monthly %}
- <div class="l-checkout__payments__box{% if schedule.monthly and schedule.amount == amount.amount or not schedule and amount.amount == club.default_monthly_amount %} is-active{% endif %}{% if amount.wide %} l-checkout__payments__box--special{% endif %} l-checkout__payments__box--{{ amount.box_variant }}">
- <h3>{{ amount.amount }} zł <span>{% trans "/mies." context "kwota na miesiąc" %}</span></h3>
+ <div class="l-checkout__payments__box{% if schedule.monthly and schedule.amount == amount.amount or not schedule and amount.amount == club.default_monthly_amount %} is-active initial-active{% endif %}{% if amount.wide %} l-checkout__payments__box--special{% endif %} l-checkout__payments__box--{{ amount.box_variant }}">
<div class="l-checkout__payments__box__btn-wrp">
- {% if amount.description %}
- <p>{{ amount.description|safe }}</p>
- {% endif %}
- <button name="monthly_amount" value="{{ amount.amount }}">{% trans "Wybierz" %}</button>
+ <button name="monthly_amount" value="{{ amount.amount }}">{{ amount.amount }} zł <span> /mies.</span></button>
</div>
</div>
{% endfor %}
<div class="l-checkout__amount">
<div class="l-checkout__input">
- <label for="kwota">{% trans "Inna kwota" %}</label>
+ <label for="id_custom_amount">{% trans "Inna kwota" %}</label>
{{ form.custom_amount }}
</div>
<button>{% trans "Dalej" %}</button>
{% block donation-step-content %}
- <div class="l-checkout__cols">
+ <div class="l-checkout__cols q-is-monthly {% if schedule.monthly %}is-monthly{% endif %}">
<div class="l-checkout__col">
<div class="l-checkout__payments__box is-active">
<h3>
{{ schedule.amount|floatformat }} zł
- {% if schedule.monthly %}
- <span>{% trans "/mies." context "kwota na miesiąc" %}</span>
- {% endif %}
+ <span class="if-monthly">{% trans "miesięcznie" %}</span>
+ <span class="if-not-monthly">{% trans "jednorazowo" %}</span>
</h3>
<img src="{% static '2022/images/checkout-img-3.jpg' %}" alt="">
+ {% if schedule.get_description %}
<p>{{ schedule.get_description }}</p>
+ {% endif %}
</div>
</div>
<div class="l-checkout__col">
+ {% include "club/donation_infobox.html" %}
+
<form method='post'>
{% csrf_token %}
{{ form.errors }}
- {{ form.amount }}
- {{ form.monthly }}
<div class="l-checkout__form">
<div class="l-checkout__form__row">
<div class="l-checkout__input">
{% block donation-step-content %}
- <div class="l-checkout__cols">
+ <div class="l-checkout__cols q-reload-is-monthly {% if schedule.monthly %}is-monthly{% endif %}">
<div class="l-checkout__col">
<div class="l-checkout__payments__box is-active">
<h3>
{{ schedule.amount|floatformat }} zł
{% if schedule.monthly %}
- <span>{% trans "/mies." context "kwota na miesiąc" %}</span>
+ <span>{% trans "miesięcznie" %}</span>
+ {% else %}
+ <span>{% trans "jednorazowo" %}</span>
{% endif %}</h3>
<img src="{% static '2022/images/checkout-img-3.jpg' %}" alt="">
</div>
</div>
<div class="l-checkout__col">
+ {% include "club/donation_infobox.html" %}
+
+ {% if schedule.monthly %}
+ <h3>Darowizna będzie pobierana automatycznie co miesiąc.</h3>
+ <p>Możesz z niej zrezygnować w dowolnej chwili, korzystając z linku który dostaniesz mailem.</p>
+ {% endif %}
+
<div class="l-checkout__form">
<div class="l-checkout__form__row full">
- <div class="iframe">
+
+
+ <div class="iframe">
+
{% for method in schedule.get_payment_methods %}
{% invite_payment method schedule %}
{% endfor %}
<div class="l-checkout__box">
<div class="l-checkout__box__header">
- <img src="{% block donation-jumbo-image %}{% static '2022/images/checkout-img-1.jpg' %}{% endblock %}" alt="Wspieraj Wolne Lektury">
+ <div class="l-checkout__box__header__img"
+ style="background-image: url({% block donation-jumbo-image %}{% static '2022/images/checkout-img-1.jpg' %}{% endblock %}">
+ </div>
<div class="l-checkout__box__header__content">
- <h1>{% trans "Wspieraj Wolne Lektury" %}</h1>
- <p>{% trans "Dziękujemy, że chcesz razem z nami uwalniać książki!" %}</p>
- <p>{% trans "Wspieraj Wolne Lektury stałą wpłatą – nawet niewielka ma wielką moc! Możesz też wesprzeć Wolne Lektury jednorazowo." %}</p>
+ {% chunk "donate-top" %}
</div>
</div>
<div class="l-checkout__steps">
{% endif %}
<div class="{% if view.step == 1 %}is-current{% else %}is-completed{% endif %}">
<span>1</span>
- <p>{% trans "Rodzaj wsparcia" %}</p>
+ <p>{% trans "Kwota wsparcia" %}</p>
</div>
{% if view.step > 1 and view.step != 4 %}
</a>
<div class="l-checkout__footer">
<div class="l-checkout__footer__content">
+ {% chunk 'donate-bottom' %}
+
<div class="l-checkout__footer__content__item">
<h3>{% trans "Transparentność jest dla nas bardzo ważna." %}</h3>
<div>
{% load i18n static %}
-<h3>{% trans "Wolisz wpłacić przez PayPal?" %}</h3>
+<h3>
+ {% if schedule.monthly %}
+ {% trans "Wolisz ustawić comiesięczną darowiznę przez PayPal?" %}
+ {% else %}
+ {% trans "Wolisz wpłacić przez PayPal?" %}
+ {% endif %}
+</h3>
<a href="{% url 'paypal_init' schedule.key %}">
<div class="iframe">
<img src="{% static 'club/paypal.png' %}" alt="PayPal">
czy wiesz, że możesz odliczyć od podstawy opodatkowania wszystkie darowizny przekazane na prowadzenie biblioteki Wolne Lektury i działalność Fundacji?
-Łącznie w {{ year }} otrzymaliśmy od Ciebie {{ total }} zł na zapewnienie dostępu do książek wszystkim dzieciakom.
+Łącznie w {{ year }} otrzymaliśmy od Ciebie {{ total }} zł na udostępnianie nowych ebooków i audiobooków.
Zestawienie darowizn za rok {{ year }} znajdziesz w załączniku!
Dane z załącznika wprowadź do formularza PIT. Pamiętaj, że w przypadku kontroli z urzędu skarbowego musisz mieć potwierdzenie wykonanych przelewów z Twojego banku.
-Podczas wypełniania swojego PIT-u możesz także przekazać 1,5% swojego podatku na Wolne Lektury. Dzięki temu ufundujesz darmową e-książkę, która trafi do tysięcy dzieciaków. Wystarczy, że w odpowiednim polu wpiszesz nazwę organizacji „fundacja Wolne Lektury” oraz numer KRS 0000070056. Wspierasz Wolne Lektury w ten sposób już od dawna? W takim razie nic nie musisz zmieniać, bo system ponownie sam wprowadzi dane fundacji.
+Podczas wypełniania swojego PIT-u możesz także przekazać 1,5% swojego podatku na Wolne Lektury. Dzięki temu ufundujesz pomożesz nam udostępniać kolejne ebooki i audiobooki w tym roku . Wystarczy, że w odpowiednim polu wpiszesz nazwę organizacji „fundacja Wolne Lektury” oraz numer KRS 0000070056. Wspierasz Wolne Lektury w ten sposób już od dawna? W takim razie nic nie musisz zmieniać, bo system ponownie sam wprowadzi dane fundacji.
Serdecznie dziękujemy za Twoje wsparcie!
-Paulina Choromańska i Jarosław Lipszyc
+Paulina Choromańska i Radosław Czajka
w imieniu całego zespołu Wolnych Lektur
{% load i18n %}
-<h3>{% trans "Podaj dane karty płatniczej" %}</h3>
+<h3>{% trans "Podaj dane karty płatniczej do comiesięcznej darowizny" %}</h3>
<div class="iframe">
<form id="theform" method='POST' action='{% url "club_payu_rec_payment" schedule.key %}'>
{% csrf_token %}
path('plan/<key>/zestawienie/<int:year>/', banner_exempt(views.YearSummaryView.as_view()), name='club_year_summary'),
path('plan/<key>/rodzaj/', banner_exempt(views.DonationStep1.as_view()), name='donation_step1'),
path('plan/<key>/dane/', banner_exempt(views.DonationStep2.as_view()), name='donation_step2'),
+ path('plan/<key>/ustaw-miesiecznie/', views.set_monthly, name='donation_set_monthly'),
path('przylacz/<key>/', views.claim, name='club_claim'),
path('anuluj/<key>/', views.cancel, name='club_cancel'),
path('notify/<int:pk>/', views.PayUNotifyView.as_view(), name='club_payu_notify'),
path('weryfikacja/', views.member_verify, name='club_member_verify'),
+
+ path('potwierdzenie/', views.receipt, name='club_receipt'),
]
from django.conf import settings
from django.contrib.auth.decorators import login_required, permission_required
from django.db.models import Sum
-from django.http import HttpResponseRedirect
+from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.utils.decorators import method_decorator
+@method_decorator(never_cache, name='dispatch')
class DonationStep1(UpdateView):
queryset = models.Schedule.objects.filter(payed_at=None)
form_class = forms.DonationStep1Form
return reverse('donation_step2', args=[self.object.key])
+@method_decorator(never_cache, name='dispatch')
class DonationStep2(UpdateView):
queryset = models.Schedule.objects.filter(payed_at=None)
form_class = forms.DonationStep2Form
return c
+def set_monthly(request, key):
+ schedule = get_object_or_404(models.Schedule, payed_at=None, key=key)
+ if request.POST:
+ schedule.monthly = request.POST.get('monthly') == 'true'
+ schedule.save(update_fields=['monthly'])
+ return JsonResponse({
+ "amount": schedule.amount,
+ "monthly": schedule.monthly,
+ })
+
+
class JoinView(CreateView):
form_class = forms.DonationStep1Form
template_name = 'club/donation_step1.html'
'result': rows
}
)
+
+
+@permission_required('club.schedule_view')
+def receipt(request):
+ email = request.POST.get('email')
+ try:
+ year = int(request.POST.get('year'))
+ except:
+ return HttpResponse('no content')
+
+ receipt = models.PayUOrder.generate_receipt(email, year)
+ if receipt:
+ content, optout, payments = receipt
+ else:
+ return HttpResponse('no content')
+ return HttpResponse(
+ content,
+ headers={
+ "Content-Type": "application/pdf",
+ "Content-Disposition": f'attachment; filename="wolnelektury-{year}-{email}.pdf"',
+ }
+ )
+
--- /dev/null
+# Generated by Django 4.0.8 on 2026-02-18 12:56
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dictionary', '0004_auto_20151221_1225'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='note',
+ name='sort_key',
+ field=models.CharField(db_collation='C', db_index=True, max_length=128),
+ ),
+ ]
class Note(models.Model):
"""Represents a single annotation from a book."""
html = models.TextField()
- sort_key = models.CharField(max_length=128, db_index=True)
+ sort_key = models.CharField(max_length=128, db_index=True, db_collation='C')
fn_type = models.CharField(max_length=10, db_index=True)
qualifiers = models.ManyToManyField(Qualifier)
language = models.CharField(max_length=10, db_index=True)
from django.conf import settings
from django.urls import reverse
-from catalogue.models import Book, Tag
+from catalogue.models import Book
+from social.models import UserList
class Poem(models.Model):
conts)
@classmethod
- def for_set(cls, tag):
- books = Book.tagged_top_level([tag])
- cont_tabs = (cls.get(b) for b in books.iterator())
+ def for_userlist(cls, ul):
+ cont_tabs = [cls.get(b) for b in ul.get_books()]
+ if not cont_tabs: return {}
return reduce(cls.join_conts, cont_tabs)
@classmethod
def get(cls, sth):
object_type = ContentType.objects.get_for_model(sth)
should_keys = {sth.id}
- if isinstance(sth, Tag):
- should_keys = set(b.pk for b in Book.tagged.with_any((sth,)).iterator())
+ if isinstance(sth, UserList):
+ should_keys = set(b.pk for b in sth.get_books())
try:
obj = cls.objects.get(content_type=object_type, object_id=sth.id)
if not obj.pickle:
except cls.DoesNotExist:
if isinstance(sth, Book):
conts = cls.for_book(sth)
- elif isinstance(sth, Tag):
- conts = cls.for_set(sth)
+ elif isinstance(sth, UserList):
+ conts = cls.for_userlist(sth)
else:
raise NotImplementedError('Lesmianator continuations: only Book and Tag supported')
from django.views.decorators import cache
from catalogue.utils import get_random_hash
-from catalogue.models import Book, Tag
+from catalogue.models import Book
+from social.models import UserList
from lesmianator.models import Poem, Continuations
def main_page(request):
last = Poem.objects.all().order_by('-created_at')[:10]
- shelves = Tag.objects.filter(user__username='lesmianator')
+ shelves = UserList.objects.filter(user__username='lesmianator')
return render(
request,
@cache.never_cache
def poem_from_set(request, shelf):
user = request.user if request.user.is_authenticated else None
- tag = get_object_or_404(Tag, category='set', slug=shelf)
+ tag = get_object_or_404(UserList, slug=shelf)
text = Poem.write(Continuations.get(tag))
p = Poem(slug=get_random_hash(text), text=text, created_by=user)
- books = Book.tagged.with_any((tag,))
+ books = tag.get_books()
p.created_from = json.dumps([b.id for b in books])
p.save()
from basicauth import logged_in_or_basicauth, factory_decorator
from catalogue.models import Book, Tag
from search.utils import UnaccentSearchQuery, UnaccentSearchVector
+from social.models import UserList
import operator
import logging
return "Półki użytkownika %s" % user.username
def items(self, user):
- return Tag.objects.filter(category='set', user=user).exclude(items=None)
+ return UserList.objects.filter(user=user, deleted=False)
def item_title(self, item):
return item.name
return "Spis utworów na stronie http://WolneLektury.pl"
def get_object(self, request, slug):
- return get_object_or_404(Tag, category='set', slug=slug, user=request.user)
+ return get_object_or_404(UserList, deleted=False, slug=slug, user=request.user)
def items(self, tag):
- return Book.tagged.with_any([tag])
+ return tag.get_books()
@piwik_track
--- /dev/null
+# Generated by Django 4.0.8 on 2026-02-18 12:56
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('pdcounter', '0003_alter_author_options_alter_bookstub_options_and_more'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='author',
+ name='sort_key',
+ field=models.CharField(db_collation='C', db_index=True, max_length=120, verbose_name='klucz sortowania'),
+ ),
+ ]
class Author(models.Model):
name = models.CharField('imię i nazwisko', max_length=50, db_index=True)
slug = models.SlugField('slug', max_length=120, db_index=True, unique=True)
- sort_key = models.CharField('klucz sortowania', max_length=120, db_index=True)
+ sort_key = models.CharField('klucz sortowania', max_length=120, db_index=True, db_collation='C')
description = models.TextField('opis', blank=True)
death = models.IntegerField('rok śmierci', blank=True, null=True)
gazeta_link = models.CharField(blank=True, max_length=240)
{% if author.alive %}
<p>
{% trans "Dzieła tego autora objęte są prawem autorskim." %}
- {% blocktrans trimmed with url='https://domenapubliczna.org/co-to-jest-domena-publiczna/' %}
- <a href="{{ url }}">Dowiedz się</a>, dlaczego biblioteki internetowe nie mogą
+ {% blocktrans trimmed %}
+ Biblioteki internetowe nie mogą
udostępniać dzieł tego autora.
{% endblocktrans %}
</p>
</p>
<div class='countdown' data-until='{{ pd_counter|date_to_utc|utc_for_js }}'></div>
<p>
- {% blocktrans trimmed with url='https://domenapubliczna.org/co-to-jest-domena-publiczna/' %}
- <a href="{{ url }}">Dowiedz się</a>, dlaczego biblioteki internetowe nie mogą
+ {% blocktrans trimmed %}
+ Biblioteki internetowe nie mogą
udostępniać dzieł tego autora.
{% endblocktrans %}
</p>
{% endblocktrans %}</p>
<div class='countdown' data-until='{{ pd_counter|date_to_utc|utc_for_js }}'></div>
<p>
- {% blocktrans trimmed with url='https://domenapubliczna.org/co-to-jest-domena-publiczna/' %}
- <a href="{{ url }}">Dowiedz się</a>, dlaczego biblioteki internetowe nie mogą
+ {% blocktrans trimmed %}
+ Biblioteki internetowe nie mogą
udostępniać tego utworu.
{% endblocktrans %}
</p>
{% else %}
<p>
{% trans "Ten utwór objęty jest prawem autorskim." %}
- {% blocktrans trimmed with url='https://domenapubliczna.org/co-to-jest-domena-publiczna/' %}
- <a href="{{ url }}">Dowiedz się</a>, dlaczego biblioteki internetowe nie mogą
+ {% blocktrans trimmed %}
+ Biblioteki internetowe nie mogą
udostępniać tego utworu.
{% endblocktrans %}
</p>
--- /dev/null
+from django.urls import path
+from . import views
+
+
+urlpatterns = [
+ path('deviceTokens/', views.DeviceTokensView.as_view()),
+]
--- /dev/null
+from rest_framework import serializers
+from rest_framework.generics import ListCreateAPIView
+from rest_framework.permissions import IsAuthenticated
+from api.utils import never_cache
+from api.fields import AbsoluteURLField
+from push import models
+
+
+class DeviceTokenSerializer(serializers.ModelSerializer):
+ deleted = serializers.BooleanField(default=False, write_only=True)
+ # Explicit definition to disable unique validator.
+ token = serializers.CharField()
+
+ class Meta:
+ model = models.DeviceToken
+ fields = ['token', 'created_at', 'updated_at', 'deleted']
+ read_only_fields = ['created_at', 'updated_at']
+
+ def save(self):
+ if self.validated_data['deleted']:
+ self.destroy(self.validated_data)
+ else:
+ return self.create(self.validated_data)
+
+ def create(self, validated_data):
+ obj, created = models.DeviceToken.objects.get_or_create(
+ user=self.context['request'].user,
+ token=validated_data['token'],
+ )
+ return obj
+
+ def destroy(self, validated_data):
+ models.DeviceToken.objects.filter(
+ user=self.context['request'].user,
+ token=validated_data['token']
+ ).delete()
+
+@never_cache
+class DeviceTokensView(ListCreateAPIView):
+ permission_classes = [IsAuthenticated]
+ serializer_class = DeviceTokenSerializer
+
+ def get_queryset(self):
+ return models.DeviceToken.objects.filter(user=self.request.user)
--- /dev/null
+# Generated by Django 4.0.8 on 2025-08-26 07:47
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('push', '0004_alter_notification_body_alter_notification_image_and_more'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='DeviceToken',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('token', models.CharField(max_length=1024)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ ]
--- /dev/null
+# Generated by Django 4.0.8 on 2025-09-03 12:56
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('push', '0005_devicetoken'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='devicetoken',
+ options={'ordering': ('-updated_at',)},
+ ),
+ migrations.AlterField(
+ model_name='devicetoken',
+ name='token',
+ field=models.CharField(max_length=1024, unique=True),
+ ),
+ ]
def __str__(self):
return '%s: %s' % (self.timestamp, self.title)
+
+
+class DeviceToken(models.Model):
+ user = models.ForeignKey('auth.User', models.CASCADE)
+ token = models.CharField(max_length=1024, unique=True)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ class Meta:
+ ordering = ('-updated_at',)
def populate_from_wikidata(self, wikidata_id):
client = Client()
+ client.opener.addheaders = [(
+ 'User-Agent', 'Wolne Lektury / https://wolnelektury.pl / Python-wikidata'
+ )]
entity = client.get(wikidata_id)
self.label = entity.label.get('pl', entity.label) or ''
]
etags.append(d)
- unused_tags = Tag.objects.exclude(category='set').filter(items=None, book=None)
+ unused_tags = Tag.objects.filter(items=None, book=None)
return render(request, 'reporting/main.html', {
'media_types': media_types,
--- /dev/null
+# This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
+#
+from django.urls import path
+from . import views
+
+
+urlpatterns = [
+ path('search/hint/', views.HintView.as_view()),
+ path('search/', views.SearchView.as_view()),
+ path('search/books/', views.BookSearchView.as_view()),
+ path('search/text/', views.TextSearchView.as_view()),
+]
--- /dev/null
+# This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
+#
+from rest_framework.generics import ListAPIView
+from rest_framework.response import Response
+from rest_framework import serializers
+from rest_framework.views import APIView
+import catalogue.models
+import catalogue.api.serializers
+from search.views import get_hints
+from search.forms import SearchFilters
+
+
+class HintView(APIView):
+ def get(self, request):
+ term = request.query_params.get('q')
+ hints = get_hints(term, request.user)
+ for h in hints:
+ if h.get('img'):
+ h['img'] = request.build_absolute_uri(h['img'])
+ return Response(hints)
+
+
+class SearchView(APIView):
+ def get(self, request):
+ term = self.request.query_params.get('q')
+ f = SearchFilters({'q': term})
+ if f.is_valid():
+ r = f.results()
+ res = {}
+ rl = res['author'] = []
+ c = {'request': request}
+ for item in r['author']:
+ rl.append(
+ catalogue.api.serializers.AuthorSerializer(item, context=c).data
+ )
+ rl = res['genre'] = []
+ for item in r['genre']:
+ rl.append(
+ catalogue.api.serializers.GenreSerializer(item, context=c).data
+ )
+ rl = res['theme'] = []
+ for item in r['theme']:
+ rl.append(
+ catalogue.api.serializers.ThemeSerializer(item, context=c).data
+ )
+
+ return Response(res)
+
+
+class BookSearchView(ListAPIView):
+ serializer_class = catalogue.api.serializers.BookSerializer2
+
+ def get_queryset(self):
+ term = self.request.query_params.get('q')
+ f = SearchFilters({'q': term})
+ if f.is_valid():
+ r = f.results()
+ return r['book']
+ return []
+
+
+
+class SnippetSerializer(serializers.ModelSerializer):
+ anchor = serializers.CharField(source='sec')
+ headline = serializers.CharField()
+
+ class Meta:
+ model = catalogue.models.Snippet
+ fields = ['anchor', 'headline']
+
+
+class BookSnippetsSerializer(serializers.Serializer):
+ book = catalogue.api.serializers.BookSerializer2()
+ snippets = SnippetSerializer(many=True)
+
+
+class TextSearchView(ListAPIView):
+ serializer_class = BookSnippetsSerializer
+
+ def get_queryset(self):
+ term = self.request.query_params.get('q')
+ f = SearchFilters({'q': term})
+ if f.is_valid():
+ r = f.results()
+ r = list({
+ 'book': book,
+ 'snippets': snippets
+ } for (book, snippets) in r['snippet'].items())
+ return r
+ return []
+
import catalogue.models
import infopages.models
+import social.models
from .forms import SearchFilters
import re
import json
return query_syntax_chars.sub(replace, query)
-@cache.never_cache
-def hint(request, mozhint=False, param='term'):
- prefix = request.GET.get(param, '')
- if len(prefix) < 2:
- return JsonResponse([], safe=False)
-
- prefix = re_escape(' '.join(remove_query_syntax_chars(prefix).split()))
-
- try:
- limit = int(request.GET.get('max', ''))
- except ValueError:
- limit = 20
- else:
- if limit < 1:
- limit = 20
-
+def get_hints(prefix, user=None, limit=10):
+ if not prefix: return []
data = []
if len(data) < limit:
authors = catalogue.models.Tag.objects.filter(
'label': author.name,
'url': author.get_absolute_url(),
'img': get_thumbnail(author.photo, '72x72', crop='top').url if author.photo else '',
+ 'slug': author.slug,
+ 'id': author.id,
}
for author in authors[:limit - len(data)]
])
- if request.user.is_authenticated and len(data) < limit:
- tags = catalogue.models.Tag.objects.filter(
- category='set', user=request.user, name_pl__iregex='\m' + prefix).only('name', 'id', 'slug', 'category')
+
+ if user is not None and user.is_authenticated and len(data) < limit:
+ tags = social.models.UserList.objects.filter(
+ user=user, name__iregex='\m' + prefix).only('name', 'id', 'slug')
data.extend([
{
- 'type': 'set',
+ 'type': 'userlist',
'label': tag.name,
'url': tag.get_absolute_url(),
+ 'slug': tag.slug,
}
for tag in tags[:limit - len(data)]
])
'type': tag.category,
'label': tag.name,
'url': tag.get_absolute_url(),
+ 'slug': tag.slug,
+ 'id': tag.id,
}
for tag in tags[:limit - len(data)]
])
'type': 'collection',
'label': collection.title,
'url': collection.get_absolute_url(),
+ 'slug': collection.slug,
}
for collection in collections[:limit - len(data)]
])
'author': author_str,
'url': b.get_absolute_url(),
'img': get_thumbnail(b.cover_clean, '72x72').url if b.cover_clean else '',
+ 'slug': b.slug,
}
)
if len(data) < limit:
'type': 'info',
'label': info.title,
'url': info.get_absolute_url(),
+ 'slug': info.slug,
}
for info in infos[:limit - len(data)]
])
+ return data
+
+
+@cache.never_cache
+def hint(request, mozhint=False, param='term'):
+ prefix = request.GET.get(param, '')
+ if len(prefix) < 2:
+ return JsonResponse([], safe=False)
+
+ prefix = re_escape(' '.join(remove_query_syntax_chars(prefix).split()))
+
+ try:
+ limit = int(request.GET.get('max', ''))
+ except ValueError:
+ limit = 20
+ else:
+ if limit < 1:
+ limit = 20
+
+ data = get_hints(
+ prefix,
+ user=request.user if request.user.is_authenticated else None,
+ limit=limit
+ )
if mozhint:
data = [
from django.forms.widgets import TextInput
from admin_ordering.admin import OrderableAdmin
from social.models import Cite, BannerGroup, Carousel, CarouselItem
-
+from social import models
class CiteForm(ModelForm):
class Meta:
admin.site.register(Carousel, CarouselAdmin)
+
+admin.site.register(models.UserList)
--- /dev/null
+# This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
+#
+from django.urls import path
+from stats.utils import piwik_track_view
+from . import views
+
+
+urlpatterns = [
+ path('settings/', views.SettingsView.as_view()),
+
+ path('like/<slug:slug>/',
+ piwik_track_view(views.LikeView2.as_view()),
+ name='social_api_like'),
+ path('likes/', views.LikesView.as_view()),
+ path('my-likes/', views.MyLikesView.as_view()),
+
+ path('lists/', views.ListsView.as_view()),
+ path('lists/<slug:slug>/', views.ListView.as_view()),
+ path('lists/<slug:slug>/<slug:book>/', views.ListItemView.as_view()),
+
+ path('progress/', views.ProgressListView.as_view()),
+ path('progress/<slug:slug>/', views.ProgressView.as_view()),
+ path('progress/<slug:slug>/text/', views.TextProgressView.as_view()),
+ path('progress/<slug:slug>/audio/', views.AudioProgressView.as_view()),
+
+ path('sync/progress/', views.ProgressSyncView.as_view()),
+ path('sync/userlist/', views.UserListSyncView.as_view()),
+ path('sync/userlistitem/', views.UserListItemSyncView.as_view()),
+ path('sync/bookmark/', views.BookmarkSyncView.as_view()),
+]
+
+
# This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
#
+from datetime import datetime
+from django.db.models import Q
from django.http import Http404
-from rest_framework.generics import ListAPIView, get_object_or_404
-from rest_framework.permissions import IsAuthenticated
+from django.utils.timezone import now, utc
+from rest_framework.generics import ListAPIView, ListCreateAPIView, RetrieveAPIView, RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, get_object_or_404
+from rest_framework.permissions import SAFE_METHODS, IsAuthenticated, IsAuthenticatedOrReadOnly
from rest_framework.response import Response
+from rest_framework import serializers
from rest_framework.views import APIView
from api.models import BookUserData
-from api.utils import vary_on_auth
+from api.utils import vary_on_auth, never_cache
from catalogue.api.helpers import order_books, books_after
from catalogue.api.serializers import BookSerializer
from catalogue.models import Book
+import catalogue.models
+from social.views import get_sets_for_book_ids
from social.utils import likes
+from social import models
+import bookmarks.models
+from bookmarks.api.views import BookmarkSerializer
-@vary_on_auth
+class SettingsSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = models.UserProfile
+ fields = ['notifications']
+
+
+class SettingsView(RetrieveUpdateAPIView):
+ permission_classes = [IsAuthenticated]
+ serializer_class = SettingsSerializer
+
+ def get_object(self):
+ return models.UserProfile.get_for(self.request.user)
+
+
+@never_cache
class LikeView(APIView):
permission_classes = [IsAuthenticated]
book = get_object_or_404(Book, slug=slug)
action = request.query_params.get('action', 'like')
if action == 'like':
- book.like(request.user)
+ models.UserList.like(request.user, book)
elif action == 'unlike':
- book.unlike(request.user)
+ models.UserList.unlike(request.user, book)
return Response({})
+@never_cache
+class LikeView2(APIView):
+ permission_classes = [IsAuthenticated]
+
+ def get(self, request, slug):
+ book = get_object_or_404(Book, slug=slug)
+ return Response({"likes": likes(request.user, book)})
+
+ def put(self, request, slug):
+ book = get_object_or_404(Book, slug=slug)
+ models.UserList.like(request.user, book)
+ return Response({"likes": likes(request.user, book)})
+
+ def delete(self, request, slug):
+ book = get_object_or_404(Book, slug=slug)
+ models.UserList.unlike(request.user, book)
+ return Response({"likes": likes(request.user, book)})
+
+
+@never_cache
+class LikesView(APIView):
+ permission_classes = [IsAuthenticated]
+
+ def get(self, request):
+ slugs = request.GET.getlist('slug')
+ books = Book.objects.filter(slug__in=slugs)
+ books = {b.id: b.slug for b in books}
+ ids = books.keys()
+ res = get_sets_for_book_ids(ids, request.user)
+ res = {books[bid]: v for bid, v in res.items()}
+
+ return Response(res)
+
+
+@never_cache
+class MyLikesView(APIView):
+ permission_classes = [IsAuthenticated]
+
+ def get(self, request):
+ ul = models.UserList.get_favorites_list(request.user)
+ if ul is None:
+ return Response([])
+ return Response(
+ ul.userlistitem_set.exclude(deleted=True).exclude(book=None).values_list('book__slug', flat=True)
+ )
+
+
+class UserListItemsField(serializers.Field):
+ def to_representation(self, value):
+ return value.userlistitem_set.exclude(deleted=True).exclude(book=None).values_list('book__slug', flat=True)
+
+ def to_internal_value(self, value):
+ return {'books': catalogue.models.Book.objects.filter(slug__in=value)}
+
+
+class UserListSerializer(serializers.ModelSerializer):
+ client_id = serializers.CharField(write_only=True, required=False)
+ books = UserListItemsField(source='*', required=False)
+ timestamp = serializers.IntegerField(required=False)
+
+ class Meta:
+ model = models.UserList
+ fields = [
+ 'timestamp',
+ 'client_id',
+ 'name',
+ 'slug',
+ 'favorites',
+ 'deleted',
+ 'books',
+ ]
+ read_only_fields = [
+ 'favorites',
+ 'slug',
+ ]
+ extra_kwargs = {
+ 'slug': {
+ 'required': False
+ }
+ }
+
+ def create(self, validated_data):
+ instance = models.UserList.get_by_name(
+ validated_data['user'],
+ validated_data['name'],
+ create=True
+ )
+ if 'books' in validated_data:
+ instance.userlistitem_set.all().delete()
+ for book in validated_data['books']:
+ instance.append(book)
+ return instance
+
+ def update(self, instance, validated_data):
+ super().update(instance, validated_data)
+ if 'books' in validated_data:
+ instance.userlistitem_set.all().delete()
+ for book in validated_data['books']:
+ instance.append(instance)
+ return instance
+
+
+class UserListBooksSerializer(UserListSerializer):
+ class Meta:
+ model = models.UserList
+ fields = ['books']
+
+
+class UserListItemSerializer(serializers.ModelSerializer):
+ client_id = serializers.CharField(write_only=True, required=False)
+ favorites = serializers.BooleanField(required=False)
+ list_slug = serializers.SlugRelatedField(
+ queryset=models.UserList.objects.all(),
+ source='list',
+ slug_field='slug',
+ required=False,
+ )
+ timestamp = serializers.IntegerField(required=False)
+ book_slug = serializers.SlugRelatedField(
+ queryset=Book.objects.all(),
+ source='book',
+ slug_field='slug',
+ required=False
+ )
+
+ class Meta:
+ model = models.UserListItem
+ fields = [
+ 'client_id',
+ 'uuid',
+ 'order',
+ 'list_slug',
+ 'timestamp',
+ 'favorites',
+ 'deleted',
+
+ 'book_slug',
+ 'fragment',
+ 'quote',
+ 'bookmark',
+ 'note',
+ ]
+ extra_kwargs = {
+ 'order': {
+ 'required': False
+ }
+ }
+
+
+@never_cache
+class ListsView(ListCreateAPIView):
+ permission_classes = [IsAuthenticated]
+ #pagination_class = None
+ serializer_class = UserListSerializer
+
+ def get_queryset(self):
+ return models.UserList.objects.filter(
+ user=self.request.user,
+ favorites=False,
+ deleted=False
+ )
+
+ def perform_create(self, serializer):
+ serializer.save(user=self.request.user)
+
+
+@never_cache
+class ListView(RetrieveUpdateDestroyAPIView):
+ # TODO: check if can modify
+ permission_classes = [IsAuthenticatedOrReadOnly]
+ serializer_class = UserListSerializer
+
+ def get_object(self):
+ if self.request.method in SAFE_METHODS:
+ q = Q(deleted=False)
+ if self.request.user.is_authenticated:
+ q |= Q(user=self.request.user)
+ return get_object_or_404(
+ models.UserList,
+ q,
+ slug=self.kwargs['slug'],
+ )
+ else:
+ return get_object_or_404(
+ models.UserList.all_objects.all(),
+ slug=self.kwargs['slug'],
+ user=self.request.user)
+
+ def perform_update(self, serializer):
+ serializer.save(user=self.request.user)
+
+ def post(self, request, slug):
+ serializer = UserListBooksSerializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ instance = self.get_object()
+ for book in serializer.validated_data['books']:
+ instance.append(book)
+ return Response(self.get_serializer(instance).data)
+
+ def perform_destroy(self, instance):
+ instance.deleted = True
+ instance.updated_at = now()
+ instance.save()
+
+
+@never_cache
+class ListItemView(APIView):
+ permission_classes = [IsAuthenticated]
+
+ def delete(self, request, slug, book):
+ instance = get_object_or_404(
+ models.UserList, slug=slug, user=self.request.user)
+ book = get_object_or_404(catalogue.models.Book, slug=book)
+ instance.remove(book=book)
+ return Response(UserListSerializer(instance).data)
+
+
@vary_on_auth
class ShelfView(ListAPIView):
permission_classes = [IsAuthenticated]
after = self.request.query_params.get('after')
count = int(self.request.query_params.get('count', 50))
if state == 'likes':
- books = Book.tagged.with_any(self.request.user.tag_set.all())
+ books = Book.objects.filter(userlistitem__list__user=self.request.user)
else:
ids = BookUserData.objects.filter(user=self.request.user, complete=state == 'complete')\
.values_list('book_id', flat=True)
return books
+
+
+class ProgressSerializer(serializers.ModelSerializer):
+ book = serializers.HyperlinkedRelatedField(
+ read_only=True,
+ view_name='catalogue_api_book',
+ lookup_field='slug'
+ )
+ book_slug = serializers.SlugRelatedField(
+ queryset=Book.objects.all(),
+ source='book',
+ slug_field='slug')
+ timestamp = serializers.IntegerField(required=False)
+
+ class Meta:
+ model = models.Progress
+ fields = [
+ 'timestamp',
+ 'book', 'book_slug', 'last_mode', 'text_percent',
+ 'text_anchor',
+ 'audio_percent',
+ 'audio_timestamp',
+ 'implicit_text_percent',
+ 'implicit_text_anchor',
+ 'implicit_audio_percent',
+ 'implicit_audio_timestamp',
+ ]
+ extra_kwargs = {
+ 'last_mode': {
+ 'required': False,
+ 'default': 'text',
+ }
+ }
+
+
+class TextProgressSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = models.Progress
+ fields = [
+ 'text_percent',
+ 'text_anchor',
+ ]
+ read_only_fields = ['text_percent']
+
+class AudioProgressSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = models.Progress
+ fields = ['audio_percent', 'audio_timestamp']
+ read_only_fields = ['audio_percent']
+
+
+@never_cache
+class ProgressListView(ListAPIView):
+ permission_classes = [IsAuthenticated]
+ serializer_class = ProgressSerializer
+
+ def get_queryset(self):
+ return models.Progress.objects.filter(user=self.request.user).order_by('-updated_at')
+
+
+class ProgressMixin:
+ def get_object(self):
+ try:
+ return models.Progress.objects.get(user=self.request.user, book__slug=self.kwargs['slug'])
+ except models.Progress.DoesNotExist:
+ book = get_object_or_404(Book, slug=self.kwargs['slug'])
+ return models.Progress(user=self.request.user, book=book)
+
+
+
+@never_cache
+class ProgressView(ProgressMixin, RetrieveAPIView):
+ permission_classes = [IsAuthenticated]
+ serializer_class = ProgressSerializer
+
+
+@never_cache
+class TextProgressView(ProgressMixin, RetrieveUpdateAPIView):
+ permission_classes = [IsAuthenticated]
+ serializer_class = TextProgressSerializer
+
+ def perform_update(self, serializer):
+ serializer.instance.last_mode = 'text'
+ serializer.save()
+
+
+@never_cache
+class AudioProgressView(ProgressMixin, RetrieveUpdateAPIView):
+ permission_classes = [IsAuthenticated]
+ serializer_class = AudioProgressSerializer
+
+ def perform_update(self, serializer):
+ serializer.instance.last_mode = 'audio'
+ serializer.save()
+
+
+
+@never_cache
+class SyncView(ListAPIView):
+ permission_classes = [IsAuthenticated]
+ sync_id_field = 'slug'
+ sync_id_serializer_field = 'slug'
+ sync_user_field = 'user'
+
+ def get_queryset(self):
+ try:
+ timestamp = int(self.request.GET.get('ts'))
+ except:
+ timestamp = 0
+
+ timestamp = datetime.fromtimestamp(timestamp, tz=utc)
+
+ data = []
+ return self.get_queryset_for_ts(timestamp)
+
+ def get_queryset_for_ts(self, timestamp):
+ return self.model.objects.filter(
+ updated_at__gt=timestamp,
+ **{
+ self.sync_user_field: self.request.user
+ }
+ ).order_by('updated_at')
+
+ def get_instance(self, user, data):
+ sync_id = data.get(self.sync_id_serializer_field)
+ if not sync_id:
+ return None
+ return self.model.objects.filter(**{
+ self.sync_user_field: user,
+ self.sync_id_field: sync_id
+ }).first()
+
+ def post(self, request):
+ new_ids = []
+ data = request.data
+ if not isinstance(data, list):
+ raise serializers.ValidationError('Payload should be a list')
+ for item in data:
+ instance = self.get_instance(request.user, item)
+ ser = self.get_serializer(
+ instance=instance,
+ data=item
+ )
+ ser.is_valid(raise_exception=True)
+ synced_instance = self.model.sync(
+ request.user,
+ instance,
+ ser.validated_data
+ )
+ if instance is None and 'client_id' in ser.validated_data and synced_instance is not None:
+ new_ids.append({
+ 'client_id': ser.validated_data['client_id'],
+ self.sync_id_serializer_field: getattr(synced_instance, self.sync_id_field),
+ })
+ return Response(new_ids)
+
+
+class ProgressSyncView(SyncView):
+ model = models.Progress
+ serializer_class = ProgressSerializer
+
+ sync_id_field = 'book__slug'
+ sync_id_serializer_field = 'book_slug'
+
+
+class UserListSyncView(SyncView):
+ model = models.UserList
+ serializer_class = UserListSerializer
+
+
+class UserListItemSyncView(SyncView):
+ model = models.UserListItem
+ serializer_class = UserListItemSerializer
+
+ sync_id_field = 'uuid'
+ sync_id_serializer_field = 'uuid'
+ sync_user_field = 'list__user'
+
+ def get_queryset_for_ts(self, timestamp):
+ qs = self.model.all_objects.filter(
+ updated_at__gt=timestamp,
+ **{
+ self.sync_user_field: self.request.user
+ }
+ )
+ if self.request.query_params.get('favorites'):
+ qs = qs.filter(list__favorites=True)
+ return qs.order_by('updated_at')
+
+
+class BookmarkSyncView(SyncView):
+ model = bookmarks.models.Bookmark
+ serializer_class = BookmarkSerializer
+
+ sync_id_field = 'uuid'
+ sync_id_serializer_field = 'uuid'
+
+ def get_instance(self, user, data):
+ ret = super().get_instance(user, data)
+ if ret is None:
+ if data.get('location'):
+ ret = self.model.get_by_location(user, data['location'])
+ return ret
#
from django import forms
-from catalogue.models import Book, Tag
-from social.utils import get_set
-
-
-class UserSetsForm(forms.Form):
- def __init__(self, book, user, *args, **kwargs):
- super(UserSetsForm, self).__init__(*args, **kwargs)
- self.fields['set_ids'] = forms.ChoiceField(
- choices=[(tag.id, tag.name) for tag in Tag.objects.filter(category='set', user=user).iterator()],
- )
+from catalogue.models import Book
+from . import models
class AddSetForm(forms.Form):
def save(self, user):
name = self.cleaned_data['name'].strip()
if not name: return
- tag = get_set(user, name)
+ ul = models.UserList.get_by_name(user, name, create=True)
try:
book = Book.objects.get(id=self.cleaned_data['book'])
except Book.DoesNotExist:
return
try:
- book.tag_relations.create(tag=tag)
+ ul.append(book=book)
except:
pass
- return book, tag
+ return book, ul
class RemoveSetForm(forms.Form):
def save(self, user):
slug = self.cleaned_data['slug']
try:
- tag = Tag.objects.get(user=user, slug=slug)
- except Tag.DoesNotExist:
+ ul = models.UserList.objects.get(user=user, slug=slug)
+ except models.UserList.DoesNotExist:
return
try:
book = Book.objects.get(id=self.cleaned_data['book'])
return
try:
- book.tag_relations.filter(tag=tag).delete()
+ ul.userlistitem_set.filter(book=book).delete()
except:
pass
- return book, tag
+ return book, ul
--- /dev/null
+# Generated by Django 4.0.8 on 2025-02-24 15:19
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('social', '0016_alter_bannergroup_options_alter_carousel_options_and_more'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='UserConfirmation',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('key', models.CharField(max_length=128, unique=True)),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ ]
--- /dev/null
+# Generated by Django 4.0.8 on 2025-05-07 13:21
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('catalogue', '0049_book_html_nonotes_file_book_html_nonotes_file_etag_and_more'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('social', '0017_userconfirmation'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Progress',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('last_mode', models.CharField(choices=[('text', 'text'), ('audio', 'audio')], max_length=64)),
+ ('text_percent', models.FloatField(blank=True, null=True)),
+ ('text_anchor', models.CharField(blank=True, max_length=64)),
+ ('audio_percent', models.FloatField(blank=True, null=True)),
+ ('audio_timestamp', models.FloatField(blank=True, null=True)),
+ ('implicit_text_percent', models.FloatField(blank=True, null=True)),
+ ('implicit_text_anchor', models.CharField(blank=True, max_length=64)),
+ ('implicit_audio_percent', models.FloatField(blank=True, null=True)),
+ ('implicit_audio_timestamp', models.FloatField(blank=True, null=True)),
+ ('book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='catalogue.book')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'unique_together': {('user', 'book')},
+ },
+ ),
+ ]
--- /dev/null
+# Generated by Django 4.0.8 on 2025-07-08 14:40
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('social', '0018_progress'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='progress',
+ name='deleted',
+ field=models.BooleanField(default=False),
+ ),
+ ]
--- /dev/null
+# Generated by Django 4.0.8 on 2025-07-14 13:39
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('catalogue', '0049_book_html_nonotes_file_book_html_nonotes_file_etag_and_more'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('bookmarks', '0002_quote'),
+ ('social', '0019_progress_deleted'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='UserList',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('slug', models.SlugField(unique=True)),
+ ('name', models.CharField(max_length=1024)),
+ ('favorites', models.BooleanField(default=False)),
+ ('public', models.BooleanField(default=False)),
+ ('deleted', models.BooleanField(default=False)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField()),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='UserListItem',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('order', models.IntegerField()),
+ ('deleted', models.BooleanField(default=False)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField()),
+ ('note', models.TextField(blank=True)),
+ ('book', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='catalogue.book')),
+ ('bookmark', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='bookmarks.bookmark')),
+ ('fragment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='catalogue.fragment')),
+ ('list', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='social.userlist')),
+ ('quote', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='bookmarks.quote')),
+ ],
+ ),
+ ]
--- /dev/null
+# Generated by Django 4.0.8 on 2025-07-14 13:40
+
+from django.db import migrations
+from django.utils.timezone import now
+
+
+def move_sets_to_userlists(apps, schema_editor):
+ UserList = apps.get_model('social', 'UserList')
+ UserListItem = apps.get_model('social', 'UserListItem')
+ Tag = apps.get_model('catalogue', 'Tag')
+
+ for tag in Tag.objects.filter(category='set'):
+ print()
+ print(tag)
+ ul = UserList.objects.create(
+ slug=tag.slug,
+ user=tag.user,
+ name=tag.name,
+ favorites=not tag.name,
+ public=not tag.name,
+ created_at=tag.created_at,
+ updated_at=tag.changed_at,
+ )
+
+ for i, item in enumerate(tag.items.all()):
+ #assert item.content_type_id == 12, item.content_type_id
+ print(item)
+ ul.userlistitem_set.create(
+ order=i + 1,
+ created_at=ul.updated_at,
+ updated_at=ul.updated_at,
+ book_id=item.object_id
+ )
+
+ tag.delete()
+
+
+def rollback_userlists_to_sets(apps, schema_editor):
+ UserList = apps.get_model('social', 'UserList')
+ UserListItem = apps.get_model('social', 'UserListItem')
+ Tag = apps.get_model('catalogue', 'Tag')
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('social', '0020_userlist_userlistitem'),
+ ('catalogue', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.RunPython(
+ move_sets_to_userlists,
+ rollback_userlists_to_sets
+ )
+ ]
--- /dev/null
+# Generated by Django 4.0.8 on 2025-07-22 13:09
+
+from django.db import migrations, models
+import django.utils.timezone
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('social', '0021_move_sets'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='userlist',
+ name='reported_timestamp',
+ field=models.DateTimeField(default=django.utils.timezone.now),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='userlistitem',
+ name='reported_timestamp',
+ field=models.DateTimeField(default=django.utils.timezone.now),
+ preserve_default=False,
+ ),
+ migrations.AddField(
+ model_name='userlistitem',
+ name='uuid',
+ field=models.UUIDField(editable=False, null=True),
+ ),
+ migrations.AlterField(
+ model_name='userlist',
+ name='updated_at',
+ field=models.DateTimeField(auto_now=True),
+ ),
+ migrations.AlterField(
+ model_name='userlistitem',
+ name='updated_at',
+ field=models.DateTimeField(auto_now=True),
+ ),
+ ]
--- /dev/null
+# Generated by Django 4.0.8 on 2025-07-22 13:13
+
+from django.db import migrations, transaction
+import uuid
+
+
+def gen_uuid(apps, schema_editor):
+ UserListItem = apps.get_model("social", "UserListItem")
+ while UserListItem.objects.filter(uuid__isnull=True).exists():
+ print(UserListItem.objects.filter(uuid__isnull=True).count(), 'rows left')
+ with transaction.atomic():
+ for row in UserListItem.objects.filter(uuid__isnull=True)[:1000]:
+ row.uuid = uuid.uuid4()
+ row.save(update_fields=["uuid"])
+
+
+class Migration(migrations.Migration):
+ atomic = False
+
+ dependencies = [
+ ('social', '0022_userlist_reported_timestamp_and_more'),
+ ]
+
+ operations = [
+ migrations.RunPython(gen_uuid, reverse_code=migrations.RunPython.noop),
+ ]
--- /dev/null
+# Generated by Django 4.0.8 on 2025-07-22 13:13
+
+from django.db import migrations, models
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('social', '0023_auto_20250722_1513'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='userlistitem',
+ name='uuid',
+ field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
+ ),
+ ]
--- /dev/null
+# Generated by Django 4.0.8 on 2025-07-29 12:44
+
+from django.db import migrations, models
+import django.utils.timezone
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('social', '0024_auto_20250722_1513'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='progress',
+ name='reported_timestamp',
+ field=models.DateTimeField(default=django.utils.timezone.now),
+ preserve_default=False,
+ ),
+ migrations.AlterField(
+ model_name='userlistitem',
+ name='uuid',
+ field=models.UUIDField(blank=True, default=uuid.uuid4, editable=False, unique=True),
+ ),
+ ]
--- /dev/null
+# Generated by Django 4.0.8 on 2025-08-22 13:18
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('social', '0025_progress_reported_timestamp_alter_userlistitem_uuid'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='UserProfile',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('notifications', models.BooleanField(default=False)),
+ ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ ]
# This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
#
+from datetime import datetime
+import uuid
+from oauthlib.common import urlencode, generate_token
from random import randint
from django.db import models
from django.conf import settings
+from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
+from django.core.mail import send_mail
from django.urls import reverse
+from django.utils.timezone import now, utc
from catalogue.models import Book
+from catalogue.utils import get_random_hash
from wolnelektury.utils import cached_render, clear_cached_renders
+from .syncable import Syncable
class BannerGroup(models.Model):
def get_banner(self):
return self.banner or self.banner_group.get_banner()
+
+
+class UserProfile(models.Model):
+ user = models.OneToOneField(User, models.CASCADE)
+ notifications = models.BooleanField(default=False)
+
+ @classmethod
+ def get_for(cls, user):
+ obj, created = cls.objects.get_or_create(user=user)
+ return obj
+
+
+class UserConfirmation(models.Model):
+ user = models.ForeignKey(User, models.CASCADE)
+ created_at = models.DateTimeField(auto_now_add=True)
+ key = models.CharField(max_length=128, unique=True)
+
+ def send(self):
+ send_mail(
+ 'Potwierdź konto w bibliotece Wolne Lektury',
+ f'https://beta.wolnelektury.pl/ludzie/potwierdz/{self.key}/',
+ settings.CONTACT_EMAIL,
+ [self.user.email]
+ )
+
+ def use(self):
+ user = self.user
+ user.is_active = True
+ user.save()
+ self.delete()
+
+ @classmethod
+ def request(cls, user):
+ cls.objects.create(
+ user=user,
+ key=generate_token()
+ ).send()
+
+
+class Progress(Syncable, models.Model):
+ user = models.ForeignKey(User, models.CASCADE)
+ book = models.ForeignKey('catalogue.Book', models.CASCADE)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+ reported_timestamp = models.DateTimeField()
+ deleted = models.BooleanField(default=False)
+ last_mode = models.CharField(max_length=64, choices=[
+ ('text', 'text'),
+ ('audio', 'audio'),
+ ])
+ text_percent = models.FloatField(null=True, blank=True)
+ text_anchor = models.CharField(max_length=64, blank=True)
+ audio_percent = models.FloatField(null=True, blank=True)
+ audio_timestamp = models.FloatField(null=True, blank=True)
+ implicit_text_percent = models.FloatField(null=True, blank=True)
+ implicit_text_anchor = models.CharField(max_length=64, blank=True)
+ implicit_audio_percent = models.FloatField(null=True, blank=True)
+ implicit_audio_timestamp = models.FloatField(null=True, blank=True)
+
+ syncable_fields = [
+ 'deleted',
+ 'last_mode', 'text_anchor', 'audio_timestamp'
+ ]
+
+ class Meta:
+ unique_together = [('user', 'book')]
+
+ @property
+ def timestamp(self):
+ return self.updated_at.timestamp()
+
+ @classmethod
+ def create_from_data(cls, user, data):
+ return cls.objects.create(
+ user=user,
+ book=data['book'],
+ reported_timestamp=now(),
+ )
+
+ def save(self, *args, **kwargs):
+ try:
+ audio_l = self.book.get_audio_length()
+ except:
+ audio_l = 60
+ if self.text_anchor:
+ self.text_percent = 33
+ if audio_l:
+ self.implicit_audio_percent = 40
+ self.implicit_audio_timestamp = audio_l * .4
+ if self.audio_timestamp:
+ if self.audio_timestamp > audio_l:
+ self.audio_timestamp = audio_l
+ if audio_l:
+ self.audio_percent = 100 * self.audio_timestamp / audio_l
+ self.implicit_text_percent = 60
+ self.implicit_text_anchor = 'f20'
+ return super().save(*args, **kwargs)
+
+
+class ActiveManager(models.Manager):
+ def get_queryset(self):
+ return super().get_queryset().filter(deleted=False)
+
+
+class UserList(Syncable, models.Model):
+ slug = models.SlugField(unique=True)
+ user = models.ForeignKey(User, models.CASCADE)
+ name = models.CharField(max_length=1024)
+ favorites = models.BooleanField(default=False)
+ public = models.BooleanField(default=False)
+ deleted = models.BooleanField(default=False)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+ reported_timestamp = models.DateTimeField()
+
+ syncable_fields = ['name', 'public', 'deleted']
+
+ objects = ActiveManager()
+ all_objects = models.Manager()
+
+ def get_absolute_url(self):
+ return reverse(
+ 'tagged_object_list',
+ args=[f'polka/{self.slug}']
+ )
+
+ def __str__(self):
+ return self.name
+
+ @property
+ def url_chunk(self):
+ return f'polka/{self.slug}'
+
+ @classmethod
+ def create_from_data(cls, user, data):
+ return cls.create(user, data['name'])
+
+ @classmethod
+ def create(cls, user, name):
+ n = now()
+ return cls.objects.create(
+ user=user,
+ name=name,
+ slug=get_random_hash(name),
+ updated_at=n,
+ reported_timestamp=n,
+ )
+
+ @classmethod
+ def get_by_name(cls, user, name, create=False):
+ l = cls.objects.filter(
+ user=user,
+ name=name
+ ).first()
+ if l is None and create:
+ l = cls.create(user, name)
+ return l
+
+ @classmethod
+ def get_favorites_list(cls, user, create=False):
+ try:
+ return cls.objects.get(
+ user=user,
+ favorites=True
+ )
+ except cls.DoesNotExist:
+ n = now()
+ if create:
+ return cls.objects.create(
+ user=user,
+ favorites=True,
+ slug=get_random_hash('favorites'),
+ updated_at=n,
+ reported_timestamp=n,
+ )
+ else:
+ return None
+ except cls.MultipleObjectsReturned:
+ # merge?
+ lists = list(cls.objects.filter(user=user, favorites=True))
+ for l in lists[1:]:
+ l.userlistitem_set.all().update(
+ list=lists[0]
+ )
+ l.delete()
+ return lists[0]
+
+ @classmethod
+ def likes(cls, user, book):
+ ls = cls.get_favorites_list(user)
+ if ls is None:
+ return False
+ return ls.userlistitem_set.filter(deleted=False, book=book).exists()
+
+ def append(self, book):
+ n = now()
+ items = self.userlistitem_set.filter(
+ book=book,
+ )
+ if items.exists():
+ items.update(
+ deleted=False,
+ reported_timestamp=n,
+ )
+ item = items.first()
+ else:
+ item = self.userlistitem_set.create(
+ book=book,
+ order=(self.userlistitem_set.aggregate(m=models.Max('order'))['m'] or 0) + 1,
+ updated_at=n,
+ reported_timestamp=n,
+ )
+ book.update_popularity()
+ return item
+
+ def remove(self, book):
+ self.userlistitem_set.filter(book=book).update(
+ deleted=True,
+ updated_at=now()
+ )
+ book.update_popularity()
+
+ @classmethod
+ def like(cls, user, book):
+ ul = cls.get_favorites_list(user, create=True)
+ ul.append(book)
+
+ @classmethod
+ def unlike(cls, user, book):
+ ul = cls.get_favorites_list(user)
+ if ul is not None:
+ ul.remove(book)
+
+ def get_books(self):
+ return [item.book for item in self.userlistitem_set.exclude(deleted=True).exclude(book=None)]
+
+
+class UserListItem(Syncable, models.Model):
+ list = models.ForeignKey(UserList, models.CASCADE)
+ uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False, blank=True)
+ order = models.IntegerField()
+ deleted = models.BooleanField(default=False)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+ reported_timestamp = models.DateTimeField()
+
+ book = models.ForeignKey('catalogue.Book', models.SET_NULL, null=True, blank=True)
+ fragment = models.ForeignKey('catalogue.Fragment', models.SET_NULL, null=True, blank=True)
+ quote = models.ForeignKey('bookmarks.Quote', models.SET_NULL, null=True, blank=True)
+ bookmark = models.ForeignKey('bookmarks.Bookmark', models.SET_NULL, null=True, blank=True)
+
+ note = models.TextField(blank=True)
+
+ syncable_fields = ['order', 'deleted', 'book', 'fragment', 'quote', 'bookmark', 'note']
+
+ objects = ActiveManager()
+ all_objects = models.Manager()
+
+ @classmethod
+ def create_from_data(cls, user, data):
+ if data.get('favorites'):
+ l = UserList.get_favorites_list(user, create=True)
+ else:
+ l = data['list']
+ try:
+ assert l.user == user
+ except AssertionError:
+ return
+ return l.append(book=data['book'])
+
+ @property
+ def favorites(self):
+ return self.list.favorites
--- /dev/null
+from datetime import datetime
+from django.utils.timezone import now, utc
+
+
+class Syncable:
+ @classmethod
+ def sync(cls, user, instance, data):
+ ts = data.get('timestamp')
+ if ts is None:
+ ts = now()
+ else:
+ ts = datetime.fromtimestamp(ts, tz=utc)
+
+ if instance is not None:
+ if ts and ts < instance.reported_timestamp:
+ return
+
+ if instance is None:
+ if data.get('deleted'):
+ return
+ instance = cls.create_from_data(user, data)
+ if instance is None:
+ return
+
+ instance.reported_timestamp = ts
+ for f in cls.syncable_fields:
+ if f in data:
+ setattr(instance, f, data[f])
+
+ instance.save()
+ return instance
+
+ @property
+ def timestamp(self):
+ return self.updated_at.timestamp()
+
+ @classmethod
+ def create_from_data(cls, user, data):
+ raise NotImplementedError
{% endblock %}
{% block main %}
+{% for list in favs %}
<div class="l-section">
<div class="l-author__header">
- <h1>{% trans "Półka" %}</h1>
+ <h1>{% trans "Ulubione" %}</h1>
</div>
</div>
<div class="l-books__header">
<div class="l-books__input">
<i class="icon icon-filter"></i>
- <input type="text" placeholder="{% trans 'filtry, tytuł' %}" class="quick-filter" data-for="book-list" data-filters="with-filter">
- </div>
- <div class="l-books__sorting">
- <span>{% trans "Sortuj:" %}</span>
- <div>
- <button data-order="data-pop">{% trans "najpopularniejsze" %}</button>
- <button class="is-active">{% trans "alfabetycznie" %}</button>
- <!--button>chronologicznie</button-->
- </div>
+ <input type="text" placeholder="{% trans 'filtry, tytuł' %}" class="quick-filter" data-for="book-list-l{{ list.id }}" data-filters="with-filter">
</div>
</div>
</div>
-
- <div class="with-filter">
-
- <div class="row">
- <h2 class="header"> </h2>
- {% if suggest %}
- <div class="filter-container">
- {% with list_type='book' %}
- {% for tag in suggest %}
- <span class="filter filter-category-{{ tag.category }}">
- <a href="{% catalogue_url list_type tag %}">{{ tag }}</a>
- </span>
- {% endfor %}
- {% endwith %}
- </div>
+ <div class="l-section l-section--col">
+ <div class="l-books__grid" id="book-list-l{{ list.id }}">
+ {% for item in list.userlistitem_set.all %}
+ {% with book=item.book %}
+ {% if book %}
+ {% include "catalogue/book_box.html" %}
{% endif %}
+ {% endwith %}
+ {% endfor %}
</div>
</div>
+{% endfor %}
+{% for list in others %}
+ <div class="l-section" style="margin-top:60px;">
+ <div class="l-author__header">
+ <h1>{{ list.name }}</h1>
+ </div>
+ </div>
+ <div class="l-section">
+ <div class="l-books__header">
+ <div class="l-books__input">
+ <i class="icon icon-filter"></i>
+ <input type="text" placeholder="{% trans 'filtry, tytuł' %}" class="quick-filter" data-for="book-list-l{{ list.id }}" data-filters="with-filter">
+ </div>
+ </div>
+ </div>
<div class="l-section l-section--col">
- <div class="l-books__grid" id="book-list">
- {% for book in books %}
- {% include "catalogue/book_box.html" %}
+ <div class="l-books__grid" id="book-list-l{{ list.id }}">
+ {% for item in list.userlistitem_set.all %}
+ {% with book=item.book %}
+ {% if book %}
+ {% include "catalogue/book_box.html" %}
+ {% endif %}
+ {% endwith %}
{% endfor %}
</div>
</div>
+{% endfor %}
{% endblock %}
+++ /dev/null
-{% spaceless %}
- <ul class='social-shelf-tags'>
- {% for tag in tags %}
- <li><a href="{{ tag.get_absolute_url }}">{{ tag.name }}</a></li>
- {% endfor %}
- </ul>
-{% endspaceless %}
\ No newline at end of file
--- /dev/null
+{% extends "base_simple.html" %}
+{% load i18n %}
+
+
+{% block body %}
+ <h1>{% trans "Konto potwierdzone" %}</h1>
+
+ <p class="normal-text">
+ {% blocktrans with user=user %}Konto <strong>{{ user }}</strong> zostało potwierdzone. Możesz się teraz zalogować.{% endblocktrans %}
+ </p>
+{% endblock body %}
+
#
import re
from django import template
-from django.utils.functional import lazy
from django.utils.cache import add_never_cache_headers
from catalogue.models import Book, Fragment
from social.utils import likes, get_or_choose_cite, choose_cite as cs
return Fragment.tagged.with_all([author]).order_by('?')[:number]
-@register.simple_tag(takes_context=True)
-def book_shelf_tags(context, book_id):
- request = context['request']
- if not request.user.is_authenticated:
- return ''
- book = Book.objects.get(pk=book_id)
- lks = likes(request.user, book, request)
-
- def get_value():
- if not lks:
- return ''
- tags = book.tags.filter(category='set', user=request.user).exclude(name='')
- if not tags:
- return ''
- ctx = {'tags': tags}
- return template.loader.render_to_string('social/shelf_tags.html', ctx)
- return lazy(get_value, str)()
-
-
@register.inclusion_tag('social/carousel.html', takes_context=True)
def carousel(context, placement):
banners = Carousel.get(placement).carouselitem_set.all()#first().get_banner()
urlpatterns = [
+ path('potwierdz/<str:key>/', views.confirm_user, name='social_confirm_user'),
path('lektura/<slug:slug>/lubie/', views.like_book, name='social_like_book'),
path('dodaj-tag/', views.AddSetView.as_view(), name='social_add_set_tag'),
path('usun-tag/', views.RemoveSetView.as_view(), name='social_remove_set_tag'),
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.utils.functional import lazy
-from catalogue.models import Book, Tag
-from catalogue import utils
-from catalogue.tasks import touch_tag
+from catalogue.models import Book
from social.models import Cite
+from social import models
def likes(user, work, request=None):
return False
if request is None:
- return work.tags.filter(category='set', user=user).exists()
+ return models.UserList.likes(user, work)
if not hasattr(request, 'social_likes'):
# tuple: unchecked, checked, liked
if likes_t[0]:
ids = tuple(likes_t[0])
likes_t[0].clear()
- likes_t[2].update(Tag.intermediary_table_model.objects.filter(
- content_type_id=ct.pk, tag__user_id=user.pk,
- object_id__in=ids
- ).distinct().values_list('object_id', flat=True))
+ ls = models.UserList.get_favorites_list(user)
+ if ls is None:
+ return False
+ likes_t[2].update(
+ ls.userlistitem_set.filter(deleted=False).filter(
+ book_id__in=ids).values_list('book_id', flat=True))
likes_t[1].update(ids)
return work.pk in likes_t[2]
return lazy(_likes, bool)()
-def get_set(user, name):
- """Returns a tag for use by the user. Creates it, if necessary."""
- try:
- tag = Tag.objects.get(category='set', user=user, name=name)
- except Tag.DoesNotExist:
- tag = Tag.objects.create(
- category='set', user=user, name=name, slug=utils.get_random_hash(name), sort_key=name.lower())
- except Tag.MultipleObjectsReturned:
- # fix duplicated noname shelf
- tags = list(Tag.objects.filter(category='set', user=user, name=name))
- tag = tags[0]
- for other_tag in tags[1:]:
- for item in other_tag.items.all():
- Tag.objects.remove_tag(item, other_tag)
- Tag.objects.add_tag(item, tag)
- other_tag.delete()
- return tag
-
-
-def set_sets(user, work, sets):
- """Set tags used for given work by a given user."""
-
- old_sets = list(work.tags.filter(category='set', user=user))
-
- work.tags = sets + list(
- work.tags.filter(~Q(category='set') | ~Q(user=user)))
-
- for shelf in [shelf for shelf in old_sets if shelf not in sets]:
- touch_tag(shelf)
- for shelf in [shelf for shelf in sets if shelf not in old_sets]:
- touch_tag(shelf)
-
- # delete empty tags
- Tag.objects.filter(category='set', user=user, items=None).delete()
-
- if isinstance(work, Book):
- work.update_popularity()
-
-
def cites_for_tags(tags):
"""Returns a QuerySet with all Cites for books with given tags."""
return Cite.objects.filter(book__in=Book.tagged.with_all(tags))
from django.views.decorators.http import require_POST
from django.views.generic.edit import FormView
-from catalogue.models import Book, Tag
-import catalogue.models.tag
-from social import forms
+from catalogue.models import Book
+from social import forms, models
from wolnelektury.utils import is_ajax
# ====================
-@require_POST
+@login_required
def like_book(request, slug):
- if not request.user.is_authenticated:
- return HttpResponseForbidden('Login required.')
book = get_object_or_404(Book, slug=slug)
- book.like(request.user)
+ if request.method != 'POST':
+ return redirect(book)
+
+ models.UserList.like(request.user, book)
if is_ajax(request):
return JsonResponse({"success": True, "msg": "ok", "like": True})
form_class = forms.RemoveSetForm
-@require_POST
+@login_required
def unlike_book(request, slug):
- if not request.user.is_authenticated:
- return HttpResponseForbidden('Login required.')
book = get_object_or_404(Book, slug=slug)
- book.unlike(request.user)
+ if request.method != 'POST':
+ return redirect(book)
+
+ models.UserList.unlike(request.user, book)
if is_ajax(request):
return JsonResponse({"success": True, "msg": "ok", "like": False})
return redirect(book)
+@never_cache
@login_required
def my_shelf(request):
template_name = 'social/my_shelf.html'
- tags = list(request.user.tag_set.all())
- suggest = [t for t in tags if t.name]
- print(suggest)
+ favs = request.user.userlist_set.filter(deleted=False, favorites=True)
+ others = request.user.userlist_set.filter(deleted=False, favorites=False)
+ ulists = list(request.user.userlist_set.filter(deleted=False))
+ suggest = [t for t in ulists if t.name]
return render(request, template_name, {
- 'tags': tags,
- 'books': Book.tagged.with_any(tags),
- 'suggest': suggest,
+ 'favs': favs,
+ 'others': others,
+ #'books': Book.objects.filter(userlistitem__list__user=request.user),
})
def get_sets_for_book_ids(book_ids, user):
data = {}
- tagged = catalogue.models.tag.TagRelation.objects.filter(
- tag__user=user,
- #content_type= # for books,
- object_id__in=book_ids
- ).order_by('tag__sort_key')
+ tagged = models.UserListItem.objects.filter(
+ list__user=user,
+ book_id__in=book_ids,
+ deleted=False,
+ list__deleted=False
+ ).order_by('list__name')
for t in tagged:
- # related?
- item = data.setdefault(t.object_id, [])
- if t.tag.name:
- item.append({
- "slug": t.tag.slug,
- "url": t.tag.get_absolute_url(),
- "name": t.tag.name,
- })
+ item = data.setdefault(t.book_id, [])
+ item.append({
+ "slug": t.list.slug,
+ "url": t.list.get_absolute_url(),
+ "name": t.list.name,
+ "favorites": t.list.favorites,
+ })
for b in book_ids:
if b not in data:
data[b] = None
@login_required
def my_tags(request):
term = request.GET.get('term', '')
- tags = Tag.objects.filter(user=request.user).order_by('sort_key')
+ tags = models.UserList.objects.filter(user=request.user).order_by('name')
if term:
- tags = tags.filter(name__icontains=term)
+ ulists = tags.filter(name__icontains=term)
return JsonResponse(
[
- t.name for t in tags
+ ul.name for ul in ulists
], safe=False
)
+
+
+def confirm_user(request, key):
+ uc = get_object_or_404(models.UserConfirmation, key=key)
+ user = uc.user
+ uc.use()
+ return render(request, 'social/user_confirmation.html', {
+ 'user': user,
+ })
def __str__(self):
return self.name
+ def save(self, *args, **kwargs):
+ super().save(*args, **kwargs)
+ for page in SponsorPage.objects.all():
+ page.save()
+
def description(self):
if len(self._description):
return self._description
'django.contrib.postgres',
'admin_ordering',
'rest_framework',
+ 'django_filters',
'fnp_django_pagination',
'pipeline',
'sorl.thumbnail',
# Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
#
AUTHENTICATION_BACKENDS = [
- 'django.contrib.auth.backends.ModelBackend',
- # 'allauth.account.auth_backends.AuthenticationBackend',
+ #'django.contrib.auth.backends.ModelBackend',
+ 'allauth.account.auth_backends.AuthenticationBackend',
]
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 2
+ACCOUNT_AUTHENTICATION_METHOD = "username_email"
+FEATURE_CONFIRM_USER = False
+
LOGIN_URL = '/uzytkownik/login/'
LOGIN_REDIRECT_URL = '/'
# This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
#
-from os import path
-from .paths import PROJECT_DIR
+import os
DEBUG = True
CONTACT_EMAIL = 'fundacja@wolnelektury.pl'
+ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split()
+
CACHE_MIDDLEWARE_SECONDS = 3 * 60
+CACHES = {
+ 'default': {
+ 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
+ 'TIMEOUT': 180,
+ 'LOCATION': [
+ 'memcached:11211',
+ ],
+ },
+ 'template_fragments': {
+ 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
+ 'TIMEOUT': 86400,
+ 'LOCATION': [
+ 'memcached:11211',
+ ],
+ },
+}
+
DATABASES = {
'default': {
- 'ENGINE': 'django.db.backends.sqlite3', # 'postgresql_psycopg2'
- 'NAME': path.join(PROJECT_DIR, 'dev.db'),
- 'USER': '', # Not used with sqlite3.
- 'PASSWORD': '', # Not used with sqlite3.
- 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
+ 'ENGINE': 'django.db.backends.postgresql',
+ 'HOST': 'db',
+ 'USER': os.environ.get('POSTGRES_USER'),
+ 'PASSWORD': os.environ.get('POSTGRES_PASSWORD'),
+ 'NAME': os.environ.get('POSTGRES_USER'),
}
}
# This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
#
+CELERY_BROKER_URL = 'redis://redis'
CELERY_TASK_EAGER_PROPAGATES = True
CELERY_SEND_TASK_ERROR_EMAILS = True
CELERY_ACCEPT_CONTENT = ['pickle']
'rest_framework.authentication.SessionAuthentication',
),
'DEFAULT_PAGINATION_CLASS': 'api.pagination.WLLimitOffsetPagination',
- 'PAGE_SIZE': 10,
+ 'PAGE_SIZE': 20,
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning',
}
}
-HAYSTACK_CONNECTIONS = {
- 'default': {
- 'ENGINE': 'haystack.backends.simple_backend.SimpleEngine',
- },
-}
-
-
FORMS_BUILDER_USE_SITES = False
FORMS_BUILDER_EDITABLE_FIELD_MAX_LENGTH = True
FORMS_BUILDER_EDITABLE_SLUGS = True
LATEST_BLOG_POSTS = "https://fundacja.wolnelektury.pl/feed/?cat=-135"
-CATALOGUE_COUNTERS_FILE = os.path.join(VAR_DIR, 'catalogue_counters.p')
+CATALOGUE_COUNTERS_FILE = os.path.join(VAR_DIR, 'counters/catalogue_counters.p')
+LESMIANATOR_PICKLE = os.path.join(VAR_DIR, 'counters/lesmianator.p')
+
+NO_SEARCH_INDEX = False
CATALOGUE_MIN_INITIALS = 60
VARIANTS = {
}
-EPUB_FUNDRAISING = []
-
CIVICRM_BASE = None
CIVICRM_KEY = None
SEARCH_USE_UNACCENT = False
FEATURE_SYNCHRO = False
+
+FEATURE_API_REGISTER = False
.c-media__actions {
display: flex;
- column-gap: 38px;
+ column-gap: 20px;
row-gap: 10px;
}
display: inline;
}
}
+ div.title {
+ font-size: 12px;
+ line-height: 140%;
+ letter-spacing: 0.05em;
+ list-style-type: decimal;
+ margin: 5px 0;
+ color: white;
+ cursor: pointer;
+ }
}
}
.annoy-banner-off {
font-size: 1.2rem !important;
}
-
+
@include rwd($break-flow) {
.annoy-banner-inner {
padding: 0;
}
.annoy-banner-off {
-
+
}
}
}
-.annoy-banner_crisis-container {
+.annoy-banner_crisis-container,
+.annoy-banner_top-container {
position: sticky;
top: 0;
- height: 160px;
z-index: 10;
box-shadow: 0 0 10px black;
display: flex;
align-items:center;
cursor: pointer;
- @media screen and (min-height: 480px) {
- height: 33vh;
- top: calc(-33vh + 160px);
+ &.annoy-banner_top-container {
+ padding: 16px 0;
+
+ @media screen and (max-height: 700px) {
+ padding: 5px 0;
+ }
}
+ &.annoy-banner_crisis-container {
+ height: 160px;
- @media screen and (max-width: 940px) {
- padding: 10px 0;
- height: auto;
- top: 0;
+ @media screen and (min-height: 480px) {
+ height: 33vh;
+ top: calc(-33vh + 160px);
+ }
+
+ @media screen and (max-width: 940px) {
+ padding: 10px 0;
+ height: auto;
+ top: 0;
+ }
}
- .annoy-banner_crisis {
+
+ .annoy-banner_crisis,
+ .annoy-banner_top {
position: sticky;
top: 0;
width: 100%;
.image-box {
position: relative;
img {
- height: 159px;
+ max-height: 159px;
display: block;
@media screen and (max-width: 700px) {
- height: 120px;
+ max-height: 120px;
}
}
}
}
}
+ @media screen and (max-height: 700px) {
+ flex-direction: row;
+ justify-content: space-between;
+ p {
+ font-size: .9em;
+ }
+ }
+
+
.text {
background: #edc016;
padding: 1em;
border: 3px solid black;
}
+
+ @media screen and (max-height: 400px) {
+ align-items: center;
+ .text {
+ height: 1.2em;
+ overflow: hidden;
+ }
+ }
+
a {
color: #c32721;
}
display: block;
transition: background-color .2s;
+ @media screen and (max-height: 700px) {
+ font-size: .9em;
+ }
+ @media screen and (max-height: 400px) {
+ white-space: nowrap;
+ }
+
&:hover {
background: #ffd430;
text-decoration: none;
}
}
}
- &.annoy-banner-style_crisis_quiet {
+ &.annoy-banner-style_crisis_quiet,
+ &.annoy-banner_top-container {
background: black;
color: white;
.annoy-banner-inner {
}
}
}
+
+
+
+.annoy-banner_book-page {
+ background-color: #ffd430;
+ color: #083F4D;
+ border-radius: 10px;
+ padding: 15px 20px;
+ margin-top: 20px;
+
+ .annoy-banner-inner {
+ display: flex;
+ flex-direction: row;
+ gap: 20px;
+ align-items: flex-start;
+ justify-content: space-between;
+
+ p {
+ margin: 0;
+ }
+ a {
+ line-height: 1.35;
+ color: #c32721;
+ white-space: nowrap;
+ border: solid #c32721;
+ border-width: 0 0 1px;
+
+ &:hover {
+ text-decoration: none;
+ border-bottom-width: 2px;
+ }
+ }
+ }
+}
+
+.annoy-banner_book-page-center {
+ background: white;
+ margin-top: 50px;
+ padding: 20px;
+ border: 1px solid #018189;
+ border-radius: 15px;
+ color:#018189;
+
+ .annoy-banner-inner {
+ display: flex;
+ gap: 15px;
+ align-items: center;
+
+ img {
+ width: 150px;
+ }
+ p {
+ margin: 0;
+ }
+ }
+}
@include rwd($break-flow) {
@include font-size(16px);
line-height: 20px;
- padding: 19px 20px;
+ padding: 19px 0;
}
.icon {
font-size: 21px;
- margin-right: 15px;
+ margin-right: 10px;
+ }
+
+ img {
+ height: 21px;
}
&:hover {
display: flex;
background: #083F4D;
- img {
+ .l-checkout__box__header__img {
display: none;
+ background-position: center;
+ background-size: cover;
@include rwd($break-flow) {
display: block;
+ width: 50%;
}
}
}
}
}
.l-checkout__payments__box__btn-wrp {
- padding: 0 20px 20px 20px;
+ padding: 20px;
margin-bottom: 0;
margin-top: auto;
- @include rwd($break-flow) {
- padding-top: 20px;
- }
}
p {
margin-top: 0;
}
}
button {
- height: 56px;
+ margin: 0;
+ font-family: "Source Sans Pro",sans-serif;
+ font-weight: bold;
+ font-size: 44px;
+ letter-spacing: -0.01em;
+ min-height: 90px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all $ease-out 250ms;
+
background: #FFFFFF;
border: 1px solid #92BD39;
border-radius: 3px;
width: 100%;
outline: 0;
cursor: pointer;
- font-weight: 600;
- font-size: 16px;
- line-height: 24px;
- display: flex;
- align-items: center;
- justify-content: center;
text-align: center;
color: #083F4D;
- transition: background $ease-out 250ms;
+ flex-direction: column;
@include rwd($break-flow) {
- font-size: 20px;
- line-height: 25px;
+ flex-direction: row;
+ line-height: 130%;
+ }
+
+ span {
+ font-weight: 600;
+ font-size: 25px;
+ letter-spacing: -0.01em;
+ color: #92BD39;
+ margin-left: 10px;
+ transition: opacity $ease-out 250ms;
}
&:hover {
&:hover {
background: #83AD2B;
}
+ span {
+ color: white;
+ }
}
}
}
}
}
+
+
+.if-monthly { display: none; }
+.is-monthly {
+ .if-monthly {
+ display: block;
+ }
+ .if-not-monthly {
+ display: none;
+ }
+}
+
+
+.checkout-infobar {
+ margin: 0 0 20px;
+ padding: 20px;
+ border-radius: 10px;
+ border: 1px solid #edc016;
+ background: #edc016;
+}
color: white;
}
}
+
+ .menubar-donate {
+ color: #fff;
+ background: #c92834;
+ padding: 9px 20px 11px;
+ font-weight: 600;
+ margin-right: 20px;
+ border-radius: 15px;
+ }
}
.user {
display: block;
left: 16px;
}
+
+ .menubar-donate {
+ display: none;
+ }
}
}
}
h3, .subtitle2 {
font-size: 1.5em;
margin: 1.5em 0 1em 0;
+ padding-right: 48px;
font-weight: normal;
line-height: 1.5em;
}
}
}
}
+
+
+
+#sidebar {
+ position: absolute;
+ left: 0;
+ top: 20px;
+ width: 200px;
+
+ h2 {
+ font-size: 20px;
+ margin-bottom: 1em;
+ }
+
+ .other-text-close {
+ display: none;
+ }
+
+ #other {
+ display: none;
+ @include rwd($break-wide) {
+ display: block;
+ }
+ ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+
+ .book-mini-box {
+ position: relative;
+ width: 137px;
+ .language {
+ position: absolute;
+ top: 163px;
+ left: 10px;
+ color: white;
+ background: black;
+ padding: 0 10px;
+ border-radius: 10px;
+ font-size: 14px;
+ line-height: 20px;
+
+ }
+ .author, .title {
+ display: block;
+ font-size: 14px;
+ line-height: 18px;
+ }
+ }
+ }
+ }
+}
+.with-other-text {
+ #sidebar {
+ .other-text-close {
+ display: block;
+ background: red;
+ padding: 10px;
+ margin: 10px 0;
+ border-radius: 10px;
+ color: white;
+ text-decoration: none;
+ }
+ }
+ #main-text {
+ #book-text {
+ .other {
+ overflow: hidden;
+ margin: 10px 40px 10px 50px;
+ padding: 10px 20px 0 10px;
+ background: #eee;
+ border-left: 10px double #ddd;
+
+ h3 {
+ margin: 0;
+ padding: 0;
+ }
+
+ .paragraph {
+ padding: 0;
+ }
+ }
+ }
+ }
+}
+
+:lang(pl),
+:lang(en),
+:lang(de),
+:lang(fr),
+:lang(lt),
+:lang(uk)
+{
+ direction: ltr;
+}
+:lang(he) {
+ direction: rtl;
+}
#menu {
- padding-bottom: 50px;
* {
box-sizing: content-box;
}
$("#menu-other").show();
+ function insertOtherText(text) {
+ let tree = $(text);
+ let lang = tree.attr('lang') || 'pl';
+
+ // toc?
+ // themes?
+
+ let cursor = $(".main-text-body #book-text").children().first();
+ // wstawiamy przed kursorem
+ lastTarget = '';
+ tree.children().each((i, e) => {
+ let $e = $(e);
+
+ if ($e.hasClass('anchor')) return;
+ if ($e.hasClass('numeracja')) return;
+ if ($e.attr('id') == 'toc') return;
+ if ($e.attr('id') == 'nota_red') return;
+ if ($e.attr('id') == 'themes') return;
+ if ($e.attr('name') && $e.attr('name').startsWith('sec')) return;
+
+ if ($e.hasClass('target')) {
+ let target = $e.attr('name');
+
+ while (lastTarget != target) {
+ let nc = cursor.next();
+ if (!nc.length) {
+ break;
+ }
+ cursor = nc;
+ lastTarget = cursor.attr('name');
+ }
+
+ while (true) {
+ let nc = cursor.next();
+ if (!nc.length) {
+ break;
+ }
+ cursor = nc;
+ lastTarget = cursor.attr('name');
+ if (lastTarget) break;
+ }
+
+ } else {
+ let d = $('<div class="other">');
+ d.attr('lang', lang);
+ d.append(e);
+ d.insertBefore(cursor);
+ }
+ });
+ }
+
/* Load other version of text. */
$(".display-other").click(function(e) {
e.preventDefault();
release_menu();
- $("#other-text").show();
+ $(".other").remove();
$("body").addClass('with-other-text');
$.ajax($(this).attr('data-other'), {
success: function(text) {
- $("#other-text-body").html(text);
+ insertOtherText(text);
$("#other-text-waiter").hide();
- $("#other-text-body").show();
- loaded_text($("#other-text-body"));
+ loaded_text($(".other"));
}
});
_paq.push(['trackEvent', 'html', 'other-text']);
});
+
+
+
/* Remove other version of text. */
$(".other-text-close").click(function(e) {
release_menu();
e.preventDefault();
- $("#other-text").hide();
+ $(".other").remove();
$("body").removeClass('with-other-text');
- $("#other-text-body").html("");
_paq.push(['trackEvent', 'html', 'other-text-close']);
});
function upd_t() {
$text = $('#main-text #book-text');
+ if (!$text.length) return;
+
texttop = $text.offset().top;
$footnotes = $('#footnotes', $text);
}
var map_enabled = false;
- var marker = L.circleMarker([0,0]);
+ var marker = null;
var map = null;
function enable_map() {
+ if (!$("#reference-map").length) return;
+
$("#reference-map").show('slow');
if (map_enabled) return;
L.tileLayer('https://tile.thunderforest.com/cycle/{z}/{x}/{y}.png?apikey=a8a97f0ae5134403ac38c1a075b03e15', {
attribution: 'Maps © <a href="http://www.thunderforest.com">Thunderforest</a>, Data © <a href="http://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>'
}).addTo(map);
+ marker = L.circleMarker([0,0]);
map_enabled = true;
}
* Search terms: "jQuery Switch" and "Zepto Switch"
*/
+/* WL changes:
+ support times > 24h
+*/
+
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
s = (s && typeof s === 'number') ? s : 0;
var myTime = new Date(s * 1000),
- hour = myTime.getUTCHours(),
+ hour = myTime.getUTCHours() + (myTime.getUTCDate() - 1) * 24,
min = this.options.timeFormat.showHour ? myTime.getUTCMinutes() : myTime.getUTCMinutes() + hour * 60,
sec = this.options.timeFormat.showMin ? myTime.getUTCSeconds() : myTime.getUTCSeconds() + min * 60,
strHour = (this.options.timeFormat.padHour && hour < 10) ? "0" + hour : hour,
/*! jPlayer 2.9.2 for jQuery ~ (c) 2009-2014 Happyworm Ltd ~ MIT License */
-!function(a,b){"function"==typeof define&&define.amd?define(["jquery"],b):b("object"==typeof exports?require("jquery"):a.jQuery?a.jQuery:a.Zepto)}(this,function(a,b){a.fn.jPlayer=function(c){var d="jPlayer",e="string"==typeof c,f=Array.prototype.slice.call(arguments,1),g=this;return c=!e&&f.length?a.extend.apply(null,[!0,c].concat(f)):c,e&&"_"===c.charAt(0)?g:(this.each(e?function(){var e=a(this).data(d),h=e&&a.isFunction(e[c])?e[c].apply(e,f):e;return h!==e&&h!==b?(g=h,!1):void 0}:function(){var b=a(this).data(d);b?b.option(c||{}):a(this).data(d,new a.jPlayer(c,this))}),g)},a.jPlayer=function(b,c){if(arguments.length){this.element=a(c),this.options=a.extend(!0,{},this.options,b);var d=this;this.element.bind("remove.jPlayer",function(){d.destroy()}),this._init()}},"function"!=typeof a.fn.stop&&(a.fn.stop=function(){}),a.jPlayer.emulateMethods="load play pause",a.jPlayer.emulateStatus="src readyState networkState currentTime duration paused ended playbackRate",a.jPlayer.emulateOptions="muted volume",a.jPlayer.reservedEvent="ready flashreset resize repeat error warning",a.jPlayer.event={},a.each(["ready","setmedia","flashreset","resize","repeat","click","error","warning","loadstart","progress","suspend","abort","emptied","stalled","play","pause","loadedmetadata","loadeddata","waiting","playing","canplay","canplaythrough","seeking","seeked","timeupdate","ended","ratechange","durationchange","volumechange"],function(){a.jPlayer.event[this]="jPlayer_"+this}),a.jPlayer.htmlEvent=["loadstart","abort","emptied","stalled","loadedmetadata","canplay","canplaythrough"],a.jPlayer.pause=function(){a.jPlayer.prototype.destroyRemoved(),a.each(a.jPlayer.prototype.instances,function(a,b){b.data("jPlayer").status.srcSet&&b.jPlayer("pause")})},a.jPlayer.timeFormat={showHour:!1,showMin:!0,showSec:!0,padHour:!1,padMin:!0,padSec:!0,sepHour:":",sepMin:":",sepSec:""};var c=function(){this.init()};c.prototype={init:function(){this.options={timeFormat:a.jPlayer.timeFormat}},time:function(a){a=a&&"number"==typeof a?a:0;var b=new Date(1e3*a),c=b.getUTCHours(),d=this.options.timeFormat.showHour?b.getUTCMinutes():b.getUTCMinutes()+60*c,e=this.options.timeFormat.showMin?b.getUTCSeconds():b.getUTCSeconds()+60*d,f=this.options.timeFormat.padHour&&10>c?"0"+c:c,g=this.options.timeFormat.padMin&&10>d?"0"+d:d,h=this.options.timeFormat.padSec&&10>e?"0"+e:e,i="";return i+=this.options.timeFormat.showHour?f+this.options.timeFormat.sepHour:"",i+=this.options.timeFormat.showMin?g+this.options.timeFormat.sepMin:"",i+=this.options.timeFormat.showSec?h+this.options.timeFormat.sepSec:""}};var d=new c;a.jPlayer.convertTime=function(a){return d.time(a)},a.jPlayer.uaBrowser=function(a){var b=a.toLowerCase(),c=/(webkit)[ \/]([\w.]+)/,d=/(opera)(?:.*version)?[ \/]([\w.]+)/,e=/(msie) ([\w.]+)/,f=/(mozilla)(?:.*? rv:([\w.]+))?/,g=c.exec(b)||d.exec(b)||e.exec(b)||b.indexOf("compatible")<0&&f.exec(b)||[];return{browser:g[1]||"",version:g[2]||"0"}},a.jPlayer.uaPlatform=function(a){var b=a.toLowerCase(),c=/(ipad|iphone|ipod|android|blackberry|playbook|windows ce|webos)/,d=/(ipad|playbook)/,e=/(android)/,f=/(mobile)/,g=c.exec(b)||[],h=d.exec(b)||!f.exec(b)&&e.exec(b)||[];return g[1]&&(g[1]=g[1].replace(/\s/g,"_")),{platform:g[1]||"",tablet:h[1]||""}},a.jPlayer.browser={},a.jPlayer.platform={};var e=a.jPlayer.uaBrowser(navigator.userAgent);e.browser&&(a.jPlayer.browser[e.browser]=!0,a.jPlayer.browser.version=e.version);var f=a.jPlayer.uaPlatform(navigator.userAgent);f.platform&&(a.jPlayer.platform[f.platform]=!0,a.jPlayer.platform.mobile=!f.tablet,a.jPlayer.platform.tablet=!!f.tablet),a.jPlayer.getDocMode=function(){var b;return a.jPlayer.browser.msie&&(document.documentMode?b=document.documentMode:(b=5,document.compatMode&&"CSS1Compat"===document.compatMode&&(b=7))),b},a.jPlayer.browser.documentMode=a.jPlayer.getDocMode(),a.jPlayer.nativeFeatures={init:function(){var a,b,c,d=document,e=d.createElement("video"),f={w3c:["fullscreenEnabled","fullscreenElement","requestFullscreen","exitFullscreen","fullscreenchange","fullscreenerror"],moz:["mozFullScreenEnabled","mozFullScreenElement","mozRequestFullScreen","mozCancelFullScreen","mozfullscreenchange","mozfullscreenerror"],webkit:["","webkitCurrentFullScreenElement","webkitRequestFullScreen","webkitCancelFullScreen","webkitfullscreenchange",""],webkitVideo:["webkitSupportsFullscreen","webkitDisplayingFullscreen","webkitEnterFullscreen","webkitExitFullscreen","",""],ms:["","msFullscreenElement","msRequestFullscreen","msExitFullscreen","MSFullscreenChange","MSFullscreenError"]},g=["w3c","moz","webkit","webkitVideo","ms"];for(this.fullscreen=a={support:{w3c:!!d[f.w3c[0]],moz:!!d[f.moz[0]],webkit:"function"==typeof d[f.webkit[3]],webkitVideo:"function"==typeof e[f.webkitVideo[2]],ms:"function"==typeof e[f.ms[2]]},used:{}},b=0,c=g.length;c>b;b++){var h=g[b];if(a.support[h]){a.spec=h,a.used[h]=!0;break}}if(a.spec){var i=f[a.spec];a.api={fullscreenEnabled:!0,fullscreenElement:function(a){return a=a?a:d,a[i[1]]},requestFullscreen:function(a){return a[i[2]]()},exitFullscreen:function(a){return a=a?a:d,a[i[3]]()}},a.event={fullscreenchange:i[4],fullscreenerror:i[5]}}else a.api={fullscreenEnabled:!1,fullscreenElement:function(){return null},requestFullscreen:function(){},exitFullscreen:function(){}},a.event={}}},a.jPlayer.nativeFeatures.init(),a.jPlayer.focus=null,a.jPlayer.keyIgnoreElementNames="A INPUT TEXTAREA SELECT BUTTON";var g=function(b){var c,d=a.jPlayer.focus;d&&(a.each(a.jPlayer.keyIgnoreElementNames.split(/\s+/g),function(a,d){return b.target.nodeName.toUpperCase()===d.toUpperCase()?(c=!0,!1):void 0}),c||a.each(d.options.keyBindings,function(c,e){return e&&a.isFunction(e.fn)&&("number"==typeof e.key&&b.which===e.key||"string"==typeof e.key&&b.key===e.key)?(b.preventDefault(),e.fn(d),!1):void 0}))};a.jPlayer.keys=function(b){var c="keydown.jPlayer";a(document.documentElement).unbind(c),b&&a(document.documentElement).bind(c,g)},a.jPlayer.keys(!0),a.jPlayer.prototype={count:0,version:{script:"2.9.2",needFlash:"2.9.0",flash:"unknown"},options:{swfPath:"js",solution:"html, flash",supplied:"mp3",auroraFormats:"wav",preload:"metadata",volume:.8,muted:!1,remainingDuration:!1,toggleDuration:!1,captureDuration:!0,playbackRate:1,defaultPlaybackRate:1,minPlaybackRate:.5,maxPlaybackRate:4,wmode:"opaque",backgroundColor:"#000000",cssSelectorAncestor:"#jp_container_1",cssSelector:{videoPlay:".jp-video-play",play:".jp-play",pause:".jp-pause",stop:".jp-stop",seekBar:".jp-seek-bar",playBar:".jp-play-bar",mute:".jp-mute",unmute:".jp-unmute",volumeBar:".jp-volume-bar",volumeBarValue:".jp-volume-bar-value",volumeMax:".jp-volume-max",playbackRateBar:".jp-playback-rate-bar",playbackRateBarValue:".jp-playback-rate-bar-value",currentTime:".jp-current-time",duration:".jp-duration",title:".jp-title",fullScreen:".jp-full-screen",restoreScreen:".jp-restore-screen",repeat:".jp-repeat",repeatOff:".jp-repeat-off",gui:".jp-gui",noSolution:".jp-no-solution"},stateClass:{playing:"jp-state-playing",seeking:"jp-state-seeking",muted:"jp-state-muted",looped:"jp-state-looped",fullScreen:"jp-state-full-screen",noVolume:"jp-state-no-volume"},useStateClassSkin:!1,autoBlur:!0,smoothPlayBar:!1,fullScreen:!1,fullWindow:!1,autohide:{restored:!1,full:!0,fadeIn:200,fadeOut:600,hold:1e3},loop:!1,repeat:function(b){b.jPlayer.options.loop?a(this).unbind(".jPlayerRepeat").bind(a.jPlayer.event.ended+".jPlayer.jPlayerRepeat",function(){a(this).jPlayer("play")}):a(this).unbind(".jPlayerRepeat")},nativeVideoControls:{},noFullWindow:{msie:/msie [0-6]\./,ipad:/ipad.*?os [0-4]\./,iphone:/iphone/,ipod:/ipod/,android_pad:/android [0-3]\.(?!.*?mobile)/,android_phone:/(?=.*android)(?!.*chrome)(?=.*mobile)/,blackberry:/blackberry/,windows_ce:/windows ce/,iemobile:/iemobile/,webos:/webos/},noVolume:{ipad:/ipad/,iphone:/iphone/,ipod:/ipod/,android_pad:/android(?!.*?mobile)/,android_phone:/android.*?mobile/,blackberry:/blackberry/,windows_ce:/windows ce/,iemobile:/iemobile/,webos:/webos/,playbook:/playbook/},timeFormat:{},keyEnabled:!1,audioFullScreen:!1,keyBindings:{play:{key:80,fn:function(a){a.status.paused?a.play():a.pause()}},fullScreen:{key:70,fn:function(a){(a.status.video||a.options.audioFullScreen)&&a._setOption("fullScreen",!a.options.fullScreen)}},muted:{key:77,fn:function(a){a._muted(!a.options.muted)}},volumeUp:{key:190,fn:function(a){a.volume(a.options.volume+.1)}},volumeDown:{key:188,fn:function(a){a.volume(a.options.volume-.1)}},loop:{key:76,fn:function(a){a._loop(!a.options.loop)}}},verticalVolume:!1,verticalPlaybackRate:!1,globalVolume:!1,idPrefix:"jp",noConflict:"jQuery",emulateHtml:!1,consoleAlerts:!0,errorAlerts:!1,warningAlerts:!1},optionsAudio:{size:{width:"0px",height:"0px",cssClass:""},sizeFull:{width:"0px",height:"0px",cssClass:""}},optionsVideo:{size:{width:"480px",height:"270px",cssClass:"jp-video-270p"},sizeFull:{width:"100%",height:"100%",cssClass:"jp-video-full"}},instances:{},status:{src:"",media:{},paused:!0,format:{},formatType:"",waitForPlay:!0,waitForLoad:!0,srcSet:!1,video:!1,seekPercent:0,currentPercentRelative:0,currentPercentAbsolute:0,currentTime:0,duration:0,remaining:0,videoWidth:0,videoHeight:0,readyState:0,networkState:0,playbackRate:1,ended:0},internal:{ready:!1},solution:{html:!0,aurora:!0,flash:!0},format:{mp3:{codec:"audio/mpeg",flashCanPlay:!0,media:"audio"},m4a:{codec:'audio/mp4; codecs="mp4a.40.2"',flashCanPlay:!0,media:"audio"},m3u8a:{codec:'application/vnd.apple.mpegurl; codecs="mp4a.40.2"',flashCanPlay:!1,media:"audio"},m3ua:{codec:"audio/mpegurl",flashCanPlay:!1,media:"audio"},oga:{codec:'audio/ogg; codecs="vorbis, opus"',flashCanPlay:!1,media:"audio"},flac:{codec:"audio/x-flac",flashCanPlay:!1,media:"audio"},wav:{codec:'audio/wav; codecs="1"',flashCanPlay:!1,media:"audio"},webma:{codec:'audio/webm; codecs="vorbis"',flashCanPlay:!1,media:"audio"},fla:{codec:"audio/x-flv",flashCanPlay:!0,media:"audio"},rtmpa:{codec:'audio/rtmp; codecs="rtmp"',flashCanPlay:!0,media:"audio"},m4v:{codec:'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',flashCanPlay:!0,media:"video"},m3u8v:{codec:'application/vnd.apple.mpegurl; codecs="avc1.42E01E, mp4a.40.2"',flashCanPlay:!1,media:"video"},m3uv:{codec:"audio/mpegurl",flashCanPlay:!1,media:"video"},ogv:{codec:'video/ogg; codecs="theora, vorbis"',flashCanPlay:!1,media:"video"},webmv:{codec:'video/webm; codecs="vorbis, vp8"',flashCanPlay:!1,media:"video"},flv:{codec:"video/x-flv",flashCanPlay:!0,media:"video"},rtmpv:{codec:'video/rtmp; codecs="rtmp"',flashCanPlay:!0,media:"video"}},_init:function(){var c=this;if(this.element.empty(),this.status=a.extend({},this.status),this.internal=a.extend({},this.internal),this.options.timeFormat=a.extend({},a.jPlayer.timeFormat,this.options.timeFormat),this.internal.cmdsIgnored=a.jPlayer.platform.ipad||a.jPlayer.platform.iphone||a.jPlayer.platform.ipod,this.internal.domNode=this.element.get(0),this.options.keyEnabled&&!a.jPlayer.focus&&(a.jPlayer.focus=this),this.androidFix={setMedia:!1,play:!1,pause:!1,time:0/0},a.jPlayer.platform.android&&(this.options.preload="auto"!==this.options.preload?"metadata":"auto"),this.formats=[],this.solutions=[],this.require={},this.htmlElement={},this.html={},this.html.audio={},this.html.video={},this.aurora={},this.aurora.formats=[],this.aurora.properties=[],this.flash={},this.css={},this.css.cs={},this.css.jq={},this.ancestorJq=[],this.options.volume=this._limitValue(this.options.volume,0,1),a.each(this.options.supplied.toLowerCase().split(","),function(b,d){var e=d.replace(/^\s+|\s+$/g,"");if(c.format[e]){var f=!1;a.each(c.formats,function(a,b){return e===b?(f=!0,!1):void 0}),f||c.formats.push(e)}}),a.each(this.options.solution.toLowerCase().split(","),function(b,d){var e=d.replace(/^\s+|\s+$/g,"");if(c.solution[e]){var f=!1;a.each(c.solutions,function(a,b){return e===b?(f=!0,!1):void 0}),f||c.solutions.push(e)}}),a.each(this.options.auroraFormats.toLowerCase().split(","),function(b,d){var e=d.replace(/^\s+|\s+$/g,"");if(c.format[e]){var f=!1;a.each(c.aurora.formats,function(a,b){return e===b?(f=!0,!1):void 0}),f||c.aurora.formats.push(e)}}),this.internal.instance="jp_"+this.count,this.instances[this.internal.instance]=this.element,this.element.attr("id")||this.element.attr("id",this.options.idPrefix+"_jplayer_"+this.count),this.internal.self=a.extend({},{id:this.element.attr("id"),jq:this.element}),this.internal.audio=a.extend({},{id:this.options.idPrefix+"_audio_"+this.count,jq:b}),this.internal.video=a.extend({},{id:this.options.idPrefix+"_video_"+this.count,jq:b}),this.internal.flash=a.extend({},{id:this.options.idPrefix+"_flash_"+this.count,jq:b,swf:this.options.swfPath+(".swf"!==this.options.swfPath.toLowerCase().slice(-4)?(this.options.swfPath&&"/"!==this.options.swfPath.slice(-1)?"/":"")+"jquery.jplayer.swf":"")}),this.internal.poster=a.extend({},{id:this.options.idPrefix+"_poster_"+this.count,jq:b}),a.each(a.jPlayer.event,function(a,d){c.options[a]!==b&&(c.element.bind(d+".jPlayer",c.options[a]),c.options[a]=b)}),this.require.audio=!1,this.require.video=!1,a.each(this.formats,function(a,b){c.require[c.format[b].media]=!0}),this.options=this.require.video?a.extend(!0,{},this.optionsVideo,this.options):a.extend(!0,{},this.optionsAudio,this.options),this._setSize(),this.status.nativeVideoControls=this._uaBlocklist(this.options.nativeVideoControls),this.status.noFullWindow=this._uaBlocklist(this.options.noFullWindow),this.status.noVolume=this._uaBlocklist(this.options.noVolume),a.jPlayer.nativeFeatures.fullscreen.api.fullscreenEnabled&&this._fullscreenAddEventListeners(),this._restrictNativeVideoControls(),this.htmlElement.poster=document.createElement("img"),this.htmlElement.poster.id=this.internal.poster.id,this.htmlElement.poster.onload=function(){(!c.status.video||c.status.waitForPlay)&&c.internal.poster.jq.show()},this.element.append(this.htmlElement.poster),this.internal.poster.jq=a("#"+this.internal.poster.id),this.internal.poster.jq.css({width:this.status.width,height:this.status.height}),this.internal.poster.jq.hide(),this.internal.poster.jq.bind("click.jPlayer",function(){c._trigger(a.jPlayer.event.click)}),this.html.audio.available=!1,this.require.audio&&(this.htmlElement.audio=document.createElement("audio"),this.htmlElement.audio.id=this.internal.audio.id,this.html.audio.available=!!this.htmlElement.audio.canPlayType&&this._testCanPlayType(this.htmlElement.audio)),this.html.video.available=!1,this.require.video&&(this.htmlElement.video=document.createElement("video"),this.htmlElement.video.id=this.internal.video.id,this.html.video.available=!!this.htmlElement.video.canPlayType&&this._testCanPlayType(this.htmlElement.video)),this.flash.available=this._checkForFlash(10.1),this.html.canPlay={},this.aurora.canPlay={},this.flash.canPlay={},a.each(this.formats,function(b,d){c.html.canPlay[d]=c.html[c.format[d].media].available&&""!==c.htmlElement[c.format[d].media].canPlayType(c.format[d].codec),c.aurora.canPlay[d]=a.inArray(d,c.aurora.formats)>-1,c.flash.canPlay[d]=c.format[d].flashCanPlay&&c.flash.available}),this.html.desired=!1,this.aurora.desired=!1,this.flash.desired=!1,a.each(this.solutions,function(b,d){if(0===b)c[d].desired=!0;else{var e=!1,f=!1;a.each(c.formats,function(a,b){c[c.solutions[0]].canPlay[b]&&("video"===c.format[b].media?f=!0:e=!0)}),c[d].desired=c.require.audio&&!e||c.require.video&&!f}}),this.html.support={},this.aurora.support={},this.flash.support={},a.each(this.formats,function(a,b){c.html.support[b]=c.html.canPlay[b]&&c.html.desired,c.aurora.support[b]=c.aurora.canPlay[b]&&c.aurora.desired,c.flash.support[b]=c.flash.canPlay[b]&&c.flash.desired}),this.html.used=!1,this.aurora.used=!1,this.flash.used=!1,a.each(this.solutions,function(b,d){a.each(c.formats,function(a,b){return c[d].support[b]?(c[d].used=!0,!1):void 0})}),this._resetActive(),this._resetGate(),this._cssSelectorAncestor(this.options.cssSelectorAncestor),this.html.used||this.aurora.used||this.flash.used?this.css.jq.noSolution.length&&this.css.jq.noSolution.hide():(this._error({type:a.jPlayer.error.NO_SOLUTION,context:"{solution:'"+this.options.solution+"', supplied:'"+this.options.supplied+"'}",message:a.jPlayer.errorMsg.NO_SOLUTION,hint:a.jPlayer.errorHint.NO_SOLUTION}),this.css.jq.noSolution.length&&this.css.jq.noSolution.show()),this.flash.used){var d,e="jQuery="+encodeURI(this.options.noConflict)+"&id="+encodeURI(this.internal.self.id)+"&vol="+this.options.volume+"&muted="+this.options.muted;if(a.jPlayer.browser.msie&&(Number(a.jPlayer.browser.version)<9||a.jPlayer.browser.documentMode<9)){var f='<object id="'+this.internal.flash.id+'" classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" width="0" height="0" tabindex="-1"></object>',g=['<param name="movie" value="'+this.internal.flash.swf+'" />','<param name="FlashVars" value="'+e+'" />','<param name="allowScriptAccess" value="always" />','<param name="bgcolor" value="'+this.options.backgroundColor+'" />','<param name="wmode" value="'+this.options.wmode+'" />'];d=document.createElement(f);for(var h=0;h<g.length;h++)d.appendChild(document.createElement(g[h]))}else{var i=function(a,b,c){var d=document.createElement("param");d.setAttribute("name",b),d.setAttribute("value",c),a.appendChild(d)};d=document.createElement("object"),d.setAttribute("id",this.internal.flash.id),d.setAttribute("name",this.internal.flash.id),d.setAttribute("data",this.internal.flash.swf),d.setAttribute("type","application/x-shockwave-flash"),d.setAttribute("width","1"),d.setAttribute("height","1"),d.setAttribute("tabindex","-1"),i(d,"flashvars",e),i(d,"allowscriptaccess","always"),i(d,"bgcolor",this.options.backgroundColor),i(d,"wmode",this.options.wmode)}this.element.append(d),this.internal.flash.jq=a(d)}this.status.playbackRateEnabled=this.html.used&&!this.flash.used?this._testPlaybackRate("audio"):!1,this._updatePlaybackRate(),this.html.used&&(this.html.audio.available&&(this._addHtmlEventListeners(this.htmlElement.audio,this.html.audio),this.element.append(this.htmlElement.audio),this.internal.audio.jq=a("#"+this.internal.audio.id)),this.html.video.available&&(this._addHtmlEventListeners(this.htmlElement.video,this.html.video),this.element.append(this.htmlElement.video),this.internal.video.jq=a("#"+this.internal.video.id),this.internal.video.jq.css(this.status.nativeVideoControls?{width:this.status.width,height:this.status.height}:{width:"0px",height:"0px"}),this.internal.video.jq.bind("click.jPlayer",function(){c._trigger(a.jPlayer.event.click)}))),this.aurora.used,this.options.emulateHtml&&this._emulateHtmlBridge(),!this.html.used&&!this.aurora.used||this.flash.used||setTimeout(function(){c.internal.ready=!0,c.version.flash="n/a",c._trigger(a.jPlayer.event.repeat),c._trigger(a.jPlayer.event.ready)},100),this._updateNativeVideoControls(),this.css.jq.videoPlay.length&&this.css.jq.videoPlay.hide(),a.jPlayer.prototype.count++},destroy:function(){this.clearMedia(),this._removeUiClass(),this.css.jq.currentTime.length&&this.css.jq.currentTime.text(""),this.css.jq.duration.length&&this.css.jq.duration.text(""),a.each(this.css.jq,function(a,b){b.length&&b.unbind(".jPlayer")}),this.internal.poster.jq.unbind(".jPlayer"),this.internal.video.jq&&this.internal.video.jq.unbind(".jPlayer"),this._fullscreenRemoveEventListeners(),this===a.jPlayer.focus&&(a.jPlayer.focus=null),this.options.emulateHtml&&this._destroyHtmlBridge(),this.element.removeData("jPlayer"),this.element.unbind(".jPlayer"),this.element.empty(),delete this.instances[this.internal.instance]},destroyRemoved:function(){var b=this;a.each(this.instances,function(a,c){b.element!==c&&(c.data("jPlayer")||(c.jPlayer("destroy"),delete b.instances[a]))})},enable:function(){},disable:function(){},_testCanPlayType:function(a){try{return a.canPlayType(this.format.mp3.codec),!0}catch(b){return!1}},_testPlaybackRate:function(a){var b,c=.5;a="string"==typeof a?a:"audio",b=document.createElement(a);try{return"playbackRate"in b?(b.playbackRate=c,b.playbackRate===c):!1}catch(d){return!1}},_uaBlocklist:function(b){var c=navigator.userAgent.toLowerCase(),d=!1;return a.each(b,function(a,b){return b&&b.test(c)?(d=!0,!1):void 0}),d},_restrictNativeVideoControls:function(){this.require.audio&&this.status.nativeVideoControls&&(this.status.nativeVideoControls=!1,this.status.noFullWindow=!0)},_updateNativeVideoControls:function(){this.html.video.available&&this.html.used&&(this.htmlElement.video.controls=this.status.nativeVideoControls,this._updateAutohide(),this.status.nativeVideoControls&&this.require.video?(this.internal.poster.jq.hide(),this.internal.video.jq.css({width:this.status.width,height:this.status.height})):this.status.waitForPlay&&this.status.video&&(this.internal.poster.jq.show(),this.internal.video.jq.css({width:"0px",height:"0px"})))},_addHtmlEventListeners:function(b,c){var d=this;b.preload=this.options.preload,b.muted=this.options.muted,b.volume=this.options.volume,this.status.playbackRateEnabled&&(b.defaultPlaybackRate=this.options.defaultPlaybackRate,b.playbackRate=this.options.playbackRate),b.addEventListener("progress",function(){c.gate&&(d.internal.cmdsIgnored&&this.readyState>0&&(d.internal.cmdsIgnored=!1),d._getHtmlStatus(b),d._updateInterface(),d._trigger(a.jPlayer.event.progress))},!1),b.addEventListener("loadeddata",function(){c.gate&&(d.androidFix.setMedia=!1,d.androidFix.play&&(d.androidFix.play=!1,d.play(d.androidFix.time)),d.androidFix.pause&&(d.androidFix.pause=!1,d.pause(d.androidFix.time)),d._trigger(a.jPlayer.event.loadeddata))},!1),b.addEventListener("timeupdate",function(){c.gate&&(d._getHtmlStatus(b),d._updateInterface(),d._trigger(a.jPlayer.event.timeupdate))},!1),b.addEventListener("durationchange",function(){c.gate&&(d._getHtmlStatus(b),d._updateInterface(),d._trigger(a.jPlayer.event.durationchange))},!1),b.addEventListener("play",function(){c.gate&&(d._updateButtons(!0),d._html_checkWaitForPlay(),d._trigger(a.jPlayer.event.play))},!1),b.addEventListener("playing",function(){c.gate&&(d._updateButtons(!0),d._seeked(),d._trigger(a.jPlayer.event.playing))},!1),b.addEventListener("pause",function(){c.gate&&(d._updateButtons(!1),d._trigger(a.jPlayer.event.pause))},!1),b.addEventListener("waiting",function(){c.gate&&(d._seeking(),d._trigger(a.jPlayer.event.waiting))},!1),b.addEventListener("seeking",function(){c.gate&&(d._seeking(),d._trigger(a.jPlayer.event.seeking))},!1),b.addEventListener("seeked",function(){c.gate&&(d._seeked(),d._trigger(a.jPlayer.event.seeked))},!1),b.addEventListener("volumechange",function(){c.gate&&(d.options.volume=b.volume,d.options.muted=b.muted,d._updateMute(),d._updateVolume(),d._trigger(a.jPlayer.event.volumechange))},!1),b.addEventListener("ratechange",function(){c.gate&&(d.options.defaultPlaybackRate=b.defaultPlaybackRate,d.options.playbackRate=b.playbackRate,d._updatePlaybackRate(),d._trigger(a.jPlayer.event.ratechange))},!1),b.addEventListener("suspend",function(){c.gate&&(d._seeked(),d._trigger(a.jPlayer.event.suspend))},!1),b.addEventListener("ended",function(){c.gate&&(a.jPlayer.browser.webkit||(d.htmlElement.media.currentTime=0),d.htmlElement.media.pause(),d._updateButtons(!1),d._getHtmlStatus(b,!0),d._updateInterface(),d._trigger(a.jPlayer.event.ended))},!1),b.addEventListener("error",function(){c.gate&&(d._updateButtons(!1),d._seeked(),d.status.srcSet&&(clearTimeout(d.internal.htmlDlyCmdId),d.status.waitForLoad=!0,d.status.waitForPlay=!0,d.status.video&&!d.status.nativeVideoControls&&d.internal.video.jq.css({width:"0px",height:"0px"}),d._validString(d.status.media.poster)&&!d.status.nativeVideoControls&&d.internal.poster.jq.show(),d.css.jq.videoPlay.length&&d.css.jq.videoPlay.show(),d._error({type:a.jPlayer.error.URL,context:d.status.src,message:a.jPlayer.errorMsg.URL,hint:a.jPlayer.errorHint.URL})))},!1),a.each(a.jPlayer.htmlEvent,function(e,f){b.addEventListener(this,function(){c.gate&&d._trigger(a.jPlayer.event[f])},!1)})},_addAuroraEventListeners:function(b,c){var d=this;b.volume=100*this.options.volume,b.on("progress",function(){c.gate&&(d.internal.cmdsIgnored&&this.readyState>0&&(d.internal.cmdsIgnored=!1),d._getAuroraStatus(b),d._updateInterface(),d._trigger(a.jPlayer.event.progress),b.duration>0&&d._trigger(a.jPlayer.event.timeupdate))},!1),b.on("ready",function(){c.gate&&d._trigger(a.jPlayer.event.loadeddata)},!1),b.on("duration",function(){c.gate&&(d._getAuroraStatus(b),d._updateInterface(),d._trigger(a.jPlayer.event.durationchange))},!1),b.on("end",function(){c.gate&&(d._updateButtons(!1),d._getAuroraStatus(b,!0),d._updateInterface(),d._trigger(a.jPlayer.event.ended))},!1),b.on("error",function(){c.gate&&(d._updateButtons(!1),d._seeked(),d.status.srcSet&&(d.status.waitForLoad=!0,d.status.waitForPlay=!0,d.status.video&&!d.status.nativeVideoControls&&d.internal.video.jq.css({width:"0px",height:"0px"}),d._validString(d.status.media.poster)&&!d.status.nativeVideoControls&&d.internal.poster.jq.show(),d.css.jq.videoPlay.length&&d.css.jq.videoPlay.show(),d._error({type:a.jPlayer.error.URL,context:d.status.src,message:a.jPlayer.errorMsg.URL,hint:a.jPlayer.errorHint.URL})))},!1)},_getHtmlStatus:function(a,b){var c=0,d=0,e=0,f=0;isFinite(a.duration)&&(this.status.duration=a.duration),c=a.currentTime,d=this.status.duration>0?100*c/this.status.duration:0,"object"==typeof a.seekable&&a.seekable.length>0?(e=this.status.duration>0?100*a.seekable.end(a.seekable.length-1)/this.status.duration:100,f=this.status.duration>0?100*a.currentTime/a.seekable.end(a.seekable.length-1):0):(e=100,f=d),b&&(c=0,f=0,d=0),this.status.seekPercent=e,this.status.currentPercentRelative=f,this.status.currentPercentAbsolute=d,this.status.currentTime=c,this.status.remaining=this.status.duration-this.status.currentTime,this.status.videoWidth=a.videoWidth,this.status.videoHeight=a.videoHeight,this.status.readyState=a.readyState,this.status.networkState=a.networkState,this.status.playbackRate=a.playbackRate,this.status.ended=a.ended},_getAuroraStatus:function(a,b){var c=0,d=0,e=0,f=0;this.status.duration=a.duration/1e3,c=a.currentTime/1e3,d=this.status.duration>0?100*c/this.status.duration:0,a.buffered>0?(e=this.status.duration>0?a.buffered*this.status.duration/this.status.duration:100,f=this.status.duration>0?c/(a.buffered*this.status.duration):0):(e=100,f=d),b&&(c=0,f=0,d=0),this.status.seekPercent=e,this.status.currentPercentRelative=f,this.status.currentPercentAbsolute=d,this.status.currentTime=c,this.status.remaining=this.status.duration-this.status.currentTime,this.status.readyState=4,this.status.networkState=0,this.status.playbackRate=1,this.status.ended=!1},_resetStatus:function(){this.status=a.extend({},this.status,a.jPlayer.prototype.status)},_trigger:function(b,c,d){var e=a.Event(b);e.jPlayer={},e.jPlayer.version=a.extend({},this.version),e.jPlayer.options=a.extend(!0,{},this.options),e.jPlayer.status=a.extend(!0,{},this.status),e.jPlayer.html=a.extend(!0,{},this.html),e.jPlayer.aurora=a.extend(!0,{},this.aurora),e.jPlayer.flash=a.extend(!0,{},this.flash),c&&(e.jPlayer.error=a.extend({},c)),d&&(e.jPlayer.warning=a.extend({},d)),this.element.trigger(e)},jPlayerFlashEvent:function(b,c){if(b===a.jPlayer.event.ready)if(this.internal.ready){if(this.flash.gate){if(this.status.srcSet){var d=this.status.currentTime,e=this.status.paused;this.setMedia(this.status.media),this.volumeWorker(this.options.volume),d>0&&(e?this.pause(d):this.play(d))}this._trigger(a.jPlayer.event.flashreset)}}else this.internal.ready=!0,this.internal.flash.jq.css({width:"0px",height:"0px"}),this.version.flash=c.version,this.version.needFlash!==this.version.flash&&this._error({type:a.jPlayer.error.VERSION,context:this.version.flash,message:a.jPlayer.errorMsg.VERSION+this.version.flash,hint:a.jPlayer.errorHint.VERSION}),this._trigger(a.jPlayer.event.repeat),this._trigger(b);if(this.flash.gate)switch(b){case a.jPlayer.event.progress:this._getFlashStatus(c),this._updateInterface(),this._trigger(b);break;case a.jPlayer.event.timeupdate:this._getFlashStatus(c),this._updateInterface(),this._trigger(b);break;case a.jPlayer.event.play:this._seeked(),this._updateButtons(!0),this._trigger(b);break;case a.jPlayer.event.pause:this._updateButtons(!1),this._trigger(b);break;case a.jPlayer.event.ended:this._updateButtons(!1),this._trigger(b);break;case a.jPlayer.event.click:this._trigger(b);break;case a.jPlayer.event.error:this.status.waitForLoad=!0,this.status.waitForPlay=!0,this.status.video&&this.internal.flash.jq.css({width:"0px",height:"0px"}),this._validString(this.status.media.poster)&&this.internal.poster.jq.show(),this.css.jq.videoPlay.length&&this.status.video&&this.css.jq.videoPlay.show(),this.status.video?this._flash_setVideo(this.status.media):this._flash_setAudio(this.status.media),this._updateButtons(!1),this._error({type:a.jPlayer.error.URL,context:c.src,message:a.jPlayer.errorMsg.URL,hint:a.jPlayer.errorHint.URL});break;case a.jPlayer.event.seeking:this._seeking(),this._trigger(b);break;case a.jPlayer.event.seeked:this._seeked(),this._trigger(b);break;case a.jPlayer.event.ready:break;default:this._trigger(b)}return!1},_getFlashStatus:function(a){this.status.seekPercent=a.seekPercent,this.status.currentPercentRelative=a.currentPercentRelative,this.status.currentPercentAbsolute=a.currentPercentAbsolute,this.status.currentTime=a.currentTime,this.status.duration=a.duration,this.status.remaining=a.duration-a.currentTime,this.status.videoWidth=a.videoWidth,this.status.videoHeight=a.videoHeight,this.status.readyState=4,this.status.networkState=0,this.status.playbackRate=1,this.status.ended=!1},_updateButtons:function(a){a===b?a=!this.status.paused:this.status.paused=!a,a?this.addStateClass("playing"):this.removeStateClass("playing"),!this.status.noFullWindow&&this.options.fullWindow?this.addStateClass("fullScreen"):this.removeStateClass("fullScreen"),this.options.loop?this.addStateClass("looped"):this.removeStateClass("looped"),this.css.jq.play.length&&this.css.jq.pause.length&&(a?(this.css.jq.play.hide(),this.css.jq.pause.show()):(this.css.jq.play.show(),this.css.jq.pause.hide())),this.css.jq.restoreScreen.length&&this.css.jq.fullScreen.length&&(this.status.noFullWindow?(this.css.jq.fullScreen.hide(),this.css.jq.restoreScreen.hide()):this.options.fullWindow?(this.css.jq.fullScreen.hide(),this.css.jq.restoreScreen.show()):(this.css.jq.fullScreen.show(),this.css.jq.restoreScreen.hide())),this.css.jq.repeat.length&&this.css.jq.repeatOff.length&&(this.options.loop?(this.css.jq.repeat.hide(),this.css.jq.repeatOff.show()):(this.css.jq.repeat.show(),this.css.jq.repeatOff.hide()))},_updateInterface:function(){this.css.jq.seekBar.length&&this.css.jq.seekBar.width(this.status.seekPercent+"%"),this.css.jq.playBar.length&&(this.options.smoothPlayBar?this.css.jq.playBar.stop().animate({width:this.status.currentPercentAbsolute+"%"},250,"linear"):this.css.jq.playBar.width(this.status.currentPercentRelative+"%"));var a="";this.css.jq.currentTime.length&&(a=this._convertTime(this.status.currentTime),a!==this.css.jq.currentTime.text()&&this.css.jq.currentTime.text(this._convertTime(this.status.currentTime)));var b="",c=this.status.duration,d=this.status.remaining;this.css.jq.duration.length&&("string"==typeof this.status.media.duration?b=this.status.media.duration:("number"==typeof this.status.media.duration&&(c=this.status.media.duration,d=c-this.status.currentTime),b=this.options.remainingDuration?(d>0?"-":"")+this._convertTime(d):this._convertTime(c)),b!==this.css.jq.duration.text()&&this.css.jq.duration.text(b))},_convertTime:c.prototype.time,_seeking:function(){this.css.jq.seekBar.length&&this.css.jq.seekBar.addClass("jp-seeking-bg"),this.addStateClass("seeking")},_seeked:function(){this.css.jq.seekBar.length&&this.css.jq.seekBar.removeClass("jp-seeking-bg"),this.removeStateClass("seeking")},_resetGate:function(){this.html.audio.gate=!1,this.html.video.gate=!1,this.aurora.gate=!1,this.flash.gate=!1},_resetActive:function(){this.html.active=!1,this.aurora.active=!1,this.flash.active=!1},_escapeHtml:function(a){return a.split("&").join("&").split("<").join("<").split(">").join(">").split('"').join(""")},_qualifyURL:function(a){var b=document.createElement("div");
-return b.innerHTML='<a href="'+this._escapeHtml(a)+'">x</a>',b.firstChild.href},_absoluteMediaUrls:function(b){var c=this;return a.each(b,function(a,d){d&&c.format[a]&&"data:"!==d.substr(0,5)&&(b[a]=c._qualifyURL(d))}),b},addStateClass:function(a){this.ancestorJq.length&&this.ancestorJq.addClass(this.options.stateClass[a])},removeStateClass:function(a){this.ancestorJq.length&&this.ancestorJq.removeClass(this.options.stateClass[a])},setMedia:function(b){var c=this,d=!1,e=this.status.media.poster!==b.poster;this._resetMedia(),this._resetGate(),this._resetActive(),this.androidFix.setMedia=!1,this.androidFix.play=!1,this.androidFix.pause=!1,b=this._absoluteMediaUrls(b),a.each(this.formats,function(e,f){var g="video"===c.format[f].media;return a.each(c.solutions,function(e,h){if(c[h].support[f]&&c._validString(b[f])){var i="html"===h,j="aurora"===h;return g?(i?(c.html.video.gate=!0,c._html_setVideo(b),c.html.active=!0):(c.flash.gate=!0,c._flash_setVideo(b),c.flash.active=!0),c.css.jq.videoPlay.length&&c.css.jq.videoPlay.show(),c.status.video=!0):(i?(c.html.audio.gate=!0,c._html_setAudio(b),c.html.active=!0,a.jPlayer.platform.android&&(c.androidFix.setMedia=!0)):j?(c.aurora.gate=!0,c._aurora_setAudio(b),c.aurora.active=!0):(c.flash.gate=!0,c._flash_setAudio(b),c.flash.active=!0),c.css.jq.videoPlay.length&&c.css.jq.videoPlay.hide(),c.status.video=!1),d=!0,!1}}),d?!1:void 0}),d?(this.status.nativeVideoControls&&this.html.video.gate||this._validString(b.poster)&&(e?this.htmlElement.poster.src=b.poster:this.internal.poster.jq.show()),"string"==typeof b.title&&(this.css.jq.title.length&&this.css.jq.title.html(b.title),this.htmlElement.audio&&this.htmlElement.audio.setAttribute("title",b.title),this.htmlElement.video&&this.htmlElement.video.setAttribute("title",b.title)),this.status.srcSet=!0,this.status.media=a.extend({},b),this._updateButtons(!1),this._updateInterface(),this._trigger(a.jPlayer.event.setmedia)):this._error({type:a.jPlayer.error.NO_SUPPORT,context:"{supplied:'"+this.options.supplied+"'}",message:a.jPlayer.errorMsg.NO_SUPPORT,hint:a.jPlayer.errorHint.NO_SUPPORT})},_resetMedia:function(){this._resetStatus(),this._updateButtons(!1),this._updateInterface(),this._seeked(),this.internal.poster.jq.hide(),clearTimeout(this.internal.htmlDlyCmdId),this.html.active?this._html_resetMedia():this.aurora.active?this._aurora_resetMedia():this.flash.active&&this._flash_resetMedia()},clearMedia:function(){this._resetMedia(),this.html.active?this._html_clearMedia():this.aurora.active?this._aurora_clearMedia():this.flash.active&&this._flash_clearMedia(),this._resetGate(),this._resetActive()},load:function(){this.status.srcSet?this.html.active?this._html_load():this.aurora.active?this._aurora_load():this.flash.active&&this._flash_load():this._urlNotSetError("load")},focus:function(){this.options.keyEnabled&&(a.jPlayer.focus=this)},play:function(a){var b="object"==typeof a;b&&this.options.useStateClassSkin&&!this.status.paused?this.pause(a):(a="number"==typeof a?a:0/0,this.status.srcSet?(this.focus(),this.html.active?this._html_play(a):this.aurora.active?this._aurora_play(a):this.flash.active&&this._flash_play(a)):this._urlNotSetError("play"))},videoPlay:function(){this.play()},pause:function(a){a="number"==typeof a?a:0/0,this.status.srcSet?this.html.active?this._html_pause(a):this.aurora.active?this._aurora_pause(a):this.flash.active&&this._flash_pause(a):this._urlNotSetError("pause")},tellOthers:function(b,c){var d=this,e="function"==typeof c,f=Array.prototype.slice.call(arguments);"string"==typeof b&&(e&&f.splice(1,1),a.jPlayer.prototype.destroyRemoved(),a.each(this.instances,function(){d.element!==this&&(!e||c.call(this.data("jPlayer"),d))&&this.jPlayer.apply(this,f)}))},pauseOthers:function(a){this.tellOthers("pause",function(){return this.status.srcSet},a)},stop:function(){this.status.srcSet?this.html.active?this._html_pause(0):this.aurora.active?this._aurora_pause(0):this.flash.active&&this._flash_pause(0):this._urlNotSetError("stop")},playHead:function(a){a=this._limitValue(a,0,100),this.status.srcSet?this.html.active?this._html_playHead(a):this.aurora.active?this._aurora_playHead(a):this.flash.active&&this._flash_playHead(a):this._urlNotSetError("playHead")},_muted:function(a){this.mutedWorker(a),this.options.globalVolume&&this.tellOthers("mutedWorker",function(){return this.options.globalVolume},a)},mutedWorker:function(b){this.options.muted=b,this.html.used&&this._html_setProperty("muted",b),this.aurora.used&&this._aurora_mute(b),this.flash.used&&this._flash_mute(b),this.html.video.gate||this.html.audio.gate||(this._updateMute(b),this._updateVolume(this.options.volume),this._trigger(a.jPlayer.event.volumechange))},mute:function(a){var c="object"==typeof a;c&&this.options.useStateClassSkin&&this.options.muted?this._muted(!1):(a=a===b?!0:!!a,this._muted(a))},unmute:function(a){a=a===b?!0:!!a,this._muted(!a)},_updateMute:function(a){a===b&&(a=this.options.muted),a?this.addStateClass("muted"):this.removeStateClass("muted"),this.css.jq.mute.length&&this.css.jq.unmute.length&&(this.status.noVolume?(this.css.jq.mute.hide(),this.css.jq.unmute.hide()):a?(this.css.jq.mute.hide(),this.css.jq.unmute.show()):(this.css.jq.mute.show(),this.css.jq.unmute.hide()))},volume:function(a){this.volumeWorker(a),this.options.globalVolume&&this.tellOthers("volumeWorker",function(){return this.options.globalVolume},a)},volumeWorker:function(b){b=this._limitValue(b,0,1),this.options.volume=b,this.html.used&&this._html_setProperty("volume",b),this.aurora.used&&this._aurora_volume(b),this.flash.used&&this._flash_volume(b),this.html.video.gate||this.html.audio.gate||(this._updateVolume(b),this._trigger(a.jPlayer.event.volumechange))},volumeBar:function(b){if(this.css.jq.volumeBar.length){var c=a(b.currentTarget),d=c.offset(),e=b.pageX-d.left,f=c.width(),g=c.height()-b.pageY+d.top,h=c.height();this.volume(this.options.verticalVolume?g/h:e/f)}this.options.muted&&this._muted(!1)},_updateVolume:function(a){a===b&&(a=this.options.volume),a=this.options.muted?0:a,this.status.noVolume?(this.addStateClass("noVolume"),this.css.jq.volumeBar.length&&this.css.jq.volumeBar.hide(),this.css.jq.volumeBarValue.length&&this.css.jq.volumeBarValue.hide(),this.css.jq.volumeMax.length&&this.css.jq.volumeMax.hide()):(this.removeStateClass("noVolume"),this.css.jq.volumeBar.length&&this.css.jq.volumeBar.show(),this.css.jq.volumeBarValue.length&&(this.css.jq.volumeBarValue.show(),this.css.jq.volumeBarValue[this.options.verticalVolume?"height":"width"](100*a+"%")),this.css.jq.volumeMax.length&&this.css.jq.volumeMax.show())},volumeMax:function(){this.volume(1),this.options.muted&&this._muted(!1)},_cssSelectorAncestor:function(b){var c=this;this.options.cssSelectorAncestor=b,this._removeUiClass(),this.ancestorJq=b?a(b):[],b&&1!==this.ancestorJq.length&&this._warning({type:a.jPlayer.warning.CSS_SELECTOR_COUNT,context:b,message:a.jPlayer.warningMsg.CSS_SELECTOR_COUNT+this.ancestorJq.length+" found for cssSelectorAncestor.",hint:a.jPlayer.warningHint.CSS_SELECTOR_COUNT}),this._addUiClass(),a.each(this.options.cssSelector,function(a,b){c._cssSelector(a,b)}),this._updateInterface(),this._updateButtons(),this._updateAutohide(),this._updateVolume(),this._updateMute()},_cssSelector:function(b,c){var d=this;if("string"==typeof c)if(a.jPlayer.prototype.options.cssSelector[b]){if(this.css.jq[b]&&this.css.jq[b].length&&this.css.jq[b].unbind(".jPlayer"),this.options.cssSelector[b]=c,this.css.cs[b]=this.options.cssSelectorAncestor+" "+c,this.css.jq[b]=c?a(this.css.cs[b]):[],this.css.jq[b].length&&this[b]){var e=function(c){c.preventDefault(),d[b](c),d.options.autoBlur?a(this).blur():a(this).focus()};this.css.jq[b].bind("click.jPlayer",e)}c&&1!==this.css.jq[b].length&&this._warning({type:a.jPlayer.warning.CSS_SELECTOR_COUNT,context:this.css.cs[b],message:a.jPlayer.warningMsg.CSS_SELECTOR_COUNT+this.css.jq[b].length+" found for "+b+" method.",hint:a.jPlayer.warningHint.CSS_SELECTOR_COUNT})}else this._warning({type:a.jPlayer.warning.CSS_SELECTOR_METHOD,context:b,message:a.jPlayer.warningMsg.CSS_SELECTOR_METHOD,hint:a.jPlayer.warningHint.CSS_SELECTOR_METHOD});else this._warning({type:a.jPlayer.warning.CSS_SELECTOR_STRING,context:c,message:a.jPlayer.warningMsg.CSS_SELECTOR_STRING,hint:a.jPlayer.warningHint.CSS_SELECTOR_STRING})},duration:function(a){this.options.toggleDuration&&(this.options.captureDuration&&a.stopPropagation(),this._setOption("remainingDuration",!this.options.remainingDuration))},seekBar:function(b){if(this.css.jq.seekBar.length){var c=a(b.currentTarget),d=c.offset(),e=b.pageX-d.left,f=c.width(),g=100*e/f;this.playHead(g)}},playbackRate:function(a){this._setOption("playbackRate",a)},playbackRateBar:function(b){if(this.css.jq.playbackRateBar.length){var c,d,e=a(b.currentTarget),f=e.offset(),g=b.pageX-f.left,h=e.width(),i=e.height()-b.pageY+f.top,j=e.height();c=this.options.verticalPlaybackRate?i/j:g/h,d=c*(this.options.maxPlaybackRate-this.options.minPlaybackRate)+this.options.minPlaybackRate,this.playbackRate(d)}},_updatePlaybackRate:function(){var a=this.options.playbackRate,b=(a-this.options.minPlaybackRate)/(this.options.maxPlaybackRate-this.options.minPlaybackRate);this.status.playbackRateEnabled?(this.css.jq.playbackRateBar.length&&this.css.jq.playbackRateBar.show(),this.css.jq.playbackRateBarValue.length&&(this.css.jq.playbackRateBarValue.show(),this.css.jq.playbackRateBarValue[this.options.verticalPlaybackRate?"height":"width"](100*b+"%"))):(this.css.jq.playbackRateBar.length&&this.css.jq.playbackRateBar.hide(),this.css.jq.playbackRateBarValue.length&&this.css.jq.playbackRateBarValue.hide())},repeat:function(a){var b="object"==typeof a;this._loop(b&&this.options.useStateClassSkin&&this.options.loop?!1:!0)},repeatOff:function(){this._loop(!1)},_loop:function(b){this.options.loop!==b&&(this.options.loop=b,this._updateButtons(),this._trigger(a.jPlayer.event.repeat))},option:function(c,d){var e=c;if(0===arguments.length)return a.extend(!0,{},this.options);if("string"==typeof c){var f=c.split(".");if(d===b){for(var g=a.extend(!0,{},this.options),h=0;h<f.length;h++){if(g[f[h]]===b)return this._warning({type:a.jPlayer.warning.OPTION_KEY,context:c,message:a.jPlayer.warningMsg.OPTION_KEY,hint:a.jPlayer.warningHint.OPTION_KEY}),b;g=g[f[h]]}return g}e={};for(var i=e,j=0;j<f.length;j++)j<f.length-1?(i[f[j]]={},i=i[f[j]]):i[f[j]]=d}return this._setOptions(e),this},_setOptions:function(b){var c=this;return a.each(b,function(a,b){c._setOption(a,b)}),this},_setOption:function(b,c){var d=this;switch(b){case"volume":this.volume(c);break;case"muted":this._muted(c);break;case"globalVolume":this.options[b]=c;break;case"cssSelectorAncestor":this._cssSelectorAncestor(c);break;case"cssSelector":a.each(c,function(a,b){d._cssSelector(a,b)});break;case"playbackRate":this.options[b]=c=this._limitValue(c,this.options.minPlaybackRate,this.options.maxPlaybackRate),this.html.used&&this._html_setProperty("playbackRate",c),this._updatePlaybackRate();break;case"defaultPlaybackRate":this.options[b]=c=this._limitValue(c,this.options.minPlaybackRate,this.options.maxPlaybackRate),this.html.used&&this._html_setProperty("defaultPlaybackRate",c),this._updatePlaybackRate();break;case"minPlaybackRate":this.options[b]=c=this._limitValue(c,.1,this.options.maxPlaybackRate-.1),this._updatePlaybackRate();break;case"maxPlaybackRate":this.options[b]=c=this._limitValue(c,this.options.minPlaybackRate+.1,16),this._updatePlaybackRate();break;case"fullScreen":if(this.options[b]!==c){var e=a.jPlayer.nativeFeatures.fullscreen.used.webkitVideo;(!e||e&&!this.status.waitForPlay)&&(e||(this.options[b]=c),c?this._requestFullscreen():this._exitFullscreen(),e||this._setOption("fullWindow",c))}break;case"fullWindow":this.options[b]!==c&&(this._removeUiClass(),this.options[b]=c,this._refreshSize());break;case"size":this.options.fullWindow||this.options[b].cssClass===c.cssClass||this._removeUiClass(),this.options[b]=a.extend({},this.options[b],c),this._refreshSize();break;case"sizeFull":this.options.fullWindow&&this.options[b].cssClass!==c.cssClass&&this._removeUiClass(),this.options[b]=a.extend({},this.options[b],c),this._refreshSize();break;case"autohide":this.options[b]=a.extend({},this.options[b],c),this._updateAutohide();break;case"loop":this._loop(c);break;case"remainingDuration":this.options[b]=c,this._updateInterface();break;case"toggleDuration":this.options[b]=c;break;case"nativeVideoControls":this.options[b]=a.extend({},this.options[b],c),this.status.nativeVideoControls=this._uaBlocklist(this.options.nativeVideoControls),this._restrictNativeVideoControls(),this._updateNativeVideoControls();break;case"noFullWindow":this.options[b]=a.extend({},this.options[b],c),this.status.nativeVideoControls=this._uaBlocklist(this.options.nativeVideoControls),this.status.noFullWindow=this._uaBlocklist(this.options.noFullWindow),this._restrictNativeVideoControls(),this._updateButtons();break;case"noVolume":this.options[b]=a.extend({},this.options[b],c),this.status.noVolume=this._uaBlocklist(this.options.noVolume),this._updateVolume(),this._updateMute();break;case"emulateHtml":this.options[b]!==c&&(this.options[b]=c,c?this._emulateHtmlBridge():this._destroyHtmlBridge());break;case"timeFormat":this.options[b]=a.extend({},this.options[b],c);break;case"keyEnabled":this.options[b]=c,c||this!==a.jPlayer.focus||(a.jPlayer.focus=null);break;case"keyBindings":this.options[b]=a.extend(!0,{},this.options[b],c);break;case"audioFullScreen":this.options[b]=c;break;case"autoBlur":this.options[b]=c}return this},_refreshSize:function(){this._setSize(),this._addUiClass(),this._updateSize(),this._updateButtons(),this._updateAutohide(),this._trigger(a.jPlayer.event.resize)},_setSize:function(){this.options.fullWindow?(this.status.width=this.options.sizeFull.width,this.status.height=this.options.sizeFull.height,this.status.cssClass=this.options.sizeFull.cssClass):(this.status.width=this.options.size.width,this.status.height=this.options.size.height,this.status.cssClass=this.options.size.cssClass),this.element.css({width:this.status.width,height:this.status.height})},_addUiClass:function(){this.ancestorJq.length&&this.ancestorJq.addClass(this.status.cssClass)},_removeUiClass:function(){this.ancestorJq.length&&this.ancestorJq.removeClass(this.status.cssClass)},_updateSize:function(){this.internal.poster.jq.css({width:this.status.width,height:this.status.height}),!this.status.waitForPlay&&this.html.active&&this.status.video||this.html.video.available&&this.html.used&&this.status.nativeVideoControls?this.internal.video.jq.css({width:this.status.width,height:this.status.height}):!this.status.waitForPlay&&this.flash.active&&this.status.video&&this.internal.flash.jq.css({width:this.status.width,height:this.status.height})},_updateAutohide:function(){var a=this,b="mousemove.jPlayer",c=".jPlayerAutohide",d=b+c,e=function(b){var c,d,e=!1;"undefined"!=typeof a.internal.mouse?(c=a.internal.mouse.x-b.pageX,d=a.internal.mouse.y-b.pageY,e=Math.floor(c)>0||Math.floor(d)>0):e=!0,a.internal.mouse={x:b.pageX,y:b.pageY},e&&a.css.jq.gui.fadeIn(a.options.autohide.fadeIn,function(){clearTimeout(a.internal.autohideId),a.internal.autohideId=setTimeout(function(){a.css.jq.gui.fadeOut(a.options.autohide.fadeOut)},a.options.autohide.hold)})};this.css.jq.gui.length&&(this.css.jq.gui.stop(!0,!0),clearTimeout(this.internal.autohideId),delete this.internal.mouse,this.element.unbind(c),this.css.jq.gui.unbind(c),this.status.nativeVideoControls?this.css.jq.gui.hide():this.options.fullWindow&&this.options.autohide.full||!this.options.fullWindow&&this.options.autohide.restored?(this.element.bind(d,e),this.css.jq.gui.bind(d,e),this.css.jq.gui.hide()):this.css.jq.gui.show())},fullScreen:function(a){var b="object"==typeof a;b&&this.options.useStateClassSkin&&this.options.fullScreen?this._setOption("fullScreen",!1):this._setOption("fullScreen",!0)},restoreScreen:function(){this._setOption("fullScreen",!1)},_fullscreenAddEventListeners:function(){var b=this,c=a.jPlayer.nativeFeatures.fullscreen;c.api.fullscreenEnabled&&c.event.fullscreenchange&&("function"!=typeof this.internal.fullscreenchangeHandler&&(this.internal.fullscreenchangeHandler=function(){b._fullscreenchange()}),document.addEventListener(c.event.fullscreenchange,this.internal.fullscreenchangeHandler,!1))},_fullscreenRemoveEventListeners:function(){var b=a.jPlayer.nativeFeatures.fullscreen;this.internal.fullscreenchangeHandler&&document.removeEventListener(b.event.fullscreenchange,this.internal.fullscreenchangeHandler,!1)},_fullscreenchange:function(){this.options.fullScreen&&!a.jPlayer.nativeFeatures.fullscreen.api.fullscreenElement()&&this._setOption("fullScreen",!1)},_requestFullscreen:function(){var b=this.ancestorJq.length?this.ancestorJq[0]:this.element[0],c=a.jPlayer.nativeFeatures.fullscreen;c.used.webkitVideo&&(b=this.htmlElement.video),c.api.fullscreenEnabled&&c.api.requestFullscreen(b)},_exitFullscreen:function(){var b,c=a.jPlayer.nativeFeatures.fullscreen;c.used.webkitVideo&&(b=this.htmlElement.video),c.api.fullscreenEnabled&&c.api.exitFullscreen(b)},_html_initMedia:function(b){var c=a(this.htmlElement.media).empty();a.each(b.track||[],function(a,b){var d=document.createElement("track");d.setAttribute("kind",b.kind?b.kind:""),d.setAttribute("src",b.src?b.src:""),d.setAttribute("srclang",b.srclang?b.srclang:""),d.setAttribute("label",b.label?b.label:""),b.def&&d.setAttribute("default",b.def),c.append(d)}),this.htmlElement.media.src=this.status.src,"none"!==this.options.preload&&this._html_load(),this._trigger(a.jPlayer.event.timeupdate)},_html_setFormat:function(b){var c=this;a.each(this.formats,function(a,d){return c.html.support[d]&&b[d]?(c.status.src=b[d],c.status.format[d]=!0,c.status.formatType=d,!1):void 0})},_html_setAudio:function(a){this._html_setFormat(a),this.htmlElement.media=this.htmlElement.audio,this._html_initMedia(a)},_html_setVideo:function(a){this._html_setFormat(a),this.status.nativeVideoControls&&(this.htmlElement.video.poster=this._validString(a.poster)?a.poster:""),this.htmlElement.media=this.htmlElement.video,this._html_initMedia(a)},_html_resetMedia:function(){this.htmlElement.media&&(this.htmlElement.media.id!==this.internal.video.id||this.status.nativeVideoControls||this.internal.video.jq.css({width:"0px",height:"0px"}),this.htmlElement.media.pause())},_html_clearMedia:function(){this.htmlElement.media&&(this.htmlElement.media.src="about:blank",this.htmlElement.media.load())},_html_load:function(){this.status.waitForLoad&&(this.status.waitForLoad=!1,this.htmlElement.media.load()),clearTimeout(this.internal.htmlDlyCmdId)},_html_play:function(a){var b=this,c=this.htmlElement.media;if(this.androidFix.pause=!1,this._html_load(),this.androidFix.setMedia)this.androidFix.play=!0,this.androidFix.time=a;else if(isNaN(a))c.play();else{this.internal.cmdsIgnored&&c.play();try{if(c.seekable&&!("object"==typeof c.seekable&&c.seekable.length>0))throw 1;c.currentTime=a,c.play()}catch(d){return void(this.internal.htmlDlyCmdId=setTimeout(function(){b.play(a)},250))}}this._html_checkWaitForPlay()},_html_pause:function(a){var b=this,c=this.htmlElement.media;if(this.androidFix.play=!1,a>0?this._html_load():clearTimeout(this.internal.htmlDlyCmdId),c.pause(),this.androidFix.setMedia)this.androidFix.pause=!0,this.androidFix.time=a;else if(!isNaN(a))try{if(c.seekable&&!("object"==typeof c.seekable&&c.seekable.length>0))throw 1;c.currentTime=a}catch(d){return void(this.internal.htmlDlyCmdId=setTimeout(function(){b.pause(a)},250))}a>0&&this._html_checkWaitForPlay()},_html_playHead:function(a){var b=this,c=this.htmlElement.media;this._html_load();try{if("object"==typeof c.seekable&&c.seekable.length>0)c.currentTime=a*c.seekable.end(c.seekable.length-1)/100;else{if(!(c.duration>0)||isNaN(c.duration))throw"e";c.currentTime=a*c.duration/100}}catch(d){return void(this.internal.htmlDlyCmdId=setTimeout(function(){b.playHead(a)},250))}this.status.waitForLoad||this._html_checkWaitForPlay()},_html_checkWaitForPlay:function(){this.status.waitForPlay&&(this.status.waitForPlay=!1,this.css.jq.videoPlay.length&&this.css.jq.videoPlay.hide(),this.status.video&&(this.internal.poster.jq.hide(),this.internal.video.jq.css({width:this.status.width,height:this.status.height})))},_html_setProperty:function(a,b){this.html.audio.available&&(this.htmlElement.audio[a]=b),this.html.video.available&&(this.htmlElement.video[a]=b)},_aurora_setAudio:function(b){var c=this;a.each(this.formats,function(a,d){return c.aurora.support[d]&&b[d]?(c.status.src=b[d],c.status.format[d]=!0,c.status.formatType=d,!1):void 0}),this.aurora.player=new AV.Player.fromURL(this.status.src),this._addAuroraEventListeners(this.aurora.player,this.aurora),"auto"===this.options.preload&&(this._aurora_load(),this.status.waitForLoad=!1)},_aurora_resetMedia:function(){this.aurora.player&&this.aurora.player.stop()},_aurora_clearMedia:function(){},_aurora_load:function(){this.status.waitForLoad&&(this.status.waitForLoad=!1,this.aurora.player.preload())},_aurora_play:function(b){this.status.waitForLoad||isNaN(b)||this.aurora.player.seek(b),this.aurora.player.playing||this.aurora.player.play(),this.status.waitForLoad=!1,this._aurora_checkWaitForPlay(),this._updateButtons(!0),this._trigger(a.jPlayer.event.play)},_aurora_pause:function(b){isNaN(b)||this.aurora.player.seek(1e3*b),this.aurora.player.pause(),b>0&&this._aurora_checkWaitForPlay(),this._updateButtons(!1),this._trigger(a.jPlayer.event.pause)},_aurora_playHead:function(a){this.aurora.player.duration>0&&this.aurora.player.seek(a*this.aurora.player.duration/100),this.status.waitForLoad||this._aurora_checkWaitForPlay()},_aurora_checkWaitForPlay:function(){this.status.waitForPlay&&(this.status.waitForPlay=!1)},_aurora_volume:function(a){this.aurora.player.volume=100*a},_aurora_mute:function(a){a?(this.aurora.properties.lastvolume=this.aurora.player.volume,this.aurora.player.volume=0):this.aurora.player.volume=this.aurora.properties.lastvolume,this.aurora.properties.muted=a},_flash_setAudio:function(b){var c=this;try{a.each(this.formats,function(a,d){if(c.flash.support[d]&&b[d]){switch(d){case"m4a":case"fla":c._getMovie().fl_setAudio_m4a(b[d]);break;case"mp3":c._getMovie().fl_setAudio_mp3(b[d]);break;case"rtmpa":c._getMovie().fl_setAudio_rtmp(b[d])}return c.status.src=b[d],c.status.format[d]=!0,c.status.formatType=d,!1}}),"auto"===this.options.preload&&(this._flash_load(),this.status.waitForLoad=!1)}catch(d){this._flashError(d)}},_flash_setVideo:function(b){var c=this;try{a.each(this.formats,function(a,d){if(c.flash.support[d]&&b[d]){switch(d){case"m4v":case"flv":c._getMovie().fl_setVideo_m4v(b[d]);break;case"rtmpv":c._getMovie().fl_setVideo_rtmp(b[d])}return c.status.src=b[d],c.status.format[d]=!0,c.status.formatType=d,!1}}),"auto"===this.options.preload&&(this._flash_load(),this.status.waitForLoad=!1)}catch(d){this._flashError(d)}},_flash_resetMedia:function(){this.internal.flash.jq.css({width:"0px",height:"0px"}),this._flash_pause(0/0)},_flash_clearMedia:function(){try{this._getMovie().fl_clearMedia()}catch(a){this._flashError(a)}},_flash_load:function(){try{this._getMovie().fl_load()}catch(a){this._flashError(a)}this.status.waitForLoad=!1},_flash_play:function(a){try{this._getMovie().fl_play(a)}catch(b){this._flashError(b)}this.status.waitForLoad=!1,this._flash_checkWaitForPlay()},_flash_pause:function(a){try{this._getMovie().fl_pause(a)}catch(b){this._flashError(b)}a>0&&(this.status.waitForLoad=!1,this._flash_checkWaitForPlay())},_flash_playHead:function(a){try{this._getMovie().fl_play_head(a)}catch(b){this._flashError(b)}this.status.waitForLoad||this._flash_checkWaitForPlay()},_flash_checkWaitForPlay:function(){this.status.waitForPlay&&(this.status.waitForPlay=!1,this.css.jq.videoPlay.length&&this.css.jq.videoPlay.hide(),this.status.video&&(this.internal.poster.jq.hide(),this.internal.flash.jq.css({width:this.status.width,height:this.status.height})))},_flash_volume:function(a){try{this._getMovie().fl_volume(a)}catch(b){this._flashError(b)}},_flash_mute:function(a){try{this._getMovie().fl_mute(a)}catch(b){this._flashError(b)}},_getMovie:function(){return document[this.internal.flash.id]},_getFlashPluginVersion:function(){var a,b=0;if(window.ActiveXObject)try{if(a=new ActiveXObject("ShockwaveFlash.ShockwaveFlash")){var c=a.GetVariable("$version");c&&(c=c.split(" ")[1].split(","),b=parseInt(c[0],10)+"."+parseInt(c[1],10))}}catch(d){}else navigator.plugins&&navigator.mimeTypes.length>0&&(a=navigator.plugins["Shockwave Flash"],a&&(b=navigator.plugins["Shockwave Flash"].description.replace(/.*\s(\d+\.\d+).*/,"$1")));return 1*b},_checkForFlash:function(a){var b=!1;return this._getFlashPluginVersion()>=a&&(b=!0),b},_validString:function(a){return a&&"string"==typeof a},_limitValue:function(a,b,c){return b>a?b:a>c?c:a},_urlNotSetError:function(b){this._error({type:a.jPlayer.error.URL_NOT_SET,context:b,message:a.jPlayer.errorMsg.URL_NOT_SET,hint:a.jPlayer.errorHint.URL_NOT_SET})},_flashError:function(b){var c;c=this.internal.ready?"FLASH_DISABLED":"FLASH",this._error({type:a.jPlayer.error[c],context:this.internal.flash.swf,message:a.jPlayer.errorMsg[c]+b.message,hint:a.jPlayer.errorHint[c]}),this.internal.flash.jq.css({width:"1px",height:"1px"})},_error:function(b){this._trigger(a.jPlayer.event.error,b),this.options.errorAlerts&&this._alert("Error!"+(b.message?"\n"+b.message:"")+(b.hint?"\n"+b.hint:"")+"\nContext: "+b.context)},_warning:function(c){this._trigger(a.jPlayer.event.warning,b,c),this.options.warningAlerts&&this._alert("Warning!"+(c.message?"\n"+c.message:"")+(c.hint?"\n"+c.hint:"")+"\nContext: "+c.context)},_alert:function(a){var b="jPlayer "+this.version.script+" : id='"+this.internal.self.id+"' : "+a;this.options.consoleAlerts?window.console&&window.console.log&&window.console.log(b):alert(b)},_emulateHtmlBridge:function(){var b=this;a.each(a.jPlayer.emulateMethods.split(/\s+/g),function(a,c){b.internal.domNode[c]=function(a){b[c](a)}}),a.each(a.jPlayer.event,function(c,d){var e=!0;a.each(a.jPlayer.reservedEvent.split(/\s+/g),function(a,b){return b===c?(e=!1,!1):void 0}),e&&b.element.bind(d+".jPlayer.jPlayerHtml",function(){b._emulateHtmlUpdate();var a=document.createEvent("Event");a.initEvent(c,!1,!0),b.internal.domNode.dispatchEvent(a)})})},_emulateHtmlUpdate:function(){var b=this;a.each(a.jPlayer.emulateStatus.split(/\s+/g),function(a,c){b.internal.domNode[c]=b.status[c]}),a.each(a.jPlayer.emulateOptions.split(/\s+/g),function(a,c){b.internal.domNode[c]=b.options[c]})},_destroyHtmlBridge:function(){var b=this;this.element.unbind(".jPlayerHtml");var c=a.jPlayer.emulateMethods+" "+a.jPlayer.emulateStatus+" "+a.jPlayer.emulateOptions;a.each(c.split(/\s+/g),function(a,c){delete b.internal.domNode[c]})}},a.jPlayer.error={FLASH:"e_flash",FLASH_DISABLED:"e_flash_disabled",NO_SOLUTION:"e_no_solution",NO_SUPPORT:"e_no_support",URL:"e_url",URL_NOT_SET:"e_url_not_set",VERSION:"e_version"},a.jPlayer.errorMsg={FLASH:"jPlayer's Flash fallback is not configured correctly, or a command was issued before the jPlayer Ready event. Details: ",FLASH_DISABLED:"jPlayer's Flash fallback has been disabled by the browser due to the CSS rules you have used. Details: ",NO_SOLUTION:"No solution can be found by jPlayer in this browser. Neither HTML nor Flash can be used.",NO_SUPPORT:"It is not possible to play any media format provided in setMedia() on this browser using your current options.",URL:"Media URL could not be loaded.",URL_NOT_SET:"Attempt to issue media playback commands, while no media url is set.",VERSION:"jPlayer "+a.jPlayer.prototype.version.script+" needs Jplayer.swf version "+a.jPlayer.prototype.version.needFlash+" but found "},a.jPlayer.errorHint={FLASH:"Check your swfPath option and that Jplayer.swf is there.",FLASH_DISABLED:"Check that you have not display:none; the jPlayer entity or any ancestor.",NO_SOLUTION:"Review the jPlayer options: support and supplied.",NO_SUPPORT:"Video or audio formats defined in the supplied option are missing.",URL:"Check media URL is valid.",URL_NOT_SET:"Use setMedia() to set the media URL.",VERSION:"Update jPlayer files."},a.jPlayer.warning={CSS_SELECTOR_COUNT:"e_css_selector_count",CSS_SELECTOR_METHOD:"e_css_selector_method",CSS_SELECTOR_STRING:"e_css_selector_string",OPTION_KEY:"e_option_key"},a.jPlayer.warningMsg={CSS_SELECTOR_COUNT:"The number of css selectors found did not equal one: ",CSS_SELECTOR_METHOD:"The methodName given in jPlayer('cssSelector') is not a valid jPlayer method.",CSS_SELECTOR_STRING:"The methodCssSelector given in jPlayer('cssSelector') is not a String or is empty.",OPTION_KEY:"The option requested in jPlayer('option') is undefined."},a.jPlayer.warningHint={CSS_SELECTOR_COUNT:"Check your css selector and the ancestor.",CSS_SELECTOR_METHOD:"Check your method name.",CSS_SELECTOR_STRING:"Check your css selector is a string.",OPTION_KEY:"Check your option name."}});
\ No newline at end of file
+!function(a,b){"function"==typeof define&&define.amd?define(["jquery"],b):b("object"==typeof exports?require("jquery"):a.jQuery?a.jQuery:a.Zepto)}(this,function(a,b){a.fn.jPlayer=function(c){var d="jPlayer",e="string"==typeof c,f=Array.prototype.slice.call(arguments,1),g=this;return c=!e&&f.length?a.extend.apply(null,[!0,c].concat(f)):c,e&&"_"===c.charAt(0)?g:(this.each(e?function(){var e=a(this).data(d),h=e&&a.isFunction(e[c])?e[c].apply(e,f):e;return h!==e&&h!==b?(g=h,!1):void 0}:function(){var b=a(this).data(d);b?b.option(c||{}):a(this).data(d,new a.jPlayer(c,this))}),g)},a.jPlayer=function(b,c){if(arguments.length){this.element=a(c),this.options=a.extend(!0,{},this.options,b);var d=this;this.element.bind("remove.jPlayer",function(){d.destroy()}),this._init()}},"function"!=typeof a.fn.stop&&(a.fn.stop=function(){}),a.jPlayer.emulateMethods="load play pause",a.jPlayer.emulateStatus="src readyState networkState currentTime duration paused ended playbackRate",a.jPlayer.emulateOptions="muted volume",a.jPlayer.reservedEvent="ready flashreset resize repeat error warning",a.jPlayer.event={},a.each(["ready","setmedia","flashreset","resize","repeat","click","error","warning","loadstart","progress","suspend","abort","emptied","stalled","play","pause","loadedmetadata","loadeddata","waiting","playing","canplay","canplaythrough","seeking","seeked","timeupdate","ended","ratechange","durationchange","volumechange"],function(){a.jPlayer.event[this]="jPlayer_"+this}),a.jPlayer.htmlEvent=["loadstart","abort","emptied","stalled","loadedmetadata","canplay","canplaythrough"],a.jPlayer.pause=function(){a.jPlayer.prototype.destroyRemoved(),a.each(a.jPlayer.prototype.instances,function(a,b){b.data("jPlayer").status.srcSet&&b.jPlayer("pause")})},a.jPlayer.timeFormat={showHour:!1,showMin:!0,showSec:!0,padHour:!1,padMin:!0,padSec:!0,sepHour:":",sepMin:":",sepSec:""};var c=function(){this.init()};c.prototype={init:function(){this.options={timeFormat:a.jPlayer.timeFormat}},time:function(a){a=a&&"number"==typeof a?a:0;var b=new Date(1e3*a),c=b.getUTCHours()+(b.getUTCDate()-1)*24,d=this.options.timeFormat.showHour?b.getUTCMinutes():b.getUTCMinutes()+60*c,e=this.options.timeFormat.showMin?b.getUTCSeconds():b.getUTCSeconds()+60*d,f=this.options.timeFormat.padHour&&10>c?"0"+c:c,g=this.options.timeFormat.padMin&&10>d?"0"+d:d,h=this.options.timeFormat.padSec&&10>e?"0"+e:e,i="";return i+=this.options.timeFormat.showHour?f+this.options.timeFormat.sepHour:"",i+=this.options.timeFormat.showMin?g+this.options.timeFormat.sepMin:"",i+=this.options.timeFormat.showSec?h+this.options.timeFormat.sepSec:""}};var d=new c;a.jPlayer.convertTime=function(a){return d.time(a)},a.jPlayer.uaBrowser=function(a){var b=a.toLowerCase(),c=/(webkit)[ \/]([\w.]+)/,d=/(opera)(?:.*version)?[ \/]([\w.]+)/,e=/(msie) ([\w.]+)/,f=/(mozilla)(?:.*? rv:([\w.]+))?/,g=c.exec(b)||d.exec(b)||e.exec(b)||b.indexOf("compatible")<0&&f.exec(b)||[];return{browser:g[1]||"",version:g[2]||"0"}},a.jPlayer.uaPlatform=function(a){var b=a.toLowerCase(),c=/(ipad|iphone|ipod|android|blackberry|playbook|windows ce|webos)/,d=/(ipad|playbook)/,e=/(android)/,f=/(mobile)/,g=c.exec(b)||[],h=d.exec(b)||!f.exec(b)&&e.exec(b)||[];return g[1]&&(g[1]=g[1].replace(/\s/g,"_")),{platform:g[1]||"",tablet:h[1]||""}},a.jPlayer.browser={},a.jPlayer.platform={};var e=a.jPlayer.uaBrowser(navigator.userAgent);e.browser&&(a.jPlayer.browser[e.browser]=!0,a.jPlayer.browser.version=e.version);var f=a.jPlayer.uaPlatform(navigator.userAgent);f.platform&&(a.jPlayer.platform[f.platform]=!0,a.jPlayer.platform.mobile=!f.tablet,a.jPlayer.platform.tablet=!!f.tablet),a.jPlayer.getDocMode=function(){var b;return a.jPlayer.browser.msie&&(document.documentMode?b=document.documentMode:(b=5,document.compatMode&&"CSS1Compat"===document.compatMode&&(b=7))),b},a.jPlayer.browser.documentMode=a.jPlayer.getDocMode(),a.jPlayer.nativeFeatures={init:function(){var a,b,c,d=document,e=d.createElement("video"),f={w3c:["fullscreenEnabled","fullscreenElement","requestFullscreen","exitFullscreen","fullscreenchange","fullscreenerror"],moz:["mozFullScreenEnabled","mozFullScreenElement","mozRequestFullScreen","mozCancelFullScreen","mozfullscreenchange","mozfullscreenerror"],webkit:["","webkitCurrentFullScreenElement","webkitRequestFullScreen","webkitCancelFullScreen","webkitfullscreenchange",""],webkitVideo:["webkitSupportsFullscreen","webkitDisplayingFullscreen","webkitEnterFullscreen","webkitExitFullscreen","",""],ms:["","msFullscreenElement","msRequestFullscreen","msExitFullscreen","MSFullscreenChange","MSFullscreenError"]},g=["w3c","moz","webkit","webkitVideo","ms"];for(this.fullscreen=a={support:{w3c:!!d[f.w3c[0]],moz:!!d[f.moz[0]],webkit:"function"==typeof d[f.webkit[3]],webkitVideo:"function"==typeof e[f.webkitVideo[2]],ms:"function"==typeof e[f.ms[2]]},used:{}},b=0,c=g.length;c>b;b++){var h=g[b];if(a.support[h]){a.spec=h,a.used[h]=!0;break}}if(a.spec){var i=f[a.spec];a.api={fullscreenEnabled:!0,fullscreenElement:function(a){return a=a?a:d,a[i[1]]},requestFullscreen:function(a){return a[i[2]]()},exitFullscreen:function(a){return a=a?a:d,a[i[3]]()}},a.event={fullscreenchange:i[4],fullscreenerror:i[5]}}else a.api={fullscreenEnabled:!1,fullscreenElement:function(){return null},requestFullscreen:function(){},exitFullscreen:function(){}},a.event={}}},a.jPlayer.nativeFeatures.init(),a.jPlayer.focus=null,a.jPlayer.keyIgnoreElementNames="A INPUT TEXTAREA SELECT BUTTON";var g=function(b){var c,d=a.jPlayer.focus;d&&(a.each(a.jPlayer.keyIgnoreElementNames.split(/\s+/g),function(a,d){return b.target.nodeName.toUpperCase()===d.toUpperCase()?(c=!0,!1):void 0}),c||a.each(d.options.keyBindings,function(c,e){return e&&a.isFunction(e.fn)&&("number"==typeof e.key&&b.which===e.key||"string"==typeof e.key&&b.key===e.key)?(b.preventDefault(),e.fn(d),!1):void 0}))};a.jPlayer.keys=function(b){var c="keydown.jPlayer";a(document.documentElement).unbind(c),b&&a(document.documentElement).bind(c,g)},a.jPlayer.keys(!0),a.jPlayer.prototype={count:0,version:{script:"2.9.2",needFlash:"2.9.0",flash:"unknown"},options:{swfPath:"js",solution:"html, flash",supplied:"mp3",auroraFormats:"wav",preload:"metadata",volume:.8,muted:!1,remainingDuration:!1,toggleDuration:!1,captureDuration:!0,playbackRate:1,defaultPlaybackRate:1,minPlaybackRate:.5,maxPlaybackRate:4,wmode:"opaque",backgroundColor:"#000000",cssSelectorAncestor:"#jp_container_1",cssSelector:{videoPlay:".jp-video-play",play:".jp-play",pause:".jp-pause",stop:".jp-stop",seekBar:".jp-seek-bar",playBar:".jp-play-bar",mute:".jp-mute",unmute:".jp-unmute",volumeBar:".jp-volume-bar",volumeBarValue:".jp-volume-bar-value",volumeMax:".jp-volume-max",playbackRateBar:".jp-playback-rate-bar",playbackRateBarValue:".jp-playback-rate-bar-value",currentTime:".jp-current-time",duration:".jp-duration",title:".jp-title",fullScreen:".jp-full-screen",restoreScreen:".jp-restore-screen",repeat:".jp-repeat",repeatOff:".jp-repeat-off",gui:".jp-gui",noSolution:".jp-no-solution"},stateClass:{playing:"jp-state-playing",seeking:"jp-state-seeking",muted:"jp-state-muted",looped:"jp-state-looped",fullScreen:"jp-state-full-screen",noVolume:"jp-state-no-volume"},useStateClassSkin:!1,autoBlur:!0,smoothPlayBar:!1,fullScreen:!1,fullWindow:!1,autohide:{restored:!1,full:!0,fadeIn:200,fadeOut:600,hold:1e3},loop:!1,repeat:function(b){b.jPlayer.options.loop?a(this).unbind(".jPlayerRepeat").bind(a.jPlayer.event.ended+".jPlayer.jPlayerRepeat",function(){a(this).jPlayer("play")}):a(this).unbind(".jPlayerRepeat")},nativeVideoControls:{},noFullWindow:{msie:/msie [0-6]\./,ipad:/ipad.*?os [0-4]\./,iphone:/iphone/,ipod:/ipod/,android_pad:/android [0-3]\.(?!.*?mobile)/,android_phone:/(?=.*android)(?!.*chrome)(?=.*mobile)/,blackberry:/blackberry/,windows_ce:/windows ce/,iemobile:/iemobile/,webos:/webos/},noVolume:{ipad:/ipad/,iphone:/iphone/,ipod:/ipod/,android_pad:/android(?!.*?mobile)/,android_phone:/android.*?mobile/,blackberry:/blackberry/,windows_ce:/windows ce/,iemobile:/iemobile/,webos:/webos/,playbook:/playbook/},timeFormat:{},keyEnabled:!1,audioFullScreen:!1,keyBindings:{play:{key:80,fn:function(a){a.status.paused?a.play():a.pause()}},fullScreen:{key:70,fn:function(a){(a.status.video||a.options.audioFullScreen)&&a._setOption("fullScreen",!a.options.fullScreen)}},muted:{key:77,fn:function(a){a._muted(!a.options.muted)}},volumeUp:{key:190,fn:function(a){a.volume(a.options.volume+.1)}},volumeDown:{key:188,fn:function(a){a.volume(a.options.volume-.1)}},loop:{key:76,fn:function(a){a._loop(!a.options.loop)}}},verticalVolume:!1,verticalPlaybackRate:!1,globalVolume:!1,idPrefix:"jp",noConflict:"jQuery",emulateHtml:!1,consoleAlerts:!0,errorAlerts:!1,warningAlerts:!1},optionsAudio:{size:{width:"0px",height:"0px",cssClass:""},sizeFull:{width:"0px",height:"0px",cssClass:""}},optionsVideo:{size:{width:"480px",height:"270px",cssClass:"jp-video-270p"},sizeFull:{width:"100%",height:"100%",cssClass:"jp-video-full"}},instances:{},status:{src:"",media:{},paused:!0,format:{},formatType:"",waitForPlay:!0,waitForLoad:!0,srcSet:!1,video:!1,seekPercent:0,currentPercentRelative:0,currentPercentAbsolute:0,currentTime:0,duration:0,remaining:0,videoWidth:0,videoHeight:0,readyState:0,networkState:0,playbackRate:1,ended:0},internal:{ready:!1},solution:{html:!0,aurora:!0,flash:!0},format:{mp3:{codec:"audio/mpeg",flashCanPlay:!0,media:"audio"},m4a:{codec:'audio/mp4; codecs="mp4a.40.2"',flashCanPlay:!0,media:"audio"},m3u8a:{codec:'application/vnd.apple.mpegurl; codecs="mp4a.40.2"',flashCanPlay:!1,media:"audio"},m3ua:{codec:"audio/mpegurl",flashCanPlay:!1,media:"audio"},oga:{codec:'audio/ogg; codecs="vorbis, opus"',flashCanPlay:!1,media:"audio"},flac:{codec:"audio/x-flac",flashCanPlay:!1,media:"audio"},wav:{codec:'audio/wav; codecs="1"',flashCanPlay:!1,media:"audio"},webma:{codec:'audio/webm; codecs="vorbis"',flashCanPlay:!1,media:"audio"},fla:{codec:"audio/x-flv",flashCanPlay:!0,media:"audio"},rtmpa:{codec:'audio/rtmp; codecs="rtmp"',flashCanPlay:!0,media:"audio"},m4v:{codec:'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',flashCanPlay:!0,media:"video"},m3u8v:{codec:'application/vnd.apple.mpegurl; codecs="avc1.42E01E, mp4a.40.2"',flashCanPlay:!1,media:"video"},m3uv:{codec:"audio/mpegurl",flashCanPlay:!1,media:"video"},ogv:{codec:'video/ogg; codecs="theora, vorbis"',flashCanPlay:!1,media:"video"},webmv:{codec:'video/webm; codecs="vorbis, vp8"',flashCanPlay:!1,media:"video"},flv:{codec:"video/x-flv",flashCanPlay:!0,media:"video"},rtmpv:{codec:'video/rtmp; codecs="rtmp"',flashCanPlay:!0,media:"video"}},_init:function(){var c=this;if(this.element.empty(),this.status=a.extend({},this.status),this.internal=a.extend({},this.internal),this.options.timeFormat=a.extend({},a.jPlayer.timeFormat,this.options.timeFormat),this.internal.cmdsIgnored=a.jPlayer.platform.ipad||a.jPlayer.platform.iphone||a.jPlayer.platform.ipod,this.internal.domNode=this.element.get(0),this.options.keyEnabled&&!a.jPlayer.focus&&(a.jPlayer.focus=this),this.androidFix={setMedia:!1,play:!1,pause:!1,time:0/0},a.jPlayer.platform.android&&(this.options.preload="auto"!==this.options.preload?"metadata":"auto"),this.formats=[],this.solutions=[],this.require={},this.htmlElement={},this.html={},this.html.audio={},this.html.video={},this.aurora={},this.aurora.formats=[],this.aurora.properties=[],this.flash={},this.css={},this.css.cs={},this.css.jq={},this.ancestorJq=[],this.options.volume=this._limitValue(this.options.volume,0,1),a.each(this.options.supplied.toLowerCase().split(","),function(b,d){var e=d.replace(/^\s+|\s+$/g,"");if(c.format[e]){var f=!1;a.each(c.formats,function(a,b){return e===b?(f=!0,!1):void 0}),f||c.formats.push(e)}}),a.each(this.options.solution.toLowerCase().split(","),function(b,d){var e=d.replace(/^\s+|\s+$/g,"");if(c.solution[e]){var f=!1;a.each(c.solutions,function(a,b){return e===b?(f=!0,!1):void 0}),f||c.solutions.push(e)}}),a.each(this.options.auroraFormats.toLowerCase().split(","),function(b,d){var e=d.replace(/^\s+|\s+$/g,"");if(c.format[e]){var f=!1;a.each(c.aurora.formats,function(a,b){return e===b?(f=!0,!1):void 0}),f||c.aurora.formats.push(e)}}),this.internal.instance="jp_"+this.count,this.instances[this.internal.instance]=this.element,this.element.attr("id")||this.element.attr("id",this.options.idPrefix+"_jplayer_"+this.count),this.internal.self=a.extend({},{id:this.element.attr("id"),jq:this.element}),this.internal.audio=a.extend({},{id:this.options.idPrefix+"_audio_"+this.count,jq:b}),this.internal.video=a.extend({},{id:this.options.idPrefix+"_video_"+this.count,jq:b}),this.internal.flash=a.extend({},{id:this.options.idPrefix+"_flash_"+this.count,jq:b,swf:this.options.swfPath+(".swf"!==this.options.swfPath.toLowerCase().slice(-4)?(this.options.swfPath&&"/"!==this.options.swfPath.slice(-1)?"/":"")+"jquery.jplayer.swf":"")}),this.internal.poster=a.extend({},{id:this.options.idPrefix+"_poster_"+this.count,jq:b}),a.each(a.jPlayer.event,function(a,d){c.options[a]!==b&&(c.element.bind(d+".jPlayer",c.options[a]),c.options[a]=b)}),this.require.audio=!1,this.require.video=!1,a.each(this.formats,function(a,b){c.require[c.format[b].media]=!0}),this.options=this.require.video?a.extend(!0,{},this.optionsVideo,this.options):a.extend(!0,{},this.optionsAudio,this.options),this._setSize(),this.status.nativeVideoControls=this._uaBlocklist(this.options.nativeVideoControls),this.status.noFullWindow=this._uaBlocklist(this.options.noFullWindow),this.status.noVolume=this._uaBlocklist(this.options.noVolume),a.jPlayer.nativeFeatures.fullscreen.api.fullscreenEnabled&&this._fullscreenAddEventListeners(),this._restrictNativeVideoControls(),this.htmlElement.poster=document.createElement("img"),this.htmlElement.poster.id=this.internal.poster.id,this.htmlElement.poster.onload=function(){(!c.status.video||c.status.waitForPlay)&&c.internal.poster.jq.show()},this.element.append(this.htmlElement.poster),this.internal.poster.jq=a("#"+this.internal.poster.id),this.internal.poster.jq.css({width:this.status.width,height:this.status.height}),this.internal.poster.jq.hide(),this.internal.poster.jq.bind("click.jPlayer",function(){c._trigger(a.jPlayer.event.click)}),this.html.audio.available=!1,this.require.audio&&(this.htmlElement.audio=document.createElement("audio"),this.htmlElement.audio.id=this.internal.audio.id,this.html.audio.available=!!this.htmlElement.audio.canPlayType&&this._testCanPlayType(this.htmlElement.audio)),this.html.video.available=!1,this.require.video&&(this.htmlElement.video=document.createElement("video"),this.htmlElement.video.id=this.internal.video.id,this.html.video.available=!!this.htmlElement.video.canPlayType&&this._testCanPlayType(this.htmlElement.video)),this.flash.available=this._checkForFlash(10.1),this.html.canPlay={},this.aurora.canPlay={},this.flash.canPlay={},a.each(this.formats,function(b,d){c.html.canPlay[d]=c.html[c.format[d].media].available&&""!==c.htmlElement[c.format[d].media].canPlayType(c.format[d].codec),c.aurora.canPlay[d]=a.inArray(d,c.aurora.formats)>-1,c.flash.canPlay[d]=c.format[d].flashCanPlay&&c.flash.available}),this.html.desired=!1,this.aurora.desired=!1,this.flash.desired=!1,a.each(this.solutions,function(b,d){if(0===b)c[d].desired=!0;else{var e=!1,f=!1;a.each(c.formats,function(a,b){c[c.solutions[0]].canPlay[b]&&("video"===c.format[b].media?f=!0:e=!0)}),c[d].desired=c.require.audio&&!e||c.require.video&&!f}}),this.html.support={},this.aurora.support={},this.flash.support={},a.each(this.formats,function(a,b){c.html.support[b]=c.html.canPlay[b]&&c.html.desired,c.aurora.support[b]=c.aurora.canPlay[b]&&c.aurora.desired,c.flash.support[b]=c.flash.canPlay[b]&&c.flash.desired}),this.html.used=!1,this.aurora.used=!1,this.flash.used=!1,a.each(this.solutions,function(b,d){a.each(c.formats,function(a,b){return c[d].support[b]?(c[d].used=!0,!1):void 0})}),this._resetActive(),this._resetGate(),this._cssSelectorAncestor(this.options.cssSelectorAncestor),this.html.used||this.aurora.used||this.flash.used?this.css.jq.noSolution.length&&this.css.jq.noSolution.hide():(this._error({type:a.jPlayer.error.NO_SOLUTION,context:"{solution:'"+this.options.solution+"', supplied:'"+this.options.supplied+"'}",message:a.jPlayer.errorMsg.NO_SOLUTION,hint:a.jPlayer.errorHint.NO_SOLUTION}),this.css.jq.noSolution.length&&this.css.jq.noSolution.show()),this.flash.used){var d,e="jQuery="+encodeURI(this.options.noConflict)+"&id="+encodeURI(this.internal.self.id)+"&vol="+this.options.volume+"&muted="+this.options.muted;if(a.jPlayer.browser.msie&&(Number(a.jPlayer.browser.version)<9||a.jPlayer.browser.documentMode<9)){var f='<object id="'+this.internal.flash.id+'" classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" width="0" height="0" tabindex="-1"></object>',g=['<param name="movie" value="'+this.internal.flash.swf+'" />','<param name="FlashVars" value="'+e+'" />','<param name="allowScriptAccess" value="always" />','<param name="bgcolor" value="'+this.options.backgroundColor+'" />','<param name="wmode" value="'+this.options.wmode+'" />'];d=document.createElement(f);for(var h=0;h<g.length;h++)d.appendChild(document.createElement(g[h]))}else{var i=function(a,b,c){var d=document.createElement("param");d.setAttribute("name",b),d.setAttribute("value",c),a.appendChild(d)};d=document.createElement("object"),d.setAttribute("id",this.internal.flash.id),d.setAttribute("name",this.internal.flash.id),d.setAttribute("data",this.internal.flash.swf),d.setAttribute("type","application/x-shockwave-flash"),d.setAttribute("width","1"),d.setAttribute("height","1"),d.setAttribute("tabindex","-1"),i(d,"flashvars",e),i(d,"allowscriptaccess","always"),i(d,"bgcolor",this.options.backgroundColor),i(d,"wmode",this.options.wmode)}this.element.append(d),this.internal.flash.jq=a(d)}this.status.playbackRateEnabled=this.html.used&&!this.flash.used?this._testPlaybackRate("audio"):!1,this._updatePlaybackRate(),this.html.used&&(this.html.audio.available&&(this._addHtmlEventListeners(this.htmlElement.audio,this.html.audio),this.element.append(this.htmlElement.audio),this.internal.audio.jq=a("#"+this.internal.audio.id)),this.html.video.available&&(this._addHtmlEventListeners(this.htmlElement.video,this.html.video),this.element.append(this.htmlElement.video),this.internal.video.jq=a("#"+this.internal.video.id),this.internal.video.jq.css(this.status.nativeVideoControls?{width:this.status.width,height:this.status.height}:{width:"0px",height:"0px"}),this.internal.video.jq.bind("click.jPlayer",function(){c._trigger(a.jPlayer.event.click)}))),this.aurora.used,this.options.emulateHtml&&this._emulateHtmlBridge(),!this.html.used&&!this.aurora.used||this.flash.used||setTimeout(function(){c.internal.ready=!0,c.version.flash="n/a",c._trigger(a.jPlayer.event.repeat),c._trigger(a.jPlayer.event.ready)},100),this._updateNativeVideoControls(),this.css.jq.videoPlay.length&&this.css.jq.videoPlay.hide(),a.jPlayer.prototype.count++},destroy:function(){this.clearMedia(),this._removeUiClass(),this.css.jq.currentTime.length&&this.css.jq.currentTime.text(""),this.css.jq.duration.length&&this.css.jq.duration.text(""),a.each(this.css.jq,function(a,b){b.length&&b.unbind(".jPlayer")}),this.internal.poster.jq.unbind(".jPlayer"),this.internal.video.jq&&this.internal.video.jq.unbind(".jPlayer"),this._fullscreenRemoveEventListeners(),this===a.jPlayer.focus&&(a.jPlayer.focus=null),this.options.emulateHtml&&this._destroyHtmlBridge(),this.element.removeData("jPlayer"),this.element.unbind(".jPlayer"),this.element.empty(),delete this.instances[this.internal.instance]},destroyRemoved:function(){var b=this;a.each(this.instances,function(a,c){b.element!==c&&(c.data("jPlayer")||(c.jPlayer("destroy"),delete b.instances[a]))})},enable:function(){},disable:function(){},_testCanPlayType:function(a){try{return a.canPlayType(this.format.mp3.codec),!0}catch(b){return!1}},_testPlaybackRate:function(a){var b,c=.5;a="string"==typeof a?a:"audio",b=document.createElement(a);try{return"playbackRate"in b?(b.playbackRate=c,b.playbackRate===c):!1}catch(d){return!1}},_uaBlocklist:function(b){var c=navigator.userAgent.toLowerCase(),d=!1;return a.each(b,function(a,b){return b&&b.test(c)?(d=!0,!1):void 0}),d},_restrictNativeVideoControls:function(){this.require.audio&&this.status.nativeVideoControls&&(this.status.nativeVideoControls=!1,this.status.noFullWindow=!0)},_updateNativeVideoControls:function(){this.html.video.available&&this.html.used&&(this.htmlElement.video.controls=this.status.nativeVideoControls,this._updateAutohide(),this.status.nativeVideoControls&&this.require.video?(this.internal.poster.jq.hide(),this.internal.video.jq.css({width:this.status.width,height:this.status.height})):this.status.waitForPlay&&this.status.video&&(this.internal.poster.jq.show(),this.internal.video.jq.css({width:"0px",height:"0px"})))},_addHtmlEventListeners:function(b,c){var d=this;b.preload=this.options.preload,b.muted=this.options.muted,b.volume=this.options.volume,this.status.playbackRateEnabled&&(b.defaultPlaybackRate=this.options.defaultPlaybackRate,b.playbackRate=this.options.playbackRate),b.addEventListener("progress",function(){c.gate&&(d.internal.cmdsIgnored&&this.readyState>0&&(d.internal.cmdsIgnored=!1),d._getHtmlStatus(b),d._updateInterface(),d._trigger(a.jPlayer.event.progress))},!1),b.addEventListener("loadeddata",function(){c.gate&&(d.androidFix.setMedia=!1,d.androidFix.play&&(d.androidFix.play=!1,d.play(d.androidFix.time)),d.androidFix.pause&&(d.androidFix.pause=!1,d.pause(d.androidFix.time)),d._trigger(a.jPlayer.event.loadeddata))},!1),b.addEventListener("timeupdate",function(){c.gate&&(d._getHtmlStatus(b),d._updateInterface(),d._trigger(a.jPlayer.event.timeupdate))},!1),b.addEventListener("durationchange",function(){c.gate&&(d._getHtmlStatus(b),d._updateInterface(),d._trigger(a.jPlayer.event.durationchange))},!1),b.addEventListener("play",function(){c.gate&&(d._updateButtons(!0),d._html_checkWaitForPlay(),d._trigger(a.jPlayer.event.play))},!1),b.addEventListener("playing",function(){c.gate&&(d._updateButtons(!0),d._seeked(),d._trigger(a.jPlayer.event.playing))},!1),b.addEventListener("pause",function(){c.gate&&(d._updateButtons(!1),d._trigger(a.jPlayer.event.pause))},!1),b.addEventListener("waiting",function(){c.gate&&(d._seeking(),d._trigger(a.jPlayer.event.waiting))},!1),b.addEventListener("seeking",function(){c.gate&&(d._seeking(),d._trigger(a.jPlayer.event.seeking))},!1),b.addEventListener("seeked",function(){c.gate&&(d._seeked(),d._trigger(a.jPlayer.event.seeked))},!1),b.addEventListener("volumechange",function(){c.gate&&(d.options.volume=b.volume,d.options.muted=b.muted,d._updateMute(),d._updateVolume(),d._trigger(a.jPlayer.event.volumechange))},!1),b.addEventListener("ratechange",function(){c.gate&&(d.options.defaultPlaybackRate=b.defaultPlaybackRate,d.options.playbackRate=b.playbackRate,d._updatePlaybackRate(),d._trigger(a.jPlayer.event.ratechange))},!1),b.addEventListener("suspend",function(){c.gate&&(d._seeked(),d._trigger(a.jPlayer.event.suspend))},!1),b.addEventListener("ended",function(){c.gate&&(a.jPlayer.browser.webkit||(d.htmlElement.media.currentTime=0),d.htmlElement.media.pause(),d._updateButtons(!1),d._getHtmlStatus(b,!0),d._updateInterface(),d._trigger(a.jPlayer.event.ended))},!1),b.addEventListener("error",function(){c.gate&&(d._updateButtons(!1),d._seeked(),d.status.srcSet&&(clearTimeout(d.internal.htmlDlyCmdId),d.status.waitForLoad=!0,d.status.waitForPlay=!0,d.status.video&&!d.status.nativeVideoControls&&d.internal.video.jq.css({width:"0px",height:"0px"}),d._validString(d.status.media.poster)&&!d.status.nativeVideoControls&&d.internal.poster.jq.show(),d.css.jq.videoPlay.length&&d.css.jq.videoPlay.show(),d._error({type:a.jPlayer.error.URL,context:d.status.src,message:a.jPlayer.errorMsg.URL,hint:a.jPlayer.errorHint.URL})))},!1),a.each(a.jPlayer.htmlEvent,function(e,f){b.addEventListener(this,function(){c.gate&&d._trigger(a.jPlayer.event[f])},!1)})},_addAuroraEventListeners:function(b,c){var d=this;b.volume=100*this.options.volume,b.on("progress",function(){c.gate&&(d.internal.cmdsIgnored&&this.readyState>0&&(d.internal.cmdsIgnored=!1),d._getAuroraStatus(b),d._updateInterface(),d._trigger(a.jPlayer.event.progress),b.duration>0&&d._trigger(a.jPlayer.event.timeupdate))},!1),b.on("ready",function(){c.gate&&d._trigger(a.jPlayer.event.loadeddata)},!1),b.on("duration",function(){c.gate&&(d._getAuroraStatus(b),d._updateInterface(),d._trigger(a.jPlayer.event.durationchange))},!1),b.on("end",function(){c.gate&&(d._updateButtons(!1),d._getAuroraStatus(b,!0),d._updateInterface(),d._trigger(a.jPlayer.event.ended))},!1),b.on("error",function(){c.gate&&(d._updateButtons(!1),d._seeked(),d.status.srcSet&&(d.status.waitForLoad=!0,d.status.waitForPlay=!0,d.status.video&&!d.status.nativeVideoControls&&d.internal.video.jq.css({width:"0px",height:"0px"}),d._validString(d.status.media.poster)&&!d.status.nativeVideoControls&&d.internal.poster.jq.show(),d.css.jq.videoPlay.length&&d.css.jq.videoPlay.show(),d._error({type:a.jPlayer.error.URL,context:d.status.src,message:a.jPlayer.errorMsg.URL,hint:a.jPlayer.errorHint.URL})))},!1)},_getHtmlStatus:function(a,b){var c=0,d=0,e=0,f=0;isFinite(a.duration)&&(this.status.duration=a.duration),c=a.currentTime,d=this.status.duration>0?100*c/this.status.duration:0,"object"==typeof a.seekable&&a.seekable.length>0?(e=this.status.duration>0?100*a.seekable.end(a.seekable.length-1)/this.status.duration:100,f=this.status.duration>0?100*a.currentTime/a.seekable.end(a.seekable.length-1):0):(e=100,f=d),b&&(c=0,f=0,d=0),this.status.seekPercent=e,this.status.currentPercentRelative=f,this.status.currentPercentAbsolute=d,this.status.currentTime=c,this.status.remaining=this.status.duration-this.status.currentTime,this.status.videoWidth=a.videoWidth,this.status.videoHeight=a.videoHeight,this.status.readyState=a.readyState,this.status.networkState=a.networkState,this.status.playbackRate=a.playbackRate,this.status.ended=a.ended},_getAuroraStatus:function(a,b){var c=0,d=0,e=0,f=0;this.status.duration=a.duration/1e3,c=a.currentTime/1e3,d=this.status.duration>0?100*c/this.status.duration:0,a.buffered>0?(e=this.status.duration>0?a.buffered*this.status.duration/this.status.duration:100,f=this.status.duration>0?c/(a.buffered*this.status.duration):0):(e=100,f=d),b&&(c=0,f=0,d=0),this.status.seekPercent=e,this.status.currentPercentRelative=f,this.status.currentPercentAbsolute=d,this.status.currentTime=c,this.status.remaining=this.status.duration-this.status.currentTime,this.status.readyState=4,this.status.networkState=0,this.status.playbackRate=1,this.status.ended=!1},_resetStatus:function(){this.status=a.extend({},this.status,a.jPlayer.prototype.status)},_trigger:function(b,c,d){var e=a.Event(b);e.jPlayer={},e.jPlayer.version=a.extend({},this.version),e.jPlayer.options=a.extend(!0,{},this.options),e.jPlayer.status=a.extend(!0,{},this.status),e.jPlayer.html=a.extend(!0,{},this.html),e.jPlayer.aurora=a.extend(!0,{},this.aurora),e.jPlayer.flash=a.extend(!0,{},this.flash),c&&(e.jPlayer.error=a.extend({},c)),d&&(e.jPlayer.warning=a.extend({},d)),this.element.trigger(e)},jPlayerFlashEvent:function(b,c){if(b===a.jPlayer.event.ready)if(this.internal.ready){if(this.flash.gate){if(this.status.srcSet){var d=this.status.currentTime,e=this.status.paused;this.setMedia(this.status.media),this.volumeWorker(this.options.volume),d>0&&(e?this.pause(d):this.play(d))}this._trigger(a.jPlayer.event.flashreset)}}else this.internal.ready=!0,this.internal.flash.jq.css({width:"0px",height:"0px"}),this.version.flash=c.version,this.version.needFlash!==this.version.flash&&this._error({type:a.jPlayer.error.VERSION,context:this.version.flash,message:a.jPlayer.errorMsg.VERSION+this.version.flash,hint:a.jPlayer.errorHint.VERSION}),this._trigger(a.jPlayer.event.repeat),this._trigger(b);if(this.flash.gate)switch(b){case a.jPlayer.event.progress:this._getFlashStatus(c),this._updateInterface(),this._trigger(b);break;case a.jPlayer.event.timeupdate:this._getFlashStatus(c),this._updateInterface(),this._trigger(b);break;case a.jPlayer.event.play:this._seeked(),this._updateButtons(!0),this._trigger(b);break;case a.jPlayer.event.pause:this._updateButtons(!1),this._trigger(b);break;case a.jPlayer.event.ended:this._updateButtons(!1),this._trigger(b);break;case a.jPlayer.event.click:this._trigger(b);break;case a.jPlayer.event.error:this.status.waitForLoad=!0,this.status.waitForPlay=!0,this.status.video&&this.internal.flash.jq.css({width:"0px",height:"0px"}),this._validString(this.status.media.poster)&&this.internal.poster.jq.show(),this.css.jq.videoPlay.length&&this.status.video&&this.css.jq.videoPlay.show(),this.status.video?this._flash_setVideo(this.status.media):this._flash_setAudio(this.status.media),this._updateButtons(!1),this._error({type:a.jPlayer.error.URL,context:c.src,message:a.jPlayer.errorMsg.URL,hint:a.jPlayer.errorHint.URL});break;case a.jPlayer.event.seeking:this._seeking(),this._trigger(b);break;case a.jPlayer.event.seeked:this._seeked(),this._trigger(b);break;case a.jPlayer.event.ready:break;default:this._trigger(b)}return!1},_getFlashStatus:function(a){this.status.seekPercent=a.seekPercent,this.status.currentPercentRelative=a.currentPercentRelative,this.status.currentPercentAbsolute=a.currentPercentAbsolute,this.status.currentTime=a.currentTime,this.status.duration=a.duration,this.status.remaining=a.duration-a.currentTime,this.status.videoWidth=a.videoWidth,this.status.videoHeight=a.videoHeight,this.status.readyState=4,this.status.networkState=0,this.status.playbackRate=1,this.status.ended=!1},_updateButtons:function(a){a===b?a=!this.status.paused:this.status.paused=!a,a?this.addStateClass("playing"):this.removeStateClass("playing"),!this.status.noFullWindow&&this.options.fullWindow?this.addStateClass("fullScreen"):this.removeStateClass("fullScreen"),this.options.loop?this.addStateClass("looped"):this.removeStateClass("looped"),this.css.jq.play.length&&this.css.jq.pause.length&&(a?(this.css.jq.play.hide(),this.css.jq.pause.show()):(this.css.jq.play.show(),this.css.jq.pause.hide())),this.css.jq.restoreScreen.length&&this.css.jq.fullScreen.length&&(this.status.noFullWindow?(this.css.jq.fullScreen.hide(),this.css.jq.restoreScreen.hide()):this.options.fullWindow?(this.css.jq.fullScreen.hide(),this.css.jq.restoreScreen.show()):(this.css.jq.fullScreen.show(),this.css.jq.restoreScreen.hide())),this.css.jq.repeat.length&&this.css.jq.repeatOff.length&&(this.options.loop?(this.css.jq.repeat.hide(),this.css.jq.repeatOff.show()):(this.css.jq.repeat.show(),this.css.jq.repeatOff.hide()))},_updateInterface:function(){this.css.jq.seekBar.length&&this.css.jq.seekBar.width(this.status.seekPercent+"%"),this.css.jq.playBar.length&&(this.options.smoothPlayBar?this.css.jq.playBar.stop().animate({width:this.status.currentPercentAbsolute+"%"},250,"linear"):this.css.jq.playBar.width(this.status.currentPercentRelative+"%"));var a="";this.css.jq.currentTime.length&&(a=this._convertTime(this.status.currentTime),a!==this.css.jq.currentTime.text()&&this.css.jq.currentTime.text(this._convertTime(this.status.currentTime)));var b="",c=this.status.duration,d=this.status.remaining;this.css.jq.duration.length&&("string"==typeof this.status.media.duration?b=this.status.media.duration:("number"==typeof this.status.media.duration&&(c=this.status.media.duration,d=c-this.status.currentTime),b=this.options.remainingDuration?(d>0?"-":"")+this._convertTime(d):this._convertTime(c)),b!==this.css.jq.duration.text()&&this.css.jq.duration.text(b))},_convertTime:c.prototype.time,_seeking:function(){this.css.jq.seekBar.length&&this.css.jq.seekBar.addClass("jp-seeking-bg"),this.addStateClass("seeking")},_seeked:function(){this.css.jq.seekBar.length&&this.css.jq.seekBar.removeClass("jp-seeking-bg"),this.removeStateClass("seeking")},_resetGate:function(){this.html.audio.gate=!1,this.html.video.gate=!1,this.aurora.gate=!1,this.flash.gate=!1},_resetActive:function(){this.html.active=!1,this.aurora.active=!1,this.flash.active=!1},_escapeHtml:function(a){return a.split("&").join("&").split("<").join("<").split(">").join(">").split('"').join(""")},_qualifyURL:function(a){var b=document.createElement("div");
+return b.innerHTML='<a href="'+this._escapeHtml(a)+'">x</a>',b.firstChild.href},_absoluteMediaUrls:function(b){var c=this;return a.each(b,function(a,d){d&&c.format[a]&&"data:"!==d.substr(0,5)&&(b[a]=c._qualifyURL(d))}),b},addStateClass:function(a){this.ancestorJq.length&&this.ancestorJq.addClass(this.options.stateClass[a])},removeStateClass:function(a){this.ancestorJq.length&&this.ancestorJq.removeClass(this.options.stateClass[a])},setMedia:function(b){var c=this,d=!1,e=this.status.media.poster!==b.poster;this._resetMedia(),this._resetGate(),this._resetActive(),this.androidFix.setMedia=!1,this.androidFix.play=!1,this.androidFix.pause=!1,b=this._absoluteMediaUrls(b),a.each(this.formats,function(e,f){var g="video"===c.format[f].media;return a.each(c.solutions,function(e,h){if(c[h].support[f]&&c._validString(b[f])){var i="html"===h,j="aurora"===h;return g?(i?(c.html.video.gate=!0,c._html_setVideo(b),c.html.active=!0):(c.flash.gate=!0,c._flash_setVideo(b),c.flash.active=!0),c.css.jq.videoPlay.length&&c.css.jq.videoPlay.show(),c.status.video=!0):(i?(c.html.audio.gate=!0,c._html_setAudio(b),c.html.active=!0,a.jPlayer.platform.android&&(c.androidFix.setMedia=!0)):j?(c.aurora.gate=!0,c._aurora_setAudio(b),c.aurora.active=!0):(c.flash.gate=!0,c._flash_setAudio(b),c.flash.active=!0),c.css.jq.videoPlay.length&&c.css.jq.videoPlay.hide(),c.status.video=!1),d=!0,!1}}),d?!1:void 0}),d?(this.status.nativeVideoControls&&this.html.video.gate||this._validString(b.poster)&&(e?this.htmlElement.poster.src=b.poster:this.internal.poster.jq.show()),"string"==typeof b.title&&(this.css.jq.title.length&&this.css.jq.title.html(b.title),this.htmlElement.audio&&this.htmlElement.audio.setAttribute("title",b.title),this.htmlElement.video&&this.htmlElement.video.setAttribute("title",b.title)),this.status.srcSet=!0,this.status.media=a.extend({},b),this._updateButtons(!1),this._updateInterface(),this._trigger(a.jPlayer.event.setmedia)):this._error({type:a.jPlayer.error.NO_SUPPORT,context:"{supplied:'"+this.options.supplied+"'}",message:a.jPlayer.errorMsg.NO_SUPPORT,hint:a.jPlayer.errorHint.NO_SUPPORT})},_resetMedia:function(){this._resetStatus(),this._updateButtons(!1),this._updateInterface(),this._seeked(),this.internal.poster.jq.hide(),clearTimeout(this.internal.htmlDlyCmdId),this.html.active?this._html_resetMedia():this.aurora.active?this._aurora_resetMedia():this.flash.active&&this._flash_resetMedia()},clearMedia:function(){this._resetMedia(),this.html.active?this._html_clearMedia():this.aurora.active?this._aurora_clearMedia():this.flash.active&&this._flash_clearMedia(),this._resetGate(),this._resetActive()},load:function(){this.status.srcSet?this.html.active?this._html_load():this.aurora.active?this._aurora_load():this.flash.active&&this._flash_load():this._urlNotSetError("load")},focus:function(){this.options.keyEnabled&&(a.jPlayer.focus=this)},play:function(a){var b="object"==typeof a;b&&this.options.useStateClassSkin&&!this.status.paused?this.pause(a):(a="number"==typeof a?a:0/0,this.status.srcSet?(this.focus(),this.html.active?this._html_play(a):this.aurora.active?this._aurora_play(a):this.flash.active&&this._flash_play(a)):this._urlNotSetError("play"))},videoPlay:function(){this.play()},pause:function(a){a="number"==typeof a?a:0/0,this.status.srcSet?this.html.active?this._html_pause(a):this.aurora.active?this._aurora_pause(a):this.flash.active&&this._flash_pause(a):this._urlNotSetError("pause")},tellOthers:function(b,c){var d=this,e="function"==typeof c,f=Array.prototype.slice.call(arguments);"string"==typeof b&&(e&&f.splice(1,1),a.jPlayer.prototype.destroyRemoved(),a.each(this.instances,function(){d.element!==this&&(!e||c.call(this.data("jPlayer"),d))&&this.jPlayer.apply(this,f)}))},pauseOthers:function(a){this.tellOthers("pause",function(){return this.status.srcSet},a)},stop:function(){this.status.srcSet?this.html.active?this._html_pause(0):this.aurora.active?this._aurora_pause(0):this.flash.active&&this._flash_pause(0):this._urlNotSetError("stop")},playHead:function(a){a=this._limitValue(a,0,100),this.status.srcSet?this.html.active?this._html_playHead(a):this.aurora.active?this._aurora_playHead(a):this.flash.active&&this._flash_playHead(a):this._urlNotSetError("playHead")},_muted:function(a){this.mutedWorker(a),this.options.globalVolume&&this.tellOthers("mutedWorker",function(){return this.options.globalVolume},a)},mutedWorker:function(b){this.options.muted=b,this.html.used&&this._html_setProperty("muted",b),this.aurora.used&&this._aurora_mute(b),this.flash.used&&this._flash_mute(b),this.html.video.gate||this.html.audio.gate||(this._updateMute(b),this._updateVolume(this.options.volume),this._trigger(a.jPlayer.event.volumechange))},mute:function(a){var c="object"==typeof a;c&&this.options.useStateClassSkin&&this.options.muted?this._muted(!1):(a=a===b?!0:!!a,this._muted(a))},unmute:function(a){a=a===b?!0:!!a,this._muted(!a)},_updateMute:function(a){a===b&&(a=this.options.muted),a?this.addStateClass("muted"):this.removeStateClass("muted"),this.css.jq.mute.length&&this.css.jq.unmute.length&&(this.status.noVolume?(this.css.jq.mute.hide(),this.css.jq.unmute.hide()):a?(this.css.jq.mute.hide(),this.css.jq.unmute.show()):(this.css.jq.mute.show(),this.css.jq.unmute.hide()))},volume:function(a){this.volumeWorker(a),this.options.globalVolume&&this.tellOthers("volumeWorker",function(){return this.options.globalVolume},a)},volumeWorker:function(b){b=this._limitValue(b,0,1),this.options.volume=b,this.html.used&&this._html_setProperty("volume",b),this.aurora.used&&this._aurora_volume(b),this.flash.used&&this._flash_volume(b),this.html.video.gate||this.html.audio.gate||(this._updateVolume(b),this._trigger(a.jPlayer.event.volumechange))},volumeBar:function(b){if(this.css.jq.volumeBar.length){var c=a(b.currentTarget),d=c.offset(),e=b.pageX-d.left,f=c.width(),g=c.height()-b.pageY+d.top,h=c.height();this.volume(this.options.verticalVolume?g/h:e/f)}this.options.muted&&this._muted(!1)},_updateVolume:function(a){a===b&&(a=this.options.volume),a=this.options.muted?0:a,this.status.noVolume?(this.addStateClass("noVolume"),this.css.jq.volumeBar.length&&this.css.jq.volumeBar.hide(),this.css.jq.volumeBarValue.length&&this.css.jq.volumeBarValue.hide(),this.css.jq.volumeMax.length&&this.css.jq.volumeMax.hide()):(this.removeStateClass("noVolume"),this.css.jq.volumeBar.length&&this.css.jq.volumeBar.show(),this.css.jq.volumeBarValue.length&&(this.css.jq.volumeBarValue.show(),this.css.jq.volumeBarValue[this.options.verticalVolume?"height":"width"](100*a+"%")),this.css.jq.volumeMax.length&&this.css.jq.volumeMax.show())},volumeMax:function(){this.volume(1),this.options.muted&&this._muted(!1)},_cssSelectorAncestor:function(b){var c=this;this.options.cssSelectorAncestor=b,this._removeUiClass(),this.ancestorJq=b?a(b):[],b&&1!==this.ancestorJq.length&&this._warning({type:a.jPlayer.warning.CSS_SELECTOR_COUNT,context:b,message:a.jPlayer.warningMsg.CSS_SELECTOR_COUNT+this.ancestorJq.length+" found for cssSelectorAncestor.",hint:a.jPlayer.warningHint.CSS_SELECTOR_COUNT}),this._addUiClass(),a.each(this.options.cssSelector,function(a,b){c._cssSelector(a,b)}),this._updateInterface(),this._updateButtons(),this._updateAutohide(),this._updateVolume(),this._updateMute()},_cssSelector:function(b,c){var d=this;if("string"==typeof c)if(a.jPlayer.prototype.options.cssSelector[b]){if(this.css.jq[b]&&this.css.jq[b].length&&this.css.jq[b].unbind(".jPlayer"),this.options.cssSelector[b]=c,this.css.cs[b]=this.options.cssSelectorAncestor+" "+c,this.css.jq[b]=c?a(this.css.cs[b]):[],this.css.jq[b].length&&this[b]){var e=function(c){c.preventDefault(),d[b](c),d.options.autoBlur?a(this).blur():a(this).focus()};this.css.jq[b].bind("click.jPlayer",e)}c&&1!==this.css.jq[b].length&&this._warning({type:a.jPlayer.warning.CSS_SELECTOR_COUNT,context:this.css.cs[b],message:a.jPlayer.warningMsg.CSS_SELECTOR_COUNT+this.css.jq[b].length+" found for "+b+" method.",hint:a.jPlayer.warningHint.CSS_SELECTOR_COUNT})}else this._warning({type:a.jPlayer.warning.CSS_SELECTOR_METHOD,context:b,message:a.jPlayer.warningMsg.CSS_SELECTOR_METHOD,hint:a.jPlayer.warningHint.CSS_SELECTOR_METHOD});else this._warning({type:a.jPlayer.warning.CSS_SELECTOR_STRING,context:c,message:a.jPlayer.warningMsg.CSS_SELECTOR_STRING,hint:a.jPlayer.warningHint.CSS_SELECTOR_STRING})},duration:function(a){this.options.toggleDuration&&(this.options.captureDuration&&a.stopPropagation(),this._setOption("remainingDuration",!this.options.remainingDuration))},seekBar:function(b){if(this.css.jq.seekBar.length){var c=a(b.currentTarget),d=c.offset(),e=b.pageX-d.left,f=c.width(),g=100*e/f;this.playHead(g)}},playbackRate:function(a){this._setOption("playbackRate",a)},playbackRateBar:function(b){if(this.css.jq.playbackRateBar.length){var c,d,e=a(b.currentTarget),f=e.offset(),g=b.pageX-f.left,h=e.width(),i=e.height()-b.pageY+f.top,j=e.height();c=this.options.verticalPlaybackRate?i/j:g/h,d=c*(this.options.maxPlaybackRate-this.options.minPlaybackRate)+this.options.minPlaybackRate,this.playbackRate(d)}},_updatePlaybackRate:function(){var a=this.options.playbackRate,b=(a-this.options.minPlaybackRate)/(this.options.maxPlaybackRate-this.options.minPlaybackRate);this.status.playbackRateEnabled?(this.css.jq.playbackRateBar.length&&this.css.jq.playbackRateBar.show(),this.css.jq.playbackRateBarValue.length&&(this.css.jq.playbackRateBarValue.show(),this.css.jq.playbackRateBarValue[this.options.verticalPlaybackRate?"height":"width"](100*b+"%"))):(this.css.jq.playbackRateBar.length&&this.css.jq.playbackRateBar.hide(),this.css.jq.playbackRateBarValue.length&&this.css.jq.playbackRateBarValue.hide())},repeat:function(a){var b="object"==typeof a;this._loop(b&&this.options.useStateClassSkin&&this.options.loop?!1:!0)},repeatOff:function(){this._loop(!1)},_loop:function(b){this.options.loop!==b&&(this.options.loop=b,this._updateButtons(),this._trigger(a.jPlayer.event.repeat))},option:function(c,d){var e=c;if(0===arguments.length)return a.extend(!0,{},this.options);if("string"==typeof c){var f=c.split(".");if(d===b){for(var g=a.extend(!0,{},this.options),h=0;h<f.length;h++){if(g[f[h]]===b)return this._warning({type:a.jPlayer.warning.OPTION_KEY,context:c,message:a.jPlayer.warningMsg.OPTION_KEY,hint:a.jPlayer.warningHint.OPTION_KEY}),b;g=g[f[h]]}return g}e={};for(var i=e,j=0;j<f.length;j++)j<f.length-1?(i[f[j]]={},i=i[f[j]]):i[f[j]]=d}return this._setOptions(e),this},_setOptions:function(b){var c=this;return a.each(b,function(a,b){c._setOption(a,b)}),this},_setOption:function(b,c){var d=this;switch(b){case"volume":this.volume(c);break;case"muted":this._muted(c);break;case"globalVolume":this.options[b]=c;break;case"cssSelectorAncestor":this._cssSelectorAncestor(c);break;case"cssSelector":a.each(c,function(a,b){d._cssSelector(a,b)});break;case"playbackRate":this.options[b]=c=this._limitValue(c,this.options.minPlaybackRate,this.options.maxPlaybackRate),this.html.used&&this._html_setProperty("playbackRate",c),this._updatePlaybackRate();break;case"defaultPlaybackRate":this.options[b]=c=this._limitValue(c,this.options.minPlaybackRate,this.options.maxPlaybackRate),this.html.used&&this._html_setProperty("defaultPlaybackRate",c),this._updatePlaybackRate();break;case"minPlaybackRate":this.options[b]=c=this._limitValue(c,.1,this.options.maxPlaybackRate-.1),this._updatePlaybackRate();break;case"maxPlaybackRate":this.options[b]=c=this._limitValue(c,this.options.minPlaybackRate+.1,16),this._updatePlaybackRate();break;case"fullScreen":if(this.options[b]!==c){var e=a.jPlayer.nativeFeatures.fullscreen.used.webkitVideo;(!e||e&&!this.status.waitForPlay)&&(e||(this.options[b]=c),c?this._requestFullscreen():this._exitFullscreen(),e||this._setOption("fullWindow",c))}break;case"fullWindow":this.options[b]!==c&&(this._removeUiClass(),this.options[b]=c,this._refreshSize());break;case"size":this.options.fullWindow||this.options[b].cssClass===c.cssClass||this._removeUiClass(),this.options[b]=a.extend({},this.options[b],c),this._refreshSize();break;case"sizeFull":this.options.fullWindow&&this.options[b].cssClass!==c.cssClass&&this._removeUiClass(),this.options[b]=a.extend({},this.options[b],c),this._refreshSize();break;case"autohide":this.options[b]=a.extend({},this.options[b],c),this._updateAutohide();break;case"loop":this._loop(c);break;case"remainingDuration":this.options[b]=c,this._updateInterface();break;case"toggleDuration":this.options[b]=c;break;case"nativeVideoControls":this.options[b]=a.extend({},this.options[b],c),this.status.nativeVideoControls=this._uaBlocklist(this.options.nativeVideoControls),this._restrictNativeVideoControls(),this._updateNativeVideoControls();break;case"noFullWindow":this.options[b]=a.extend({},this.options[b],c),this.status.nativeVideoControls=this._uaBlocklist(this.options.nativeVideoControls),this.status.noFullWindow=this._uaBlocklist(this.options.noFullWindow),this._restrictNativeVideoControls(),this._updateButtons();break;case"noVolume":this.options[b]=a.extend({},this.options[b],c),this.status.noVolume=this._uaBlocklist(this.options.noVolume),this._updateVolume(),this._updateMute();break;case"emulateHtml":this.options[b]!==c&&(this.options[b]=c,c?this._emulateHtmlBridge():this._destroyHtmlBridge());break;case"timeFormat":this.options[b]=a.extend({},this.options[b],c);break;case"keyEnabled":this.options[b]=c,c||this!==a.jPlayer.focus||(a.jPlayer.focus=null);break;case"keyBindings":this.options[b]=a.extend(!0,{},this.options[b],c);break;case"audioFullScreen":this.options[b]=c;break;case"autoBlur":this.options[b]=c}return this},_refreshSize:function(){this._setSize(),this._addUiClass(),this._updateSize(),this._updateButtons(),this._updateAutohide(),this._trigger(a.jPlayer.event.resize)},_setSize:function(){this.options.fullWindow?(this.status.width=this.options.sizeFull.width,this.status.height=this.options.sizeFull.height,this.status.cssClass=this.options.sizeFull.cssClass):(this.status.width=this.options.size.width,this.status.height=this.options.size.height,this.status.cssClass=this.options.size.cssClass),this.element.css({width:this.status.width,height:this.status.height})},_addUiClass:function(){this.ancestorJq.length&&this.ancestorJq.addClass(this.status.cssClass)},_removeUiClass:function(){this.ancestorJq.length&&this.ancestorJq.removeClass(this.status.cssClass)},_updateSize:function(){this.internal.poster.jq.css({width:this.status.width,height:this.status.height}),!this.status.waitForPlay&&this.html.active&&this.status.video||this.html.video.available&&this.html.used&&this.status.nativeVideoControls?this.internal.video.jq.css({width:this.status.width,height:this.status.height}):!this.status.waitForPlay&&this.flash.active&&this.status.video&&this.internal.flash.jq.css({width:this.status.width,height:this.status.height})},_updateAutohide:function(){var a=this,b="mousemove.jPlayer",c=".jPlayerAutohide",d=b+c,e=function(b){var c,d,e=!1;"undefined"!=typeof a.internal.mouse?(c=a.internal.mouse.x-b.pageX,d=a.internal.mouse.y-b.pageY,e=Math.floor(c)>0||Math.floor(d)>0):e=!0,a.internal.mouse={x:b.pageX,y:b.pageY},e&&a.css.jq.gui.fadeIn(a.options.autohide.fadeIn,function(){clearTimeout(a.internal.autohideId),a.internal.autohideId=setTimeout(function(){a.css.jq.gui.fadeOut(a.options.autohide.fadeOut)},a.options.autohide.hold)})};this.css.jq.gui.length&&(this.css.jq.gui.stop(!0,!0),clearTimeout(this.internal.autohideId),delete this.internal.mouse,this.element.unbind(c),this.css.jq.gui.unbind(c),this.status.nativeVideoControls?this.css.jq.gui.hide():this.options.fullWindow&&this.options.autohide.full||!this.options.fullWindow&&this.options.autohide.restored?(this.element.bind(d,e),this.css.jq.gui.bind(d,e),this.css.jq.gui.hide()):this.css.jq.gui.show())},fullScreen:function(a){var b="object"==typeof a;b&&this.options.useStateClassSkin&&this.options.fullScreen?this._setOption("fullScreen",!1):this._setOption("fullScreen",!0)},restoreScreen:function(){this._setOption("fullScreen",!1)},_fullscreenAddEventListeners:function(){var b=this,c=a.jPlayer.nativeFeatures.fullscreen;c.api.fullscreenEnabled&&c.event.fullscreenchange&&("function"!=typeof this.internal.fullscreenchangeHandler&&(this.internal.fullscreenchangeHandler=function(){b._fullscreenchange()}),document.addEventListener(c.event.fullscreenchange,this.internal.fullscreenchangeHandler,!1))},_fullscreenRemoveEventListeners:function(){var b=a.jPlayer.nativeFeatures.fullscreen;this.internal.fullscreenchangeHandler&&document.removeEventListener(b.event.fullscreenchange,this.internal.fullscreenchangeHandler,!1)},_fullscreenchange:function(){this.options.fullScreen&&!a.jPlayer.nativeFeatures.fullscreen.api.fullscreenElement()&&this._setOption("fullScreen",!1)},_requestFullscreen:function(){var b=this.ancestorJq.length?this.ancestorJq[0]:this.element[0],c=a.jPlayer.nativeFeatures.fullscreen;c.used.webkitVideo&&(b=this.htmlElement.video),c.api.fullscreenEnabled&&c.api.requestFullscreen(b)},_exitFullscreen:function(){var b,c=a.jPlayer.nativeFeatures.fullscreen;c.used.webkitVideo&&(b=this.htmlElement.video),c.api.fullscreenEnabled&&c.api.exitFullscreen(b)},_html_initMedia:function(b){var c=a(this.htmlElement.media).empty();a.each(b.track||[],function(a,b){var d=document.createElement("track");d.setAttribute("kind",b.kind?b.kind:""),d.setAttribute("src",b.src?b.src:""),d.setAttribute("srclang",b.srclang?b.srclang:""),d.setAttribute("label",b.label?b.label:""),b.def&&d.setAttribute("default",b.def),c.append(d)}),this.htmlElement.media.src=this.status.src,"none"!==this.options.preload&&this._html_load(),this._trigger(a.jPlayer.event.timeupdate)},_html_setFormat:function(b){var c=this;a.each(this.formats,function(a,d){return c.html.support[d]&&b[d]?(c.status.src=b[d],c.status.format[d]=!0,c.status.formatType=d,!1):void 0})},_html_setAudio:function(a){this._html_setFormat(a),this.htmlElement.media=this.htmlElement.audio,this._html_initMedia(a)},_html_setVideo:function(a){this._html_setFormat(a),this.status.nativeVideoControls&&(this.htmlElement.video.poster=this._validString(a.poster)?a.poster:""),this.htmlElement.media=this.htmlElement.video,this._html_initMedia(a)},_html_resetMedia:function(){this.htmlElement.media&&(this.htmlElement.media.id!==this.internal.video.id||this.status.nativeVideoControls||this.internal.video.jq.css({width:"0px",height:"0px"}),this.htmlElement.media.pause())},_html_clearMedia:function(){this.htmlElement.media&&(this.htmlElement.media.src="about:blank",this.htmlElement.media.load())},_html_load:function(){this.status.waitForLoad&&(this.status.waitForLoad=!1,this.htmlElement.media.load()),clearTimeout(this.internal.htmlDlyCmdId)},_html_play:function(a){var b=this,c=this.htmlElement.media;if(this.androidFix.pause=!1,this._html_load(),this.androidFix.setMedia)this.androidFix.play=!0,this.androidFix.time=a;else if(isNaN(a))c.play();else{this.internal.cmdsIgnored&&c.play();try{if(c.seekable&&!("object"==typeof c.seekable&&c.seekable.length>0))throw 1;c.currentTime=a,c.play()}catch(d){return void(this.internal.htmlDlyCmdId=setTimeout(function(){b.play(a)},250))}}this._html_checkWaitForPlay()},_html_pause:function(a){var b=this,c=this.htmlElement.media;if(this.androidFix.play=!1,a>0?this._html_load():clearTimeout(this.internal.htmlDlyCmdId),c.pause(),this.androidFix.setMedia)this.androidFix.pause=!0,this.androidFix.time=a;else if(!isNaN(a))try{if(c.seekable&&!("object"==typeof c.seekable&&c.seekable.length>0))throw 1;c.currentTime=a}catch(d){return void(this.internal.htmlDlyCmdId=setTimeout(function(){b.pause(a)},250))}a>0&&this._html_checkWaitForPlay()},_html_playHead:function(a){var b=this,c=this.htmlElement.media;this._html_load();try{if("object"==typeof c.seekable&&c.seekable.length>0)c.currentTime=a*c.seekable.end(c.seekable.length-1)/100;else{if(!(c.duration>0)||isNaN(c.duration))throw"e";c.currentTime=a*c.duration/100}}catch(d){return void(this.internal.htmlDlyCmdId=setTimeout(function(){b.playHead(a)},250))}this.status.waitForLoad||this._html_checkWaitForPlay()},_html_checkWaitForPlay:function(){this.status.waitForPlay&&(this.status.waitForPlay=!1,this.css.jq.videoPlay.length&&this.css.jq.videoPlay.hide(),this.status.video&&(this.internal.poster.jq.hide(),this.internal.video.jq.css({width:this.status.width,height:this.status.height})))},_html_setProperty:function(a,b){this.html.audio.available&&(this.htmlElement.audio[a]=b),this.html.video.available&&(this.htmlElement.video[a]=b)},_aurora_setAudio:function(b){var c=this;a.each(this.formats,function(a,d){return c.aurora.support[d]&&b[d]?(c.status.src=b[d],c.status.format[d]=!0,c.status.formatType=d,!1):void 0}),this.aurora.player=new AV.Player.fromURL(this.status.src),this._addAuroraEventListeners(this.aurora.player,this.aurora),"auto"===this.options.preload&&(this._aurora_load(),this.status.waitForLoad=!1)},_aurora_resetMedia:function(){this.aurora.player&&this.aurora.player.stop()},_aurora_clearMedia:function(){},_aurora_load:function(){this.status.waitForLoad&&(this.status.waitForLoad=!1,this.aurora.player.preload())},_aurora_play:function(b){this.status.waitForLoad||isNaN(b)||this.aurora.player.seek(b),this.aurora.player.playing||this.aurora.player.play(),this.status.waitForLoad=!1,this._aurora_checkWaitForPlay(),this._updateButtons(!0),this._trigger(a.jPlayer.event.play)},_aurora_pause:function(b){isNaN(b)||this.aurora.player.seek(1e3*b),this.aurora.player.pause(),b>0&&this._aurora_checkWaitForPlay(),this._updateButtons(!1),this._trigger(a.jPlayer.event.pause)},_aurora_playHead:function(a){this.aurora.player.duration>0&&this.aurora.player.seek(a*this.aurora.player.duration/100),this.status.waitForLoad||this._aurora_checkWaitForPlay()},_aurora_checkWaitForPlay:function(){this.status.waitForPlay&&(this.status.waitForPlay=!1)},_aurora_volume:function(a){this.aurora.player.volume=100*a},_aurora_mute:function(a){a?(this.aurora.properties.lastvolume=this.aurora.player.volume,this.aurora.player.volume=0):this.aurora.player.volume=this.aurora.properties.lastvolume,this.aurora.properties.muted=a},_flash_setAudio:function(b){var c=this;try{a.each(this.formats,function(a,d){if(c.flash.support[d]&&b[d]){switch(d){case"m4a":case"fla":c._getMovie().fl_setAudio_m4a(b[d]);break;case"mp3":c._getMovie().fl_setAudio_mp3(b[d]);break;case"rtmpa":c._getMovie().fl_setAudio_rtmp(b[d])}return c.status.src=b[d],c.status.format[d]=!0,c.status.formatType=d,!1}}),"auto"===this.options.preload&&(this._flash_load(),this.status.waitForLoad=!1)}catch(d){this._flashError(d)}},_flash_setVideo:function(b){var c=this;try{a.each(this.formats,function(a,d){if(c.flash.support[d]&&b[d]){switch(d){case"m4v":case"flv":c._getMovie().fl_setVideo_m4v(b[d]);break;case"rtmpv":c._getMovie().fl_setVideo_rtmp(b[d])}return c.status.src=b[d],c.status.format[d]=!0,c.status.formatType=d,!1}}),"auto"===this.options.preload&&(this._flash_load(),this.status.waitForLoad=!1)}catch(d){this._flashError(d)}},_flash_resetMedia:function(){this.internal.flash.jq.css({width:"0px",height:"0px"}),this._flash_pause(0/0)},_flash_clearMedia:function(){try{this._getMovie().fl_clearMedia()}catch(a){this._flashError(a)}},_flash_load:function(){try{this._getMovie().fl_load()}catch(a){this._flashError(a)}this.status.waitForLoad=!1},_flash_play:function(a){try{this._getMovie().fl_play(a)}catch(b){this._flashError(b)}this.status.waitForLoad=!1,this._flash_checkWaitForPlay()},_flash_pause:function(a){try{this._getMovie().fl_pause(a)}catch(b){this._flashError(b)}a>0&&(this.status.waitForLoad=!1,this._flash_checkWaitForPlay())},_flash_playHead:function(a){try{this._getMovie().fl_play_head(a)}catch(b){this._flashError(b)}this.status.waitForLoad||this._flash_checkWaitForPlay()},_flash_checkWaitForPlay:function(){this.status.waitForPlay&&(this.status.waitForPlay=!1,this.css.jq.videoPlay.length&&this.css.jq.videoPlay.hide(),this.status.video&&(this.internal.poster.jq.hide(),this.internal.flash.jq.css({width:this.status.width,height:this.status.height})))},_flash_volume:function(a){try{this._getMovie().fl_volume(a)}catch(b){this._flashError(b)}},_flash_mute:function(a){try{this._getMovie().fl_mute(a)}catch(b){this._flashError(b)}},_getMovie:function(){return document[this.internal.flash.id]},_getFlashPluginVersion:function(){var a,b=0;if(window.ActiveXObject)try{if(a=new ActiveXObject("ShockwaveFlash.ShockwaveFlash")){var c=a.GetVariable("$version");c&&(c=c.split(" ")[1].split(","),b=parseInt(c[0],10)+"."+parseInt(c[1],10))}}catch(d){}else navigator.plugins&&navigator.mimeTypes.length>0&&(a=navigator.plugins["Shockwave Flash"],a&&(b=navigator.plugins["Shockwave Flash"].description.replace(/.*\s(\d+\.\d+).*/,"$1")));return 1*b},_checkForFlash:function(a){var b=!1;return this._getFlashPluginVersion()>=a&&(b=!0),b},_validString:function(a){return a&&"string"==typeof a},_limitValue:function(a,b,c){return b>a?b:a>c?c:a},_urlNotSetError:function(b){this._error({type:a.jPlayer.error.URL_NOT_SET,context:b,message:a.jPlayer.errorMsg.URL_NOT_SET,hint:a.jPlayer.errorHint.URL_NOT_SET})},_flashError:function(b){var c;c=this.internal.ready?"FLASH_DISABLED":"FLASH",this._error({type:a.jPlayer.error[c],context:this.internal.flash.swf,message:a.jPlayer.errorMsg[c]+b.message,hint:a.jPlayer.errorHint[c]}),this.internal.flash.jq.css({width:"1px",height:"1px"})},_error:function(b){this._trigger(a.jPlayer.event.error,b),this.options.errorAlerts&&this._alert("Error!"+(b.message?"\n"+b.message:"")+(b.hint?"\n"+b.hint:"")+"\nContext: "+b.context)},_warning:function(c){this._trigger(a.jPlayer.event.warning,b,c),this.options.warningAlerts&&this._alert("Warning!"+(c.message?"\n"+c.message:"")+(c.hint?"\n"+c.hint:"")+"\nContext: "+c.context)},_alert:function(a){var b="jPlayer "+this.version.script+" : id='"+this.internal.self.id+"' : "+a;this.options.consoleAlerts?window.console&&window.console.log&&window.console.log(b):alert(b)},_emulateHtmlBridge:function(){var b=this;a.each(a.jPlayer.emulateMethods.split(/\s+/g),function(a,c){b.internal.domNode[c]=function(a){b[c](a)}}),a.each(a.jPlayer.event,function(c,d){var e=!0;a.each(a.jPlayer.reservedEvent.split(/\s+/g),function(a,b){return b===c?(e=!1,!1):void 0}),e&&b.element.bind(d+".jPlayer.jPlayerHtml",function(){b._emulateHtmlUpdate();var a=document.createEvent("Event");a.initEvent(c,!1,!0),b.internal.domNode.dispatchEvent(a)})})},_emulateHtmlUpdate:function(){var b=this;a.each(a.jPlayer.emulateStatus.split(/\s+/g),function(a,c){b.internal.domNode[c]=b.status[c]}),a.each(a.jPlayer.emulateOptions.split(/\s+/g),function(a,c){b.internal.domNode[c]=b.options[c]})},_destroyHtmlBridge:function(){var b=this;this.element.unbind(".jPlayerHtml");var c=a.jPlayer.emulateMethods+" "+a.jPlayer.emulateStatus+" "+a.jPlayer.emulateOptions;a.each(c.split(/\s+/g),function(a,c){delete b.internal.domNode[c]})}},a.jPlayer.error={FLASH:"e_flash",FLASH_DISABLED:"e_flash_disabled",NO_SOLUTION:"e_no_solution",NO_SUPPORT:"e_no_support",URL:"e_url",URL_NOT_SET:"e_url_not_set",VERSION:"e_version"},a.jPlayer.errorMsg={FLASH:"jPlayer's Flash fallback is not configured correctly, or a command was issued before the jPlayer Ready event. Details: ",FLASH_DISABLED:"jPlayer's Flash fallback has been disabled by the browser due to the CSS rules you have used. Details: ",NO_SOLUTION:"No solution can be found by jPlayer in this browser. Neither HTML nor Flash can be used.",NO_SUPPORT:"It is not possible to play any media format provided in setMedia() on this browser using your current options.",URL:"Media URL could not be loaded.",URL_NOT_SET:"Attempt to issue media playback commands, while no media url is set.",VERSION:"jPlayer "+a.jPlayer.prototype.version.script+" needs Jplayer.swf version "+a.jPlayer.prototype.version.needFlash+" but found "},a.jPlayer.errorHint={FLASH:"Check your swfPath option and that Jplayer.swf is there.",FLASH_DISABLED:"Check that you have not display:none; the jPlayer entity or any ancestor.",NO_SOLUTION:"Review the jPlayer options: support and supplied.",NO_SUPPORT:"Video or audio formats defined in the supplied option are missing.",URL:"Check media URL is valid.",URL_NOT_SET:"Use setMedia() to set the media URL.",VERSION:"Update jPlayer files."},a.jPlayer.warning={CSS_SELECTOR_COUNT:"e_css_selector_count",CSS_SELECTOR_METHOD:"e_css_selector_method",CSS_SELECTOR_STRING:"e_css_selector_string",OPTION_KEY:"e_option_key"},a.jPlayer.warningMsg={CSS_SELECTOR_COUNT:"The number of css selectors found did not equal one: ",CSS_SELECTOR_METHOD:"The methodName given in jPlayer('cssSelector') is not a valid jPlayer method.",CSS_SELECTOR_STRING:"The methodCssSelector given in jPlayer('cssSelector') is not a String or is empty.",OPTION_KEY:"The option requested in jPlayer('option') is undefined."},a.jPlayer.warningHint={CSS_SELECTOR_COUNT:"Check your css selector and the ancestor.",CSS_SELECTOR_METHOD:"Check your method name.",CSS_SELECTOR_STRING:"Check your css selector is a string.",OPTION_KEY:"Check your option name."}});
$('input', container).val($(this).val());
$('.is-active', container).removeClass('is-active');
$(this).closest('.l-checkout__payments__box').addClass('is-active');
- $('#kwota').val('');
- return false;
+ $('#id_custom_amount').val('');
+ });
+
+ $('#id_custom_amount').on('input', function() {
+ if ($(this).val() > 0) {
+ $('.l-checkout__payments__box.is-active').removeClass('is-active');
+ } else {
+ $('.l-checkout__payments__box.initial-active').addClass('is-active');
+ }
+ });
+
+ $('.donation-mod-monthly').on('click', function() {
+ $.ajax({
+ method: 'POST',
+ data: {
+ csrfmiddlewaretoken: $("[name=csrfmiddlewaretoken]").val(),
+ monthly: $(this).data('monthly'),
+ },
+ url: $(this).data('url'),
+ success: function(data) {
+ if ($(".q-reload-is-monthly").length) {
+ window.location.reload()
+ } else {
+ $(".q-is-monthly").toggleClass('is-monthly', data.monthly);
+ }
+ }
+ });
+ return false;
});
})();
$.post({
url: '/ludzie/lektura/' + $(this).attr('data-book-slug') + '/nie_lubie/',
data: {'csrfmiddlewaretoken': $('[name=csrfmiddlewaretoken]').val()},
+ dataType: 'json',
success: function() {
delete state.liked[$btn.attr('data-book')];
updateLiked($btn);
+ },
+ error: function() {
+ window.location.href = $('#login-link').attr('href');
}
})
} else {
$.post({
url: '/ludzie/lektura/' + $(this).attr('data-book-slug') + '/lubie/',
data: {'csrfmiddlewaretoken': $('[name=csrfmiddlewaretoken]').val()},
+ dataType: 'json',
success: function() {
- state.liked[$btn.attr('data-book')] = [];
+ state.liked[$btn.attr('data-book')] = [{'favorites': true}];
updateLiked($btn);
},
- error: function(e) {
- if (e.status == 403) {
- $('#login-link').click();
- }
- },
+ error: function() {
+ window.location.href = $('#login-link').attr('href')
+ }
});
}
})
function updateLiked(e) {
let bookId = $(e).attr('data-book');
- let liked = bookId in state.liked;
- $(e).toggleClass('icon-liked', liked);
+ let liked = false;
+
let $bookContainer = $('.book-container-' + bookId);
- $bookContainer.toggleClass('book-liked', liked);
let $sets = $(".sets", $bookContainer);
$sets.empty();
$.each(state.liked[bookId], (i,e) => {
- let $set = $("<span>");
- $set.attr("data-set", e.slug);
- let $setA = $("<a>").appendTo($set);
- $setA.attr("href", e.url);
- $setA.text(e.name);
- let $setX = $("<a class='close'></a>").appendTo($set);
- $sets.append($set);
+ if (e.favorites) {
+ liked = true;
+ } else {
+ let $set = $("<span>");
+ $set.attr("data-set", e.slug);
+ let $setA = $("<a>").appendTo($set);
+ $setA.attr("href", e.url);
+ $setA.text(e.name);
+ let $setX = $("<a class='close'></a>").appendTo($set);
+ $sets.append($set);
+ }
});
+
+ $(e).toggleClass('icon-liked', liked);
+ $bookContainer.toggleClass('book-liked', liked);
}
})();
<form action="{% url 'import_book' %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
<p>
- <input type="file" id="id_book_xml_file" name="book_xml_file"/>
+ <input type="file" id="id_book_xml_file" name="book_xml_file" required/>
<input type="submit" value="Import książki" />
</p>
</form>
{% load preview_ad from catalogue_tags %}
{% annoy_banner_crisis %}
+{% annoy_banner_top %}
{% annoy_banner_blackout %}
</ul>
</div>
<a href="{% url 'user_settings' %}" class="user">
- {% if request.user.is_staffs %}
+ {% if request.user.is_staff %}
<img src="{% static '2022/images/icons/user-staff.svg' %}">
{% elif request.user.membership %}
<img src="{% static '2022/images/icons/user-vip.svg' %}">
{% else %}
<img src="{% static '2022/images/icons/user.svg' %}">
{% endif %}
- </a>
+ </a>
{% else %}
<div class="l-navigation__login">
<a id="login-link" href='{% url 'login' %}?next={{ request.path }}'>{% trans "Zaloguj się" %}</a>
<a href='{% url 'register' %}?next={{ request.path }}'>{% trans "Załóż konto" %}</a>
</div>
{% endif %}
+
+<a href="/pomagam/?pk_campaign=menubar" class="menubar-donate">Wspieram!</a>
+