diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 42a6039..bf01945 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -40,29 +40,6 @@ jobs:
- name: Build
run: npx gulp build
- - name: Check for unstaged changes
- run: |
- if ! git diff --exit-code -- video2commons/frontend/static/*.min.js video2commons/frontend/templates/*.min.html; then
- echo "Built files differ from committed files."
- git diff --stat -- video2commons/frontend/static/*.min.js video2commons/frontend/templates/*.min.html
- echo "NEEDS_COMMIT=true" >> $GITHUB_ENV
- else
- echo "Built files match committed files."
- echo "NEEDS_COMMIT=false" >> $GITHUB_ENV
- fi
-
- - name: Commit and push changes
- if: env.NEEDS_COMMIT == 'true'
- run: |
- git checkout HEAD -- video2commons/frontend/static/ssu video2commons/frontend/static/uploads
- git fetch origin $GITHUB_HEAD_REF
- git checkout $GITHUB_HEAD_REF
- git config user.name "github-actions[bot]"
- git config user.email "github-actions[bot]@users.noreply.github.com"
- git add video2commons/frontend/static/*.min.js video2commons/frontend/templates/*.min.html
- git commit -m "Update built files from CI"
- git push origin $GITHUB_HEAD_REF
-
biome:
runs-on: ubuntu-latest
diff --git a/.github/workflows/deploy-cloudvps-encoders.yml b/.github/workflows/deploy-cloudvps-encoders.yml
new file mode 100644
index 0000000..cf01e53
--- /dev/null
+++ b/.github/workflows/deploy-cloudvps-encoders.yml
@@ -0,0 +1,53 @@
+name: Deploy Encoders (CloudVPS)
+
+on:
+ workflow_dispatch:
+
+ # Uncomment to allow the workflow to run automatically when changes are
+ # merged into master (only for files that affect this deployment).
+ #
+ # push:
+ # branches: [master]
+ # paths:
+ # - 'puppet/**'
+ # - 'pyproject.toml'
+ # - 'utils/deploy-cloudvps-encoders.sh'
+ # - 'uv.lock'
+ # - 'video2commons/backend/**'
+ # - 'video2commons/config.py'
+ # - 'video2commons/exceptions.py'
+ # - 'video2commons/shared/**'
+
+jobs:
+ deploy-cloudvps-encoders:
+ name: Deploy Encoders (CloudVPS)
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+
+ - name: Setup SSH
+ run: |
+ mkdir -p ~/.ssh
+ echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
+ chmod 600 ~/.ssh/id_ed25519
+ cat >> ~/.ssh/config << 'EOF'
+ Host *
+ IdentityFile ~/.ssh/id_ed25519
+ IdentitiesOnly yes
+ StrictHostKeyChecking no
+
+ Host *.wikimedia.cloud
+ ProxyJump login.toolforge.org:22
+ EOF
+ chmod 600 ~/.ssh/config
+ mkdir -p ~/.ssh/sockets
+
+ - name: Deploy
+ run: ./utils/deploy-cloudvps-encoders.sh
+ env:
+ V2C_USERNAME: ${{ secrets.V2C_USERNAME }}
+ V2C_REDIS_PW: ${{ secrets.V2C_REDIS_PW }}
+ V2C_CONSUMER_SECRET: ${{ secrets.V2C_CONSUMER_SECRET }}
+ V2C_CONSUMER_KEY: ${{ secrets.V2C_CONSUMER_KEY }}
diff --git a/.github/workflows/deploy-toolforge-video2commons-socketio.yml b/.github/workflows/deploy-toolforge-video2commons-socketio.yml
new file mode 100644
index 0000000..d40df55
--- /dev/null
+++ b/.github/workflows/deploy-toolforge-video2commons-socketio.yml
@@ -0,0 +1,47 @@
+name: Deploy Socket.IO (Toolforge)
+
+on:
+ workflow_dispatch:
+
+ # Uncomment to allow the workflow to run automatically when changes are
+ # merged into master (only for files that affect this deployment).
+ #
+ # push:
+ # branches: [master]
+ # paths:
+ # - 'package-lock.json'
+ # - 'package.json'
+ # - 'utils/deploy-toolforge-socketio.sh'
+ # - 'video2commons-socketio/**'
+
+jobs:
+ deploy-toolforge-video2commons-socketio:
+ name: Deploy Socket.IO (Toolforge)
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+
+ - name: Setup SSH
+ run: |
+ mkdir -p ~/.ssh
+ echo '${{ secrets.SSH_PRIVATE_KEY }}' > ~/.ssh/id_ed25519
+ chmod 600 ~/.ssh/id_ed25519
+ cat >> ~/.ssh/config << 'EOF'
+ Host *
+ IdentityFile ~/.ssh/id_ed25519
+ IdentitiesOnly yes
+ StrictHostKeyChecking no
+ ControlMaster auto
+ ControlPath ~/.ssh/sockets/%r@%h-%p
+ ControlPersist 60
+ EOF
+ chmod 600 ~/.ssh/config
+ mkdir -p ~/.ssh/sockets
+
+ - name: Deploy
+ run: ./utils/deploy-toolforge-socketio.sh
+ env:
+ V2C_SERVICE_NAME: video2commons-socketio
+ V2C_USERNAME: ${{ secrets.V2C_USERNAME }}
diff --git a/.github/workflows/deploy-toolforge-video2commons-test.yml b/.github/workflows/deploy-toolforge-video2commons-test.yml
new file mode 100644
index 0000000..2e33def
--- /dev/null
+++ b/.github/workflows/deploy-toolforge-video2commons-test.yml
@@ -0,0 +1,66 @@
+name: Deploy Test Frontend (Toolforge)
+
+on:
+ workflow_dispatch:
+
+ push:
+ branches: [master]
+ paths:
+ - 'Gulpfile.mjs'
+ - 'package-lock.json'
+ - 'package.json'
+ - 'pyproject.toml'
+ - 'utils/deploy-toolforge-frontend.sh'
+ - 'uv.lock'
+ - 'video2commons/config.py'
+ - 'video2commons/exceptions.py'
+ - 'video2commons/frontend/**'
+ - 'video2commons/shared/**'
+
+jobs:
+ deploy-toolforge-video2commons-test:
+ name: Deploy Test Frontend (Toolforge)
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version-file: '.nvmrc'
+ cache: 'npm'
+
+ - name: Remove broken symlinks
+ run: |
+ rm -f video2commons/frontend/static/ssu
+ rm -f video2commons/frontend/static/uploads
+
+ - name: Build
+ run: |
+ npm ci
+ npx gulp build
+
+ - name: Setup SSH
+ run: |
+ mkdir -p ~/.ssh
+ echo '${{ secrets.SSH_PRIVATE_KEY }}' > ~/.ssh/id_ed25519
+ chmod 600 ~/.ssh/id_ed25519
+ cat >> ~/.ssh/config << 'EOF'
+ Host *
+ IdentityFile ~/.ssh/id_ed25519
+ IdentitiesOnly yes
+ StrictHostKeyChecking no
+ ControlMaster auto
+ ControlPath ~/.ssh/sockets/%r@%h-%p
+ ControlPersist 60
+ EOF
+ chmod 600 ~/.ssh/config
+ mkdir -p ~/.ssh/sockets
+
+ - name: Deploy
+ run: ./utils/deploy-toolforge-frontend.sh
+ env:
+ V2C_SERVICE_NAME: video2commons-test
+ V2C_USERNAME: ${{ secrets.V2C_USERNAME }}
diff --git a/.github/workflows/deploy-toolforge-video2commons.yml b/.github/workflows/deploy-toolforge-video2commons.yml
new file mode 100644
index 0000000..9584c3e
--- /dev/null
+++ b/.github/workflows/deploy-toolforge-video2commons.yml
@@ -0,0 +1,69 @@
+name: Deploy Frontend (Toolforge)
+
+on:
+ workflow_dispatch:
+
+ # Uncomment to allow the workflow to run automatically when changes are
+ # merged into master (only for files that affect this deployment).
+ #
+ # push:
+ # branches: [master]
+ # paths:
+ # - 'Gulpfile.mjs'
+ # - 'package-lock.json'
+ # - 'package.json'
+ # - 'pyproject.toml'
+ # - 'utils/deploy-toolforge-frontend.sh'
+ # - 'uv.lock'
+ # - 'video2commons/config.py'
+ # - 'video2commons/exceptions.py'
+ # - 'video2commons/frontend/**'
+ # - 'video2commons/shared/**'
+
+jobs:
+ deploy-toolforge-video2commons:
+ name: Deploy Frontend (Toolforge)
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version-file: '.nvmrc'
+ cache: 'npm'
+
+ - name: Remove broken symlinks
+ run: |
+ rm -f video2commons/frontend/static/ssu
+ rm -f video2commons/frontend/static/uploads
+
+ - name: Build
+ run: |
+ npm ci
+ npx gulp build
+
+ - name: Setup SSH
+ run: |
+ mkdir -p ~/.ssh
+ echo '${{ secrets.SSH_PRIVATE_KEY }}' > ~/.ssh/id_ed25519
+ chmod 600 ~/.ssh/id_ed25519
+ cat >> ~/.ssh/config << 'EOF'
+ Host *
+ IdentityFile ~/.ssh/id_ed25519
+ IdentitiesOnly yes
+ StrictHostKeyChecking no
+ ControlMaster auto
+ ControlPath ~/.ssh/sockets/%r@%h-%p
+ ControlPersist 60
+ EOF
+ chmod 600 ~/.ssh/config
+ mkdir -p ~/.ssh/sockets
+
+ - name: Deploy
+ run: ./utils/deploy-toolforge-frontend.sh
+ env:
+ V2C_SERVICE_NAME: video2commons
+ V2C_USERNAME: ${{ secrets.V2C_USERNAME }}
diff --git a/.gitignore b/.gitignore
index 2f6b4c7..f7b2229 100644
--- a/.gitignore
+++ b/.gitignore
@@ -52,3 +52,5 @@ service.manifest
uwsgi.log
uwsgi.log.*
www/js/
+video2commons/frontend/static/*.min.js
+video2commons/frontend/templates/*.min.html
diff --git a/puppet/backend.pp b/puppet/backend.pp
index 7ae8bab..1c596f8 100644
--- a/puppet/backend.pp
+++ b/puppet/backend.pp
@@ -176,6 +176,7 @@
EnvironmentFile=-/etc/default/v2ccelery
WorkingDirectory=/srv/v2c
Restart=on-failure
+TimeoutStopSec=infinity
ExecStart=/bin/sh -c \'${CELERY_BIN} multi start $CELERYD_NODES \
-A $CELERY_APP --logfile=${CELERYD_LOG_FILE} \
--pidfile=${CELERYD_PID_FILE} $CELERYD_OPTS\'
diff --git a/utils/deploy.sh b/utils/deploy-cloudvps-encoders.sh
similarity index 91%
rename from utils/deploy.sh
rename to utils/deploy-cloudvps-encoders.sh
index 9179746..d83eb37 100755
--- a/utils/deploy.sh
+++ b/utils/deploy-cloudvps-encoders.sh
@@ -58,6 +58,9 @@ fi
EOF
)
+worker_count=$(echo "$encoder_hosts" | wc -l)
+success_count=0
+
while read -r encoder_host; do
echo "Applying puppet manifest to '$encoder_host' and restarting v2c service..."
@@ -67,7 +70,12 @@ while read -r encoder_host; do
echo "Failed to apply puppet manifest to '$encoder_host'" >&2
else
echo "Puppet manifest applied to '$encoder_host'"
+ success_count=$((success_count + 1))
fi
done <<< "$encoder_hosts"
-echo "Done"
+echo "Done. Updated ($success_count/$worker_count) workers"
+
+if [ "$success_count" -ne "$worker_count" ]; then
+ exit 1
+fi
diff --git a/utils/deploy-toolforge-frontend.sh b/utils/deploy-toolforge-frontend.sh
new file mode 100755
index 0000000..ad7856f
--- /dev/null
+++ b/utils/deploy-toolforge-frontend.sh
@@ -0,0 +1,73 @@
+#!/bin/bash
+
+# Note: All ssh commands share the same connection for the lifetime of this
+# script if run via the workflows.
+#
+# The follow ssh options are used by the workflow:
+# ControlMaster auto
+# ControlPath ~/.ssh/sockets/%r@%h-%p
+# ControlPersist 60
+
+bastion_host=login.toolforge.org
+
+if [ -z "$V2C_USERNAME" ]; then
+ echo "Error: V2C_USERNAME environment variable is not set" >&2
+ exit 1
+elif [ -z "$V2C_SERVICE_NAME" ]; then
+ echo "Error: V2C_SERVICE_NAME environment variable is not set" >&2
+ exit 1
+fi
+
+remote_repo_path="/data/project/$V2C_SERVICE_NAME"
+script_dir="$(cd "$(dirname "$0")" && pwd)"
+host_repo_root="$script_dir/.."
+tmp_dir="/tmp/v2c-deploy-$V2C_SERVICE_NAME"
+
+echo "Updating video2commons frontend..."
+
+# Pull in the latest changes from the repository currently in master.
+ssh "$V2C_USERNAME@$bastion_host" "become $V2C_SERVICE_NAME bash -c 'cd $remote_repo_path && git pull'"
+
+if [ $? -ne 0 ]; then
+ echo "Failed to pull most recent changes for v2c" >&2
+ exit 1
+fi
+
+# Create a temp directory for temporarily storing the minified files.
+ssh "$V2C_USERNAME@$bastion_host" "mkdir -p $tmp_dir"
+
+if [ $? -ne 0 ]; then
+ echo "Failed to create temporary directory at $tmp_dir" >&2
+ exit 1
+fi
+
+# Upload the minified files to the new temp directory.
+scp "$host_repo_root/video2commons/frontend/static/"*.min.js \
+ "$host_repo_root/video2commons/frontend/templates/"*.min.html \
+ "$V2C_USERNAME@$bastion_host:$tmp_dir/"
+
+if [ $? -ne 0 ]; then
+ echo "Failed to copy minified files to remote" >&2
+ ssh "$V2C_USERNAME@$bastion_host" "rm -rf $tmp_dir"
+ exit 1
+fi
+
+# Copy the minified files to destination with correct ownership, then cleanup.
+ssh "$V2C_USERNAME@$bastion_host" "become $V2C_SERVICE_NAME bash -c '
+ cp $tmp_dir/*.min.js $remote_repo_path/video2commons/frontend/static/
+ cp $tmp_dir/*.min.html $remote_repo_path/video2commons/frontend/templates/
+' && rm -rf $tmp_dir"
+
+if [ $? -ne 0 ]; then
+ echo "Failed to move minified files to destination" >&2
+ exit 1
+fi
+
+# Restart the webservice so any Python changes are applied.
+ssh "$V2C_USERNAME@$bastion_host" "become $V2C_SERVICE_NAME toolforge webservice python3.11 restart"
+
+# Cleanup the SSH control socket that we use to keep the connection alive
+# across multiple ssh command executions.
+ssh -O exit "$V2C_USERNAME@$bastion_host" 2>/dev/null || true
+
+echo "Done"
diff --git a/utils/deploy-toolforge-socketio.sh b/utils/deploy-toolforge-socketio.sh
new file mode 100755
index 0000000..d4edeb5
--- /dev/null
+++ b/utils/deploy-toolforge-socketio.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+# Note: All ssh commands share the same connection for the lifetime of this
+# script if run via the workflows.
+#
+# The follow ssh options are used by the workflow:
+# ControlMaster auto
+# ControlPath ~/.ssh/sockets/%r@%h-%p
+# ControlPersist 60
+
+bastion_host=login.toolforge.org
+
+if [ -z "$V2C_USERNAME" ]; then
+ echo "Error: V2C_USERNAME environment variable is not set" >&2
+ exit 1
+elif [ -z "$V2C_SERVICE_NAME" ]; then
+ echo "Error: V2C_SERVICE_NAME environment variable is not set" >&2
+ exit 1
+fi
+
+remote_repo_path="/data/project/$V2C_SERVICE_NAME"
+
+echo "Updating video2commons socket.io backend..."
+
+# Pull in the latest changes from the repository currently in master.
+ssh "$V2C_USERNAME@$bastion_host" "become $V2C_SERVICE_NAME bash -c 'cd $remote_repo_path && git pull'"
+
+if [ $? -ne 0 ]; then
+ echo "Failed to pull most recent changes for v2c" >&2
+ exit 1
+fi
+
+# Restart the webservice so any JavaScript changes are applied.
+ssh "$V2C_USERNAME@$bastion_host" "become $V2C_SERVICE_NAME toolforge webservice --backend=kubernetes node18 restart"
+
+# Cleanup the SSH control socket that we use to keep the connection alive
+# across multiple ssh command executions.
+ssh -O exit "$V2C_USERNAME@$bastion_host" 2>/dev/null || true
+
+echo "Done"
diff --git a/video2commons/frontend/static/templates.min.js b/video2commons/frontend/static/templates.min.js
deleted file mode 100644
index c1e530c..0000000
--- a/video2commons/frontend/static/templates.min.js
+++ /dev/null
@@ -1 +0,0 @@
-(window.nunjucksPrecompiled=window.nunjucksPrecompiled||{})["addTask.html"]={root:function(e,o,a,l,s){var t=0,r=0,p="";try{s(null,(p+='
')}catch(e){s(l.handleError(e,t,r))}}},(window.nunjucksPrecompiled=window.nunjucksPrecompiled||{})["confirmForm.html"]={root:function(e,o,a,l,s){var t=0,r=0,p="";try{s(null,(p+='")}catch(e){s(l.handleError(e,t,r))}}},(window.nunjucksPrecompiled=window.nunjucksPrecompiled||{})["playlistConfirmForm.html"]={root:function(e,o,a,l,s){var t=0,r=0,p="";try{p=(p=(p+='')+l.suppressValue((t=0,r=677,l.callWrap(l.contextOrFrameLookup(o,a,"_"),"_",o,["confirmmsg"])),e.opts.autoescape)+"
")}catch(e){s(l.handleError(e,t,r))}}},(window.nunjucksPrecompiled=window.nunjucksPrecompiled||{})["playlistForm.html"]={root:function(e,o,a,l,s){var t=0,r=0,p="";try{p=(p=(p=(p=(p=(p+='
')+l.suppressValue(l.memberLookup(l.contextOrFrameLookup(o,a,"task"),"title"),e.opts.autoescape)+'')+l.suppressValue((t=0,r=456,l.callWrap(l.contextOrFrameLookup(o,a,"_"),"_",o,["filename"])),e.opts.autoescape)+" ")+l.suppressValue((t=0,r=484,l.callWrap(l.contextOrFrameLookup(o,a,"_"),"_",o,["transcoding"])),e.opts.autoescape)+' ')+l.suppressValue((t=0,r=535,l.callWrap(l.contextOrFrameLookup(o,a,"_"),"_",o,["actions"])),e.opts.autoescape)+" ",a=a.push();var c=l.memberLookup(l.contextOrFrameLookup(o,a,"task"),"videos");if(c)for(var i=(c=l.fromIterator(c)).length,n=0;n')+l.suppressValue(l.memberLookup(u,"filename"),e.opts.autoescape)+' ')+l.suppressValue(l.memberLookup(u,"format"),e.opts.autoescape)+' '}i||(p=(p+=' ')+l.suppressValue((t=0,r=1231,l.callWrap(l.contextOrFrameLookup(o,a,"_"),"_",o,["noVideos"])),e.opts.autoescape)+" "),a=a.pop(),s(null,p+="
")}catch(e){s(l.handleError(e,t,r))}}},(window.nunjucksPrecompiled=window.nunjucksPrecompiled||{})["sourceForm.html"]={root:function(e,o,a,l,s){var t=0,r=0,p="";try{s(null,(p+=' URL ')+l.suppressValue((t=0,r=325,l.callWrap(l.contextOrFrameLookup(o,a,"_"),"_",o,["uploadFile"])),e.opts.autoescape)+'
'+l.suppressValue((t=0,r=922,l.callWrap(l.contextOrFrameLookup(o,a,"_"),"_",o,["keepvideo"])),e.opts.autoescape)+'
'+l.suppressValue((t=0,r=1046,l.callWrap(l.contextOrFrameLookup(o,a,"_"),"_",o,["keepaudio"])),e.opts.autoescape)+'
'+l.suppressValue((t=0,r=1174,l.callWrap(l.contextOrFrameLookup(o,a,"_"),"_",o,["importsubtitles"])),e.opts.autoescape)+'
'+l.suppressValue((t=0,r=1275,l.callWrap(l.contextOrFrameLookup(o,a,"_"),"_",o,["youtubewarning"])),e.opts.autoescape)+'
'+l.suppressValue((t=0,r=1336,l.callWrap(l.contextOrFrameLookup(o,a,"_"),"_",o,["note"])),e.opts.autoescape)+"
"+l.suppressValue((t=0,r=1359,l.callWrap(l.contextOrFrameLookup(o,a,"_"),"_",o,["notesplaylists"])),e.opts.autoescape)+" "+l.suppressValue((t=0,r=1393,l.callWrap(l.contextOrFrameLookup(o,a,"_"),"_",o,["notesuncheck"])),e.opts.autoescape)+" "+l.suppressValue((t=0,r=1425,l.callWrap(l.contextOrFrameLookup(o,a,"_"),"_",o,["notessubtitles"])),e.opts.autoescape)+" "+l.suppressValue((t=0,r=1463,l.callWrap(l.contextOrFrameLookup(o,a,"_"),"_",o,["importantnote"])),e.opts.autoescape)+" "+l.suppressValue(e.getFilter("process_link").call(o,(t=0,r=1492,l.callWrap(l.contextOrFrameLookup(o,a,"_"),"_",o,["noteslicensing"]))),e.opts.autoescape)+"
")}catch(e){s(l.handleError(e,t,r))}}},(window.nunjucksPrecompiled=window.nunjucksPrecompiled||{})["targetForm.html"]={root:function(e,o,a,l,s){var t=0,r=0,p="";try{p=(p=(p=(p=(p+=' ')+l.suppressValue((t=0,r=488,l.callWrap(l.contextOrFrameLookup(o,a,"_"),"_",o,["filedescription"])),e.opts.autoescape)+'
')+l.suppressValue((t=0,r=756,l.callWrap(l.contextOrFrameLookup(o,a,"_"),"_",o,["languagecategory"])),e.opts.autoescape)+'
')+l.suppressValue((t=0,r=993,l.callWrap(l.contextOrFrameLookup(o,a,"_"),"_",o,["datecategory"])),e.opts.autoescape)+" ";p=l.memberLookup(l.contextOrFrameLookup(o,a,"source"),"audio")&&!l.memberLookup(l.contextOrFrameLookup(o,a,"source"),"video")?(p+=' ')+l.suppressValue(e.getFilter("replace").call(o,(t=0,r=1093,l.callWrap(l.contextOrFrameLookup(o,a,"_"),"_",o,["datecategory-help"])),"$1",'"Audio files of YYYY"'),e.opts.autoescape)+" ":(p+=' ')+l.suppressValue(e.getFilter("replace").call(o,(t=0,r=1206,l.callWrap(l.contextOrFrameLookup(o,a,"_"),"_",o,["datecategory-help"])),"$1",'"Videos of YYYY", "Videos taken on YYYY-MM-DD"'),e.opts.autoescape)+" ",s(null,p=(p+='
')+l.suppressValue((t=0,r=1585,l.callWrap(l.contextOrFrameLookup(o,a,"_"),"_",o,["extensionmsg"])),e.opts.autoescape)+"
")}catch(e){s(l.handleError(e,t,r))}}};
\ No newline at end of file
diff --git a/video2commons/frontend/static/video2commons.min.js b/video2commons/frontend/static/video2commons.min.js
deleted file mode 100644
index 5208810..0000000
--- a/video2commons/frontend/static/video2commons.min.js
+++ /dev/null
@@ -1,5 +0,0 @@
-(s=>{var n,r,l,t,i,o=window.config,d=window.i18n,c=' ',e="rtl"===d["@dir"],u={abortbutton:` ${nunjucks.lib.escape(d.abort)} `,removebutton:` ${nunjucks.lib.escape(d.remove)} `,restartbutton:` ${nunjucks.lib.escape(d.restart)} `,loading:`${c} ${nunjucks.lib.escape(d.loading)} `,errorDisconnect:`${nunjucks.lib.escape(d.errorDisconnect)}
`,yourTasks:`${nunjucks.lib.escape(d.yourTasks)} `,workers:`${nunjucks.lib.escape(d.workers)} `,capacity:e?`... ${d.capacity}
`:`${d.capacity} ...
`,utilization:e?`... ${d.utilization}
`:`${d.utilization} ...
`,pending:e?`... ${d.pending}
`:`${d.pending} ...
`,addTask:` `,requestServerSide:`${nunjucks.lib.escape(d.createServerSide)} `,progressbar:'',prevbutton:` `+nunjucks.lib.escape(d.back),nextbutton:nunjucks.lib.escape(d.next)+` `,confirmbutton:nunjucks.lib.escape(d.confirm)},g="",p=(new nunjucks.Environment).addGlobal("config",o).addGlobal("_",e=>d[e]).addFilter("process_link",e=>{for(var a,t=/\{\{#a\}\}(.*?)\{\{\/a\}\}/g,i=0,o="";null!==(a=t.exec(e));)o=(o+=nunjucks.lib.escape(e.substring(i,a.index)))+(e=>{if("#"===e[0]){var a=e.indexOf("|");if(a<0){if(/^[a-z0-9-]+$/i.test(e.slice(1)))return` `}else if(/^[a-z0-9-]+$/i.test(e.substring(1,a)))return`${nunjucks.lib.escape(e.slice(a+1))} `}return`${nunjucks.lib.escape(e)} `})(a[1]),i=t.lastIndex;return o+=nunjucks.lib.escape(e.slice(i)),new nunjucks.runtime.SafeString(o)});function b(e,a){var t,i,o,n;return a?!e.video&&e.audio?(t=(e=a).match(/^Audio files of (\d{4})$/))?(t=parseInt(t[1],10),(new Date).getFullYear()({url:n.find("#url").val(),video:n.find("#video").is(":checked"),audio:n.find("#audio").is(":checked"),subtitles:n.find("#subtitles").is(":checked")}),getTargetData:()=>({filename:n.find("#filename").val().trim(),format:n.find("#format").val(),filedesc:n.find("#filedesc").val(),dateCategory:n.find("#dateCategory").val().trim(),languageCategory:n.find("#languageCategory").val()||""}),getPlaylistData:()=>{let a=[];return n.find(".video-select:checked").each(function(){var e=parseInt(s(this).val(),10),e=r.videos[e];a.push(e)}),a}},v={startTask:e=>V.apiPost("task/run",e),updateFormats:(e,a,t)=>0{var e;return a?a===r.url&&r.initialFilenameValidated?s.when():(e=r.uploadedFile[a])?(r.url=a,V.askAPI("makedesc",{filename:e.name||""},["extractor","filedesc","filename"])):V.askAPI("validateurl",{url:a},["entity_url"]).then(e=>e.entity_url?s.Deferred().reject("This video has already been uploaded: "+e.entity_url).promise():V.askAPI("extracturl",{url:a},["type","id","title","url","date","extractor","filedesc","filename","videos","queue"]).then(()=>{r.initialFilenameValidated=!0})).then(()=>{"playlist"===r.type?r.videos.forEach(e=>{e.format=r.format,e.dateCategory=y(e)}):r.dateCategory=y(r)}):s.Deferred().reject("URL cannot be empty!").promise()},updateFilename:(e,a=null)=>(null==a&&(a=r),e?e===a.filename&&a.initialFilenameValidated?s.when():V.askAPI("validatefilename",{filename:e},[],a).then(()=>V.askAPI("validatefilenameunique",{filename:e},["filename"],a)).then(()=>{a.initialFilenameValidated=!0}):s.Deferred().reject("Filename cannot be empty!").promise()),updateFiledesc:(e,a=null)=>(null==a&&(a=r),e?e===a.filedesc&&a.initialFiledescValidated?s.when():V.askAPI("validatefiledesc",{filedesc:e},["filedesc"],a).then(()=>{a.initialFiledescValidated=!0}):s.Deferred().reject("Decription cannot be empty!").promise()),validateUrl:e=>V.askAPI("validateurl",{url:e},[]).then(e=>{if(e.entity_url)return s.Deferred().reject("This video has already been uploaded: "+e.entity_url).promise()})},k={source:()=>{var e=m.getSourceData();return oldTaskData={...r},r={...r,...e,selectedVideos:[]},s.when(v.updateFormats(e.video,e.audio,oldTaskData),v.updateUrl(e.url)).then(()=>{"playlist"===r.type?r.nextStep="playlist":r.nextStep="target"})},playlist:()=>{let a=m.getPlaylistData();var e;return 0===a.length?s.Deferred().reject("Please select at least one video to upload!").promise():(e=[...a.map(e=>()=>v.updateFilename(e.filename,e)),...a.map(e=>()=>v.updateFiledesc(e.filedesc,e)),...a.map(e=>()=>v.validateUrl(e.url))],s.when(V.runWithConcurrency(e)).then(e=>{e=e.filter(e=>!!e?.error);if(0e.error).join("\n\n")).promise();r.selectedVideos=a,r.nextStep="confirm"}))},target:()=>{var e=m.getTargetData(),a="playlist"===r.type?r.videos[r.editingVideoIndex]:r,t=(a.format=e.format,b(a,e.dateCategory));return t.valid?(a.dateCategory=t.value,a.languageCategory=e.languageCategory||null,s.when(v.updateFilename(e.filename,a),v.updateFiledesc(e.filedesc,a)).then(()=>{"playlist"===r.type?r.nextStep="playlist":r.nextStep="confirm"})):s.Deferred().reject(t.error).promise()},confirm:()=>s.when().then(()=>{r.nextStep=null})};var V=window.video2commons={init:()=>{s("#content").html(u.loading),t={},V.loadCsrf(V.checkStatus),s(window).on("beforeunload",e=>{if(n?.is(":visible"))return e.preventDefault(),e.returnValue=""});var e=/^#?!(https?:\/\/.+)/;e.test(window.location.hash)?V.addTask({url:window.location.hash.match(e)[1]}):window.location.search.slice(1)&&(l=Qs.parse(window.location.search.slice(1)),V.addTask({url:l.url}))},loadCsrf:a=>{s.get("api/csrf").done(e=>{g=e.csrf,a()})},checkStatus:()=>{o.socketio_uri&&window.WebSocket&&window.io?V.checkStatusSocket():V.checkStatusLegacy()},checkStatusSocket:()=>{var e,a,t;window.socket||(a=(e=o.socketio_uri.match(/^((?:(?:https?:)?\/\/)?[^/]+)(\/.*)$/))[1],(t=window.socket=io(a,{path:e[2]})).on("connect",()=>{s.get("api/iosession").done(e=>{t.emit("auth",{iosession:e.iosession,_csrf_token:g})})}),t.on("status",e=>{V.alterTaskTableBoilerplate(()=>{V.populateResults(e)})}),t.on("update",(e,a)=>{V.alterTaskTableBoilerplate(()=>{V.updateTask(a)})}),t.on("remove",e=>{V.alterTaskTableBoilerplate(()=>{s("#task-"+e).remove()})}))},checkStatusLegacy:()=>{window.lastStatusCheck&&clearTimeout(window.lastStatusCheck);s.get("api/status").done(e=>{V.alterTaskTableBoilerplate(()=>{V.populateResults(e)}),window.lastStatusCheck=setTimeout(V.checkStatusLegacy,5e3)}).fail(()=>{s("#content").html(u.errorDisconnect)})},setupTables:()=>{s("#content").empty(),s("#content").append(u.workers),s("#content").append(u.capacity),s("#content").append(u.utilization),s("#content").append(u.pending),s("#content").append(u.yourTasks);var e=s(u.addTask),e=(s("#content").append(e),e.click(()=>{V.addTask()}),s(u.requestServerSide));s("#content").append(e.hide())},alterTaskTableBoilerplate:e=>{s("#tasktable").length||V.setupTables();var a=window.innerHeight+window.scrollY>=document.body.offsetHeight;e(),s.isEmptyObject(t)?s("#ssubtn").addClass("disabled").hide():s("#ssubtn").removeClass("disabled").show().attr("href",V.makeSSULink(t)),a&&window.scrollTo(0,document.body.scrollHeight)},populateResults:e=>{i=e.username;var a=s("#tasktable > tbody"),t=[];s.each(e.values,(e,a)=>{V.updateTask(a),t.push(a.id)}),a.find("> tr").each(function(){var e=s(this),a=V.getTaskIDFromDOMID(e.attr("id"));t.indexOf(a)<0&&e.remove()}),e.stats?(s("#capacity").text(e.stats.processing+" / "+e.stats.capacity),s("#utilization").text(Math.round(100*e.stats.utilization)+"%"),s("#pending").text(""+e.stats.pending)):(s("#capacity").text("N/A"),s("#utilization").text("N/A"),s("#pending").text("N/A"))},updateTask:e=>{var a=s("#tasktable > tbody"),o="task-"+e.id,n=s("#"+o),a=(n.length?n.attr("status")!==e.status&&(n.html(""),V.setupTaskRow(n,o,e.status)):(s("#task-new").remove(),(n=s(" ")).attr({id:o,status:e.status}),a.append(n),V.setupTaskRow(n,o,e.status)),n.find(`#${o}-title`)),a=(a.text()!==e.title&&a.text(e.title),n.find(`#${o}-hostname`)),a=(a.text()!==e.hostname&&a.text(e.hostname??"N/A"),(e,a,t)=>{var i=n.find(`#${o}-statustext`);a?(a=i.html(e).find("a").attr("href",a),t&&a.text(t)):i.text()!==e&&i.text(e)});"done"===e.status?a(p.getFilter("process_link")(d.taskDone).toString(),e.url.replace("%3A",":"),e.text):"needssu"===e.status?a(p.getFilter("process_link")(d.errorTooLarge).toString(),V.makeSSULink([e])):"fail"===e.status?(a(e.text,e.url,e.url),e.restartable?n.find(`#${o}-restartbutton`).show().off().click(function(){V.eventTask(this,"restart")}):n.find(`#${o}-restartbutton`).off().hide()):a(e.text),"progress"===e.status&&V.setProgressBar(n.find(`#${o}-progress`),e.progress),"needssu"===e.status?t[e.id]=e:delete t[e.id]},setupTaskRow:(e,a,t)=>{switch(t){case"progress":e.append(s(" ").attr("id",a+"-title")).append(s(" ").attr("id",a+"-hostname")).append(s(" ").attr("id",a+"-status").append(s(" ").attr("id",a+"-statustext"))).append(s(" ").attr("id",a+"-progress"));var i=V.eventButton(a,"abort"),i=(e.find(`#${a}-status`).append(i),e.find(`#${a}-progress`).html(u.progressbar));V.setProgressBar(i,-1),e.removeClass("success danger");break;case"done":V.appendButtons([V.eventButton(a,"remove")],e,["danger","success"],a);break;case"fail":var i=V.eventButton(a,"remove"),o=V.eventButton(a,"restart").hide();V.appendButtons([i,o],e,["success","danger"],a);break;case"needssu":V.appendButtons([V.eventButton(a,"remove")],e,["success","danger"],a);break;case"abort":V.appendButtons([],e,["success","danger"],a)}e.attr("status",t)},makeSSULink:e=>{var a=s.map(e,e=>"* "+e.url).join("\n"),e=s.map(e,e=>`| ${e.filename} | ${e.hashsum} |`).join("\n");return"https://phabricator.wikimedia.org/maniphest/task/edit/form/106/?"+s.param({title:"Server side upload for "+i,projects:"video2commons,server-side-upload-request",description:"Please upload these file(s) to Wikimedia Commons:\n\n**URLs**\n\n{{{ urls }}}\n\n//Description files are available too: append `.txt` to the URLs.//\n\n**Checksums**\n\n| **File** | **MD5** |\n{{{ checksums }}}\n\nThank you!".replace("{{{ urls }}}",a).replace("{{{ checksums }}}",e)})},setProgressBar:(e,a)=>{e=e.find(".progress-bar");a<0?(e.addClass("progress-bar-striped active").addClass("active").text(""),a=100):e.removeClass("progress-bar-striped active").text(Math.round(a)+"%"),e.attr({"aria-valuenow":a,"aria-valuemin":"0","aria-valuemax":"100",style:`width:${a}%`})},getTaskIDFromDOMID:e=>/^(?:task-)?(.+?)(?:-(?:title|statustext|progress|abortbutton|removebutton|restartbutton))?$/.exec(e)[1],eventTask:(e,a)=>{e=s(e);e.is(".disabled")||(e.off().addClass("disabled"),V.apiPost("task/"+a,{id:V.getTaskIDFromDOMID(e.attr("id"))}).done(e=>{e.error&&window.alert(e.error),V.checkStatus()}))},setText:(e,a)=>{for(var t=0;ts(u[a+"button"]).attr("id",e+`-${a}button`).off().click(function(){V.eventTask(this,a)}),appendButtons:(e,a,t,i)=>{a.append(s(" ").attr("id",i+"-title"));var o=s(" ").attr("id",i+"-status").attr("colspan","3").append(s(" ").attr("id",i+"-statustext"));e.length&&o.append(e[0]);for(var n=1;n{n||((n=s("").html(p.render("addTask.html"))).addClass("modal fade").attr({id:"addTaskDialog",role:"dialog"}),s("body").append(n),n.find("#btn-prev").html(u.prevbutton),n.find("#btn-next").html(u.nextbutton),n.find("#btn-cancel").click(()=>{V.abortUpload()}),n.find(".modal-body").keypress(e=>{13!==(e.which||e.keyCode)||s(":focus").is("textarea")||(n.find(".modal-footer #btn-next").click(),e.preventDefault())})),V.openTaskModal(e)},openTaskModal:e=>{n.find("#dialog-spinner").hide(),n.find(".modal-body").html(`
${c} `),V.newTask(e),n.modal({backdrop:"static"}),n.on("shown.bs.modal",()=>{n.find("#url").focus()}),V.reactivatePrevNextButtons()},newTask:e=>{r={step:"source",url:"",date:"",extractor:"",audio:!0,video:!0,subtitles:!0,filename:!0,formats:[],format:"",filedesc:"",dateCategory:"",languageCategory:"",uploadedFile:{},initialUrlValidated:!1,initialFilenameValidated:!1,initialFiledescValidated:!1,nextStep:"source",history:[],videos:[],selectedVideos:[],editingVideoIndex:null},s.extend(r,e),V.setupAddTaskDialog()},setupAddTaskDialog:()=>{switch(r.step){case"source":n.find(".modal-body").html(p.render("sourceForm.html")),n.find("a#fl").attr("href","//commons.wikimedia.org/wiki/Commons:Licensing#Acceptable_licenses"),n.find("a#pd").attr("href","//commons.wikimedia.org/wiki/Commons:Licensing#Material_in_the_public_domain"),n.find("a#fu").attr("href","//commons.wikimedia.org/wiki/Commons:FU"),n.find("#url").val(r.url).on("input",function(){r.url!==s(this).val()&&(r.url=s(this).val());r.url.match(/(https?:\/\/)?(www\.)?(youtube|youtu|youtube-nocookie)\.(com|be)\/(watch\?.*?(?=v=)v=|embed\/|v\/|.+\?v=)?([^&=%?]{11})/)?n.find("#youtube-warning").removeClass("hidden"):n.find("#youtube-warning").addClass("hidden")}).focus(),n.find("#video").prop("checked",r.video),n.find("#audio").prop("checked",r.audio),n.find("#subtitles").prop("checked",r.subtitles),V.initUpload();break;case"playlist":n.find(".modal-body").html(p.render("playlistForm.html",{task:r})),n.find(".video-select").each((e,a)=>{e=r.videos[e];0<=r.selectedVideos.indexOf(e)&&s(a).prop("checked",!0)});var e=n.find(".video-select:checked").length===n.find(".video-select").length;n.find("#select-all").prop("checked",e),n.find("#select-all").off().change(function(){var e=s(this).is(":checked");n.find(".video-select").prop("checked",e)}),n.find(".video-select").off().change(()=>{var e=n.find(".video-select:checked").length===n.find(".video-select").length;n.find("#select-all").prop("checked",e)}),n.find(".btn-edit").off().click(function(){var e=s(this).data("video-index");r.selectedVideos=m.getPlaylistData(),r.editingVideoIndex=e,r.history.push(r.step),r.step="target",V.setupAddTaskDialog(),V.reactivatePrevNextButtons()});break;case"target":{let a="playlist"===r.type?r.videos[r.editingVideoIndex]:r;n.find(".modal-body").html(p.render("targetForm.html",{source:a})),n.find("#filename").val(a.filename.trim()).focus(),s.each(r.formats,(e,a)=>{n.find("#format").append(s("
").text(a))}),n.find("#format").val(a.format),n.find("#filedesc").val(a.filedesc),n.find("#dateCategory").attr("placeholder",(e=a,t=(new Date).toISOString().split("T")[0],i=(t=e.date||t).split("-")[0],o=(e=e.audio&&!e.video)?d["datecategory-audio-placeholder"]:d["datecategory-video-placeholder"],e?o.replace("$1",i):o.replace("$1",i).replace("$2",t))).val(a.dateCategory||"");i=((o=a).audio&&!o.video?f:h)(o);s.each(i,(e,a)=>{n.find("#dateCategoryOptions").append(s("
").val(a))}),n.find("#dateCategory").on("input",function(){var e=b(a,s(this).val().trim());e.valid?(n.find("#dateCategoryError").hide(),n.find("#dateCategory-group").removeClass("has-error")):(n.find("#dateCategoryError").text(e.error).show(),n.find("#dateCategory-group").addClass("has-error"))}),r.video&&r.audio?(n.find("#languageCategory-group").show(),w.forEach(e=>{n.find("#languageCategory").append(s("
").val(e.category).text(e.label))}),a.languageCategory&&n.find("#languageCategory").val(a.languageCategory)):n.find("#languageCategory-group").hide();break}case"confirm":t="playlist"===r.type?p.render("playlistConfirmForm.html",{task:r}):p.render("confirmForm.html"),o=(n.find(".modal-body").html(t),[]);r.video&&o.push(d.video),r.audio&&o.push(d.audio),r.subtitles&&o.push(d.subtitles),n.find("#keep").text(o.join(", ")),V.setText(["url","extractor","filename","format"],r),n.find("#filedesc").val(r.filedesc),n.find("#btn-next").focus()}var t,i,o},reactivatePrevNextButtons:()=>{switch(n.find("#dialog-spinner").hide(),r.step){case"source":n.find("#btn-prev").addClass("disabled").off(),n.find("#btn-next").html(u.nextbutton),V.setPrevNextButton("next");break;case"playlist":V.setPrevNextButton("prev"),n.find("#btn-next").html(u.nextbutton),V.setPrevNextButton("next");break;case"target":V.setPrevNextButton("prev"),"playlist"===r.type?n.find("#btn-next").html(u.confirmbutton):n.find("#btn-next").html(u.nextbutton),V.setPrevNextButton("next");break;case"confirm":V.setPrevNextButton("prev"),n.find("#btn-next").removeClass("disabled").html(u.confirmbutton).off().click(()=>{V.disablePrevNext(!1),n.modal("hide"),s("#tasktable > tbody").append(`
${c} `),window.scrollTo(0,document.body.scrollHeight),r.uploadedFile={};let a=[];if("playlist"===r.type)a=r.selectedVideos.map(e=>{let a=e.filedesc;return e.dateCategory&&(a+=`
-[[Category:${e.dateCategory}]]`),e.languageCategory&&(a+=`
-[[Category:${e.languageCategory}]]`),{url:e.url,extractor:e.extractor,subtitles:r.subtitles,filename:e.filename,filedesc:a,format:e.format,queue:e.queue}});else{let e=r.filedesc;r.dateCategory&&(e+=`
-[[Category:${r.dateCategory}]]`),r.languageCategory&&(e+=`
-[[Category:${r.languageCategory}]]`),a.push({url:r.url,extractor:r.extractor,subtitles:r.subtitles,filename:r.filename,filedesc:e,format:r.format,queue:r.queue})}V.runWithConcurrency(a.map(e=>()=>v.startTask(e).done(e=>{e.error&&window.alert(e.error)}))).always(()=>{V.checkStatus()})})}},setPrevNextButton:e=>{n.find("#btn-"+e).removeClass("disabled").off().click(()=>{V.processInput(e)})},disablePrevNext:e=>{n.find(".modal-body #dialog-errorbox").hide(),n.find("#btn-prev").addClass("disabled").off(),n.find("#btn-next").addClass("disabled").off(),e&&n.find("#dialog-spinner").show()},processInput:e=>{var a=r.step,t=k[a];t?"next"===e?(t=t(),V.promiseWorkingOn(t.done(()=>{V.transitionStep(e)}))):(V.transitionStep(e),V.reactivatePrevNextButtons()):console.error("Unknown step:",a)},transitionStep:e=>{"prev"===e&&0
(V.disablePrevNext(!0),e.fail(e=>{n.find(".modal-body #dialog-errorbox").length||n.find(".modal-body").append(s('
')),n.find(".modal-body #dialog-errorbox").text("Error: "+e).show()}).always(V.reactivatePrevNextButtons)),abortUpload:(e,a)=>{e&&"pending"===e.state()&&e.reject(a),window.jqXHR?.abort&&window.jqXHR.abort()},initUpload:()=>{var i;window.jqXHR=n.find("#fileupload").fileupload({dataType:"json",formData:{_csrf_token:g},maxChunkSize:4<<20,sequentialUploads:!0}).on("fileuploadadd",(e,a)=>{window.jqXHR=a.submit(),i=s.Deferred(),V.promiseWorkingOn(i.promise()),n.find("#src-url").hide(),n.find("#src-uploading").show(),n.find("#upload-abort").off().click(()=>{V.abortUpload(i,"Upload aborted.")})}).on("fileuploadchunkdone",(e,a)=>{a.result.filekey&&(a.formData.filekey=a.result.filekey),"Continue"===a.result.result?a.result.offset!==a.uploadedBytes&&V.abortUpload(i,`Unexpected offset! Expected: ${a.uploadedBytes} Returned: `+a.result.offset):a.result.error?V.abortUpload(i,a.result.error):V.abortUpload()}).on("fileuploadprogressall",(e,a)=>{V.setProgressBar(n.find("#upload-progress"),a.loaded/a.total*100)}).on("fileuploadalways",(e,a)=>{delete a.formData.filekey,V.reactivatePrevNextButtons(),n.find("#src-url").show(),n.find("#src-uploading").hide()}).on("fileuploadfail",()=>{V.abortUpload(i,"Something went wrong while uploading... try again?")}).on("fileuploaddone",(e,a)=>{var t;"Success"===a.result.result?(t="uploads:"+a.result.filekey,r.uploadedFile[t]=a.files[0],n.find("#url").val(t),i.resolve()):V.abortUpload(i,"Upload does not seem to be successful.")})},askAPI:(e,a,i,o=null)=>{null==o&&(o=r);var n=s.Deferred();return V.apiPost(e,a).done(e=>{if(e.error)n.reject(e.error);else{for(var a=0;a{n.reject("Something weird happened. Please try again.")}),n.promise()},apiPost:(e,a)=>(a._csrf_token=g,s.post("api/"+e,a)),runWithConcurrency(e,a=5){let t=e.slice(),i=s.Deferred(),o=[],n=0,r=0,l=()=>{if(r>=t.length&&0===n)i.resolve(o);else for(;n{o[a]=e}).fail(e=>{o[a]={error:e}}).always(()=>{n--,l()})}};return l(),i.promise()}};let w=[{label:"Unselected",category:""},{label:"English (en)",category:"Videos in English"},{label:"American English (en-US)",category:"Videos in American English"},{label:"Australian English (en-AU)",category:"Videos in Australian English"},{label:"British English (en-GB)",category:"Videos in British English"},{label:"Canadian English (en-CA)",category:"Videos in Canadian English"},{label:"Spanish (es)",category:"Videos in Spanish"},{label:"Portuguese (pt)",category:"Videos in Portuguese"},{label:"Brazilian Portuguese (pt-BR)",category:"Videos in Brazilian Portuguese"},{label:"Chinese (zh)",category:"Videos in Chinese"},{label:"Cantonese (yue)",category:"Videos in Cantonese"},{label:"Mandarin (cmn)",category:"Videos in Mandarin"},{label:"Japanese (ja)",category:"Videos in Japanese"},{label:"Korean (ko)",category:"Videos in Korean"},{label:"Hindi (hi)",category:"Videos in Hindi"},{label:"Arabic (ar)",category:"Videos in Arabic"},{label:"Russian (ru)",category:"Videos in Russian"},{label:"German (de)",category:"Videos in German"},{label:"Bavarian (bar)",category:"Videos in Bavarian"},{label:"French (fr)",category:"Videos in French"},{label:"Italian (it)",category:"Videos in Italian"},{label:"Dutch (nl)",category:"Videos in Dutch"},{label:"Polish (pl)",category:"Videos in Polish"},{label:"Turkish (tr)",category:"Videos in Turkish"},{label:"Albanian (sq)",category:"Videos in Albanian"},{label:"Algerian Arabic (arq)",category:"Videos in Algerian Arabic"},{label:"Egyptian Arabic (arz)",category:"Videos in Egyptian Arabic"},{label:"Moroccan Arabic (ary)",category:"Videos in Moroccan Arabic"},{label:"North Levantine Arabic (apc)",category:"Videos in North Levantine Arabic"},{label:"Amharic (am)",category:"Videos in Amharic"},{label:"American Sign Language (ase)",category:"Videos in American Sign Language"},{label:"Ancient Greek (grc)",category:"Videos in Ancient Greek"},{label:"Armenian (hy)",category:"Videos in Armenian"},{label:"Assamese (as)",category:"Videos in Assamese"},{label:"Asturian (ast)",category:"Videos in Asturian"},{label:"Avar (av)",category:"Videos in Avar"},{label:"Azerbaijani (az)",category:"Videos in Azerbaijani"},{label:"Balinese (ban)",category:"Videos in Balinese"},{label:"Balochi (bal)",category:"Videos in Balochi"},{label:"Bangla (bn)",category:"Videos in Bangla"},{label:"Basque (eu)",category:"Videos in Basque"},{label:"Belarusian (be)",category:"Videos in Belarusian"},{label:"Bislama (bi)",category:"Videos in Bislama"},{label:"Bosnian (bs)",category:"Videos in Bosnian"},{label:"Breton (br)",category:"Videos in Breton"},{label:"British Sign Language (bfi)",category:"Videos in British Sign Language"},{label:"Bulgarian (bg)",category:"Videos in Bulgarian"},{label:"Burmese (my)",category:"Videos in Burmese"},{label:"Catalan (ca)",category:"Videos in Catalan"},{label:"Cebuano (ceb)",category:"Videos in Cebuano"},{label:"Central Bikol (bcl)",category:"Videos in Central Bikol"},{label:"Chamorro (ch)",category:"Videos in Chamorro"},{label:"Cherokee (chr)",category:"Videos in Cherokee"},{label:"Cornish (kw)",category:"Videos in Cornish"},{label:"Crimean Tatar (crh)",category:"Videos in Crimean Tatar"},{label:"Croatian (hr)",category:"Videos in Croatian"},{label:"Czech (cs)",category:"Videos in Czech"},{label:"Danish (da)",category:"Videos in Danish"},{label:"Dari (prs)",category:"Videos in Dari"},{label:"Esperanto (eo)",category:"Videos in Esperanto"},{label:"Estonian (et)",category:"Videos in Estonian"},{label:"Farsi (fa)",category:"Videos in Farsi"},{label:"Fijian (fj)",category:"Videos in Fijian"},{label:"Finnish (fi)",category:"Videos in Finnish"},{label:"Galician (gl)",category:"Videos in Galician"},{label:"Georgian (ka)",category:"Videos in Georgian"},{label:"Greek (el)",category:"Videos in Greek"},{label:"Greenlandic (kl)",category:"Videos in Greenlandic"},{label:"Guarani (gn)",category:"Videos in Guarani"},{label:"Gujarati (gu)",category:"Videos in Gujarati"},{label:"Haitian Creole (ht)",category:"Videos in Haitian Creole"},{label:"Hakka (hak)",category:"Videos in Hakka"},{label:"Min Dong Chinese (cdo)",category:"Videos in Min Dong Chinese"},{label:"Hausa (ha)",category:"Videos in Hausa"},{label:"Hebrew (he)",category:"Videos in Hebrew"},{label:"Hungarian (hu)",category:"Videos in Hungarian"},{label:"Icelandic (is)",category:"Videos in Icelandic"},{label:"Ido (io)",category:"Videos in Ido"},{label:"Igbo (ig)",category:"Videos in Igbo"},{label:"Indonesian (id)",category:"Videos in Indonesian"},{label:"Irish (ga)",category:"Videos in Irish"},{label:"Jamaican Patois (jam)",category:"Videos in Jamaican Patois"},{label:"Javanese (jv)",category:"Videos in Javanese"},{label:"Judaeo-Spanish (lad)",category:"Videos in Judaeo-Spanish"},{label:"Kannada (kn)",category:"Videos in Kannada"},{label:"Kashmiri (ks)",category:"Videos in Kashmiri"},{label:"Kazakh (kk)",category:"Videos in Kazakh"},{label:"Khasi (kha)",category:"Videos in Khasi"},{label:"Khmer (km)",category:"Videos in Khmer"},{label:"Komi (kv)",category:"Videos in Komi"},{label:"Kotava (avk)",category:"Videos in Kotava"},{label:"Kurdish (ku)",category:"Videos in Kurdish"},{label:"Kyrgyz (ky)",category:"Videos in Kyrgyz"},{label:"K'iche' (quc)",category:"Videos in K'iche'"},{label:"Lakota (lkt)",category:"Videos in Lakota"},{label:"Lao (lo)",category:"Videos in Lao"},{label:"Latin (la)",category:"Videos in Latin"},{label:"Latvian (lv)",category:"Videos in Latvian"},{label:"Leonese (roa-leon)",category:"Videos in Leonese"},{label:"Limburgish (li)",category:"Videos in Limburgish"},{label:"Lithuanian (lt)",category:"Videos in Lithuanian"},{label:"Lojban (jbo)",category:"Videos in Lojban"},{label:"Low German (nds)",category:"Videos in Low German"},{label:"Lozi (loz)",category:"Videos in Lozi"},{label:"Luxembourgish (lb)",category:"Videos in Luxembourgish"},{label:"Macedonian (mk)",category:"Videos in Macedonian"},{label:"Malay (ms)",category:"Videos in Malay"},{label:"Malayalam (ml)",category:"Videos in Malayalam"},{label:"Maltese (mt)",category:"Videos in Maltese"},{label:"Manx (gv)",category:"Videos in Manx"},{label:"Mapudungun (arn)",category:"Videos in Mapudungun"},{label:"Marathi (mr)",category:"Videos in Marathi"},{label:"Marwari (mwr)",category:"Videos in Marwari"},{label:"Mirandese (mwl)",category:"Videos in Mirandese"},{label:"Mongolian (mn)",category:"Videos in Mongolian"},{label:"Mossi (mos)",category:"Videos in Mossi"},{label:"Neapolitan (nap)",category:"Videos in Neapolitan"},{label:"Sicilian (scn)",category:"Videos in Sicilian"},{label:"Nepali (ne)",category:"Videos in Nepali"},{label:"Northern Sami (se)",category:"Videos in Northern Sami"},{label:"Norwegian (no)",category:"Videos in Norwegian"},{label:"Occitan (oc)",category:"Videos in Occitan"},{label:"Odia (or)",category:"Videos in Odia"},{label:"Pashto (ps)",category:"Videos in Pashto"},{label:"Punjabi (pa)",category:"Videos in Punjabi"},{label:"Quechua (qu)",category:"Videos in Quechua"},{label:"Romanian (ro)",category:"Videos in Romanian"},{label:"Saint Lucian Creole (acf)",category:"Videos in Saint Lucian Creole"},{label:"Samoan (sm)",category:"Videos in Samoan"},{label:"Sanskrit (sa)",category:"Videos in Sanskrit"},{label:"Santali (sat)",category:"Videos in Santali"},{label:"Sardinian (sc)",category:"Videos in Sardinian"},{label:"Serbian (sr)",category:"Videos in Serbian"},{label:"Serbo-Croatian (sh)",category:"Videos in Serbo-Croatian"},{label:"Silesian (szl)",category:"Videos in Silesian"},{label:"Sindhi (sd)",category:"Videos in Sindhi"},{label:"Slovak (sk)",category:"Videos in Slovak"},{label:"Slovene (sl)",category:"Videos in Slovene"},{label:"Somali (so)",category:"Videos in Somali"},{label:"Swahili (sw)",category:"Videos in Swahili"},{label:"Swedish (sv)",category:"Videos in Swedish"},{label:"Sylheti (syl)",category:"Videos in Sylheti"},{label:"Tagalog (tl)",category:"Videos in Tagalog"},{label:"Tamazight (zgh)",category:"Videos in Tamazight"},{label:"Tamil (ta)",category:"Videos in Tamil"},{label:"Tatar (tt)",category:"Videos in Tatar"},{label:"Telugu (te)",category:"Videos in Telugu"},{label:"Tetum (tet)",category:"Videos in Tetum"},{label:"Thai (th)",category:"Videos in Thai"},{label:"Tok Pisin (tpi)",category:"Videos in Tok Pisin"},{label:"Toki Pona (tok)",category:"Videos in Toki Pona"},{label:"Tsonga (ts)",category:"Videos in Tsonga"},{label:"Tuvan (tyv)",category:"Videos in Tuvan"},{label:"Twi (tw)",category:"Videos in Twi"},{label:"Ukrainian (uk)",category:"Videos in Ukrainian"},{label:"Urdu (ur)",category:"Videos in Urdu"},{label:"Veps (vep)",category:"Videos in Veps"},{label:"Vietnamese (vi)",category:"Videos in Vietnamese"},{label:"Volapük (vo)",category:"Videos in Volapük"},{label:"Walloon (wa)",category:"Videos in Walloon"},{label:"Welsh (cy)",category:"Videos in Welsh"},{label:"West Frisian (fy)",category:"Videos in West Frisian"},{label:"Wolof (wo)",category:"Videos in Wolof"},{label:"Xhosa (xh)",category:"Videos in Xhosa"},{label:"Yiddish (yi)",category:"Videos in Yiddish"},{label:"Yoruba (yo)",category:"Videos in Yoruba"},{label:"Yucatec (yua)",category:"Videos in Yucatec"}];s(document).ready(()=>{V.init()})})(jQuery);
\ No newline at end of file
diff --git a/video2commons/frontend/templates/base.min.html b/video2commons/frontend/templates/base.min.html
deleted file mode 100644
index d192995..0000000
--- a/video2commons/frontend/templates/base.min.html
+++ /dev/null
@@ -1 +0,0 @@
-video2commons {% if lang() is rtl %} {% endif %} {% block jscss %}{% endblock %}{% block content %}{% endblock %}
\ No newline at end of file
diff --git a/video2commons/frontend/templates/error.min.html b/video2commons/frontend/templates/error.min.html
deleted file mode 100644
index 69cdefb..0000000
--- a/video2commons/frontend/templates/error.min.html
+++ /dev/null
@@ -1 +0,0 @@
-{% extends "base.min.html" %} {% block content %} {% if html_message %}{{ html_message|safe }} {% if stacktrace %}
{{ stacktrace }}{% endif %}
{% else %}{{ message }} {% if stacktrace %}
{{ stacktrace }}{% endif %}
{% endif %} {% endblock %}
\ No newline at end of file
diff --git a/video2commons/frontend/templates/main.min.html b/video2commons/frontend/templates/main.min.html
deleted file mode 100644
index 279431c..0000000
--- a/video2commons/frontend/templates/main.min.html
+++ /dev/null
@@ -1 +0,0 @@
-{% extends "base.min.html" %} {% block jscss %} {% if loggedin %} {% if config.socketio_uri %}{% endif %}{% endif %} {% endblock %} {% block content %} {% if loggedin %}{{ _('JavascriptRequired') }} {% else %} {% endif %} {% endblock %}
\ No newline at end of file