Compare commits
58 commits
f9628d96ae
...
79da1115aa
Author | SHA1 | Date | |
---|---|---|---|
79da1115aa | |||
3f3fa9ab2a | |||
c879d49d84 | |||
194b286e30 | |||
ba71665bc5 | |||
0da9097a6a | |||
017d9be45a | |||
9eb9fbe3a5 | |||
d0d1feca18 | |||
3cf3ba5c6f | |||
5591739531 | |||
7fe51fe7ac | |||
406237b837 | |||
0748265fe6 | |||
ef680080cf | |||
7eae1cf6e5 | |||
8020e5966e | |||
3f0e08a142 | |||
d7fbea4a20 | |||
c391d7f4fa | |||
34d1285e4b | |||
c67f7c122b | |||
53502a27ab | |||
a96097b8bf | |||
e37281491e | |||
722119215d | |||
212ad3cee0 | |||
277bbe45fb | |||
a3336d92b3 | |||
c2df43220b | |||
85b88c4dd6 | |||
e5339fd297 | |||
38618e2ed2 | |||
10c78fc862 | |||
dd8ea22d45 | |||
85eba0bf09 | |||
627a016515 | |||
ad2af2a257 | |||
3bc2b1bd3e | |||
5c6c7e37c7 | |||
7cfe645ab7 | |||
ff236e8828 | |||
34af2b1fab | |||
d83309d4cd | |||
59c2045ac6 | |||
a24d96b098 | |||
03daede441 | |||
9e6ecb5459 | |||
e7ede6ade0 | |||
f4bfe9294b | |||
c69ca372ee | |||
9e00dd09e3 | |||
7cc1504069 | |||
d5ace7aeca | |||
1b1442e664 | |||
18cbf0ec9c | |||
a8aa619217 | |||
ff1c929d4d |
54 changed files with 728 additions and 248 deletions
3
ctl
3
ctl
|
@ -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
36
pandora/app/oidc.py
Normal 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]
|
|
@ -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())
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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'
|
||||
|
|
32
pandora/archive/migrations/0008_file_filename_file_folder.py
Normal file
32
pandora/archive/migrations/0008_file_filename_file_folder.py
Normal 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),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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', '{}'))
|
||||
|
|
|
@ -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'
|
||||
]
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -20,3 +20,6 @@ future
|
|||
pytz
|
||||
pypdfium2
|
||||
Pillow>=10
|
||||
pillow-heif
|
||||
pillow-avif-plugin
|
||||
mozilla-django-oidc==4.0.1
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
? ' (' + (
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.'));
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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...')}
|
||||
] },
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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')
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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.')
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -67,6 +67,7 @@ function parseURL() {
|
|||
id = id.replace('/editor/', '/').replace('/player/', '/')
|
||||
type = "item"
|
||||
}
|
||||
//console.log(type, id, args)
|
||||
return [type, id, args]
|
||||
}
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue