Compare commits

...

58 commits

Author SHA1 Message Date
j
79da1115aa Merge branch 'master' into stable 2025-03-09 17:59:57 +01:00
j
3f3fa9ab2a reset itemFind 2025-02-27 21:48:23 +01:00
j
c879d49d84 include clip details in edit 2025-02-11 11:48:12 +01:00
j
194b286e30 fix keys=None 2025-02-06 15:06:27 +00:00
j
ba71665bc5 default to single format, patents have expired. 2025-02-02 11:47:20 +05:30
j
0da9097a6a use first format by default 2025-01-31 12:33:34 +05:30
j
017d9be45a add .tiff as supported image format 2025-01-31 10:40:04 +05:30
j
9eb9fbe3a5 cTA 2025-01-30 19:09:43 +05:30
j
d0d1feca18 only set annotation if it exists 2025-01-30 17:00:02 +05:30
j
3cf3ba5c6f fix multipart items 2025-01-30 10:15:41 +05:30
j
5591739531 fix seek for items 2025-01-30 08:53:25 +05:30
j
7fe51fe7ac fix featured edits 2025-01-30 00:15:07 +05:30
j
406237b837 support more image formats, server avif is supported for larger images 2025-01-29 19:44:28 +05:30
j
0748265fe6 reload editor after transcribtion is ready 2025-01-29 19:43:14 +05:30
j
ef680080cf fix end of video 2025-01-29 16:55:34 +05:30
j
7eae1cf6e5 open task dialog 2025-01-25 10:46:39 +05:30
j
8020e5966e remove debug 2025-01-25 10:30:00 +05:30
j
3f0e08a142 keep transcribing taks status 2025-01-25 10:28:53 +05:30
j
d7fbea4a20 avoid loop while selecting multiple annotations 2025-01-25 10:25:06 +05:30
j
c391d7f4fa better delete anntoation dialog 2025-01-25 10:24:24 +05:30
j
34d1285e4b transcribe audio dialog 2025-01-24 19:54:18 +05:30
j
c67f7c122b non expistent capabilities are not available 2025-01-24 19:53:59 +05:30
j
53502a27ab processing, fix first word in sentence 2025-01-24 18:06:39 +05:30
j
a96097b8bf pass function, don't call it 2025-01-24 17:33:52 +05:30
j
e37281491e typo 2025-01-24 17:31:51 +05:30
j
722119215d fix loading media view 2025-01-24 17:30:31 +05:30
j
212ad3cee0 add confirm dialog while deleting annotations 2025-01-24 17:12:29 +05:30
j
277bbe45fb fix dialog keys 2025-01-24 17:12:03 +05:30
j
a3336d92b3 title or id is required to move files 2025-01-24 15:19:09 +05:30
j
c2df43220b only update if poster is valid 2025-01-24 14:56:11 +05:30
j
85b88c4dd6 work around invalid directors 2025-01-24 14:55:58 +05:30
j
e5339fd297 close ipmort/export annotation after action 2025-01-23 16:34:31 +05:30
j
38618e2ed2 add folder/filename columns to media view 2025-01-23 16:26:25 +05:30
j
10c78fc862 allow selecting multiple annotations to get in/out range 2025-01-22 17:42:44 +05:30
j
dd8ea22d45 preview new timecode 2024-12-14 20:15:47 +00:00
j
85eba0bf09 round seek element 2024-12-14 19:46:20 +00:00
j
627a016515 better mobile seek bar 2024-12-14 19:16:41 +00:00
j
ad2af2a257 cleanup html 2024-12-14 19:01:17 +00:00
j
3bc2b1bd3e use preferred_username and fallback to name 2024-11-09 17:02:57 +00:00
j
5c6c7e37c7 remove debug 2024-11-09 16:59:47 +00:00
j
7cfe645ab7 oidc logout is same as our logout, no need for special case 2024-11-09 16:48:46 +00:00
j
ff236e8828 only add oidc urls if oidc is enabled 2024-11-08 12:47:47 +00:00
j
34af2b1fab add ocid based login 2024-11-08 12:29:35 +00:00
j
d83309d4cd ff 2024-11-07 16:28:18 +00:00
j
59c2045ac6 move first signup code into function for reuse 2024-11-07 15:57:16 +00:00
j
a24d96b098 log invalid api requests 2024-11-07 14:28:28 +00:00
j
03daede441 pass download 2024-10-18 17:49:54 +01:00
j
9e6ecb5459 avoid people accidentally adding itesm to current video 2024-10-18 15:57:10 +01:00
j
e7ede6ade0 fix sort 2024-10-13 17:20:33 +01:00
j
f4bfe9294b reuse getSortValue 2024-10-13 17:10:11 +01:00
j
c69ca372ee sort annotations 2024-10-13 17:06:15 +01:00
j
9e00dd09e3 allow adding global yt-dlp flags 2024-10-09 17:49:21 +01:00
j
7cc1504069 fix isClipsQuery check 2024-09-16 20:51:37 +01:00
j
d5ace7aeca use pandoractl to update db 2024-09-16 17:38:48 +01:00
j
1b1442e664 might not be loaded 2024-09-16 17:23:21 +01:00
j
18cbf0ec9c print pandora versions during update 2024-09-16 09:06:10 +01:00
j
a8aa619217 stop using ppa, use deb repository from code.0x2620.org instead 2024-09-11 23:01:57 +01:00
j
ff1c929d4d don't checkout oxtimelines twice 2024-09-11 15:05:21 +01:00
54 changed files with 728 additions and 248 deletions

3
ctl
View file

@ -30,9 +30,6 @@ if [ "$action" = "init" ]; then
$SUDO git clone -b $branch https://code.0x2620.org/0x2620/oxjs.git static/oxjs
fi
$SUDO mkdir -p src
if [ ! -d src/oxtimelines ]; then
$SUDO git clone -b $branch https://code.0x2620.org/0x2620/oxtimelines.git src/oxtimelines
fi
for package in oxtimelines python-ox; do
cd ${BASE}
if [ ! -d src/${package} ]; then

36
pandora/app/oidc.py Normal file
View file

@ -0,0 +1,36 @@
import unicodedata
from django.contrib.auth import get_user_model
import mozilla_django_oidc.auth
from user.utils import prepare_user
User = get_user_model()
class OIDCAuthenticationBackend(mozilla_django_oidc.auth.OIDCAuthenticationBackend):
def create_user(self, claims):
user = super(OIDCAuthenticationBackend, self).create_user(claims)
username = None
for key in ('preferred_username', 'name'):
if claims.get(key):
username = claims[key]
break
n = 1
if username and username != user.username:
uname = username
while User.objects.filter(username=uname).exclude(id=user.id).exists():
n += 1
uname = '%s (%s)' % (username, n)
user.username = uname
user.save()
prepare_user(user)
return user
def update_user(self, user, claims):
return user
def generate_username(email):
return unicodedata.normalize('NFKC', email)[:150]

View file

@ -184,6 +184,7 @@ def init(request, data):
except:
pass
config['site']['oidc'] = bool(getattr(settings, 'OIDC_RP_CLIENT_ID', False))
response['data']['site'] = config
response['data']['user'] = init_user(request.user, request)
request.session['last_init'] = str(datetime.now())

View file

@ -40,8 +40,12 @@ info_key_map = {
'display_id': 'id',
}
YT_DLP = ['yt-dlp']
if settings.YT_DLP_EXTRA:
YT_DLP += settings.YT_DLP_EXTRA
def get_info(url, referer=None):
cmd = ['yt-dlp', '-j', '--all-subs', url]
cmd = YT_DLP + ['-j', '--all-subs', url]
if referer:
cmd += ['--referer', referer]
p = subprocess.Popen(cmd,
@ -93,7 +97,7 @@ def add_subtitles(item, media, tmp):
sub.save()
def load_formats(url):
cmd = ['yt-dlp', '-q', url, '-j', '-F']
cmd = YT_DLP + ['-q', url, '-j', '-F']
p = subprocess.Popen(cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE, close_fds=True)
@ -112,7 +116,7 @@ def download(item_id, url, referer=None):
if isinstance(tmp, bytes):
tmp = tmp.decode('utf-8')
os.chdir(tmp)
cmd = ['yt-dlp', '-q', media['url']]
cmd = YT_DLP + ['-q', media['url']]
if referer:
cmd += ['--referer', referer]
elif 'referer' in media:

View file

@ -19,10 +19,13 @@ import ox.image
from ox.utils import json
from django.conf import settings
from PIL import Image, ImageOps
import pillow_avif
from pillow_heif import register_heif_opener
from .chop import Chop, make_keyframe_index
register_heif_opener()
logger = logging.getLogger('pandora.' + __name__)
img_extension = 'jpg'

View file

@ -0,0 +1,32 @@
# Generated by Django 4.2.7 on 2025-01-23 10:48
from django.db import migrations, models
def update_path(apps, schema_editor):
File = apps.get_model("archive", "File")
for file in File.objects.all():
if file.path:
parts = file.path.split('/')
file.filename = parts.pop()
file.folder = '/'.join(parts)
file.save()
class Migration(migrations.Migration):
dependencies = [
('archive', '0007_stream_archive_str_file_id_69a542_idx'),
]
operations = [
migrations.AddField(
model_name='file',
name='filename',
field=models.CharField(default='', max_length=2048),
),
migrations.AddField(
model_name='file',
name='folder',
field=models.CharField(default='', max_length=2048),
),
migrations.RunPython(update_path),
]

View file

@ -53,6 +53,9 @@ class File(models.Model):
path = models.CharField(max_length=2048, default="") # canoncial path/file
sort_path = models.CharField(max_length=2048, default="") # sort name
folder = models.CharField(max_length=2048, default="")
filename = models.CharField(max_length=2048, default="")
type = models.CharField(default="", max_length=255)
# editable
@ -194,6 +197,13 @@ class File(models.Model):
data['part'] = str(data['part'])
return data
def update_path(self):
path = self.normalize_path()
parts = path.split('/')
self.filename = parts.pop()
self.folder = '/'.join(parts)
return path
def normalize_path(self):
# FIXME: always use format_path
if settings.CONFIG['site']['folderdepth'] == 4:
@ -257,7 +267,7 @@ class File(models.Model):
update_path = False
if self.info:
if self.id:
self.path = self.normalize_path()
self.path = self.update_path()
else:
update_path = True
if self.item:
@ -290,7 +300,7 @@ class File(models.Model):
self.streams.filter(source=None, available=True).count()
super(File, self).save(*args, **kwargs)
if update_path:
self.path = self.normalize_path()
self.path = self.update_path()
super(File, self).save(*args, **kwargs)
def get_path(self, name):
@ -474,6 +484,9 @@ class File(models.Model):
'videoCodec': self.video_codec,
'wanted': self.wanted,
}
for key in ('folder', 'filename'):
if keys and key in keys:
data[key] = getattr(self, key)
if error:
data['error'] = error
for key in self.PATH_INFO:
@ -540,7 +553,7 @@ class File(models.Model):
def process_stream(self):
'''
extract derivatives from webm upload
extract derivatives from stream upload
'''
from . import tasks
return tasks.process_stream.delay(self.id)

View file

@ -56,6 +56,7 @@
"canExportAnnotations": {"friend": true, "staff": true, "admin": true},
"canImportAnnotations": {"staff": true, "admin": true},
"canImportItems": {},
"canTranscribeAudio": {},
"canManageDocuments": {"staff": true, "admin": true},
"canManageEntities": {"staff": true, "admin": true},
"canManageHome": {},
@ -1401,7 +1402,7 @@
240, 288, 360, 432, 480, 720 and 1080.
*/
"video": {
"formats": ["webm", "mp4"],
"formats": ["mp4"],
// fixme: this should be named "ratio" or "defaultRatio",
// as it also applies to clip lists (on load)
"previewRatio": 1.7777777778,

View file

@ -58,6 +58,7 @@
"canImportAnnotations": {"researcher": true, "staff": true, "admin": true},
// import needs to handle itemRequiresVideo=false first
"canImportItems": {},
"canTranscribeAudio": {},
"canManageDocuments": {"member": true, "researcher": true, "staff": true, "admin": true},
"canManageEntities": {"member": true, "researcher": true, "staff": true, "admin": true},
"canManageHome": {"staff": true, "admin": true},
@ -1887,7 +1888,7 @@
240, 288, 360, 432, 480, 720 and 1080.
*/
"video": {
"formats": ["webm", "mp4"],
"formats": ["mp4"],
"previewRatio": 1.375,
"resolutions": [240, 480]
}

View file

@ -56,6 +56,7 @@
"canExportAnnotations": {"member": true, "staff": true, "admin": true},
"canImportAnnotations": {"member": true, "staff": true, "admin": true},
"canImportItems": {"member": true, "staff": true, "admin": true},
"canTranscribeAudio": {"staff": true, "admin": true},
"canManageDocuments": {"member": true, "staff": true, "admin": true},
"canManageEntities": {"member": true, "staff": true, "admin": true},
"canManageHome": {"staff": true, "admin": true},
@ -1391,7 +1392,7 @@
240, 288, 360, 432, 480, 720 and 1080.
*/
"video": {
"formats": ["webm", "mp4"],
"formats": ["mp4"],
"previewRatio": 1.3333333333,
//supported resolutions are
//1080, 720, 480, 432, 360, 288, 240, 144, 96

View file

@ -63,6 +63,7 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution.
"canExportAnnotations": {"member": true, "staff": true, "admin": true},
"canImportAnnotations": {"member": true, "staff": true, "admin": true},
"canImportItems": {"member": true, "staff": true, "admin": true},
"canTranscribeAudio": {},
"canManageDocuments": {"member": true, "staff": true, "admin": true},
"canManageEntities": {"member": true, "staff": true, "admin": true},
"canManageHome": {"staff": true, "admin": true},
@ -1280,8 +1281,8 @@ examples (config.SITENAME.jsonc) that are part of this pan.do/ra distribution.
240, 288, 360, 432, 480, 720 and 1080.
*/
"video": {
"downloadFormat": "webm",
"formats": ["webm", "mp4"],
"downloadFormat": "mp4",
"formats": ["mp4"],
"previewRatio": 1.3333333333,
"resolutions": [240, 480]
}

View file

@ -8,6 +8,7 @@ from django.conf import settings
logger = logging.getLogger('pandora.' + __name__)
IMAGE_EXTENSIONS = ('png', 'jpg', 'webp', 'heic', 'heif')
def extract_text(pdf, page=None):
if page is not None:
@ -53,7 +54,7 @@ class FulltextMixin:
if self.file:
if self.extension == 'pdf':
return extract_text(self.file.path)
elif self.extension in ('png', 'jpg'):
elif self.extension in IMAGE_EXTENSIONS:
return ocr_image(self.file.path)
elif self.extension == 'html':
return self.data.get('text', '')
@ -180,7 +181,7 @@ class FulltextPageMixin(FulltextMixin):
if self.document.file:
if self.document.extension == 'pdf':
return extract_text(self.document.file.path, self.page)
elif self.extension in ('png', 'jpg'):
elif self.extension in IMAGE_EXTENSIONS:
return ocr_image(self.document.file.path)
elif self.extension == 'html':
# FIXME: is there a nice way to split that into pages

View file

@ -521,14 +521,17 @@ class Document(models.Model, FulltextMixin):
return save_chunk(self, self.file, chunk, offset, name, done_cb)
return False, 0
def thumbnail(self, size=None, page=None):
def thumbnail(self, size=None, page=None, accept=None):
if not self.file:
return os.path.join(settings.STATIC_ROOT, 'png/document.png')
src = self.file.path
folder = os.path.dirname(src)
if size:
size = int(size)
path = os.path.join(folder, '%d.jpg' % size)
ext = 'jpg'
if accept and 'image/avif' in accept and size > 512:
ext = 'avif'
path = os.path.join(folder, '%d.%s' % (size, ext))
else:
path = src
if self.extension == 'pdf':
@ -561,7 +564,7 @@ class Document(models.Model, FulltextMixin):
path = os.path.join(folder, '%dp%d,%s.jpg' % (size, page, ','.join(map(str, crop))))
if not os.path.exists(path):
resize_image(src, path, size=size)
elif self.extension in ('jpg', 'png', 'gif'):
elif self.extension in ('jpg', 'png', 'gif', 'webp', 'heic', 'heif'):
if os.path.exists(src):
if size and page:
crop = list(map(int, page.split(',')))
@ -574,7 +577,7 @@ class Document(models.Model, FulltextMixin):
img = open_image_rgb(path)
src = path
if size < max(img.size):
path = os.path.join(folder, '%sp%s.jpg' % (size, ','.join(map(str, crop))))
path = os.path.join(folder, '%sp%s.%s' % (size, ','.join(map(str, crop)), ext))
if not os.path.exists(path):
resize_image(src, path, size=size)
if os.path.exists(src) and not os.path.exists(path):

View file

@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
from glob import glob
import mimetypes
import os
import re
from glob import glob
import unicodedata
import ox
@ -378,15 +379,24 @@ actions.register(sortDocuments, cache=False)
def file(request, id, name=None):
document = get_document_or_404_json(request, id)
accept = request.headers.get("Accept")
mime_type = mimetypes.guess_type(document.file.path)[0]
mime_type = 'image/%s' % document.extension
if accept and 'image/' in accept and document.extension in (
'webp', 'heif', 'heic', 'avif', 'tiff'
) and mime_type not in accept:
image_size = max(document.width, document.height)
return HttpFileResponse(document.thumbnail(image_size, accept=accept))
return HttpFileResponse(document.file.path)
def thumbnail(request, id, size=256, page=None):
size = int(size)
document = get_document_or_404_json(request, id)
accept = request.headers.get("Accept")
if "q" in request.GET and page:
img = document.highlight_page(page, request.GET["q"], size)
return HttpResponse(img, content_type="image/jpeg")
return HttpFileResponse(document.thumbnail(size, page=page))
return HttpFileResponse(document.thumbnail(size, page=page, accept=accept))
@login_required_json

View file

@ -495,6 +495,9 @@ class Clip(models.Model):
'id': self.get_id(),
'index': self.index,
'volume': self.volume,
'hue': self.hue,
'saturation': self.saturation,
'lightness': self.lightness,
}
if self.annotation:
data['annotation'] = self.annotation.public_id

View file

@ -1375,7 +1375,7 @@ class Item(models.Model):
self.poster_height = self.poster.height
self.poster_width = self.poster.width
self.clear_poster_cache(self.poster.path)
if self.cache.get('posterRatio') != self.poster_width / self.poster_height:
if self.poster_width and self.cache.get('posterRatio') != self.poster_width / self.poster_height:
self.update_cache(poster_width=self.poster_width,
poster_height=self.poster_height)

View file

@ -1004,7 +1004,9 @@ def download_source(request, id, part=None):
response['Content-Disposition'] = "attachment; filename*=UTF-8''%s" % quote(filename.encode('utf-8'))
return response
def download(request, id, resolution=None, format='webm', part=None):
def download(request, id, resolution=None, format=None, part=None):
if format is None:
format = settings.CONFIG['video']['formats'][0]
item = get_object_or_404(models.Item, public_id=id)
if not resolution or int(resolution) not in settings.CONFIG['video']['resolutions']:
resolution = max(settings.CONFIG['video']['resolutions'])

View file

@ -34,8 +34,15 @@ def api(request):
return response
if request.META.get('CONTENT_TYPE') == 'application/json':
r = json.loads(request.body.decode('utf-8'))
action = r['action']
data = r.get('data', {})
if 'action' not in r:
logger.error("invalid api request: %s", r)
response = render_to_json_response(json_response(status=400,
text='Invalid request'))
response['Access-Control-Allow-Origin'] = '*'
return response
else:
action = r['action']
data = r.get('data', {})
else:
action = request.POST['action']
data = json.loads(request.POST.get('data', '{}'))

View file

@ -111,6 +111,7 @@ ROOT_URLCONF = 'urls'
INSTALLED_APPS = (
'django.contrib.auth',
'mozilla_django_oidc',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
@ -158,6 +159,27 @@ INSTALLED_APPS = (
)
AUTH_USER_MODEL = 'system.User'
AUTH_PROFILE_MODULE = 'user.UserProfile'
AUTH_CHECK_USERNAME = True
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
)
# OpenID Connect login support
LOGIN_REDIRECT_URL = "/grid"
LOGOUT_REDIRECT_URL = "/grid"
OIDC_USERNAME_ALGO = "app.oidc.generate_username"
OIDC_RP_CLIENT_ID = None
# define those in local_settings to enable OCID based login
#OIDC_RP_CLIENT_ID = '<client id>'
#OIDC_RP_CLIENT_SECRET = '<client secret>'
#OIDC_RP_SIGN_ALGO = "RS256"
#OIDC_OP_JWKS_ENDPOINT = "<jwks endpoint>"
#OIDC_OP_AUTHORIZATION_ENDPOINT = "<authorization endpoint>"
#OIDC_OP_TOKEN_ENDPOINT = "<token endpoint>"
#OIDC_OP_USER_ENDPOINT = "<user endpoint>"
# Log errors into db
LOGGING = {
@ -193,8 +215,6 @@ CACHES = {
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
AUTH_PROFILE_MODULE = 'user.UserProfile'
AUTH_CHECK_USERNAME = True
FFMPEG = 'ffmpeg'
FFPROBE = 'ffprobe'
USE_VP9 = True
@ -291,6 +311,8 @@ DATA_UPLOAD_MAX_MEMORY_SIZE = 32 * 1024 * 1024
EMPTY_CLIPS = True
YT_DLP_EXTRA = []
#you can ignore things below this line
#=========================================================================
LOCAL_APPS = []
@ -321,3 +343,7 @@ except NameError:
INSTALLED_APPS = tuple(list(INSTALLED_APPS) + LOCAL_APPS)
if OIDC_RP_CLIENT_ID:
AUTHENTICATION_BACKENDS = list(AUTHENTICATION_BACKENDS) + [
'app.oidc.OIDCAuthenticationBackend'
]

View file

@ -88,6 +88,9 @@ class Task(models.Model):
except Item.DoesNotExist:
return False
if self.status == 'transcribing':
return False
if self.item.files.filter(wanted=True, available=False).count():
status = 'pending'
elif self.item.files.filter(uploading=True).count():

View file

@ -22,8 +22,8 @@
{% endif %}
<meta property="og:site_name" content="{{ settings.SITENAME }}"/>
{% compress css file m %}
<link rel="stylesheet" href="{% static 'mobile/css/reset.css' %}"></link>
<link rel="stylesheet" href="{% static 'mobile/css/style.css' %}"></link>
<link rel="stylesheet" href="{% static 'mobile/css/reset.css' %}">
<link rel="stylesheet" href="{% static 'mobile/css/style.css' %}">
{% endcompress %}
<meta name="google" value="notranslate"/>
</head>

View file

@ -2,7 +2,7 @@
import os
import importlib
from django.urls import path, re_path
from django.urls import path, re_path, include
from oxdjango.http import HttpFileResponse
from django.conf import settings
@ -33,9 +33,15 @@ import urlalias.views
def serve_static_file(path, location, content_type):
return HttpFileResponse(location, content_type=content_type)
urlpatterns = [
#path('admin/', admin.site.urls),
]
if settings.OIDC_RP_CLIENT_ID:
urlpatterns += [
path('oidc/', include('mozilla_django_oidc.urls')),
]
urlpatterns += [
re_path(r'^api/locale.(?P<lang>.*).json$', translation.views.locale_json),
re_path(r'^api/upload/text/?$', text.views.upload),
re_path(r'^api/upload/document/?$', document.views.upload),
@ -47,7 +53,7 @@ urlpatterns = [
re_path(r'^resetUI$', user.views.reset_ui),
re_path(r'^collection/(?P<id>.*?)/icon(?P<size>\d*).jpg$', documentcollection.views.icon),
re_path(r'^documents/(?P<id>[A-Z0-9]+)/(?P<size>\d*)p(?P<page>[\d,]*).jpg$', document.views.thumbnail),
re_path(r'^documents/(?P<id>[A-Z0-9]+)/(?P<name>.*?\.[^\d]{3})$', document.views.file),
re_path(r'^documents/(?P<id>[A-Z0-9]+)/(?P<name>.*?\.[^\d]{3,4})$', document.views.file),
re_path(r'^documents/(?P<fragment>.*?)$', document.views.document),
re_path(r'^edit/(?P<id>.*?)/icon(?P<size>\d*).jpg$', edit.views.icon),
re_path(r'^list/(?P<id>.*?)/icon(?P<size>\d*).jpg$', itemlist.views.icon),

View file

@ -436,8 +436,7 @@ def has_capability(user, capability):
level = 'guest'
else:
level = user.profile.get_level()
return level in settings.CONFIG['capabilities'][capability] \
and settings.CONFIG['capabilities'][capability][level]
return settings.CONFIG['capabilities'].get(capability, {}).get(level)
def merge_users(old, new):
old.annotations.all().update(user=new)

View file

@ -3,6 +3,38 @@ from django.contrib.gis.geoip2 import GeoIP2, GeoIP2Exception
import ox
def prepare_user(user):
from django.contrib.auth import get_user_model
from django.conf import settings
from itemlist.models import List, Position
from django.db.models import Max
User = get_user_model()
first_user_qs = User.objects.all()
if user.id:
first_user_qs = first_user_qs.exclude(id=user.id)
if first_user_qs.count() == 0:
user.is_superuser = True
user.is_staff = True
user.save()
for l in settings.CONFIG['personalLists']:
list = List(name=l['title'], user=user)
for key in ('query', 'public', 'featured'):
if key in l:
setattr(list, key, l[key])
if key == 'query':
for c in list.query['conditions']:
if c['key'] == 'user':
c['value'] = c['value'].format(username=user.username)
list.save()
pos = Position(list=list, section='personal', user=user)
qs = Position.objects.filter(user=user, section='personal')
pos.position = (qs.aggregate(Max('position'))['position__max'] or 0) + 1
pos.save()
def get_ip(request):
if 'HTTP_X_FORWARDED_FOR' in request.META:
ip = request.META['HTTP_X_FORWARDED_FOR'].split(',')[0]

View file

@ -28,7 +28,7 @@ from user.models import Group
from . import models
from .decorators import capability_required_json
from .utils import rename_user
from .utils import rename_user, prepare_user
User = get_user_model()
@ -177,28 +177,10 @@ def signup(request, data):
}
})
else:
first_user = User.objects.count() == 0
user = User(username=data['username'], email=data['email'])
user.set_password(data['password'])
#make first user admin
user.is_superuser = first_user
user.is_staff = first_user
user.save()
#create default user lists:
for l in settings.CONFIG['personalLists']:
list = List(name=l['title'], user=user)
for key in ('query', 'public', 'featured'):
if key in l:
setattr(list, key, l[key])
if key == 'query':
for c in list.query['conditions']:
if c['key'] == 'user':
c['value'] = c['value'].format(username=user.username)
list.save()
pos = Position(list=list, section='personal', user=user)
qs = Position.objects.filter(user=user, section='personal')
pos.position = (qs.aggregate(Max('position'))['position__max'] or 0) + 1
pos.save()
prepare_user(user)
if request.session.session_key:
models.SessionData.objects.filter(session_key=request.session.session_key).update(user=user)
ui = json.loads(request.session.get('ui', 'null'))

View file

@ -20,3 +20,6 @@ future
pytz
pypdfium2
Pillow>=10
pillow-heif
pillow-avif-plugin
mozilla-django-oidc==4.0.1

View file

@ -106,6 +106,13 @@ pandora.ui.addFilesDialog = function(options) {
});
var selectItems = [];
selectItems.push({
id: 'one',
title: Ox._(
options.items.length > 1 ? 'Create new {0} with multiple parts' : 'Create new {0}',
[pandora.site.itemName.singular.toLowerCase()]
)
});
if (pandora.user.ui.item && options.editable) {
selectItems.push({
id: 'add',
@ -124,13 +131,6 @@ pandora.ui.addFilesDialog = function(options) {
)
});
}
selectItems.push({
id: 'one',
title: Ox._(
options.items.length > 1 ? 'Create new {0} with multiple parts' : 'Create new {0}',
[pandora.site.itemName.singular.toLowerCase()]
)
});
var $select = Ox.Select({
items: selectItems,
width: 256

View file

@ -100,6 +100,10 @@ pandora.ui.appPanel = function() {
pandora.$ui.siteDialog = pandora.ui.siteDialog(page).open();
}
} else if (['signup', 'signin'].indexOf(page) > -1) {
if (pandora.site.site.oidc) {
pandora.oidcLogin()
return
}
if (pandora.user.level == 'guest') {
if (pandora.$ui.accountDialog && pandora.$ui.accountDialog.is(':visible')) {
pandora.$ui.accountDialog.options(pandora.ui.accountDialogOptions(page));

View file

@ -21,11 +21,13 @@ pandora.ui.editor = function(data) {
censoredIcon: pandora.site.cantPlay.icon,
censoredTooltip: Ox._(pandora.site.cantPlay.text),
clickLink: pandora.clickLink,
confirmDeleteDialog: confirmDeleteDialog,
cuts: data.cuts || [],
duration: data.duration,
enableDownload: pandora.hasCapability('canDownloadVideo') >= data.rightslevel || data.editable,
enableExport: pandora.hasCapability('canExportAnnotations') || data.editable,
enableImport: pandora.hasCapability('canImportAnnotations') || data.editable,
enableTranscribe: pandora.hasCapability('canTranscribeAudio') && data.editable,
enableSetPosterFrame: !pandora.site.media.importFrames && data.editable,
enableSubtitles: ui.videoSubtitles,
find: ui.itemFind,
@ -220,7 +222,9 @@ pandora.ui.editor = function(data) {
});
}
that.updateAnnotation(data.id, result.data);
pandora.UI.set('videoPoints.' + ui.item + '.annotation', result.data.id.split('/')[1] || '');
if (result.data.id) {
pandora.UI.set('videoPoints.' + ui.item + '.annotation', result.data.id.split('/')[1] || '');
}
Ox.Request.clearCache();
};
var edit = {
@ -391,7 +395,21 @@ pandora.ui.editor = function(data) {
pandora.UI.set({videoResolution: data.resolution});
},
select: function(data) {
pandora.UI.set('videoPoints.' + ui.item + '.annotation', data.id.split('/')[1] || '');
if (Ox.isArray(data.id)) {
var range = data.id.map(id => {
return getAnnotationById(id)
})
data['in'] = Ox.min(range.map(annotation => { return annotation["in"]; }))
data['out'] = Ox.max(range.map(annotation => { return annotation["out"]; }))
pandora.UI.set('videoPoints.' + ui.item, {
annotation: '',
'in': data['in'],
out: data.out,
position: ui.videoPoints[ui.item].position
})
} else {
pandora.UI.set('videoPoints.' + ui.item + '.annotation', data.id.split('/')[1] || '');
}
},
showentityinfo: function(data) {
pandora.URL.push('/entities/' + data.id)
@ -417,6 +435,54 @@ pandora.ui.editor = function(data) {
togglesize: function(data) {
pandora.UI.set({videoSize: data.size});
},
transcribeaudio: function() {
const $dialog = pandora.ui.iconDialog({
buttons: [
Ox.Button({
id: 'cancel',
title: Ox._('Cancel')
})
.bindEvent({
click: function() {
$dialog.close();
}
}),
Ox.Button({
id: 'transcribe',
title: Ox._('Transcribe Audio')
})
.bindEvent({
click: function() {
$dialog.close();
pandora.api.transcribeAudio({
item: pandora.user.ui.item
}, function(result) {
if (result.data.taskId) {
pandora.wait(result.data.taskId, function(result) {
Ox.Request.clearCache();
if (ui.item == data.id && ui.itemView == 'editor') {
pandora.$ui.contentPanel.replaceElement(
1, pandora.$ui.item = pandora.ui.item()
);
}
})
}
})
pandora.ui.tasksDialog().open();
}
})
],
content: Ox._(
'Are you sure you want to add automated transcription to "{0}"',
[data.title]
),
keys: {enter: 'transcribe', escape: 'cancel'},
title: Ox._(
'Transcribe {0} {1}', [data.title]
)
});
$dialog.open()
},
pandora_showannotations: function(data) {
that.options({showAnnotations: data.value});
},
@ -428,6 +494,60 @@ pandora.ui.editor = function(data) {
pandora._dontSelectResult = false;
function confirmDeleteDialog(options, callback) {
const title = Ox.getObjectById(
pandora.site.layers,
getAnnotationById(options.items[0]).layer
).item
const subject = options.items.length == 1 ? title : title + 's'
const $dialog = pandora.ui.iconDialog({
buttons: [
Ox.Button({
id: 'cancel',
title: Ox._('Keep {0}', [Ox._(subject)])
})
.bindEvent({
click: function() {
$dialog.close();
}
}),
Ox.Button({
id: 'delete',
title: Ox._('Delete {0}', [Ox._(subject)])
})
.bindEvent({
click: function() {
$dialog.close();
callback()
}
})
],
content: Ox._(
'Are you sure you want delete {0} {1}?<br><br>All data will be removed.',
[options.items.length, Ox._(subject.toLowerCase())]
),
height: 96,
keys: {enter: 'delete', escape: 'cancel'},
title: Ox._(
'Delete {0}', [Ox._(subject)]
)
});
$dialog.open()
return $dialog
}
function getAnnotationById(id) {
var annotation
data.annotations.forEach(layer => {
layer.items.forEach(a => {
if (a.id == id) {
annotation = a
}
})
})
return annotation;
}
function updateBrowser() {
pandora.$ui.browser.find('img[src*="/' + ui.item + '/"]').each(function() {
$(this).attr({

View file

@ -102,6 +102,13 @@ pandora.ui.exportAnnotationsDialog = function(options) {
$button.wrap($('<a>'));
// On wrap, a reference to the link would *not* be the link in the DOM
$link = $($button.parent());
$link.on({
click: function() {
setTimeout(() => {
that.close()
}, 10)
}
})
updateLink();
}

View file

@ -424,26 +424,35 @@ pandora.ui.folders = function(section) {
}).bindEvent({
click: function() {
var $dialog = pandora.ui.iconDialog({
buttons: title != Ox._('Featured ' + folderItems) ? [
Ox.Button({title: Ox._('Sign Up...')}).bindEvent({
click: function() {
$dialog.close();
pandora.$ui.accountDialog = pandora.ui.accountDialog('signup').open();
}
}),
Ox.Button({title: Ox._('Sign In...')}).bindEvent({
click: function() {
$dialog.close();
pandora.$ui.accountDialog = pandora.ui.accountDialog('signin').open();
}
}),
{},
Ox.Button({title: Ox._('Not Now')}).bindEvent({
click: function() {
$dialog.close();
}
})
] : [
buttons: title != Ox._('Featured ' + folderItems) ? [].concat(
pandora.site.site.oidc ? []
: [
Ox.Button({title: Ox._('Sign Up...')}).bindEvent({
click: function() {
$dialog.close();
pandora.$ui.accountDialog = pandora.ui.accountDialog('signup').open();
}
})
],
[
Ox.Button({title: Ox._('Sign In...')}).bindEvent({
click: function() {
$dialog.close();
if (pandora.site.site.oidc) {
pandora.oidcLogin()
} else {
pandora.$ui.accountDialog = pandora.ui.accountDialog('signin').open();
}
}
}),
{},
Ox.Button({title: Ox._('Not Now')}).bindEvent({
click: function() {
$dialog.close();
}
})
]
): [
Ox.Button({title: Ox._('Close')}).bindEvent({
click: function() {
$dialog.close();

View file

@ -1,6 +1,10 @@
'use strict';
pandora.getItemTitle = function(itemData, includeYear) {
var director = itemData.director
if (!Ox.isArray(director)) {
director = [director]
}
return (itemData.title || Ox._('Untitled')) + (
Ox.len(itemData.director) || (includeYear && itemData.year)
? ' (' + (

View file

@ -137,24 +137,26 @@ pandora.ui.helpDialog = function() {
that.select = function(id) {
var img, $img;
$text.html(text[id || 'help']).scrollTop(0);
img = $text.find('img');
if (img) {
$img = $(img);
$img.replaceWith(
$image = Ox.ImageElement(
Ox.extend(getImageSize(), {src: $img.attr('src')})
)
.css({borderRadius: '8px'})
);
if ($text) {
$text.html(text[id || 'help']).scrollTop(0);
img = $text.find('img');
if (img) {
$img = $(img);
$img.replaceWith(
$image = Ox.ImageElement(
Ox.extend(getImageSize(), {src: $img.attr('src')})
)
.css({borderRadius: '8px'})
);
}
$text.find('td:first-child')
.css({
height: '16px',
paddingRight: '8px',
textAlign: 'right',
whiteSpace: 'nowrap'
});
}
$text.find('td:first-child')
.css({
height: '16px',
paddingRight: '8px',
textAlign: 'right',
whiteSpace: 'nowrap'
});
return that;
}

View file

@ -171,13 +171,13 @@ pandora.ui.home = function() {
}),
$signinButton = Ox.Button({
title: Ox._('Sign In'),
width: 74
width: pandora.site.site.oidc ? 156 : 74
})
.css({
position: 'absolute',
left: 0,
top: '112px',
right: '82px',
right: pandora.site.site.oidc ? '164px' : '82px',
bottom: 0,
margin: 'auto',
opacity: 0
@ -248,7 +248,13 @@ pandora.ui.home = function() {
adjustRatio();
if (pandora.user.level == 'guest') {
$signupButton.appendTo(that);
if (pandora.site.site.oidc) {
$signinButton.options({
width: 156
})
} else {
$signupButton.appendTo(that);
}
$signinButton.appendTo(that);
} else {
$preferencesButton.appendTo(that);

View file

@ -201,10 +201,11 @@ pandora.ui.importAnnotationsDialog = function(options) {
pandora.$ui.contentPanel.replaceElement(
1, pandora.$ui.item = pandora.ui.item()
);
that.close();
} else {
$status.html(Ox._('Import failed.'));
enableButtons();
}
enableButtons();
});
} else {
$status.html(Ox._('Import failed.'));

View file

@ -39,7 +39,7 @@ pandora.ui.item = function() {
}
})
if (!Ox.isEmpty(set)) {
pandora.UI.set('videoPoints.' + item, Ox.extend(videoPoints[point], set[point]))
pandora.UI.set('videoPoints.' + item, Ox.extend(videoPoints, set))
}
}
@ -63,9 +63,9 @@ pandora.ui.item = function() {
var tasks = result_.data.items.filter(function(task) { return task.item == item})
if (tasks.length > 0) {
html = Ox._(
'<i>{0}</i> is currently processed. '
'<i>{0}</i> is currently being processed. '
+ '{1} view will be available in a moment.',
[result.data.title, Ox._(pandora.user.ui.itemView)]
[result.data.title, Ox.toTitleCase(Ox._(pandora.user.ui.itemView))]
)
}
note.html(html)
@ -202,6 +202,10 @@ pandora.ui.item = function() {
}
});
}
// avoid loop while selecting multiple annotations
if (options.selected == '' && Ox.isArray(pandora.$ui[pandora.user.ui.itemView].options('selected'))) {
delete options.selected
}
pandora.$ui[pandora.user.ui.itemView].options(options);
}
}

View file

@ -270,7 +270,7 @@ pandora.ui.list = function() {
item: function(data, sort, size) {
size = 128;
var clipsQuery = pandora.getClipsQuery(),
isClipsQuery = !!clipsQuery.conditions.length,
isClipsQuery = clipsQuery.conditions.length > 1,
ratio = ui.icons == 'posters'
? (ui.showSitePosters ? pandora.site.posters.ratio : data.posterRatio) : 1,
url = pandora.getMediaURL('/' + data.id + '/' + (
@ -352,7 +352,7 @@ pandora.ui.list = function() {
},
items: function(data, callback) {
var clipsQuery = pandora.getClipsQuery(),
isClipsQuery = !!clipsQuery.conditions.length;
isClipsQuery = clipsQuery.conditions.length > 1;
pandora.api.find(Ox.extend(data, Ox.extend({
query: ui.find
}, isClipsQuery ? {clips: {

View file

@ -46,7 +46,9 @@ pandora.ui.mainMenu = function() {
{ id: 'tasks', title: Ox._('Tasks...'), disabled: isGuest },
{ id: 'archives', title: Ox._('Archives...'), disabled: /*isGuest*/ true },
{},
{ id: 'signup', title: Ox._('Sign Up...'), disabled: !isGuest },
!pandora.site.site.oidc
? { id: 'signup', title: Ox._('Sign Up...'), disabled: !isGuest }
: [],
isGuest ? { id: 'signin', title: Ox._('Sign In...')}
: { id: 'signout', title: Ox._('Sign Out...')}
] },

View file

@ -142,6 +142,22 @@ pandora.ui.mediaView = function(options) {
visible: true,
width: 360
},
{
align: 'left',
id: 'folder',
operator: '+',
title: Ox._('Folder'),
visible: false,
width: 360
},
{
align: 'left',
id: 'filename',
operator: '+',
title: Ox._('Filename'),
visible: false,
width: 360
},
{
editable: true,
id: 'version',
@ -441,6 +457,8 @@ pandora.ui.mediaView = function(options) {
});
}
}
}).on({
keyup: updateForm
});
});
@ -663,7 +681,9 @@ pandora.ui.mediaView = function(options) {
});
}
self.$moveButton.options({
disabled: self.selected.length == 0
disabled: self.selected.length == 0 || (
self.$idInput.value().length + self.$titleInput.value().length
) == 0
});
self.$menu[
self.selected.length == 0 ? 'disableItem' : 'enableItem'

View file

@ -67,7 +67,7 @@ pandora.ui.metadataDialog = function(data) {
'To update the metadata for this {0}, please enter its IMDb ID.',
[pandora.site.itemName.singular.toLowerCase()]
),
keyboard: {enter: 'update', escape: 'close'},
keys: {enter: 'update', escape: 'close'},
title: Ox._('Update Metadata')
});
}

View file

@ -354,6 +354,10 @@ appPanel
if (pandora.user.ui.itemView == 'video') {
pandora.user.ui.itemView = 'player';
}
// item find accross relodas breaks links to in/out
if (pandora.user.ui.itemFind) {
pandora.user.ui.itemFind = "";
}
Ox.extend(pandora.site, {
calendar: data.site.layers.some(function(layer) {

View file

@ -79,7 +79,8 @@ pandora.ui.tasksDialog = function(options) {
'processing': 'Processing',
'canceled': 'Canceled',
'failed': 'Failed',
'finished': 'Finished'
'finished': 'Finished',
'transcribing': 'Transcribing'
}[value] || value;
},
id: 'status',

View file

@ -8,8 +8,6 @@ pandora.ui.uploadDocumentDialog = function(options, callback) {
existingFiles = [],
uploadFiles = [],
supportedExtensions = ['gif', 'jpg', 'jpeg', 'pdf', 'png'],
filename,
ids = [],
@ -72,7 +70,7 @@ pandora.ui.uploadDocumentDialog = function(options, callback) {
});
if (!Ox.every(extensions, function(extension) {
return Ox.contains(supportedExtensions, extension)
return Ox.contains(pandora.documentExtensions, extension)
})) {
return errorDialog(
Ox._('Supported file types are GIF, JPG, PNG and PDF.')

View file

@ -420,13 +420,28 @@ pandora.createLinks = function($element) {
});
};
pandora.imageExtensions = [
'avif',
'gif',
'heic',
'heif',
'jpeg',
'jpg',
'png',
'tiff',
'webp'
];
pandora.documentExtensions = [
'pdf', /* 'epub', 'txt', */
].concat(pandora.imageExtensions);
pandora.uploadDroppedFiles = function(files) {
var documentExtensions = ['pdf', /* 'epub', 'txt', */ 'png', 'gif', 'jpg', 'jpeg'];
files = Ox.map(files, function(file) { return file });
if (files.every(function(file) {
var extension = file.name.split('.').pop().toLowerCase()
return Ox.contains(documentExtensions, extension)
return Ox.contains(pandora.documentExtensions, extension)
})) {
pandora.ui.uploadDocumentDialog({
files: files
@ -2132,7 +2147,7 @@ pandora.getSpan = function(state, val, callback) {
} else {
state.span = val;
}
} else if (Ox.contains(['gif', 'jpg', 'png'], extension)) {
} else if (Ox.contains(pandora.imageExtensions, extension)) {
values = val.split(',');
if (values.length == 4) {
state.span = values.map(function(number, index) {
@ -2650,6 +2665,11 @@ pandora.logEvent = function(data, event, element) {
}
};
pandora.oidcLogin = function() {
Ox.LoadingScreen().css({zIndex: 100}).addClass('OxScreen').appendTo(document.body).show().start()
document.location.href = '/oidc/authenticate/';
};
pandora.openLicenseDialog = function() {
if (!Ox.Focus.focusedElementIsInput() && !pandora.hasDialogOrScreen()) {
pandora.ui.licenseDialog().open().bindEvent({

View file

@ -201,3 +201,75 @@ ol li {
width: 64px;
height: 64px;
}
.seekbar {
padding: 12px 22px;
position: relative;
width: 100%;
}
.fullscreen .seekbar {
padding: 28px 22px;
}
.seekbar-progress {
height: 10px;
border: solid 1px #B1B1B1;
}
.seekbar-progress [role="progressbar"] {
height: 100%;
position: relative;
background-color: #B1B1B180;
}
.seekbar-progress [role="progressbar"]:after {
content: " ";
display: block;
width: 20px;
height: 20px;
position: absolute;
top: -6px;
right: -10px;
background-color: #B1B1B1;
border-radius: 20px;
}
.seekbar input[type="range"] {
-webkit-appearance: none;
width: 100%;
height: 100%;
margin: 0;
position: absolute;
top: 0;
left: 0;
z-index: 2;
background: transparent;
outline: 0;
border: 0;
}
.seekbar input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
display: block;
width: 48px;
height: 48px;
background-color: transparent;
}
.seekbar input[type="range"]::-moz-range-thumb {
display: block;
width: 48px;
height: 48px;
background: transparent;
border: 0;
}
.seekbar input[type="range"]::-moz-range-track {
background: transparent;
border: 0;
}
.seekbar input[type="range"]::-moz-focus-outer {
border: 0;
}

View file

@ -8,6 +8,7 @@ VideoElement <f> VideoElement Object
loop <b|false> loop playback
playbackRate <n|1> playback rate
position <n|0> start position
in <n|0> start offset
self <o> Shared private variable
([options[, self]]) -> <o:Element> VideoElement Object
loadedmetadata <!> loadedmetadata
@ -38,6 +39,7 @@ window.VideoElement = function(options) {
muted: false,
playbackRate: 1,
position: 0,
"in": 0,
volume: 1
}
Object.assign(self.options, options);
@ -166,9 +168,10 @@ window.VideoElement = function(options) {
function getCurrentTime() {
var item = self.items[self.currentItem];
return self.seeking || self.loading
var currentTime = self.seeking || self.loading
? self.currentTime
: item ? item.position + self.video.currentTime - item['in'] : 0;
: item ? item.position + self.video.currentTime - item['in'] - self.options["in"] : 0;
return currentTime
}
function getset(key, value) {
@ -528,6 +531,10 @@ window.VideoElement = function(options) {
function setCurrentTime(time) {
debug('Video', 'sCT', time);
if (self.options["in"]) {
debug('Video', 'sCT shift time by', self.options["in"], 'to', time);
time += self.options["in"]
}
var currentTime, currentItem;
self.items.forEach(function(item, i) {
if (time >= item.position

View file

@ -10,6 +10,7 @@ window.VideoPlayer = function(options) {
loop: false,
muted: false,
playbackRate: 1,
"in": 0,
position: 0,
volume: 1
}
@ -153,9 +154,11 @@ window.VideoPlayer = function(options) {
${icon.mute}
</div>
<div class="position">
<div class="bar">
<div class="progress"></div>
<div class="seekbar">
<input type="range" value="0" min='0' max='100' step='.25' />
<div class="seekbar-progress">
<div role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="38" style="width: 0%;"></div>
</div>
</div>
</div>
<div class="time">
@ -223,15 +226,31 @@ window.VideoPlayer = function(options) {
}
}
var showControls
function hideControlsLater() {
if (showControls) {
clearTimeout(showControls)
}
showControls = setTimeout(() => {
if (touching) {
hideControlsLater()
} else {
self.controls.style.opacity = that.paused ? '1' : '0'
showControls = null
}
}, 3000)
}
var toggleControls = event => {
if (event.target.tagName == "INPUT") {
if (showControls) {
clearTimeout(showControls)
}
return
}
if (self.controls.style.opacity == '0') {
event.preventDefault()
event.stopPropagation()
self.controls.style.opacity = '1'
showControls = setTimeout(() => {
self.controls.style.opacity = that.paused ? '1' : '0'
showControls = null
}, 3000)
hideControlsLater()
} else {
self.controls.style.opacity = '0'
}
@ -241,10 +260,7 @@ window.VideoPlayer = function(options) {
clearTimeout(showControls)
}
self.controls.style.opacity = '1'
showControls = setTimeout(() => {
self.controls.style.opacity = that.paused ? '1' : '0'
showControls = null
}, 3000)
hideControlsLater()
})
self.controls.addEventListener("mouseleave", event => {
if (showControls) {
@ -253,7 +269,13 @@ window.VideoPlayer = function(options) {
self.controls.style.opacity = that.paused ? '1' : '0'
showControls = null
})
self.controls.addEventListener("touchstart", event => {
touching = true
})
self.controls.addEventListener("touchstart", toggleControls)
self.controls.addEventListener("touchend", event => {
touching = false
})
self.controls.querySelector('.toggle').addEventListener("click", toggleVideo)
self.controls.querySelector('.volume').addEventListener("click", toggleSound)
self.controls.querySelector('.fullscreen-btn').addEventListener("click", toggleFullscreen)
@ -310,6 +332,7 @@ window.VideoPlayer = function(options) {
that.append(unblock)
})
var loading = true
var touching = false
that.brightness(0)
that.addEventListener("loadedmetadata", event => {
//
@ -331,42 +354,40 @@ window.VideoPlayer = function(options) {
}
})
var time = that.querySelector('.controls .time div'),
progress = that.querySelector('.controls .position .progress')
that.querySelector('.controls .position').addEventListener("click", event => {
var bar = event.target
while (bar && !bar.classList.contains('bar')) {
bar = bar.parentElement
}
if (bar && bar.classList.contains('bar')) {
event.preventDefault()
event.stopPropagation()
var rect = bar.getBoundingClientRect()
var x = event.clientX - rect.x
var percent = x / rect.width
var position = percent * self.options.duration
if (self.options.position) {
position += self.options.position
}
progress.style.width = (100 * percent) + '%'
that.currentTime(position)
}
var time = that.querySelector('.controls .time div');
const progressbar = that.querySelector('.seekbar div[role="progressbar"]');
function setProgressPosition(value) {
progressbar.style.width = value + '%';
progressbar.setAttribute('aria-valuenow', value);
}
that.querySelector('.controls .position input').addEventListener('input', event => {
event.preventDefault()
event.stopPropagation()
setProgressPosition(event.target.value)
var position = event.target.value/100 * self.options.duration
displayTime(position)
that.currentTime(position)
hideControlsLater()
})
that.addEventListener("timeupdate", event => {
var currentTime = that.currentTime(),
duration = self.options.duration
if (self.options.position) {
currentTime -= self.options.position
}
progress.style.width = (100 * currentTime / duration) + '%'
duration = formatDuration(duration)
function displayTime(currentTime) {
duration = formatDuration(self.options.duration)
currentTime = formatDuration(currentTime)
while (duration && duration.startsWith('00:')) {
duration = duration.slice(3)
}
currentTime = currentTime.slice(currentTime.length - duration.length)
time.innerText = `${currentTime} / ${duration}`
}
that.addEventListener("timeupdate", event => {
var currentTime = that.currentTime(),
duration = self.options.duration
if (self.options.position) {
currentTime -= self.options.position
}
setProgressPosition(100 * currentTime / duration)
displayTime(currentTime)
})
that.addEventListener("play", event => {

View file

@ -1,35 +1,4 @@
const getSortValue = function(value) {
var sortValue = value;
function trim(value) {
return value.replace(/^\W+(?=\w)/, '');
}
if (
isEmpty(value)
|| isNull(value)
|| isUndefined(value)
) {
sortValue = null;
} else if (isString(value)) {
// make lowercase and remove leading non-word characters
sortValue = trim(value.toLowerCase());
// move leading articles to the end
// and remove leading non-word characters
['a', 'an', 'the'].forEach(function(article) {
if (new RegExp('^' + article + ' ').test(sortValue)) {
sortValue = trim(sortValue.slice(article.length + 1))
+ ', ' + sortValue.slice(0, article.length);
return false; // break
}
});
// remove thousand separators and pad numbers
sortValue = sortValue.replace(/(\d),(?=(\d{3}))/g, '$1')
.replace(/\d+/g, function(match) {
return match.padStart(64, '0')
});
}
return sortValue;
};
const sortByKey = function(array, by) {
return array.sort(function(a, b) {
@ -122,7 +91,7 @@ async function loadEdit(id, args) {
}
}
data.edit = response['data']
if (data.edit.status !== 'public') {
if (['public', 'featured'].indexOf(data.edit.status) == -1) {
return {
site: data.site,
error: {

View file

@ -129,6 +129,10 @@ async function loadData(id, args) {
<span class="icon">${icon.down}</span>
${layerData.title}
</h3>`)
data.layers[layer] = sortBy(data.layers[layer], [
{key: "in", operator: "+"},
{key: "created", operator: "+"}
])
data.layers[layer].forEach(annotation => {
if (pandora.url) {
annotation.value = annotation.value.replace(

View file

@ -67,6 +67,7 @@ function parseURL() {
id = id.replace('/editor/', '/').replace('/player/', '/')
type = "item"
}
//console.log(type, id, args)
return [type, id, args]
}

View file

@ -75,7 +75,8 @@ function renderItem(data) {
var video = window.video = VideoPlayer({
items: data.videos,
poster: data.poster,
position: data["in"] || 0,
"in": data["in"] || 0,
position: 0,
duration: data.duration,
aspectratio: data.aspectratio
})
@ -85,16 +86,10 @@ function renderItem(data) {
video.addEventListener("loadedmetadata", event => {
//
})
video.addEventListener("timeupdate", event => {
var currentTime = video.currentTime()
if (currentTime >= data['out']) {
if (!video.paused) {
video.pause()
}
video.currentTime(data['in'])
}
function updateAnnotations(currentTime) {
div.querySelectorAll('.annotation').forEach(annot => {
var now = currentTime
var now = currentTime + (data["in"] || 0)
var start = parseFloat(annot.dataset.in)
var end = parseFloat(annot.dataset.out)
if (now >= start && now <= end) {
@ -107,8 +102,18 @@ function renderItem(data) {
}
}
})
}
video.addEventListener("timeupdate", event => {
var currentTime = video.currentTime()
if ((currentTime + (data["in"] || 0)) >= data['out']) {
if (!video.paused) {
video.pause()
}
video.currentTime(0)
}
updateAnnotations(currentTime)
})
updateAnnotations(data["position"] || 0)
if (item.next || item.previous) {
var nav = document.createElement('nav')
nav.classList.add('items')

View file

@ -125,7 +125,10 @@ const clickLink = function(event) {
}
document.location.hash = '#' + link.slice(1)
} else {
if (!link.startsWith('/m')) {
if (link.includes('/download/')) {
document.location.href = link
return
} else if (!link.startsWith('/m')) {
link = '/m' + link
}
history.pushState({}, '', link);
@ -161,3 +164,59 @@ const getVideoURL = function(id, resolution, part, track, streamId) {
return prefix + '/' + getVideoURLName(id, resolution, part, track, streamId);
};
const getSortValue = function(value) {
var sortValue = value;
function trim(value) {
return value.replace(/^\W+(?=\w)/, '');
}
if (
isEmpty(value)
|| isNull(value)
|| isUndefined(value)
) {
sortValue = null;
} else if (isString(value)) {
// make lowercase and remove leading non-word characters
sortValue = trim(value.toLowerCase());
// move leading articles to the end
// and remove leading non-word characters
['a', 'an', 'the'].forEach(function(article) {
if (new RegExp('^' + article + ' ').test(sortValue)) {
sortValue = trim(sortValue.slice(article.length + 1))
+ ', ' + sortValue.slice(0, article.length);
return false; // break
}
});
// remove thousand separators and pad numbers
sortValue = sortValue.replace(/(\d),(?=(\d{3}))/g, '$1')
.replace(/\d+/g, function(match) {
return match.padStart(64, '0')
});
}
return sortValue;
};
function sortBy(array, by, map) {
return array.sort(function(a, b) {
var aValue, bValue, index = 0, key, ret = 0;
while (ret == 0 && index < by.length) {
key = by[index].key;
aValue = getSortValue(
map && map[key] ? map[key](a[key], a) : a[key]
);
bValue = getSortValue(
map && map[key] ? map[key](b[key], b) : b[key]
);
if ((aValue === null) != (bValue === null)) {
ret = aValue === null ? 1 : -1;
} else if (aValue < bValue) {
ret = by[index].operator == '+' ? -1 : 1;
} else if (aValue > bValue) {
ret = by[index].operator == '+' ? 1 : -1;
} else {
index++;
}
}
return ret;
});
}

View file

@ -302,6 +302,10 @@ if __name__ == "__main__":
if old <= 6581:
run('./bin/pip', 'install', '-U', 'pip')
run('./bin/pip', 'install', '-r', 'requirements.txt')
if old <= 6659:
run('./bin/pip', 'install', '-r', 'requirements.txt')
if old <= 6688:
run('./bin/pip', 'install', '-r', 'requirements.txt')
else:
if len(sys.argv) == 1:
branch = get_branch()
@ -322,6 +326,7 @@ if __name__ == "__main__":
current_branch = get_branch(path)
revno = get_version(path)
if repo == 'pandora':
print("Pandora Version pre update: ", revno)
pandora_old_revno = revno
current += revno
if current_branch != branch:
@ -345,6 +350,7 @@ if __name__ == "__main__":
new += '+'
os.chdir(join(base, 'pandora'))
if pandora_old_revno != pandora_new_revno:
print("Pandora Version post update: ", pandora_new_revno)
os.chdir(base)
run('./update.py', 'postupdate', pandora_old_revno, pandora_new_revno)
os.chdir(join(base, 'pandora'))
@ -361,7 +367,7 @@ if __name__ == "__main__":
and row not in ['BEGIN;', 'COMMIT;']
]
if diff:
print('Database has changed, please make a backup and run %s db' % sys.argv[0])
print('Database has changed, please make a backup and run: sudo pandoractl update db')
elif branch != 'master':
print('pan.do/ra is at the latest release,\nyou can run "%s switch master" to switch to the development version' % sys.argv[0])
elif current != new:

View file

@ -25,53 +25,20 @@ LXC=`grep -q lxc /proc/1/environ && echo 'yes' || echo 'no'`
if [ -e /etc/os-release ]; then
. /etc/os-release
fi
if [ -z "$UBUNTU_CODENAME" ]; then
UBUNTU_CODENAME=bionic
fi
if [ "$VERSION_CODENAME" = "bullseye" ]; then
UBUNTU_CODENAME=focal
fi
if [ "$VERSION_CODENAME" = "bookworm" ]; then
UBUNTU_CODENAME=lunar
fi
export DEBIAN_FRONTEND=noninteractive
echo "deb http://ppa.launchpad.net/j/pandora/ubuntu ${UBUNTU_CODENAME} main" > /etc/apt/sources.list.d/j-pandora.list
apt-get install -y gnupg
apt-get install -y gnupg curl
if [ -e /etc/apt/trusted.gpg.d ]; then
gpg --dearmor > /etc/apt/trusted.gpg.d/j-pandora.gpg <<EOF
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1
distribution=bookworm
for version in bookworm trixie bionic focal jammy noble; do
if [ "$VERSION_CODENAME" = $version ]; then
distribution=$VERSION_CODENAME
fi
done
mI0ESXYhEgEEALl9jDTdmgpApPbjN+7b85dC92HisPUp56ifEkKJOBj0X5HhRqxs
Wjx/zlP4/XJGrHnxJyrdPxjSwAXz7bNdeggkN4JWdusTkr5GOXvggQnng0X7f/rX
oJwoEGtYOCODLPs6PC0qjh5yPzJVeiRsKUOZ7YVNnwNwdfS4D8RZvtCrABEBAAG0
FExhdW5jaHBhZCBQUEEgZm9yIGpeiLYEEwECACAFAkl2IRICGwMGCwkIBwMCBBUC
CAMEFgIDAQIeAQIXgAAKCRAohRM8AZde82FfA/9OB/64/YLaCpizHZ8f6DK3rGgF
e6mX3rFK8yOKGGL06316VhDzfzMiZSauUZ0t+lKHR/KZYeSaFwEoUoblTG/s4IIo
9aBMHWhVXJW6eifKUmTGqEn2/0UxoWQq2C3F6njMkCaP+ALOD5uzaSYGdjqAUAwS
pAAGSEQ4uz6bYSeM4Q==
=SM2a
-----END PGP PUBLIC KEY BLOCK-----
EOF
else
apt-key add - <<EOF
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1
curl -Ls https://code.0x2620.org/api/packages/0x2620/debian/repository.key -o /etc/apt/keyrings/pandora.asc
echo "deb [signed-by=/etc/apt/keyrings/pandora.asc] https://code.0x2620.org/api/packages/0x2620/debian $distribution main" > /etc/apt/sources.list.d/pandora.list
mI0ESXYhEgEEALl9jDTdmgpApPbjN+7b85dC92HisPUp56ifEkKJOBj0X5HhRqxs
Wjx/zlP4/XJGrHnxJyrdPxjSwAXz7bNdeggkN4JWdusTkr5GOXvggQnng0X7f/rX
oJwoEGtYOCODLPs6PC0qjh5yPzJVeiRsKUOZ7YVNnwNwdfS4D8RZvtCrABEBAAG0
FExhdW5jaHBhZCBQUEEgZm9yIGpeiLYEEwECACAFAkl2IRICGwMGCwkIBwMCBBUC
CAMEFgIDAQIeAQIXgAAKCRAohRM8AZde82FfA/9OB/64/YLaCpizHZ8f6DK3rGgF
e6mX3rFK8yOKGGL06316VhDzfzMiZSauUZ0t+lKHR/KZYeSaFwEoUoblTG/s4IIo
9aBMHWhVXJW6eifKUmTGqEn2/0UxoWQq2C3F6njMkCaP+ALOD5uzaSYGdjqAUAwS
pAAGSEQ4uz6bYSeM4Q==
=SM2a
-----END PGP PUBLIC KEY BLOCK-----
EOF
fi
echo 'Acquire::Languages "none";' > /etc/apt/apt.conf.d/99languages
apt-get update -qq