From ff1c929d4dcae010497fce03c18d893a76466944 Mon Sep 17 00:00:00 2001 From: j Date: Wed, 11 Sep 2024 15:05:21 +0100 Subject: [PATCH 01/57] don't checkout oxtimelines twice --- ctl | 3 --- 1 file changed, 3 deletions(-) diff --git a/ctl b/ctl index 816b843d..6e16fb87 100755 --- a/ctl +++ b/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 From a8aa619217bff2bba65468d75df331f03957d016 Mon Sep 17 00:00:00 2001 From: j Date: Wed, 11 Sep 2024 23:01:57 +0100 Subject: [PATCH 02/57] stop using ppa, use deb repository from code.0x2620.org instead --- vm/pandora_install.sh | 51 ++++++++----------------------------------- 1 file changed, 9 insertions(+), 42 deletions(-) diff --git a/vm/pandora_install.sh b/vm/pandora_install.sh index 69d52adf..38d528f9 100755 --- a/vm/pandora_install.sh +++ b/vm/pandora_install.sh @@ -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 < /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 From 18cbf0ec9c56b8ea401b70ff04ccf157a1b46418 Mon Sep 17 00:00:00 2001 From: j Date: Mon, 16 Sep 2024 09:06:10 +0100 Subject: [PATCH 03/57] print pandora versions during update --- update.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/update.py b/update.py index 04aaaa30..967b88bd 100755 --- a/update.py +++ b/update.py @@ -322,6 +322,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 +346,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')) From 1b1442e66480c3dbaaa78370eff4eab0f6b14e65 Mon Sep 17 00:00:00 2001 From: j Date: Mon, 16 Sep 2024 17:23:21 +0100 Subject: [PATCH 04/57] might not be loaded --- static/js/helpDialog.js | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/static/js/helpDialog.js b/static/js/helpDialog.js index 53b2497f..97bc51bb 100644 --- a/static/js/helpDialog.js +++ b/static/js/helpDialog.js @@ -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; } From d5ace7aecabacaccf803d180b406a8bb7642c06e Mon Sep 17 00:00:00 2001 From: j Date: Mon, 16 Sep 2024 17:38:48 +0100 Subject: [PATCH 05/57] use pandoractl to update db --- update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/update.py b/update.py index 967b88bd..f6b514b5 100755 --- a/update.py +++ b/update.py @@ -363,7 +363,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: From 7cc1504069995a98a77b79147fdb387dd1b3e030 Mon Sep 17 00:00:00 2001 From: j Date: Mon, 16 Sep 2024 20:51:37 +0100 Subject: [PATCH 06/57] fix isClipsQuery check --- static/js/list.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/js/list.js b/static/js/list.js index bc680f85..03fe29d9 100644 --- a/static/js/list.js +++ b/static/js/list.js @@ -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: { From 9e00dd09e344419f2fe538918dc81709a8671dc0 Mon Sep 17 00:00:00 2001 From: j Date: Wed, 9 Oct 2024 17:49:21 +0100 Subject: [PATCH 07/57] allow adding global yt-dlp flags --- pandora/archive/external.py | 10 +++++++--- pandora/settings.py | 2 ++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pandora/archive/external.py b/pandora/archive/external.py index 5f4ac3fd..54c55246 100644 --- a/pandora/archive/external.py +++ b/pandora/archive/external.py @@ -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: diff --git a/pandora/settings.py b/pandora/settings.py index 7268c31c..084ce8b3 100644 --- a/pandora/settings.py +++ b/pandora/settings.py @@ -291,6 +291,8 @@ DATA_UPLOAD_MAX_MEMORY_SIZE = 32 * 1024 * 1024 EMPTY_CLIPS = True +YT_DLP_EXTRA = [] + #you can ignore things below this line #========================================================================= LOCAL_APPS = [] From c69ca372ee1d8f08316212c9b0174bea106bf7ff Mon Sep 17 00:00:00 2001 From: j Date: Sun, 13 Oct 2024 17:06:15 +0100 Subject: [PATCH 08/57] sort annotations --- static/mobile/js/item.js | 1 + static/mobile/js/utils.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/static/mobile/js/item.js b/static/mobile/js/item.js index a9942536..7f81fa23 100644 --- a/static/mobile/js/item.js +++ b/static/mobile/js/item.js @@ -129,6 +129,7 @@ async function loadData(id, args) { ${icon.down} ${layerData.title} `) + data.layers[layer] = sortBy(data.layers[layer], ["+in", "+created"]) data.layers[layer].forEach(annotation => { if (pandora.url) { annotation.value = annotation.value.replace( diff --git a/static/mobile/js/utils.js b/static/mobile/js/utils.js index cbaec6a6..860db698 100644 --- a/static/mobile/js/utils.js +++ b/static/mobile/js/utils.js @@ -161,3 +161,33 @@ const getVideoURL = function(id, resolution, part, track, streamId) { return prefix + '/' + getVideoURLName(id, resolution, part, track, streamId); }; +function getSortValue(value) { + var getSortValue = Ox.cache(function getSortValue(value) { + var sortValue = value; + 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[key] ? map[key](a[key], a) : a[key] + ); + bValue = getSortValue( + 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; + }); +} From f4bfe9294b1e6cfb85ec694388a12967e4305134 Mon Sep 17 00:00:00 2001 From: j Date: Sun, 13 Oct 2024 17:10:11 +0100 Subject: [PATCH 09/57] reuse getSortValue --- static/mobile/js/edits.js | 31 ------------------------------- static/mobile/js/utils.js | 36 +++++++++++++++++++++++++++++++----- 2 files changed, 31 insertions(+), 36 deletions(-) diff --git a/static/mobile/js/edits.js b/static/mobile/js/edits.js index be9101ee..5494fcb1 100644 --- a/static/mobile/js/edits.js +++ b/static/mobile/js/edits.js @@ -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) { diff --git a/static/mobile/js/utils.js b/static/mobile/js/utils.js index 860db698..a22dc75a 100644 --- a/static/mobile/js/utils.js +++ b/static/mobile/js/utils.js @@ -161,11 +161,37 @@ const getVideoURL = function(id, resolution, part, track, streamId) { return prefix + '/' + getVideoURLName(id, resolution, part, track, streamId); }; -function getSortValue(value) { - var getSortValue = Ox.cache(function getSortValue(value) { - var sortValue = value; - return sortValue; -} +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) { From e7ede6ade085b9851a17db69469556ff7fc0cb25 Mon Sep 17 00:00:00 2001 From: j Date: Sun, 13 Oct 2024 17:20:33 +0100 Subject: [PATCH 10/57] fix sort --- static/mobile/js/item.js | 5 ++++- static/mobile/js/utils.js | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/static/mobile/js/item.js b/static/mobile/js/item.js index 7f81fa23..42c4d2cd 100644 --- a/static/mobile/js/item.js +++ b/static/mobile/js/item.js @@ -129,7 +129,10 @@ async function loadData(id, args) { ${icon.down} ${layerData.title} `) - data.layers[layer] = sortBy(data.layers[layer], ["+in", "+created"]) + 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( diff --git a/static/mobile/js/utils.js b/static/mobile/js/utils.js index a22dc75a..e3597fd5 100644 --- a/static/mobile/js/utils.js +++ b/static/mobile/js/utils.js @@ -199,10 +199,10 @@ function sortBy(array, by, map) { while (ret == 0 && index < by.length) { key = by[index].key; aValue = getSortValue( - map[key] ? map[key](a[key], a) : a[key] + map && map[key] ? map[key](a[key], a) : a[key] ); bValue = getSortValue( - map[key] ? map[key](b[key], b) : b[key] + map && map[key] ? map[key](b[key], b) : b[key] ); if ((aValue === null) != (bValue === null)) { ret = aValue === null ? 1 : -1; From 9e6ecb5459f5c964e035ac554bb33b880921c638 Mon Sep 17 00:00:00 2001 From: j Date: Fri, 18 Oct 2024 15:57:10 +0100 Subject: [PATCH 11/57] avoid people accidentally adding itesm to current video --- static/js/addFilesDialog.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/static/js/addFilesDialog.js b/static/js/addFilesDialog.js index 4bfa0a71..a8f6ecf0 100644 --- a/static/js/addFilesDialog.js +++ b/static/js/addFilesDialog.js @@ -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 From 03daede441edb4e3498c47502ee64668e7c137aa Mon Sep 17 00:00:00 2001 From: j Date: Fri, 18 Oct 2024 17:49:54 +0100 Subject: [PATCH 12/57] pass download --- static/mobile/js/utils.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/static/mobile/js/utils.js b/static/mobile/js/utils.js index e3597fd5..92d6c806 100644 --- a/static/mobile/js/utils.js +++ b/static/mobile/js/utils.js @@ -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); From a24d96b098a6584d547120edac6c6ee1f7444449 Mon Sep 17 00:00:00 2001 From: j Date: Thu, 7 Nov 2024 14:28:28 +0000 Subject: [PATCH 13/57] log invalid api requests --- pandora/oxdjango/api/views.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pandora/oxdjango/api/views.py b/pandora/oxdjango/api/views.py index 3a4a2e89..ce7df892 100644 --- a/pandora/oxdjango/api/views.py +++ b/pandora/oxdjango/api/views.py @@ -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', '{}')) From 59c2045ac6d7276db0346c3447f6228aa30f627e Mon Sep 17 00:00:00 2001 From: j Date: Thu, 7 Nov 2024 15:57:16 +0000 Subject: [PATCH 14/57] move first signup code into function for reuse --- pandora/user/utils.py | 32 ++++++++++++++++++++++++++++++++ pandora/user/views.py | 23 ++--------------------- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/pandora/user/utils.py b/pandora/user/utils.py index db557dfe..9abecaaa 100644 --- a/pandora/user/utils.py +++ b/pandora/user/utils.py @@ -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] diff --git a/pandora/user/views.py b/pandora/user/views.py index a2a678f1..588fcbf0 100644 --- a/pandora/user/views.py +++ b/pandora/user/views.py @@ -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,9 @@ 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')) From d83309d4cd36712c50c09e881389409a0f8b9100 Mon Sep 17 00:00:00 2001 From: j Date: Thu, 7 Nov 2024 16:28:18 +0000 Subject: [PATCH 15/57] ff --- pandora/user/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandora/user/views.py b/pandora/user/views.py index 588fcbf0..86e31537 100644 --- a/pandora/user/views.py +++ b/pandora/user/views.py @@ -179,6 +179,7 @@ def signup(request, data): else: user = User(username=data['username'], email=data['email']) user.set_password(data['password']) + user.save() prepare_user(user) if request.session.session_key: models.SessionData.objects.filter(session_key=request.session.session_key).update(user=user) From 34af2b1fab27c567d1c4cb835cf309fdff7667f2 Mon Sep 17 00:00:00 2001 From: j Date: Fri, 8 Nov 2024 12:29:15 +0000 Subject: [PATCH 16/57] add ocid based login --- pandora/app/oidc.py | 34 ++++++++++++++++++++++++++++++ pandora/app/views.py | 1 + pandora/settings.py | 28 +++++++++++++++++++++++-- pandora/urls.py | 4 +++- requirements.txt | 1 + static/js/account.js | 6 +++++- static/js/appPanel.js | 4 ++++ static/js/folders.js | 49 +++++++++++++++++++++++++------------------ static/js/home.js | 12 ++++++++--- static/js/mainMenu.js | 4 +++- static/js/utils.js | 14 +++++++++++++ update.py | 2 ++ 12 files changed, 131 insertions(+), 28 deletions(-) create mode 100644 pandora/app/oidc.py diff --git a/pandora/app/oidc.py b/pandora/app/oidc.py new file mode 100644 index 00000000..7739e1ba --- /dev/null +++ b/pandora/app/oidc.py @@ -0,0 +1,34 @@ +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 = claims.get("preferred_username") + 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): + print("update user", user, claims) + #user.save() + return user + + +def generate_username(email): + return unicodedata.normalize('NFKC', email)[:150] diff --git a/pandora/app/views.py b/pandora/app/views.py index 2abd1f99..734a5305 100644 --- a/pandora/app/views.py +++ b/pandora/app/views.py @@ -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()) diff --git a/pandora/settings.py b/pandora/settings.py index 084ce8b3..5045c6c9 100644 --- a/pandora/settings.py +++ b/pandora/settings.py @@ -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 = '' +#OIDC_RP_CLIENT_SECRET = '' +#OIDC_RP_SIGN_ALGO = "RS256" +#OIDC_OP_JWKS_ENDPOINT = "" +#OIDC_OP_AUTHORIZATION_ENDPOINT = "" +#OIDC_OP_TOKEN_ENDPOINT = "" +#OIDC_OP_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 @@ -323,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' + ] diff --git a/pandora/urls.py b/pandora/urls.py index 36af1aa0..36e9e8f3 100644 --- a/pandora/urls.py +++ b/pandora/urls.py @@ -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 @@ -36,6 +36,8 @@ def serve_static_file(path, location, content_type): urlpatterns = [ #path('admin/', admin.site.urls), + path('oidc/', include('mozilla_django_oidc.urls')), + re_path(r'^api/locale.(?P.*).json$', translation.views.locale_json), re_path(r'^api/upload/text/?$', text.views.upload), re_path(r'^api/upload/document/?$', document.views.upload), diff --git a/requirements.txt b/requirements.txt index b1ef09f9..3eabf484 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,3 +20,4 @@ future pytz pypdfium2 Pillow>=10 +mozilla-django-oidc==4.0.1 diff --git a/static/js/account.js b/static/js/account.js index e213330f..05b05321 100644 --- a/static/js/account.js +++ b/static/js/account.js @@ -337,7 +337,11 @@ pandora.ui.accountSignoutDialog = function() { that.close(); pandora.UI.set({page: ''}); pandora.api.signout({}, function(result) { - pandora.signout(result.data); + if (pandora.site.site.oidc) { + pandora.oidcLogout(); + } else { + pandora.signout(result.data); + } }); } }) diff --git a/static/js/appPanel.js b/static/js/appPanel.js index 5ca52e00..a2442d43 100644 --- a/static/js/appPanel.js +++ b/static/js/appPanel.js @@ -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)); diff --git a/static/js/folders.js b/static/js/folders.js index c23feedf..3d54aee9 100644 --- a/static/js/folders.js +++ b/static/js/folders.js @@ -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(); diff --git a/static/js/home.js b/static/js/home.js index 76162e7a..56649cf9 100644 --- a/static/js/home.js +++ b/static/js/home.js @@ -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); diff --git a/static/js/mainMenu.js b/static/js/mainMenu.js index 2efc217f..d3fdf493 100644 --- a/static/js/mainMenu.js +++ b/static/js/mainMenu.js @@ -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...')} ] }, diff --git a/static/js/utils.js b/static/js/utils.js index 7bf3fa5e..874e5d35 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -2650,6 +2650,20 @@ 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.oidcLogout = function() { + Ox.LoadingScreen().css({zIndex: 100}).addClass('OxScreen').appendTo(document.body).show().start() + const form = document.createElement("form"); + form.setAttribute("method", "post"); + form.setAttribute("action", "/oidc/logout/"); + document.body.appendChild(form); + form.submit(); +}; + pandora.openLicenseDialog = function() { if (!Ox.Focus.focusedElementIsInput() && !pandora.hasDialogOrScreen()) { pandora.ui.licenseDialog().open().bindEvent({ diff --git a/update.py b/update.py index f6b514b5..d414b97b 100755 --- a/update.py +++ b/update.py @@ -302,6 +302,8 @@ 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') else: if len(sys.argv) == 1: branch = get_branch() From ff236e882884899c96724874e53bf473a8fd17da Mon Sep 17 00:00:00 2001 From: j Date: Fri, 8 Nov 2024 12:47:47 +0000 Subject: [PATCH 17/57] only add oidc urls if oidc is enabled --- pandora/urls.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pandora/urls.py b/pandora/urls.py index 36e9e8f3..993dda65 100644 --- a/pandora/urls.py +++ b/pandora/urls.py @@ -33,11 +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), - - path('oidc/', include('mozilla_django_oidc.urls')), - +] +if settings.OIDC_RP_CLIENT_ID: + urlpatterns += [ + path('oidc/', include('mozilla_django_oidc.urls')), + ] +urlpatterns += [ re_path(r'^api/locale.(?P.*).json$', translation.views.locale_json), re_path(r'^api/upload/text/?$', text.views.upload), re_path(r'^api/upload/document/?$', document.views.upload), From 7cfe645ab73adfbb72c8dd9c6b5977f5025dda9b Mon Sep 17 00:00:00 2001 From: j Date: Sat, 9 Nov 2024 16:48:46 +0000 Subject: [PATCH 18/57] oidc logout is same as our logout, no need for special case --- static/js/account.js | 6 +----- static/js/utils.js | 9 --------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/static/js/account.js b/static/js/account.js index 05b05321..e213330f 100644 --- a/static/js/account.js +++ b/static/js/account.js @@ -337,11 +337,7 @@ pandora.ui.accountSignoutDialog = function() { that.close(); pandora.UI.set({page: ''}); pandora.api.signout({}, function(result) { - if (pandora.site.site.oidc) { - pandora.oidcLogout(); - } else { - pandora.signout(result.data); - } + pandora.signout(result.data); }); } }) diff --git a/static/js/utils.js b/static/js/utils.js index 874e5d35..6c0bb43b 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -2655,15 +2655,6 @@ pandora.oidcLogin = function() { document.location.href = '/oidc/authenticate/'; }; -pandora.oidcLogout = function() { - Ox.LoadingScreen().css({zIndex: 100}).addClass('OxScreen').appendTo(document.body).show().start() - const form = document.createElement("form"); - form.setAttribute("method", "post"); - form.setAttribute("action", "/oidc/logout/"); - document.body.appendChild(form); - form.submit(); -}; - pandora.openLicenseDialog = function() { if (!Ox.Focus.focusedElementIsInput() && !pandora.hasDialogOrScreen()) { pandora.ui.licenseDialog().open().bindEvent({ From 5c6c7e37c7a5509f422d3d21ef228493ed36dad8 Mon Sep 17 00:00:00 2001 From: j Date: Sat, 9 Nov 2024 16:59:47 +0000 Subject: [PATCH 19/57] remove debug --- pandora/app/oidc.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pandora/app/oidc.py b/pandora/app/oidc.py index 7739e1ba..31a5adcd 100644 --- a/pandora/app/oidc.py +++ b/pandora/app/oidc.py @@ -25,8 +25,6 @@ class OIDCAuthenticationBackend(mozilla_django_oidc.auth.OIDCAuthenticationBacke return user def update_user(self, user, claims): - print("update user", user, claims) - #user.save() return user From 3bc2b1bd3ed0a74c5ab344890c86fcf0b766f999 Mon Sep 17 00:00:00 2001 From: j Date: Sat, 9 Nov 2024 17:02:57 +0000 Subject: [PATCH 20/57] use preferred_username and fallback to name --- pandora/app/oidc.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pandora/app/oidc.py b/pandora/app/oidc.py index 31a5adcd..be397374 100644 --- a/pandora/app/oidc.py +++ b/pandora/app/oidc.py @@ -12,7 +12,11 @@ User = get_user_model() class OIDCAuthenticationBackend(mozilla_django_oidc.auth.OIDCAuthenticationBackend): def create_user(self, claims): user = super(OIDCAuthenticationBackend, self).create_user(claims) - username = claims.get("preferred_username") + 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 From ad2af2a25718328dac5151e2f5bdcee5c1ae4255 Mon Sep 17 00:00:00 2001 From: j Date: Sat, 14 Dec 2024 19:01:17 +0000 Subject: [PATCH 21/57] cleanup html --- pandora/templates/mobile/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandora/templates/mobile/index.html b/pandora/templates/mobile/index.html index 5cda3d82..e274b941 100644 --- a/pandora/templates/mobile/index.html +++ b/pandora/templates/mobile/index.html @@ -22,8 +22,8 @@ {% endif %} {% compress css file m %} - - + + {% endcompress %} From 627a016515970e82f83e027831febbd0efe4531a Mon Sep 17 00:00:00 2001 From: j Date: Sat, 14 Dec 2024 19:16:41 +0000 Subject: [PATCH 22/57] better mobile seek bar --- static/mobile/css/style.css | 72 ++++++++++++++++++++++++++++++ static/mobile/js/VideoPlayer.js | 78 +++++++++++++++++++-------------- 2 files changed, 118 insertions(+), 32 deletions(-) diff --git a/static/mobile/css/style.css b/static/mobile/css/style.css index 2dd8e28c..1ca261f5 100644 --- a/static/mobile/css/style.css +++ b/static/mobile/css/style.css @@ -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: 14px; + height: 14px; + position: absolute; + top: -3px; + right: -7px; + border: 2px solid #B1B1B180; + background-color: #B1B1B180; +} + +.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; +} diff --git a/static/mobile/js/VideoPlayer.js b/static/mobile/js/VideoPlayer.js index af219c54..f1503387 100644 --- a/static/mobile/js/VideoPlayer.js +++ b/static/mobile/js/VideoPlayer.js @@ -153,9 +153,11 @@ window.VideoPlayer = function(options) { ${icon.mute}
-
-
- +
+ +
+
+
@@ -223,15 +225,28 @@ window.VideoPlayer = function(options) { } } var showControls + function hideControlsLater() { + 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 +256,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 +265,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 +328,7 @@ window.VideoPlayer = function(options) { that.append(unblock) }) var loading = true + var touching = false that.brightness(0) that.addEventListener("loadedmetadata", event => { // @@ -331,34 +350,29 @@ 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', function(event){ + event.preventDefault() + event.stopPropagation() + setProgressPosition(this.value) + var position = this.value/100 * self.options.duration + 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) + '%' + setProgressPosition(100 * currentTime / duration) duration = formatDuration(duration) currentTime = formatDuration(currentTime) while (duration && duration.startsWith('00:')) { From 85eba0bf0957b990a9b0684ba0d0fe3aa1ad8f50 Mon Sep 17 00:00:00 2001 From: j Date: Sat, 14 Dec 2024 19:46:20 +0000 Subject: [PATCH 23/57] round seek element --- static/mobile/css/style.css | 12 ++++++------ static/mobile/js/VideoPlayer.js | 3 +++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/static/mobile/css/style.css b/static/mobile/css/style.css index 1ca261f5..aaf5a6ee 100644 --- a/static/mobile/css/style.css +++ b/static/mobile/css/style.css @@ -226,13 +226,13 @@ ol li { .seekbar-progress [role="progressbar"]:after { content: " "; display: block; - width: 14px; - height: 14px; + width: 20px; + height: 20px; position: absolute; - top: -3px; - right: -7px; - border: 2px solid #B1B1B180; - background-color: #B1B1B180; + top: -6px; + right: -10px; + background-color: #B1B1B1; + border-radius: 20px; } .seekbar input[type="range"] { diff --git a/static/mobile/js/VideoPlayer.js b/static/mobile/js/VideoPlayer.js index f1503387..c1f34850 100644 --- a/static/mobile/js/VideoPlayer.js +++ b/static/mobile/js/VideoPlayer.js @@ -226,6 +226,9 @@ window.VideoPlayer = function(options) { } var showControls function hideControlsLater() { + if (showControls) { + clearTimeout(showControls) + } showControls = setTimeout(() => { if (touching) { hideControlsLater() From dd8ea22d450d9de722e72fe9d11d85cba7ab4de6 Mon Sep 17 00:00:00 2001 From: j Date: Sat, 14 Dec 2024 20:15:47 +0000 Subject: [PATCH 24/57] preview new timecode --- static/mobile/js/VideoPlayer.js | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/static/mobile/js/VideoPlayer.js b/static/mobile/js/VideoPlayer.js index c1f34850..95720e2d 100644 --- a/static/mobile/js/VideoPlayer.js +++ b/static/mobile/js/VideoPlayer.js @@ -360,14 +360,24 @@ window.VideoPlayer = function(options) { progressbar.setAttribute('aria-valuenow', value); } - that.querySelector('.controls .position input').addEventListener('input', function(event){ + that.querySelector('.controls .position input').addEventListener('input', event => { event.preventDefault() event.stopPropagation() - setProgressPosition(this.value) - var position = this.value/100 * self.options.duration + setProgressPosition(event.target.value) + var position = event.target.value/100 * self.options.duration + displayTime(position) that.currentTime(position) hideControlsLater() }) + 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(), @@ -376,14 +386,7 @@ window.VideoPlayer = function(options) { currentTime -= self.options.position } setProgressPosition(100 * currentTime / duration) - duration = formatDuration(duration) - currentTime = formatDuration(currentTime) - while (duration && duration.startsWith('00:')) { - duration = duration.slice(3) - } - currentTime = currentTime.slice(currentTime.length - duration.length) - time.innerText = `${currentTime} / ${duration}` - + displayTime(currentTime) }) that.addEventListener("play", event => { From 10c78fc8623d60cb0c508ed50763c9cd99b0c0c5 Mon Sep 17 00:00:00 2001 From: j Date: Wed, 22 Jan 2025 17:42:44 +0530 Subject: [PATCH 25/57] allow selecting multiple annotations to get in/out range --- static/js/editor.js | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/static/js/editor.js b/static/js/editor.js index 90558d63..5d131c56 100644 --- a/static/js/editor.js +++ b/static/js/editor.js @@ -391,7 +391,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) @@ -428,6 +442,18 @@ pandora.ui.editor = function(data) { pandora._dontSelectResult = false; + 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({ From 38618e2ed29be69504202fb58b91f22a5c52b914 Mon Sep 17 00:00:00 2001 From: j Date: Thu, 23 Jan 2025 16:26:25 +0530 Subject: [PATCH 26/57] add folder/filename columns to media view --- .../0008_file_filename_file_folder.py | 32 +++++++++++++++++++ pandora/archive/models.py | 17 ++++++++-- static/js/mediaView.js | 16 ++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 pandora/archive/migrations/0008_file_filename_file_folder.py diff --git a/pandora/archive/migrations/0008_file_filename_file_folder.py b/pandora/archive/migrations/0008_file_filename_file_folder.py new file mode 100644 index 00000000..5c8b34da --- /dev/null +++ b/pandora/archive/migrations/0008_file_filename_file_folder.py @@ -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), + ] diff --git a/pandora/archive/models.py b/pandora/archive/models.py index a5800d3d..96e5a477 100644 --- a/pandora/archive/models.py +++ b/pandora/archive/models.py @@ -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 key in keys: + data[key] = getattr(self, key) if error: data['error'] = error for key in self.PATH_INFO: diff --git a/static/js/mediaView.js b/static/js/mediaView.js index f7773148..c453c7f7 100644 --- a/static/js/mediaView.js +++ b/static/js/mediaView.js @@ -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', From e5339fd29777cbb20292f5c766fcf64aa1daa125 Mon Sep 17 00:00:00 2001 From: j Date: Thu, 23 Jan 2025 16:34:31 +0530 Subject: [PATCH 27/57] close ipmort/export annotation after action --- static/js/exportAnnotationsDialog.js | 7 +++++++ static/js/importAnnotationsDialog.js | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/static/js/exportAnnotationsDialog.js b/static/js/exportAnnotationsDialog.js index 4db7cf84..79f0142d 100644 --- a/static/js/exportAnnotationsDialog.js +++ b/static/js/exportAnnotationsDialog.js @@ -102,6 +102,13 @@ pandora.ui.exportAnnotationsDialog = function(options) { $button.wrap($('')); // 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(); } diff --git a/static/js/importAnnotationsDialog.js b/static/js/importAnnotationsDialog.js index 13e83118..2c11160b 100644 --- a/static/js/importAnnotationsDialog.js +++ b/static/js/importAnnotationsDialog.js @@ -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.')); From 85b88c4dd630172e02872fd9b88bccc486878d1f Mon Sep 17 00:00:00 2001 From: j Date: Fri, 24 Jan 2025 14:55:58 +0530 Subject: [PATCH 28/57] work around invalid directors --- static/js/getItemTitle.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/static/js/getItemTitle.js b/static/js/getItemTitle.js index 286036c6..a13c8b83 100644 --- a/static/js/getItemTitle.js +++ b/static/js/getItemTitle.js @@ -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) ? ' (' + ( From c2df43220b8e1ace2b44e5b133ae8d08e694d254 Mon Sep 17 00:00:00 2001 From: j Date: Fri, 24 Jan 2025 14:56:11 +0530 Subject: [PATCH 29/57] only update if poster is valid --- pandora/item/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandora/item/models.py b/pandora/item/models.py index be7f3303..138b4fcd 100644 --- a/pandora/item/models.py +++ b/pandora/item/models.py @@ -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) From a3336d92b35239eacb3b67223c60f19fe314540e Mon Sep 17 00:00:00 2001 From: j Date: Fri, 24 Jan 2025 15:19:09 +0530 Subject: [PATCH 30/57] title or id is required to move files --- static/js/mediaView.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/static/js/mediaView.js b/static/js/mediaView.js index c453c7f7..733c5422 100644 --- a/static/js/mediaView.js +++ b/static/js/mediaView.js @@ -457,6 +457,8 @@ pandora.ui.mediaView = function(options) { }); } } + }).on({ + keyup: updateForm() }); }); @@ -679,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' From 277bbe45fbd9241d4db28d15019c0114241ca48d Mon Sep 17 00:00:00 2001 From: j Date: Fri, 24 Jan 2025 17:12:03 +0530 Subject: [PATCH 31/57] fix dialog keys --- static/js/metadataDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/metadataDialog.js b/static/js/metadataDialog.js index 3c0a718f..015ec833 100644 --- a/static/js/metadataDialog.js +++ b/static/js/metadataDialog.js @@ -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') }); } From 212ad3cee056187ca5fd8fac39c8a8fc8f1d68b3 Mon Sep 17 00:00:00 2001 From: j Date: Fri, 24 Jan 2025 17:12:29 +0530 Subject: [PATCH 32/57] add confirm dialog while deleting annotations --- static/js/editor.js | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/static/js/editor.js b/static/js/editor.js index 5d131c56..19a9f99e 100644 --- a/static/js/editor.js +++ b/static/js/editor.js @@ -21,6 +21,7 @@ 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, @@ -442,6 +443,44 @@ pandora.ui.editor = function(data) { pandora._dontSelectResult = false; + function confirmDeleteDialog(options, callback) { + const subject = options.items.length == 1 ? Ox._('annotation') : Ox._('annotations') + const $dialog = pandora.ui.iconDialog({ + buttons: [ + Ox.Button({ + id: 'cancel', + title: Ox._('Cancel') + }) + .bindEvent({ + click: function() { + $dialog.close(); + } + }), + Ox.Button({ + id: 'delete', + title: Ox._('Delete') + }) + .bindEvent({ + click: function() { + $dialog.close(); + callback() + } + }) + ], + content: Ox._( + 'Are you sure you want delete {0} {1}', + [options.items.length, subject] + ), + height: 96, + keys: {enter: 'delete', escape: 'cancel'}, + title: Ox._( + 'Delete {0} {1}', [options.items.length, subject] + ) + }); + $dialog.open() + return $dialog + } + function getAnnotationById(id) { var annotation data.annotations.forEach(layer => { From 722119215dfa4384275c89d541d2259d0a5bf6a7 Mon Sep 17 00:00:00 2001 From: j Date: Fri, 24 Jan 2025 17:30:31 +0530 Subject: [PATCH 33/57] fix loading media view --- static/js/mediaView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/mediaView.js b/static/js/mediaView.js index 733c5422..53b054ce 100644 --- a/static/js/mediaView.js +++ b/static/js/mediaView.js @@ -674,7 +674,7 @@ pandora.ui.mediaView = function(options) { disabled: true, value: true }); - } else { + } elif (self.$switch) { self.$switch.options({ disabled: false, value: self.wasChecked From e37281491ee29139528dd123492e7d1de6c6a5c9 Mon Sep 17 00:00:00 2001 From: j Date: Fri, 24 Jan 2025 17:31:51 +0530 Subject: [PATCH 34/57] typo --- static/js/mediaView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/mediaView.js b/static/js/mediaView.js index 53b054ce..21d4dd83 100644 --- a/static/js/mediaView.js +++ b/static/js/mediaView.js @@ -674,7 +674,7 @@ pandora.ui.mediaView = function(options) { disabled: true, value: true }); - } elif (self.$switch) { + } else if (self.$switch) { self.$switch.options({ disabled: false, value: self.wasChecked From a96097b8bfcbf004f97cc3c455e28ac7463d4368 Mon Sep 17 00:00:00 2001 From: j Date: Fri, 24 Jan 2025 17:33:52 +0530 Subject: [PATCH 35/57] pass function, don't call it --- static/js/mediaView.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/js/mediaView.js b/static/js/mediaView.js index 21d4dd83..a05aa981 100644 --- a/static/js/mediaView.js +++ b/static/js/mediaView.js @@ -458,7 +458,7 @@ pandora.ui.mediaView = function(options) { } } }).on({ - keyup: updateForm() + keyup: updateForm }); }); @@ -674,7 +674,7 @@ pandora.ui.mediaView = function(options) { disabled: true, value: true }); - } else if (self.$switch) { + } else { self.$switch.options({ disabled: false, value: self.wasChecked From 53502a27abba3a694671836b671a870f3ebdab30 Mon Sep 17 00:00:00 2001 From: j Date: Fri, 24 Jan 2025 18:06:39 +0530 Subject: [PATCH 36/57] processing, fix first word in sentence --- static/js/item.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/js/item.js b/static/js/item.js index 2837fedc..79d0899c 100644 --- a/static/js/item.js +++ b/static/js/item.js @@ -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._( - '{0} is currently processed. ' + '{0} 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) From c67f7c122bb9443b0392e7ea6df867e6f05972b0 Mon Sep 17 00:00:00 2001 From: j Date: Fri, 24 Jan 2025 19:53:59 +0530 Subject: [PATCH 37/57] non expistent capabilities are not available --- pandora/user/models.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pandora/user/models.py b/pandora/user/models.py index 7f0a51a8..4234ab45 100644 --- a/pandora/user/models.py +++ b/pandora/user/models.py @@ -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) From 34d1285e4b0fbae4e43b0692c9b39c61ec39cf51 Mon Sep 17 00:00:00 2001 From: j Date: Fri, 24 Jan 2025 19:54:18 +0530 Subject: [PATCH 38/57] transcribe audio dialog --- pandora/config.padma.jsonc | 1 + pandora/config.pandora.jsonc | 1 + static/js/editor.js | 37 ++++++++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/pandora/config.padma.jsonc b/pandora/config.padma.jsonc index 3bd3b748..82fe120c 100644 --- a/pandora/config.padma.jsonc +++ b/pandora/config.padma.jsonc @@ -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}, diff --git a/pandora/config.pandora.jsonc b/pandora/config.pandora.jsonc index 51aaee24..5f08873f 100644 --- a/pandora/config.pandora.jsonc +++ b/pandora/config.pandora.jsonc @@ -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}, diff --git a/static/js/editor.js b/static/js/editor.js index 19a9f99e..82a75344 100644 --- a/static/js/editor.js +++ b/static/js/editor.js @@ -27,6 +27,7 @@ pandora.ui.editor = function(data) { 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, @@ -432,6 +433,42 @@ 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 + }) + } + }) + ], + 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}); }, From c391d7f4faf897ae2afd536a27b9d44bdf158faa Mon Sep 17 00:00:00 2001 From: j Date: Sat, 25 Jan 2025 10:24:24 +0530 Subject: [PATCH 39/57] better delete anntoation dialog --- static/js/editor.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/static/js/editor.js b/static/js/editor.js index 82a75344..70cc2e3e 100644 --- a/static/js/editor.js +++ b/static/js/editor.js @@ -481,12 +481,16 @@ pandora.ui.editor = function(data) { pandora._dontSelectResult = false; function confirmDeleteDialog(options, callback) { - const subject = options.items.length == 1 ? Ox._('annotation') : Ox._('annotations') + 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._('Cancel') + title: Ox._('Keep {0}', [Ox._(subject)]) }) .bindEvent({ click: function() { @@ -495,7 +499,7 @@ pandora.ui.editor = function(data) { }), Ox.Button({ id: 'delete', - title: Ox._('Delete') + title: Ox._('Delete {0}', [Ox._(subject)]) }) .bindEvent({ click: function() { @@ -505,13 +509,13 @@ pandora.ui.editor = function(data) { }) ], content: Ox._( - 'Are you sure you want delete {0} {1}', - [options.items.length, subject] + 'Are you sure you want delete {0} {1}?

All data will be removed.', + [options.items.length, Ox._(subject.toLowerCase())] ), height: 96, keys: {enter: 'delete', escape: 'cancel'}, title: Ox._( - 'Delete {0} {1}', [options.items.length, subject] + 'Delete {0}', [Ox._(subject)] ) }); $dialog.open() From d7fbea4a20a90feb8590013dfa7c2e75602d9403 Mon Sep 17 00:00:00 2001 From: j Date: Sat, 25 Jan 2025 10:25:06 +0530 Subject: [PATCH 40/57] avoid loop while selecting multiple annotations --- static/js/item.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/static/js/item.js b/static/js/item.js index 79d0899c..18ccd28c 100644 --- a/static/js/item.js +++ b/static/js/item.js @@ -202,6 +202,11 @@ 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 + } + console.log("!! video points update", pandora.user.ui.itemView, options) pandora.$ui[pandora.user.ui.itemView].options(options); } } From 3f0e08a14241ae553a835cb323e410f2b6404788 Mon Sep 17 00:00:00 2001 From: j Date: Sat, 25 Jan 2025 10:28:53 +0530 Subject: [PATCH 41/57] keep transcribing taks status --- pandora/taskqueue/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pandora/taskqueue/models.py b/pandora/taskqueue/models.py index 1d114a2c..0d03f920 100644 --- a/pandora/taskqueue/models.py +++ b/pandora/taskqueue/models.py @@ -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(): From 8020e5966e7c22fb430d7fc71bc0a1b4b0d0f9ac Mon Sep 17 00:00:00 2001 From: j Date: Sat, 25 Jan 2025 10:30:00 +0530 Subject: [PATCH 42/57] remove debug --- static/js/item.js | 1 - 1 file changed, 1 deletion(-) diff --git a/static/js/item.js b/static/js/item.js index 18ccd28c..929b3794 100644 --- a/static/js/item.js +++ b/static/js/item.js @@ -206,7 +206,6 @@ pandora.ui.item = function() { if (options.selected == '' && Ox.isArray(pandora.$ui[pandora.user.ui.itemView].options('selected'))) { delete options.selected } - console.log("!! video points update", pandora.user.ui.itemView, options) pandora.$ui[pandora.user.ui.itemView].options(options); } } From 7eae1cf6e50e9ddb607bf63e408e4c0b61055980 Mon Sep 17 00:00:00 2001 From: j Date: Sat, 25 Jan 2025 10:46:39 +0530 Subject: [PATCH 43/57] open task dialog --- static/js/editor.js | 1 + static/js/tasksDialog.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/static/js/editor.js b/static/js/editor.js index 70cc2e3e..0138f239 100644 --- a/static/js/editor.js +++ b/static/js/editor.js @@ -455,6 +455,7 @@ pandora.ui.editor = function(data) { pandora.api.transcribeAudio({ item: pandora.user.ui.item }) + pandora.ui.tasksDialog().open(); } }) ], diff --git a/static/js/tasksDialog.js b/static/js/tasksDialog.js index dc5ec8fd..e2582cc6 100644 --- a/static/js/tasksDialog.js +++ b/static/js/tasksDialog.js @@ -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', From ef680080cfb3569377d9788176822682c93f4a76 Mon Sep 17 00:00:00 2001 From: j Date: Wed, 29 Jan 2025 16:55:34 +0530 Subject: [PATCH 44/57] fix end of video --- static/js/item.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/item.js b/static/js/item.js index 929b3794..b158be02 100644 --- a/static/js/item.js +++ b/static/js/item.js @@ -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)) } } From 0748265fe6398cfa1596e12a186a65a0f3e3ad57 Mon Sep 17 00:00:00 2001 From: j Date: Wed, 29 Jan 2025 19:43:14 +0530 Subject: [PATCH 45/57] reload editor after transcribtion is ready --- static/js/editor.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/static/js/editor.js b/static/js/editor.js index 0138f239..a4b066d1 100644 --- a/static/js/editor.js +++ b/static/js/editor.js @@ -454,6 +454,17 @@ pandora.ui.editor = function(data) { $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(); } From 406237b837b608e09af3e74a43b529230ae9bb52 Mon Sep 17 00:00:00 2001 From: j Date: Wed, 29 Jan 2025 19:44:28 +0530 Subject: [PATCH 46/57] support more image formats, server avif is supported for larger images --- pandora/archive/extract.py | 3 +++ pandora/document/fulltext.py | 5 +++-- pandora/document/models.py | 11 +++++++---- pandora/document/views.py | 9 ++++++++- pandora/urls.py | 2 +- requirements.txt | 2 ++ static/js/uploadDocumentDialog.js | 2 +- static/js/utils.js | 6 ++++-- update.py | 2 ++ 9 files changed, 31 insertions(+), 11 deletions(-) diff --git a/pandora/archive/extract.py b/pandora/archive/extract.py index 3d6c74fe..7b9f24ea 100644 --- a/pandora/archive/extract.py +++ b/pandora/archive/extract.py @@ -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' diff --git a/pandora/document/fulltext.py b/pandora/document/fulltext.py index c4c9aaff..5f2a3e50 100644 --- a/pandora/document/fulltext.py +++ b/pandora/document/fulltext.py @@ -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 diff --git a/pandora/document/models.py b/pandora/document/models.py index 68819f39..3b3a0262 100644 --- a/pandora/document/models.py +++ b/pandora/document/models.py @@ -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): diff --git a/pandora/document/views.py b/pandora/document/views.py index c2d9f631..6657f92c 100644 --- a/pandora/document/views.py +++ b/pandora/document/views.py @@ -378,15 +378,22 @@ actions.register(sortDocuments, cache=False) def file(request, id, name=None): document = get_document_or_404_json(request, id) + accept = request.headers.get("Accept") + if accept and 'image/' in accept and document.extension in ( + 'webp', 'heif', 'heic', 'avif' + ) and document.extension not in accept: + image_size = max(document.width, document.height) + return HttpFileResponse(document.thumbnail(image_size)) 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 diff --git a/pandora/urls.py b/pandora/urls.py index 993dda65..f6b66ae2 100644 --- a/pandora/urls.py +++ b/pandora/urls.py @@ -53,7 +53,7 @@ urlpatterns += [ re_path(r'^resetUI$', user.views.reset_ui), re_path(r'^collection/(?P.*?)/icon(?P\d*).jpg$', documentcollection.views.icon), re_path(r'^documents/(?P[A-Z0-9]+)/(?P\d*)p(?P[\d,]*).jpg$', document.views.thumbnail), - re_path(r'^documents/(?P[A-Z0-9]+)/(?P.*?\.[^\d]{3})$', document.views.file), + re_path(r'^documents/(?P[A-Z0-9]+)/(?P.*?\.[^\d]{3,4})$', document.views.file), re_path(r'^documents/(?P.*?)$', document.views.document), re_path(r'^edit/(?P.*?)/icon(?P\d*).jpg$', edit.views.icon), re_path(r'^list/(?P.*?)/icon(?P\d*).jpg$', itemlist.views.icon), diff --git a/requirements.txt b/requirements.txt index 3eabf484..f4085fe9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,4 +20,6 @@ future pytz pypdfium2 Pillow>=10 +pillow-heif +pillow-avif-plugin mozilla-django-oidc==4.0.1 diff --git a/static/js/uploadDocumentDialog.js b/static/js/uploadDocumentDialog.js index 5a9193ca..50eaeb0a 100644 --- a/static/js/uploadDocumentDialog.js +++ b/static/js/uploadDocumentDialog.js @@ -8,7 +8,7 @@ pandora.ui.uploadDocumentDialog = function(options, callback) { existingFiles = [], uploadFiles = [], - supportedExtensions = ['gif', 'jpg', 'jpeg', 'pdf', 'png'], + supportedExtensions = ['gif', 'jpg', 'jpeg', 'pdf', 'png', 'webp', 'heic', 'heif', 'avif'], filename, diff --git a/static/js/utils.js b/static/js/utils.js index 6c0bb43b..29e0d75c 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -421,7 +421,7 @@ pandora.createLinks = function($element) { }; pandora.uploadDroppedFiles = function(files) { - var documentExtensions = ['pdf', /* 'epub', 'txt', */ 'png', 'gif', 'jpg', 'jpeg']; + var documentExtensions = ['pdf', /* 'epub', 'txt', */ 'png', 'gif', 'jpg', 'jpeg', 'webp', 'heic', 'heif', 'avif']; files = Ox.map(files, function(file) { return file }); if (files.every(function(file) { @@ -2132,7 +2132,9 @@ pandora.getSpan = function(state, val, callback) { } else { state.span = val; } - } else if (Ox.contains(['gif', 'jpg', 'png'], extension)) { + } else if (Ox.contains([ + 'gif', 'gif', 'jpg', 'png', 'webp', 'heic', 'heif', 'avif' + ], extension)) { values = val.split(','); if (values.length == 4) { state.span = values.map(function(number, index) { diff --git a/update.py b/update.py index d414b97b..c5e7603e 100755 --- a/update.py +++ b/update.py @@ -304,6 +304,8 @@ if __name__ == "__main__": 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() From 7fe51fe7ac538620837499d14647edd9d9836d25 Mon Sep 17 00:00:00 2001 From: j Date: Thu, 30 Jan 2025 00:15:07 +0530 Subject: [PATCH 47/57] fix featured edits --- static/mobile/js/edits.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/mobile/js/edits.js b/static/mobile/js/edits.js index 5494fcb1..f691d618 100644 --- a/static/mobile/js/edits.js +++ b/static/mobile/js/edits.js @@ -91,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: { From 5591739531277074984b6fbad2f6e9e95069ffdf Mon Sep 17 00:00:00 2001 From: j Date: Thu, 30 Jan 2025 08:53:25 +0530 Subject: [PATCH 48/57] fix seek for items --- static/mobile/js/VideoElement.js | 8 ++++++-- static/mobile/js/VideoPlayer.js | 1 + static/mobile/js/main.js | 1 + static/mobile/js/render.js | 27 ++++++++++++++++----------- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/static/mobile/js/VideoElement.js b/static/mobile/js/VideoElement.js index 8238d95d..9abe27d9 100644 --- a/static/mobile/js/VideoElement.js +++ b/static/mobile/js/VideoElement.js @@ -8,6 +8,7 @@ VideoElement VideoElement Object loop loop playback playbackRate playback rate position start position + in start offset self Shared private variable ([options[, self]]) -> 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) { @@ -508,6 +511,7 @@ window.VideoElement = function(options) { } function setCurrentItemTime(currentTime) { + currentTime += self.options["in"] debug('Video', 'sCIT', currentTime, self.video.currentTime, 'delta', currentTime - self.video.currentTime); isReady(self.video, function(video) { diff --git a/static/mobile/js/VideoPlayer.js b/static/mobile/js/VideoPlayer.js index 95720e2d..b6e6ca57 100644 --- a/static/mobile/js/VideoPlayer.js +++ b/static/mobile/js/VideoPlayer.js @@ -10,6 +10,7 @@ window.VideoPlayer = function(options) { loop: false, muted: false, playbackRate: 1, + "in": 0, position: 0, volume: 1 } diff --git a/static/mobile/js/main.js b/static/mobile/js/main.js index c84b5260..421eacc2 100644 --- a/static/mobile/js/main.js +++ b/static/mobile/js/main.js @@ -67,6 +67,7 @@ function parseURL() { id = id.replace('/editor/', '/').replace('/player/', '/') type = "item" } + //console.log(type, id, args) return [type, id, args] } diff --git a/static/mobile/js/render.js b/static/mobile/js/render.js index 307c5d2f..40422e55 100644 --- a/static/mobile/js/render.js +++ b/static/mobile/js/render.js @@ -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') From 3cf3ba5c6fac9a38560a3f946f5c759c1528a24f Mon Sep 17 00:00:00 2001 From: j Date: Thu, 30 Jan 2025 10:15:41 +0530 Subject: [PATCH 49/57] fix multipart items --- static/mobile/js/VideoElement.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/static/mobile/js/VideoElement.js b/static/mobile/js/VideoElement.js index 9abe27d9..31fbcb8e 100644 --- a/static/mobile/js/VideoElement.js +++ b/static/mobile/js/VideoElement.js @@ -511,7 +511,6 @@ window.VideoElement = function(options) { } function setCurrentItemTime(currentTime) { - currentTime += self.options["in"] debug('Video', 'sCIT', currentTime, self.video.currentTime, 'delta', currentTime - self.video.currentTime); isReady(self.video, function(video) { @@ -532,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 From d0d1feca1818f8edf91aa1150eafea15882e156c Mon Sep 17 00:00:00 2001 From: j Date: Thu, 30 Jan 2025 17:00:02 +0530 Subject: [PATCH 50/57] only set annotation if it exists --- static/js/editor.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/js/editor.js b/static/js/editor.js index a4b066d1..24f9e557 100644 --- a/static/js/editor.js +++ b/static/js/editor.js @@ -222,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 = { From 9eb9fbe3a520465dd9810ce6c822eb69bc4eff48 Mon Sep 17 00:00:00 2001 From: j Date: Thu, 30 Jan 2025 19:09:43 +0530 Subject: [PATCH 51/57] cTA --- pandora/config.0xdb.jsonc | 1 + pandora/config.indiancinema.jsonc | 1 + 2 files changed, 2 insertions(+) diff --git a/pandora/config.0xdb.jsonc b/pandora/config.0xdb.jsonc index 61dfd6e2..9170569a 100644 --- a/pandora/config.0xdb.jsonc +++ b/pandora/config.0xdb.jsonc @@ -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": {}, diff --git a/pandora/config.indiancinema.jsonc b/pandora/config.indiancinema.jsonc index 11e7eb03..1dae93b4 100644 --- a/pandora/config.indiancinema.jsonc +++ b/pandora/config.indiancinema.jsonc @@ -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}, From 017d9be45aaff96017e97f7d1d1779e6e2710637 Mon Sep 17 00:00:00 2001 From: j Date: Fri, 31 Jan 2025 10:40:04 +0530 Subject: [PATCH 52/57] add .tiff as supported image format --- pandora/document/views.py | 11 +++++++---- static/js/uploadDocumentDialog.js | 4 +--- static/js/utils.js | 23 ++++++++++++++++++----- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/pandora/document/views.py b/pandora/document/views.py index 6657f92c..8ce0f35b 100644 --- a/pandora/document/views.py +++ b/pandora/document/views.py @@ -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 @@ -379,11 +380,13 @@ 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' - ) and document.extension not in accept: + 'webp', 'heif', 'heic', 'avif', 'tiff' + ) and mime_type not in accept: image_size = max(document.width, document.height) - return HttpFileResponse(document.thumbnail(image_size)) + return HttpFileResponse(document.thumbnail(image_size, accept=accept)) return HttpFileResponse(document.file.path) def thumbnail(request, id, size=256, page=None): diff --git a/static/js/uploadDocumentDialog.js b/static/js/uploadDocumentDialog.js index 50eaeb0a..7b80a241 100644 --- a/static/js/uploadDocumentDialog.js +++ b/static/js/uploadDocumentDialog.js @@ -8,8 +8,6 @@ pandora.ui.uploadDocumentDialog = function(options, callback) { existingFiles = [], uploadFiles = [], - supportedExtensions = ['gif', 'jpg', 'jpeg', 'pdf', 'png', 'webp', 'heic', 'heif', 'avif'], - 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.') diff --git a/static/js/utils.js b/static/js/utils.js index 29e0d75c..ea28e18f 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -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', 'webp', 'heic', 'heif', 'avif']; 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,9 +2147,7 @@ pandora.getSpan = function(state, val, callback) { } else { state.span = val; } - } else if (Ox.contains([ - 'gif', 'gif', 'jpg', 'png', 'webp', 'heic', 'heif', 'avif' - ], extension)) { + } else if (Ox.contains(pandora.imageExtensions, extension)) { values = val.split(','); if (values.length == 4) { state.span = values.map(function(number, index) { From 0da9097a6acdc65b2f1457b54300f1b52fa3ebe8 Mon Sep 17 00:00:00 2001 From: j Date: Fri, 31 Jan 2025 12:33:34 +0530 Subject: [PATCH 53/57] use first format by default --- pandora/archive/models.py | 2 +- pandora/item/views.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pandora/archive/models.py b/pandora/archive/models.py index 96e5a477..7423f1f3 100644 --- a/pandora/archive/models.py +++ b/pandora/archive/models.py @@ -553,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) diff --git a/pandora/item/views.py b/pandora/item/views.py index 8ad2db2e..1670fade 100644 --- a/pandora/item/views.py +++ b/pandora/item/views.py @@ -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']) From ba71665bc53250513b95e62dc4af50447df40930 Mon Sep 17 00:00:00 2001 From: j Date: Fri, 31 Jan 2025 12:36:09 +0530 Subject: [PATCH 54/57] default to single format, patents have expired. --- pandora/config.0xdb.jsonc | 2 +- pandora/config.indiancinema.jsonc | 2 +- pandora/config.padma.jsonc | 2 +- pandora/config.pandora.jsonc | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pandora/config.0xdb.jsonc b/pandora/config.0xdb.jsonc index 9170569a..0d4b1aae 100644 --- a/pandora/config.0xdb.jsonc +++ b/pandora/config.0xdb.jsonc @@ -1402,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, diff --git a/pandora/config.indiancinema.jsonc b/pandora/config.indiancinema.jsonc index 1dae93b4..9c22a02f 100644 --- a/pandora/config.indiancinema.jsonc +++ b/pandora/config.indiancinema.jsonc @@ -1888,7 +1888,7 @@ 240, 288, 360, 432, 480, 720 and 1080. */ "video": { - "formats": ["webm", "mp4"], + "formats": ["mp4"], "previewRatio": 1.375, "resolutions": [240, 480] } diff --git a/pandora/config.padma.jsonc b/pandora/config.padma.jsonc index 82fe120c..a3e41988 100644 --- a/pandora/config.padma.jsonc +++ b/pandora/config.padma.jsonc @@ -1392,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 diff --git a/pandora/config.pandora.jsonc b/pandora/config.pandora.jsonc index 5f08873f..b0a64ab4 100644 --- a/pandora/config.pandora.jsonc +++ b/pandora/config.pandora.jsonc @@ -1281,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] } From 194b286e30c9c33001be3107ba69bb37bb08ad80 Mon Sep 17 00:00:00 2001 From: j Date: Thu, 6 Feb 2025 15:06:27 +0000 Subject: [PATCH 55/57] fix keys=None --- pandora/archive/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandora/archive/models.py b/pandora/archive/models.py index 7423f1f3..4bbd5da1 100644 --- a/pandora/archive/models.py +++ b/pandora/archive/models.py @@ -485,7 +485,7 @@ class File(models.Model): 'wanted': self.wanted, } for key in ('folder', 'filename'): - if key in keys: + if keys and key in keys: data[key] = getattr(self, key) if error: data['error'] = error From c879d49d84440826395808a880b950e14e84878a Mon Sep 17 00:00:00 2001 From: j Date: Tue, 11 Feb 2025 11:48:12 +0100 Subject: [PATCH 56/57] include clip details in edit --- pandora/edit/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pandora/edit/models.py b/pandora/edit/models.py index 5008ac45..94ad7a82 100644 --- a/pandora/edit/models.py +++ b/pandora/edit/models.py @@ -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 From 3f3fa9ab2aa4a6effd38d2fb0a46f77286a0b911 Mon Sep 17 00:00:00 2001 From: j Date: Thu, 27 Feb 2025 21:48:23 +0100 Subject: [PATCH 57/57] reset itemFind --- static/js/pandora.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/static/js/pandora.js b/static/js/pandora.js index caeef67a..dcf6ba1a 100644 --- a/static/js/pandora.js +++ b/static/js/pandora.js @@ -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) {