mirror of
https://github.com/9001/copyparty.git
synced 2025-11-01 04:23:34 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae3a01038b | ||
|
|
e47a2a4ca2 | ||
|
|
95ea6d5f78 | ||
|
|
7d290f6b8f | ||
|
|
9db617ed5a | ||
|
|
514456940a | ||
|
|
33feefd9cd | ||
|
|
65e14cf348 | ||
|
|
1d61bcc4f3 | ||
|
|
c38bbaca3c | ||
|
|
246d245ebc | ||
|
|
f269a710e2 | ||
|
|
051998429c | ||
|
|
432cdd640f | ||
|
|
9ed9b0964e | ||
|
|
6a97b3526d | ||
|
|
451d757996 | ||
|
|
f9e9eba3b1 | ||
|
|
2a9a6aebd9 | ||
|
|
adbb6c449e | ||
|
|
3993605324 | ||
|
|
0ae574ec2c | ||
|
|
c56ded828c |
84
README.md
84
README.md
@@ -16,11 +16,6 @@ turn your phone or raspi into a portable file server with resumable uploads/down
|
||||
📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [thumbnails](#thumbnails) // [md-viewer](#markdown-viewer) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [ie4](#browser-support)
|
||||
|
||||
|
||||
## breaking changes \o/
|
||||
|
||||
this is the readme for v0.12 which has a different expression for volume permissions (`-v`); see [the v0.11.x readme](https://github.com/9001/copyparty/tree/15b59822112dda56cee576df30f331252fc62628#readme) for stuff regarding the [current stable release](https://github.com/9001/copyparty/releases/tag/v0.11.47)
|
||||
|
||||
|
||||
## readme toc
|
||||
|
||||
* top
|
||||
@@ -41,6 +36,7 @@ this is the readme for v0.12 which has a different expression for volume permiss
|
||||
* [uploading](#uploading)
|
||||
* [file-search](#file-search)
|
||||
* [file manager](#file-manager)
|
||||
* [batch rename](#batch-rename)
|
||||
* [markdown viewer](#markdown-viewer)
|
||||
* [other tricks](#other-tricks)
|
||||
* [searching](#searching)
|
||||
@@ -125,30 +121,31 @@ summary: all planned features work! now please enjoy the bloatening
|
||||
* ☑ [accounts](#accounts-and-volumes)
|
||||
* upload
|
||||
* ☑ basic: plain multipart, ie6 support
|
||||
* ☑ up2k: js, resumable, multithreaded
|
||||
* ☑ [up2k](#uploading): js, resumable, multithreaded
|
||||
* ☑ stash: simple PUT filedropper
|
||||
* ☑ unpost: undo/delete accidental uploads
|
||||
* ☑ symlink/discard existing files (content-matching)
|
||||
* download
|
||||
* ☑ single files in browser
|
||||
* ☑ folders as zip / tar files
|
||||
* ☑ [folders as zip / tar files](#zip-downloads)
|
||||
* ☑ FUSE client (read-only)
|
||||
* browser
|
||||
* ☑ navpane (directory tree sidebar)
|
||||
* ☑ file manager (cut/paste, delete, [batch-rename](#batch-rename))
|
||||
* ☑ audio player (with OS media controls)
|
||||
* ☑ thumbnails
|
||||
* ☑ image gallery with webm player
|
||||
* ☑ [thumbnails](#thumbnails)
|
||||
* ☑ ...of images using Pillow
|
||||
* ☑ ...of videos using FFmpeg
|
||||
* ☑ cache eviction (max-age; maybe max-size eventually)
|
||||
* ☑ image gallery with webm player
|
||||
* ☑ SPA (browse while uploading)
|
||||
* if you use the navpane to navigate, not folders in the file list
|
||||
* server indexing
|
||||
* ☑ locate files by contents
|
||||
* ☑ [locate files by contents](#file-search)
|
||||
* ☑ search by name/path/date/size
|
||||
* ☑ search by ID3-tags etc.
|
||||
* ☑ [search by ID3-tags etc.](#searching)
|
||||
* markdown
|
||||
* ☑ viewer
|
||||
* ☑ [viewer](#markdown-viewer)
|
||||
* ☑ editor (sure why not)
|
||||
|
||||
|
||||
@@ -181,7 +178,7 @@ small collection of user feedback
|
||||
* this is an msys2 bug, the regular windows edition of python is fine
|
||||
|
||||
* VirtualBox: sqlite throws `Disk I/O Error` when running in a VM and the up2k database is in a vboxsf
|
||||
* use `--hist` or the `hist` volflag (`-v [...]:chist=/tmp/foo`) to place the db inside the vm instead
|
||||
* use `--hist` or the `hist` volflag (`-v [...]:c,hist=/tmp/foo`) to place the db inside the vm instead
|
||||
|
||||
|
||||
# accounts and volumes
|
||||
@@ -373,6 +370,53 @@ if you have the required permissions, you can cut/paste, rename, and delete file
|
||||
you can move files across browser tabs (cut in one tab, paste in another)
|
||||
|
||||
|
||||
## batch rename
|
||||
|
||||

|
||||
|
||||
select some files and press F2 to bring up the rename UI
|
||||
|
||||
quick explanation of the buttons,
|
||||
* `[✅ apply rename]` confirms and begins renaming
|
||||
* `[❌ cancel]` aborts and closes the rename window
|
||||
* `[↺ reset]` reverts any filename changes back to the original name
|
||||
* `[decode]` does a URL-decode on the filename, fixing stuff like `&` and `%20`
|
||||
* `[advanced]` toggles advanced mode
|
||||
|
||||
advanced mode: rename files based on rules to decide the new names, based on the original name (regex), or based on the tags collected from the file (artist/title/...), or a mix of both
|
||||
|
||||
in advanced mode,
|
||||
* `[case]` toggles case-sensitive regex
|
||||
* `regex` is the regex pattern to apply to the original filename; any files which don't match will be skipped
|
||||
* `format` is the new filename, taking values from regex capturing groups and/or from file tags
|
||||
* very loosely based on foobar2000 syntax
|
||||
* `presets` lets you save rename rules for later
|
||||
|
||||
available functions:
|
||||
* `$lpad(text, length, pad_char)`
|
||||
* `$rpad(text, length, pad_char)`
|
||||
|
||||
so,
|
||||
|
||||
say you have a file named [`meganeko - Eclipse - 07 Sirius A.mp3`](https://www.youtube.com/watch?v=-dtb0vDPruI) (absolutely fantastic album btw) and the tags are: `Album:Eclipse`, `Artist:meganeko`, `Title:Sirius A`, `tn:7`
|
||||
|
||||
you could use just regex to rename it:
|
||||
* `regex` = `(.*) - (.*) - ([0-9]{2}) (.*)`
|
||||
* `format` = `(3). (1) - (4)`
|
||||
* `output` = `07. meganeko - Sirius A.mp3`
|
||||
|
||||
or you could use just tags:
|
||||
* `format` = `$lpad((tn),2,0). (artist) - (title).(ext)`
|
||||
* `output` = `7. meganeko - Sirius A.mp3`
|
||||
|
||||
or a mix of both:
|
||||
* `regex` = ` - ([0-9]{2}) `
|
||||
* `format` = `(1). (artist) - (title).(ext)`
|
||||
* `output` = `07. meganeko - Sirius A.mp3`
|
||||
|
||||
the metadata keys you can use in the format field are the ones in the file-browser table header (whatever is collected with `-mte` and `-mtp`)
|
||||
|
||||
|
||||
## markdown viewer
|
||||
|
||||

|
||||
@@ -415,9 +459,9 @@ through arguments:
|
||||
* `-e2tsr` deletes all existing tags, does a full reindex
|
||||
|
||||
the same arguments can be set as volume flags, in addition to `d2d` and `d2t` for disabling:
|
||||
* `-v ~/music::r:ce2dsa:ce2tsr` does a full reindex of everything on startup
|
||||
* `-v ~/music::r:cd2d` disables **all** indexing, even if any `-e2*` are on
|
||||
* `-v ~/music::r:cd2t` disables all `-e2t*` (tags), does not affect `-e2d*`
|
||||
* `-v ~/music::r:c,e2dsa:c,e2tsr` does a full reindex of everything on startup
|
||||
* `-v ~/music::r:c,d2d` disables **all** indexing, even if any `-e2*` are on
|
||||
* `-v ~/music::r:c,d2t` disables all `-e2t*` (tags), does not affect `-e2d*`
|
||||
|
||||
note:
|
||||
* `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and `e2ts` would then reindex those, unless there is a new copyparty version with new parsers and the release note says otherwise
|
||||
@@ -436,7 +480,7 @@ if you set `--no-hash`, you can enable hashing for specific volumes using flag `
|
||||
copyparty creates a subfolder named `.hist` inside each volume where it stores the database, thumbnails, and some other stuff
|
||||
|
||||
this can instead be kept in a single place using the `--hist` argument, or the `hist=` volume flag, or a mix of both:
|
||||
* `--hist ~/.cache/copyparty -v ~/music::r:chist=-` sets `~/.cache/copyparty` as the default place to put volume info, but `~/music` gets the regular `.hist` subfolder (`-` restores default behavior)
|
||||
* `--hist ~/.cache/copyparty -v ~/music::r:c,hist=-` sets `~/.cache/copyparty` as the default place to put volume info, but `~/music` gets the regular `.hist` subfolder (`-` restores default behavior)
|
||||
|
||||
note:
|
||||
* markdown edits are always stored in a local `.hist` subdirectory
|
||||
@@ -447,10 +491,12 @@ note:
|
||||
## metadata from audio files
|
||||
|
||||
`-mte` decides which tags to index and display in the browser (and also the display order), this can be changed per-volume:
|
||||
* `-v ~/music::r:cmte=title,artist` indexes and displays *title* followed by *artist*
|
||||
* `-v ~/music::r:c,mte=title,artist` indexes and displays *title* followed by *artist*
|
||||
|
||||
if you add/remove a tag from `mte` you will need to run with `-e2tsr` once to rebuild the database, otherwise only new files will be affected
|
||||
|
||||
but instead of using `-mte`, `-mth` is a better way to hide tags in the browser: these tags will not be displayed by default, but they still get indexed and become searchable, and users can choose to unhide them in the settings pane
|
||||
|
||||
`-mtm` can be used to add or redefine a metadata mapping, say you have media files with `foo` and `bar` tags and you want them to display as `qux` in the browser (preferring `foo` if both are present), then do `-mtm qux=foo,bar` and now you can `-mte artist,title,qux`
|
||||
|
||||
tags that start with a `.` such as `.bpm` and `.dur`(ation) indicate numeric value
|
||||
@@ -471,7 +517,7 @@ copyparty can invoke external programs to collect additional metadata for files
|
||||
|
||||
* `-mtp .bpm=~/bin/audio-bpm.py` will execute `~/bin/audio-bpm.py` with the audio file as argument 1 to provide the `.bpm` tag, if that does not exist in the audio metadata
|
||||
* `-mtp key=f,t5,~/bin/audio-key.py` uses `~/bin/audio-key.py` to get the `key` tag, replacing any existing metadata tag (`f,`), aborting if it takes longer than 5sec (`t5,`)
|
||||
* `-v ~/music::r:cmtp=.bpm=~/bin/audio-bpm.py:cmtp=key=f,t5,~/bin/audio-key.py` both as a per-volume config wow this is getting ugly
|
||||
* `-v ~/music::r:c,mtp=.bpm=~/bin/audio-bpm.py:c,mtp=key=f,t5,~/bin/audio-key.py` both as a per-volume config wow this is getting ugly
|
||||
|
||||
*but wait, there's more!* `-mtp` can be used for non-audio files as well using the `a` flag: `ay` only do audio files, `an` only do non-audio files, or `ad` do all files (d as in dontcare)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ some of these rely on libraries which are not MIT-compatible
|
||||
|
||||
* [audio-bpm.py](./audio-bpm.py) detects the BPM of music using the BeatRoot Vamp Plugin; imports GPL2
|
||||
* [audio-key.py](./audio-key.py) detects the melodic key of music using the Mixxx fork of keyfinder; imports GPL3
|
||||
* [media-hash.py](./media-hash.py) generates checksums for audio and video streams; uses FFmpeg (LGPL or GPL)
|
||||
|
||||
|
||||
# dependencies
|
||||
@@ -18,7 +19,10 @@ run [`install-deps.sh`](install-deps.sh) to build/install most dependencies requ
|
||||
|
||||
# usage from copyparty
|
||||
|
||||
`copyparty -e2dsa -e2ts -mtp key=f,audio-key.py -mtp .bpm=f,audio-bpm.py`
|
||||
`copyparty -e2dsa -e2ts` followed by any combination of these:
|
||||
* `-mtp key=f,audio-key.py`
|
||||
* `-mtp .bpm=f,audio-bpm.py`
|
||||
* `-mtp ahash,vhash=f,media-hash.py`
|
||||
|
||||
* `f,` makes the detected value replace any existing values
|
||||
* the `.` in `.bpm` indicates numeric value
|
||||
@@ -29,6 +33,9 @@ run [`install-deps.sh`](install-deps.sh) to build/install most dependencies requ
|
||||
## usage with volume-flags
|
||||
|
||||
instead of affecting all volumes, you can set the options for just one volume like so:
|
||||
```
|
||||
copyparty -v /mnt/nas/music:/music:r:cmtp=key=f,audio-key.py:cmtp=.bpm=f,audio-bpm.py:ce2dsa:ce2ts
|
||||
```
|
||||
|
||||
`copyparty -v /mnt/nas/music:/music:r:c,e2dsa:c,e2ts` immediately followed by any combination of these:
|
||||
|
||||
* `:c,mtp=key=f,audio-key.py`
|
||||
* `:c,mtp=.bpm=f,audio-bpm.py`
|
||||
* `:c,mtp=ahash,vhash=f,media-hash.py`
|
||||
|
||||
73
bin/mtag/media-hash.py
Normal file
73
bin/mtag/media-hash.py
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import base64
|
||||
import hashlib
|
||||
import subprocess as sp
|
||||
|
||||
try:
|
||||
from copyparty.util import fsenc
|
||||
except:
|
||||
|
||||
def fsenc(p):
|
||||
return p
|
||||
|
||||
|
||||
"""
|
||||
dep: ffmpeg
|
||||
"""
|
||||
|
||||
|
||||
def det():
|
||||
# fmt: off
|
||||
cmd = [
|
||||
"ffmpeg",
|
||||
"-nostdin",
|
||||
"-hide_banner",
|
||||
"-v", "fatal",
|
||||
"-i", fsenc(sys.argv[1]),
|
||||
"-f", "framemd5",
|
||||
"-"
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
p = sp.Popen(cmd, stdout=sp.PIPE)
|
||||
# ps = io.TextIOWrapper(p.stdout, encoding="utf-8")
|
||||
ps = p.stdout
|
||||
|
||||
chans = {}
|
||||
for ln in ps:
|
||||
if ln.startswith(b"#stream#"):
|
||||
break
|
||||
|
||||
m = re.match(r"^#media_type ([0-9]): ([a-zA-Z])", ln.decode("utf-8"))
|
||||
if m:
|
||||
chans[m.group(1)] = m.group(2)
|
||||
|
||||
hashers = [hashlib.sha512(), hashlib.sha512()]
|
||||
for ln in ps:
|
||||
n = int(ln[:1])
|
||||
v = ln.rsplit(b",", 1)[-1].strip()
|
||||
hashers[n].update(v)
|
||||
|
||||
r = {}
|
||||
for k, v in chans.items():
|
||||
dg = hashers[int(k)].digest()[:12]
|
||||
dg = base64.urlsafe_b64encode(dg).decode("ascii")
|
||||
r[v[0].lower() + "hash"] = dg
|
||||
|
||||
print(json.dumps(r, indent=4))
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
det()
|
||||
except:
|
||||
pass # mute
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -15,8 +15,11 @@
|
||||
# https://user-images.githubusercontent.com/241032/126040249-cb535cc7-c599-4931-a796-a5d9af691bad.png
|
||||
#
|
||||
# enable line-buffering for realtime logging (slight performance cost):
|
||||
# modify ExecStart and prefix it with `/bin/stdbuf -oL` like so:
|
||||
# ExecStart=/bin/stdbuf -oL /usr/bin/python3 [...]
|
||||
# modify ExecStart and prefix it with `/usr/bin/stdbuf -oL` like so:
|
||||
# ExecStart=/usr/bin/stdbuf -oL /usr/bin/python3 [...]
|
||||
# but some systemd versions require this instead (higher performance cost):
|
||||
# inside the [Service] block, add the following line:
|
||||
# Environment=PYTHONUNBUFFERED=x
|
||||
|
||||
[Unit]
|
||||
Description=copyparty file server
|
||||
|
||||
@@ -24,6 +24,7 @@ from .__init__ import E, WINDOWS, VT100, PY2, unicode
|
||||
from .__version__ import S_VERSION, S_BUILD_DT, CODENAME
|
||||
from .svchub import SvcHub
|
||||
from .util import py_desc, align_tab, IMPLICATIONS
|
||||
from .authsrv import re_vol
|
||||
|
||||
HAVE_SSL = True
|
||||
try:
|
||||
@@ -326,7 +327,7 @@ def run_argparse(argv, formatter):
|
||||
ap2.add_argument("-e2d", action="store_true", help="enable up2k database")
|
||||
ap2.add_argument("-e2ds", action="store_true", help="enable up2k db-scanner, sets -e2d")
|
||||
ap2.add_argument("-e2dsa", action="store_true", help="scan all folders (for search), sets -e2ds")
|
||||
ap2.add_argument("--hist", metavar="PATH", type=u, help="where to store volume state")
|
||||
ap2.add_argument("--hist", metavar="PATH", type=u, help="where to store volume data (db, thumbs)")
|
||||
ap2.add_argument("--no-hash", action="store_true", help="disable hashing during e2ds folder scans")
|
||||
ap2.add_argument("--re-int", metavar="SEC", type=int, default=30, help="disk rescan check interval")
|
||||
ap2.add_argument("--re-maxage", metavar="SEC", type=int, default=0, help="disk rescan volume interval (0=off)")
|
||||
@@ -341,7 +342,9 @@ def run_argparse(argv, formatter):
|
||||
ap2.add_argument("--no-mtag-ff", action="store_true", help="never use FFprobe as tag reader")
|
||||
ap2.add_argument("-mtm", metavar="M=t,t,t", type=u, action="append", help="add/replace metadata mapping")
|
||||
ap2.add_argument("-mte", metavar="M,M,M", type=u, help="tags to index/display (comma-sep.)",
|
||||
default="circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,ac,vc,res,.fps")
|
||||
default="circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,vc,ac,res,.fps,ahash,vhash")
|
||||
ap2.add_argument("-mth", metavar="M,M,M", type=u, help="tags to hide by default (comma-sep.)",
|
||||
default=".vq,.aq,vc,ac,res,.fps")
|
||||
ap2.add_argument("-mtp", metavar="M=[f,]bin", type=u, action="append", help="read tag M using bin")
|
||||
|
||||
ap2 = ap.add_argument_group('appearance options')
|
||||
@@ -396,10 +399,16 @@ def main(argv=None):
|
||||
nstrs = []
|
||||
anymod = False
|
||||
for ostr in al.v or []:
|
||||
m = re_vol.match(ostr)
|
||||
if not m:
|
||||
# not our problem
|
||||
nstrs.append(ostr)
|
||||
continue
|
||||
|
||||
src, dst, perms = m.groups()
|
||||
na = [src, dst]
|
||||
mod = False
|
||||
oa = ostr.split(":")
|
||||
na = oa[:2]
|
||||
for opt in oa[2:]:
|
||||
for opt in perms.split(":"):
|
||||
if re.match("c[^,]", opt):
|
||||
mod = True
|
||||
na.append("c," + opt[1:])
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# coding: utf-8
|
||||
|
||||
VERSION = (0, 12, 8)
|
||||
VERSION = (0, 12, 12)
|
||||
CODENAME = "fil\033[33med"
|
||||
BUILD_DT = (2021, 8, 1)
|
||||
BUILD_DT = (2021, 8, 6)
|
||||
|
||||
S_VERSION = ".".join(map(str, VERSION))
|
||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||
|
||||
@@ -310,6 +310,12 @@ class VFS(object):
|
||||
yield f
|
||||
|
||||
|
||||
if WINDOWS:
|
||||
re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):(.*)$")
|
||||
else:
|
||||
re_vol = re.compile(r"^([^:]*):([^:]*):(.*)$")
|
||||
|
||||
|
||||
class AuthSrv(object):
|
||||
"""verifies users against given paths"""
|
||||
|
||||
@@ -319,11 +325,6 @@ class AuthSrv(object):
|
||||
self.warn_anonwrite = warn_anonwrite
|
||||
self.line_ctr = 0
|
||||
|
||||
if WINDOWS:
|
||||
self.re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):(.*)$")
|
||||
else:
|
||||
self.re_vol = re.compile(r"^([^:]*):([^:]*):(.*)$")
|
||||
|
||||
self.mutex = threading.Lock()
|
||||
self.reload()
|
||||
|
||||
@@ -453,7 +454,7 @@ class AuthSrv(object):
|
||||
# list of src:dst:permset:permset:...
|
||||
# permset is <rwmd>[,username][,username] or <c>,<flag>[=args]
|
||||
for v_str in self.args.v:
|
||||
m = self.re_vol.match(v_str)
|
||||
m = re_vol.match(v_str)
|
||||
if not m:
|
||||
raise Exception("invalid -v argument: [{}]".format(v_str))
|
||||
|
||||
@@ -624,9 +625,11 @@ class AuthSrv(object):
|
||||
if k1 in vol.flags:
|
||||
vol.flags[k2] = True
|
||||
|
||||
# default tag-list if unset
|
||||
# default tag cfgs if unset
|
||||
if "mte" not in vol.flags:
|
||||
vol.flags["mte"] = self.args.mte
|
||||
if "mth" not in vol.flags:
|
||||
vol.flags["mth"] = self.args.mth
|
||||
|
||||
# append parsers from argv to volume-flags
|
||||
self._read_volflag(vol.flags, "mtp", self.args.mtp, True)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
from copyparty.authsrv import AuthSrv
|
||||
|
||||
import sys
|
||||
import signal
|
||||
@@ -9,6 +8,7 @@ import threading
|
||||
from .broker_util import ExceptionalQueue
|
||||
from .httpsrv import HttpSrv
|
||||
from .util import FAKE_MP
|
||||
from copyparty.authsrv import AuthSrv
|
||||
|
||||
|
||||
class MpWorker(object):
|
||||
|
||||
@@ -1755,7 +1755,7 @@ class HttpCli(object):
|
||||
"acct": self.uname,
|
||||
"perms": json.dumps(perms),
|
||||
"taglist": [],
|
||||
"tag_order": [],
|
||||
"def_hcols": [],
|
||||
"have_up2k_idx": ("e2d" in vn.flags),
|
||||
"have_tags_idx": ("e2t" in vn.flags),
|
||||
"have_mv": (not self.args.no_mv),
|
||||
@@ -1952,8 +1952,8 @@ class HttpCli(object):
|
||||
j2a["logues"] = logues
|
||||
j2a["taglist"] = taglist
|
||||
|
||||
if "mte" in vn.flags:
|
||||
j2a["tag_order"] = json.dumps(vn.flags["mte"].split(","))
|
||||
if "mth" in vn.flags:
|
||||
j2a["def_hcols"] = vn.flags["mth"].split(",")
|
||||
|
||||
if self.args.css_browser:
|
||||
j2a["css"] = self.args.css_browser
|
||||
|
||||
@@ -174,25 +174,26 @@ class HttpSrv(object):
|
||||
now = time.time()
|
||||
|
||||
if now - (self.tp_time or now) > 300:
|
||||
m = "httpserver threadpool died: tpt {:.2f}, now {:.2f}, nthr {}, ncli {}"
|
||||
self.log(self.name, m.format(self.tp_time, now, self.tp_nthr, self.ncli), 1)
|
||||
self.tp_time = None
|
||||
self.tp_q = None
|
||||
|
||||
if self.tp_q:
|
||||
self.tp_q.put((sck, addr))
|
||||
with self.mutex:
|
||||
self.ncli += 1
|
||||
with self.mutex:
|
||||
self.ncli += 1
|
||||
if self.tp_q:
|
||||
self.tp_time = self.tp_time or now
|
||||
self.tp_ncli = max(self.tp_ncli, self.ncli + 1)
|
||||
self.tp_ncli = max(self.tp_ncli, self.ncli)
|
||||
if self.tp_nthr < self.ncli + 4:
|
||||
self.start_threads(8)
|
||||
return
|
||||
|
||||
self.tp_q.put((sck, addr))
|
||||
return
|
||||
|
||||
if not self.args.no_htp:
|
||||
m = "looks like the httpserver threadpool died; please make an issue on github and tell me the story of how you pulled that off, thanks and dog bless\n"
|
||||
self.log(self.name, m, 1)
|
||||
|
||||
with self.mutex:
|
||||
self.ncli += 1
|
||||
|
||||
thr = threading.Thread(
|
||||
target=self.thr_client,
|
||||
args=(sck, addr),
|
||||
|
||||
@@ -434,7 +434,15 @@ class MTag(object):
|
||||
try:
|
||||
v = getattr(md.info, attr)
|
||||
except:
|
||||
continue
|
||||
if k != "ac":
|
||||
continue
|
||||
|
||||
try:
|
||||
v = str(md.info).split(".")[1]
|
||||
if v.startswith("ogg"):
|
||||
v = v[3:]
|
||||
except:
|
||||
continue
|
||||
|
||||
if not v:
|
||||
continue
|
||||
|
||||
@@ -205,8 +205,8 @@ class ThumbSrv(object):
|
||||
try:
|
||||
fun(abspath, tpath)
|
||||
except:
|
||||
msg = "{} failed on {}\n{}"
|
||||
self.log(msg.format(fun.__name__, abspath, min_ex()), 3)
|
||||
msg = "{} could not create thumbnail of {}\n{}"
|
||||
self.log(msg.format(fun.__name__, abspath, min_ex()), "1;30")
|
||||
with open(tpath, "wb") as _:
|
||||
pass
|
||||
|
||||
@@ -286,8 +286,9 @@ class ThumbSrv(object):
|
||||
cmd += seek
|
||||
cmd += [
|
||||
b"-i", fsenc(abspath),
|
||||
b"-map", b"0:v:0",
|
||||
b"-vf", scale,
|
||||
b"-vframes", b"1",
|
||||
b"-frames:v", b"1",
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
@@ -308,8 +309,9 @@ class ThumbSrv(object):
|
||||
|
||||
ret, sout, serr = runcmd(cmd)
|
||||
if ret != 0:
|
||||
msg = ["ff: {}".format(x) for x in serr.split("\n")]
|
||||
self.log("FFmpeg failed:\n" + "\n".join(msg), c="1;30")
|
||||
m = "FFmpeg failed (probably a corrupt video file):\n"
|
||||
m += "\n".join(["ff: {}".format(x) for x in serr.split("\n")])
|
||||
self.log(m, c="1;30")
|
||||
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
|
||||
|
||||
def poke(self, tdir):
|
||||
|
||||
@@ -1422,9 +1422,10 @@ class Up2k(object):
|
||||
if not srem:
|
||||
raise Pebkac(400, "mv: cannot move a mountpoint")
|
||||
|
||||
st = bos.stat(sabs)
|
||||
if stat.S_ISREG(st.st_mode):
|
||||
return self._mv_file(uname, svp, dvp)
|
||||
st = bos.lstat(sabs)
|
||||
if stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode):
|
||||
with self.mutex:
|
||||
return self._mv_file(uname, svp, dvp)
|
||||
|
||||
jail = svn.get_dbv(srem)[0]
|
||||
permsets = [[True, False, True]]
|
||||
@@ -1449,7 +1450,8 @@ class Up2k(object):
|
||||
raise Pebkac(500, "mv: bug at {}, top {}".format(svpf, svp))
|
||||
|
||||
dvpf = dvp + svpf[len(svp) :]
|
||||
self._mv_file(uname, svpf, dvpf)
|
||||
with self.mutex:
|
||||
self._mv_file(uname, svpf, dvpf)
|
||||
|
||||
rmdirs(self.log_func, scandir, True, sabs)
|
||||
return "k"
|
||||
@@ -1541,13 +1543,15 @@ class Up2k(object):
|
||||
self.log("forgetting {}".format(vrem))
|
||||
if wark:
|
||||
self.log("found {} in db".format(wark))
|
||||
if self._relink(wark, ptop, vrem, None):
|
||||
drop_tags = False
|
||||
if drop_tags:
|
||||
if self._relink(wark, ptop, vrem, None):
|
||||
drop_tags = False
|
||||
|
||||
if drop_tags:
|
||||
q = "delete from mt where w=?"
|
||||
cur.execute(q, (wark[:16],))
|
||||
self.db_rm(cur, srd, sfn)
|
||||
|
||||
self.db_rm(cur, srd, sfn)
|
||||
|
||||
reg = self.registry.get(ptop)
|
||||
if reg:
|
||||
@@ -1599,7 +1603,7 @@ class Up2k(object):
|
||||
# deleting final remaining full copy; swap it with a symlink
|
||||
slabs = list(sorted(links.keys()))[0]
|
||||
ptop, rem = links.pop(slabs)
|
||||
self.log("linkswap [{}] and [{}]".format(sabs, dabs))
|
||||
self.log("linkswap [{}] and [{}]".format(sabs, slabs))
|
||||
bos.unlink(slabs)
|
||||
bos.rename(sabs, slabs)
|
||||
self._symlink(slabs, sabs, False)
|
||||
|
||||
@@ -22,9 +22,6 @@ html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
padding-bottom: 5em;
|
||||
}
|
||||
pre, code, tt {
|
||||
font-family: monospace, monospace;
|
||||
}
|
||||
@@ -810,6 +807,7 @@ input.eq_gain {
|
||||
#wrap {
|
||||
margin-top: 2em;
|
||||
min-height: 90vh;
|
||||
padding-bottom: 5em;
|
||||
}
|
||||
#tree {
|
||||
display: none;
|
||||
@@ -1081,19 +1079,39 @@ html.light #ggrid a:hover {
|
||||
padding: 1em;
|
||||
z-index: 765;
|
||||
}
|
||||
html.light #rui {
|
||||
color: #fff;
|
||||
}
|
||||
#rui div+div {
|
||||
margin-top: 1em;
|
||||
}
|
||||
#rui table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
#rui td {
|
||||
padding: .2em .5em;
|
||||
#rui td+td {
|
||||
padding: .2em 0 .2em .5em;
|
||||
}
|
||||
#rn_vadv input {
|
||||
font-family: monospace, monospace;
|
||||
}
|
||||
#rui td+td,
|
||||
#rui td input {
|
||||
#rui td input[type="text"] {
|
||||
width: 100%;
|
||||
}
|
||||
#rn_f.m td:first-child {
|
||||
white-space: nowrap;
|
||||
}
|
||||
#rn_f.m td+td {
|
||||
width: 50%;
|
||||
}
|
||||
#rn_f .err td {
|
||||
background: #c00;
|
||||
}
|
||||
#rn_f .err input[readonly] {
|
||||
background: #600;
|
||||
color: #fc0;
|
||||
}
|
||||
#rui input[readonly] {
|
||||
color: #fff;
|
||||
background: #444;
|
||||
@@ -1109,6 +1127,7 @@ html.light #ggrid a:hover {
|
||||
#barbuf,
|
||||
#barpos,
|
||||
#u2conf label,
|
||||
#rui label,
|
||||
#ops {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
<script>
|
||||
var acct = "{{ acct }}",
|
||||
perms = {{ perms }},
|
||||
tag_order_cfg = {{ tag_order }},
|
||||
def_hcols = {{ def_hcols|tojson }},
|
||||
have_up2k_idx = {{ have_up2k_idx|tojson }},
|
||||
have_tags_idx = {{ have_tags_idx|tojson }},
|
||||
have_mv = {{ have_mv|tojson }},
|
||||
|
||||
@@ -1460,6 +1460,105 @@ function play_linked() {
|
||||
})();
|
||||
|
||||
|
||||
|
||||
function fmt_ren(re, md, fmt) {
|
||||
var ptr = 0;
|
||||
function dive(stop_ch) {
|
||||
var ret = '', ng = 0;
|
||||
while (ptr < fmt.length) {
|
||||
var dbg = fmt.slice(ptr),
|
||||
ch = fmt[ptr++];
|
||||
|
||||
if (ch == '\\') {
|
||||
ret += fmt[ptr++];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == ')' || ch == ']' || ch == stop_ch)
|
||||
return [ng, ret];
|
||||
|
||||
if (ch == '[') {
|
||||
var r2 = dive();
|
||||
if (r2[0] == 0)
|
||||
ret += r2[1];
|
||||
}
|
||||
else if (ch == '(') {
|
||||
var end = fmt.indexOf(')', ptr);
|
||||
if (end < 0)
|
||||
throw 'the ( was never closed: ' + fmt.slice(0, ptr);
|
||||
|
||||
var arg = fmt.slice(ptr, end), v = null;
|
||||
ptr = end + 1;
|
||||
|
||||
if (arg != parseInt(arg))
|
||||
v = md[arg];
|
||||
else {
|
||||
arg = parseInt(arg);
|
||||
if (arg >= re.length)
|
||||
throw 'matching group ' + arg + ' exceeds ' + (re.length - 0);
|
||||
|
||||
v = re[arg];
|
||||
}
|
||||
|
||||
if (v !== null && v !== undefined)
|
||||
ret += v;
|
||||
else
|
||||
ng++;
|
||||
}
|
||||
else if (ch == '$') {
|
||||
ch = fmt[ptr++];
|
||||
var end = fmt.indexOf('(', ptr);
|
||||
if (end < 0)
|
||||
throw 'no function name after the $ here: ' + fmt.slice(0, ptr);
|
||||
|
||||
var fun = fmt.slice(ptr - 1, end);
|
||||
ptr = end + 1;
|
||||
|
||||
if (fun == "lpad") {
|
||||
var str = dive(',')[1];
|
||||
var len = dive(',')[1];
|
||||
var chr = dive()[1];
|
||||
if (!len || !chr)
|
||||
throw 'invalid arguments to ' + fun;
|
||||
|
||||
if (!str.length)
|
||||
ng += 1;
|
||||
|
||||
while (str.length < len)
|
||||
str = chr + str;
|
||||
|
||||
ret += str;
|
||||
}
|
||||
else if (fun == "rpad") {
|
||||
var str = dive(',')[1];
|
||||
var len = dive(',')[1];
|
||||
var chr = dive()[1];
|
||||
if (!len || !chr)
|
||||
throw 'invalid arguments to ' + fun;
|
||||
|
||||
if (!str.length)
|
||||
ng += 1;
|
||||
|
||||
while (str.length < len)
|
||||
str += chr;
|
||||
|
||||
ret += str;
|
||||
}
|
||||
else throw 'function not implemented: "' + fun + '"';
|
||||
}
|
||||
else ret += ch;
|
||||
}
|
||||
return [ng, ret];
|
||||
}
|
||||
try {
|
||||
return [true, dive()[1]];
|
||||
}
|
||||
catch (ex) {
|
||||
return [false, ex];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var fileman = (function () {
|
||||
var bren = ebi('fren'),
|
||||
bdel = ebi('fdel'),
|
||||
@@ -1493,16 +1592,47 @@ var fileman = (function () {
|
||||
return toast.err(3, 'cannot rename:\nyou do not have “move” permission in this folder');
|
||||
|
||||
var sel = msel.getsel();
|
||||
if (sel.length !== 1)
|
||||
return toast.err(3, 'select exactly 1 item to rename');
|
||||
if (!sel.length)
|
||||
return toast.err(3, 'select at least one item to rename');
|
||||
|
||||
var src = sel[0].vp;
|
||||
if (src.endsWith('/'))
|
||||
src = src.slice(0, -1);
|
||||
var f = [],
|
||||
base = vsplit(sel[0].vp)[0],
|
||||
mkeys;
|
||||
|
||||
var vsp = vsplit(src),
|
||||
base = vsp[0],
|
||||
ofn = uricom_dec(vsp[1])[0];
|
||||
for (var a = 0; a < sel.length; a++) {
|
||||
var vp = sel[a].vp;
|
||||
if (vp.endsWith('/'))
|
||||
vp = vp.slice(0, -1);
|
||||
|
||||
var vsp = vsplit(vp);
|
||||
if (base != vsp[0])
|
||||
return toast.err(0, 'bug:\n' + base + '\n' + vsp[0]);
|
||||
|
||||
var vars = ft2dict(ebi(sel[a].id).closest('tr'));
|
||||
mkeys = vars[1].concat(vars[2]);
|
||||
|
||||
var md = vars[0];
|
||||
for (var k in md) {
|
||||
if (!md.hasOwnProperty(k))
|
||||
continue;
|
||||
|
||||
md[k.toLowerCase()] = md[k];
|
||||
k = k.toLowerCase();
|
||||
|
||||
if (k.startsWith('.'))
|
||||
md[k.slice(1)] = md[k];
|
||||
}
|
||||
md.t = md.ext;
|
||||
md.date = md.ts;
|
||||
md.size = md.sz;
|
||||
|
||||
f.push({
|
||||
"src": vp,
|
||||
"ofn": uricom_dec(vsp[1])[0],
|
||||
"md": vars[0],
|
||||
"ok": true
|
||||
});
|
||||
}
|
||||
|
||||
var rui = ebi('rui');
|
||||
if (!rui) {
|
||||
@@ -1510,62 +1640,221 @@ var fileman = (function () {
|
||||
rui.setAttribute('id', 'rui');
|
||||
document.body.appendChild(rui);
|
||||
}
|
||||
var html = [
|
||||
'<h1>rename file</h1>',
|
||||
'<div><table>',
|
||||
'<tr><td>old:</td><td><input type="text" id="rn_old" readonly /></td></tr>',
|
||||
'<tr><td>new:</td><td><input type="text" id="rn_new" /></td></tr>',
|
||||
'</table></div>',
|
||||
|
||||
var html = sel.length > 1 ? ['<div>'] : [
|
||||
'<div>',
|
||||
'<button id="rn_dec">url-decode</button>',
|
||||
'|',
|
||||
'<button id="rn_reset">↺ reset</button>',
|
||||
'<button id="rn_cancel">❌ cancel</button>',
|
||||
'<button id="rn_apply">✅ apply rename</button>',
|
||||
'</div>',
|
||||
'<div><table>'
|
||||
'<button class="rn_dec" n="0" tt="may fix some cases of broken filenames">url-decode</button>',
|
||||
'//',
|
||||
'<button class="rn_reset" n="0" tt="reset modified filenames back to the original ones">↺ reset</button>'
|
||||
];
|
||||
|
||||
var vars = ft2dict(ebi(sel[0].id).closest('tr')),
|
||||
keys = vars[1].concat(vars[2]);
|
||||
|
||||
vars = vars[0];
|
||||
for (var a = 0; a < keys.length; a++)
|
||||
html.push('<tr><td>' + esc(keys[a]) + '</td><td><input type="text" readonly value="' + esc(vars[keys[a]]) + '" /></td></tr>');
|
||||
html = html.concat([
|
||||
'<button id="rn_cancel" tt="abort and close this window">❌ cancel</button>',
|
||||
'<button id="rn_apply">✅ apply rename</button>',
|
||||
'<a id="rn_adv" class="tgl btn" href="#" tt="batch / metadata / pattern renaming">advanced</a>',
|
||||
'<a id="rn_case" class="tgl btn" href="#" tt="case-sensitive regex">case</a>',
|
||||
'</div>',
|
||||
'<div id="rn_vadv"><table>',
|
||||
'<tr><td>regex</td><td><input type="text" id="rn_re" tt="regex search pattern to apply to original filenames; capturing groups can be referenced in the format field below like <code>(1)</code> and <code>(2)</code> and so on" /></td></tr>',
|
||||
'<tr><td>format</td><td><input type="text" id="rn_fmt" tt="inspired by foobar2000:$N<code>(title)</code> is replaced by song title,$N<code>[(artist) - ](title)</code> skips the first part if artist is blank$N<code>$lpad((tn),2,0)</code> pads tracknumber to 2 digits" /></td></tr>',
|
||||
'<tr><td>preset</td><td><select id="rn_pre"></select>',
|
||||
'<button id="rn_pdel">❌ delete</button>',
|
||||
'<button id="rn_pnew">💾 save as</button>',
|
||||
'</td></tr>',
|
||||
'</table></div>'
|
||||
]);
|
||||
|
||||
if (sel.length == 1)
|
||||
html.push(
|
||||
'<div><table id="rn_f">\n' +
|
||||
'<tr><td>old:</td><td><input type="text" id="rn_old" n="0" readonly /></td></tr>\n' +
|
||||
'<tr><td>new:</td><td><input type="text" id="rn_new" n="0" /></td></tr>');
|
||||
else {
|
||||
html.push(
|
||||
'<div><table id="rn_f" class="m">' +
|
||||
'<tr><td></td><td>new name</td><td>old name</td></tr>');
|
||||
for (var a = 0; a < f.length; a++)
|
||||
html.push(
|
||||
'<tr><td>' +
|
||||
'<button class="rn_dec" n="' + a + '">decode</button>',
|
||||
'<button class="rn_reset" n="' + a + '">↺ reset</button></td>',
|
||||
'<td><input type="text" id="rn_new" n="' + a + '" /></td>' +
|
||||
'<td><input type="text" id="rn_old" n="' + a + '" readonly /></td></tr>');
|
||||
}
|
||||
html.push('</table></div>');
|
||||
rui.innerHTML = html.join('\n');
|
||||
var iold = ebi('rn_old'),
|
||||
inew = ebi('rn_new');
|
||||
|
||||
function rn_reset() {
|
||||
inew.value = iold.value;
|
||||
inew.focus();
|
||||
inew.setSelectionRange(0, inew.value.lastIndexOf('.'), "forward");
|
||||
if (sel.length == 1) {
|
||||
html.push('<div><p style="margin:.6em 0">tags for the selected file (read-only, just for reference):</p><table>');
|
||||
for (var a = 0; a < mkeys.length; a++)
|
||||
html.push('<tr><td>' + esc(mkeys[a]) + '</td><td><input type="text" readonly value="' + esc(f[0].md[mkeys[a]]) + '" /></td></tr>');
|
||||
|
||||
html.push('</table></div>');
|
||||
}
|
||||
|
||||
rui.innerHTML = html.join('\n');
|
||||
for (var a = 0; a < f.length; a++) {
|
||||
var k = '[n="' + a + '"]';
|
||||
f[a].iold = QS('#rn_old' + k);
|
||||
f[a].inew = QS('#rn_new' + k);
|
||||
f[a].inew.value = f[a].iold.value = f[a].ofn;
|
||||
|
||||
(function (a) {
|
||||
f[a].inew.onkeydown = function (e) {
|
||||
rn_ok(a, true);
|
||||
|
||||
if (e.key == 'Escape')
|
||||
return rn_cancel();
|
||||
|
||||
if (e.key == 'Enter')
|
||||
return rn_apply();
|
||||
};
|
||||
QS('.rn_dec' + k).onclick = function () {
|
||||
f[a].inew.value = uricom_dec(f[a].inew.value)[0];
|
||||
};
|
||||
QS('.rn_reset' + k).onclick = function () {
|
||||
rn_reset(a);
|
||||
};
|
||||
})(a);
|
||||
}
|
||||
rn_reset(0);
|
||||
tt.att(rui);
|
||||
|
||||
var adv = bcfg_get('rn_adv', false),
|
||||
cs = bcfg_get('rn_case', false);
|
||||
|
||||
function sadv() {
|
||||
ebi('rn_vadv').style.display = ebi('rn_case').style.display = adv ? '' : 'none';
|
||||
}
|
||||
sadv();
|
||||
|
||||
function rn_ok(n, ok) {
|
||||
f[n].ok = ok;
|
||||
clmod(f[n].inew.closest('tr'), 'err', !ok);
|
||||
}
|
||||
|
||||
function rn_reset(n) {
|
||||
f[n].inew.value = f[n].iold.value = f[n].ofn;
|
||||
f[n].inew.focus();
|
||||
f[n].inew.setSelectionRange(0, f[n].inew.value.lastIndexOf('.'), "forward");
|
||||
}
|
||||
function rn_cancel() {
|
||||
rui.parentNode.removeChild(rui);
|
||||
}
|
||||
|
||||
inew.onkeydown = function (e) {
|
||||
ebi('rn_cancel').onclick = rn_cancel;
|
||||
ebi('rn_apply').onclick = rn_apply;
|
||||
ebi('rn_adv').onclick = function () {
|
||||
adv = !adv;
|
||||
bcfg_set('rn_adv', adv);
|
||||
sadv();
|
||||
};
|
||||
ebi('rn_case').onclick = function () {
|
||||
cs = !cs;
|
||||
bcfg_set('rn_case', cs);
|
||||
};
|
||||
|
||||
var ire = ebi('rn_re'),
|
||||
ifmt = ebi('rn_fmt'),
|
||||
ipre = ebi('rn_pre'),
|
||||
idel = ebi('rn_pdel'),
|
||||
inew = ebi('rn_pnew'),
|
||||
defp = '$lpad((tn),2,0). [(artist) - ](title).(ext)';
|
||||
|
||||
var presets = {};
|
||||
presets[defp] = ['', defp];
|
||||
presets = jread("rn_pre", presets);
|
||||
|
||||
function spresets() {
|
||||
var keys = Object.keys(presets), o;
|
||||
keys.sort();
|
||||
ipre.innerHTML = '<option value=""></option>';
|
||||
for (var a = 0; a < keys.length; a++) {
|
||||
o = mknod('option');
|
||||
o.setAttribute('value', keys[a]);
|
||||
o.textContent = keys[a];
|
||||
ipre.appendChild(o);
|
||||
}
|
||||
}
|
||||
inew.onclick = function () {
|
||||
var name = prompt('provide a name for your new preset', ifmt.value);
|
||||
if (!name)
|
||||
return toast.warn(3, 'aborted');
|
||||
|
||||
presets[name] = [ire.value, ifmt.value];
|
||||
jwrite('rn_pre', presets);
|
||||
spresets();
|
||||
ipre.value = name;
|
||||
};
|
||||
idel.onclick = function () {
|
||||
delete presets[ipre.value];
|
||||
jwrite('rn_pre', presets);
|
||||
spresets();
|
||||
};
|
||||
ipre.oninput = function () {
|
||||
var cfg = presets[ipre.value];
|
||||
if (cfg) {
|
||||
ire.value = cfg[0];
|
||||
ifmt.value = cfg[1];
|
||||
}
|
||||
ifmt.oninput();
|
||||
};
|
||||
spresets();
|
||||
|
||||
ire.onkeydown = ifmt.onkeydown = function (e) {
|
||||
if (e.key == 'Escape')
|
||||
return rn_cancel();
|
||||
|
||||
if (e.key == 'Enter')
|
||||
return rn_apply();
|
||||
};
|
||||
ebi('rn_cancel').onclick = rn_cancel;
|
||||
ebi('rn_reset').onclick = rn_reset;
|
||||
ebi('rn_apply').onclick = rn_apply;
|
||||
ebi('rn_dec').onclick = function () {
|
||||
inew.value = uricom_dec(inew.value)[0];
|
||||
|
||||
ire.oninput = ifmt.oninput = function (e) {
|
||||
var ptn = ire.value,
|
||||
fmt = ifmt.value,
|
||||
re = null;
|
||||
|
||||
if (!fmt)
|
||||
return;
|
||||
|
||||
try {
|
||||
if (ptn)
|
||||
re = new RegExp(ptn, cs ? 'i' : '');
|
||||
}
|
||||
catch (ex) {
|
||||
return toast.err(5, 'invalid regex:\n' + ex);
|
||||
}
|
||||
toast.hide();
|
||||
|
||||
for (var a = 0; a < f.length; a++) {
|
||||
var m = re ? re.exec(f[a].ofn) : null,
|
||||
ok, txt = '';
|
||||
|
||||
if (re && !m) {
|
||||
txt = 'regex did not match';
|
||||
ok = false;
|
||||
}
|
||||
else {
|
||||
var ret = fmt_ren(m, f[a].md, fmt);
|
||||
ok = ret[0];
|
||||
txt = ret[1];
|
||||
}
|
||||
rn_ok(a, ok);
|
||||
f[a].inew.value = (ok ? '' : 'ERROR: ') + txt;
|
||||
}
|
||||
};
|
||||
|
||||
iold.value = ofn;
|
||||
rn_reset();
|
||||
|
||||
function rn_apply() {
|
||||
var dst = base + uricom_enc(inew.value, false);
|
||||
while (f.length && (!f[0].ok || f[0].ofn == f[0].inew.value))
|
||||
f.shift();
|
||||
|
||||
if (!f.length) {
|
||||
toast.ok(2, 'rename OK');
|
||||
treectl.goto(get_evpath());
|
||||
return rn_cancel();
|
||||
}
|
||||
|
||||
toast.inf(0, 'renaming ' + f.length + ' items\n\n' + f[0].ofn);
|
||||
var dst = base + uricom_enc(f[0].inew.value, false);
|
||||
|
||||
function rename_cb() {
|
||||
if (this.readyState != XMLHttpRequest.DONE)
|
||||
@@ -1576,15 +1865,16 @@ var fileman = (function () {
|
||||
toast.err(9, 'rename failed:\n' + msg);
|
||||
return;
|
||||
}
|
||||
toast.ok(2, 'rename OK');
|
||||
treectl.goto(get_evpath());
|
||||
rn_cancel();
|
||||
|
||||
f.shift().inew.value = '( OK )';
|
||||
return rn_apply();
|
||||
}
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', src + '?move=' + dst, true);
|
||||
xhr.open('GET', f[0].src + '?move=' + dst, true);
|
||||
xhr.onreadystatechange = rename_cb;
|
||||
xhr.send();
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
r.delete = function (e) {
|
||||
@@ -3029,6 +3319,8 @@ var filecols = (function () {
|
||||
"q": "quality / bitrate",
|
||||
"Ac": "audio codec",
|
||||
"Vc": "video codec",
|
||||
"Ahash": "audio checksum",
|
||||
"Vhash": "video checksum",
|
||||
"Res": "resolution",
|
||||
"T": "filetype",
|
||||
"aq": "audio quality / bitrate",
|
||||
@@ -3040,6 +3332,21 @@ var filecols = (function () {
|
||||
"hz": "sample rate"
|
||||
};
|
||||
|
||||
if (JSON.stringify(def_hcols) != sread('hfilecols')) {
|
||||
console.log("applying default hidden-cols");
|
||||
jwrite('hfilecols', def_hcols);
|
||||
for (var a = 0; a < def_hcols.length; a++) {
|
||||
var t = def_hcols[a];
|
||||
t = t.slice(0, 1).toUpperCase() + t.slice(1);
|
||||
if (t.startsWith("."))
|
||||
t = t.slice(1);
|
||||
|
||||
if (hidden.indexOf(t) == -1)
|
||||
hidden.push(t);
|
||||
}
|
||||
jwrite("filecols", hidden);
|
||||
}
|
||||
|
||||
var add_btns = function () {
|
||||
var ths = QSA('#files th>span');
|
||||
for (var a = 0, aa = ths.length; a < aa; a++) {
|
||||
|
||||
@@ -16,9 +16,6 @@ html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
padding-bottom: 5em;
|
||||
}
|
||||
#box {
|
||||
padding: .5em 1em;
|
||||
background: #2c2c2c;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
set -e
|
||||
echo
|
||||
|
||||
help() { exec cat <<'EOF'
|
||||
|
||||
# optional args:
|
||||
#
|
||||
@@ -26,6 +27,8 @@ echo
|
||||
#
|
||||
# `no-dd` saves ~2k by removing the mouse cursor
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
# port install gnutar findutils gsed coreutils
|
||||
gtar=$(command -v gtar || command -v gnutar) || true
|
||||
@@ -73,6 +76,7 @@ while [ ! -z "$1" ]; do
|
||||
no-cm) no_cm=1 ; ;;
|
||||
no-sh) do_sh= ; ;;
|
||||
no-py) do_py= ; ;;
|
||||
*) help ; ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
@@ -204,19 +208,24 @@ done
|
||||
rm -rf copyparty/web/mde.* copyparty/web/deps/easymde*
|
||||
echo h > copyparty/web/mde.html
|
||||
f=copyparty/web/md.html
|
||||
sed -r '/edit2">edit \(fancy/d' <$f >t && tmv "$f"
|
||||
sed -r '/edit2">edit \(fancy/d' <$f >t
|
||||
tmv "$f"
|
||||
}
|
||||
|
||||
[ $no_fnt ] && {
|
||||
rm -f copyparty/web/deps/scp.woff2
|
||||
f=copyparty/web/md.css
|
||||
sed -r '/scp\.woff2/d' <$f >t && tmv "$f"
|
||||
gzip -d "$f"
|
||||
sed -r '/scp\.woff2/d' <$f >t
|
||||
tmv "$f"
|
||||
}
|
||||
|
||||
[ $no_dd ] && {
|
||||
rm -rf copyparty/web/dd
|
||||
f=copyparty/web/browser.css
|
||||
sed -r 's/(cursor: )url\([^)]+\), (pointer)/\1\2/; /[0-9]+% \{cursor:/d; /animation: cursor/d' <$f >t && tmv "$f"
|
||||
gzip -d "$f"
|
||||
sed -r 's/(cursor: )url\([^)]+\), (pointer)/\1\2/; /[0-9]+% \{cursor:/d; /animation: cursor/d' <$f >t
|
||||
tmv "$f"
|
||||
}
|
||||
|
||||
[ $repack ] ||
|
||||
@@ -251,7 +260,7 @@ done
|
||||
|
||||
gzres() {
|
||||
command -v pigz &&
|
||||
pk='pigz -11 -J 34 -I 256' ||
|
||||
pk='pigz -11 -I 256' ||
|
||||
pk='gzip'
|
||||
|
||||
echo "$pk"
|
||||
|
||||
@@ -124,7 +124,7 @@ def tc1():
|
||||
|
||||
arg = "{}:{}:{}".format(pd, ud, p, hp)
|
||||
if hp:
|
||||
arg += ":chist=" + hp
|
||||
arg += ":c,hist=" + hp
|
||||
|
||||
args += ["-v", arg]
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ class Cfg(Namespace):
|
||||
nih=True,
|
||||
mtp=[],
|
||||
mte="a",
|
||||
mth="",
|
||||
hist=None,
|
||||
no_hash=False,
|
||||
css_browser=None,
|
||||
|
||||
@@ -21,6 +21,7 @@ class Cfg(Namespace):
|
||||
ex2 = {
|
||||
"mtp": [],
|
||||
"mte": "a",
|
||||
"mth": "",
|
||||
"hist": None,
|
||||
"no_hash": False,
|
||||
"css_browser": None,
|
||||
|
||||
Reference in New Issue
Block a user