copyparty 1.13.1__py3-none-any.whl → 1.13.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
copyparty/szip.py CHANGED
@@ -1,12 +1,12 @@
1
1
  # coding: utf-8
2
2
  from __future__ import print_function, unicode_literals
3
3
 
4
- import argparse
5
4
  import calendar
6
5
  import stat
7
6
  import time
8
7
  import zlib
9
8
 
9
+ from .authsrv import AuthSrv
10
10
  from .bos import bos
11
11
  from .sutil import StreamArc, errdesc
12
12
  from .util import min_ex, sanitize_fn, spack, sunpack, yieldfile
@@ -213,13 +213,13 @@ class StreamZip(StreamArc):
213
213
  def __init__(
214
214
  self,
215
215
  log ,
216
- args ,
216
+ asrv ,
217
217
  fgen ,
218
218
  utf8 = False,
219
219
  pre_crc = False,
220
220
  **kwargs
221
221
  ) :
222
- super(StreamZip, self).__init__(log, args, fgen)
222
+ super(StreamZip, self).__init__(log, asrv, fgen)
223
223
 
224
224
  self.utf8 = utf8
225
225
  self.pre_crc = pre_crc
@@ -296,7 +296,7 @@ class StreamZip(StreamArc):
296
296
  mbuf = b""
297
297
 
298
298
  if errors:
299
- errf, txt = errdesc(errors)
299
+ errf, txt = errdesc(self.asrv.vfs, errors)
300
300
  self.log("\n".join(([repr(errf)] + txt[1:])))
301
301
  for x in self.ser(errf):
302
302
  yield x
copyparty/th_cli.py CHANGED
@@ -104,6 +104,11 @@ class ThumbCli(object):
104
104
 
105
105
  fmt = sfmt
106
106
 
107
+ elif fmt[:1] == "p" and not is_au:
108
+ t = "cannot thumbnail [%s]: png only allowed for waveforms"
109
+ self.log(t % (rem), 6)
110
+ return None
111
+
107
112
  histpath = self.asrv.vfs.histtab.get(ptop)
108
113
  if not histpath:
109
114
  self.log("no histpath for [{}]".format(ptop))
copyparty/th_srv.py CHANGED
@@ -15,7 +15,7 @@ from queue import Queue
15
15
  from .__init__ import ANYWIN, TYPE_CHECKING
16
16
  from .authsrv import VFS
17
17
  from .bos import bos
18
- from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe
18
+ from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, au_unpk, ffprobe
19
19
  from .util import BytesIO # type: ignore
20
20
  from .util import (
21
21
  FFMPEG_URL,
@@ -294,6 +294,12 @@ class ThumbSrv(object):
294
294
  ext = abspath.split(".")[-1].lower()
295
295
  png_ok = False
296
296
  funs = []
297
+
298
+ if ext in self.args.au_unpk:
299
+ ap_unpk = au_unpk(self.log, self.args.au_unpk, abspath, vn)
300
+ else:
301
+ ap_unpk = abspath
302
+
297
303
  if not bos.path.exists(tpath):
298
304
  for lib in self.args.th_dec:
299
305
  if lib == "pil" and ext in self.fmt_pil:
@@ -313,9 +319,6 @@ class ThumbSrv(object):
313
319
  else:
314
320
  funs.append(self.conv_spec)
315
321
 
316
- if not png_ok and tpath.endswith(".png"):
317
- raise Pebkac(400, "png only allowed for waveforms")
318
-
319
322
  tdir, tfn = os.path.split(tpath)
320
323
  ttpath = os.path.join(tdir, "w", tfn)
321
324
  try:
@@ -325,7 +328,10 @@ class ThumbSrv(object):
325
328
 
326
329
  for fun in funs:
327
330
  try:
328
- fun(abspath, ttpath, fmt, vn)
331
+ if not png_ok and tpath.endswith(".png"):
332
+ raise Exception("png only allowed for waveforms")
333
+
334
+ fun(ap_unpk, ttpath, fmt, vn)
329
335
  break
330
336
  except Exception as ex:
331
337
  msg = "{} could not create thumbnail of {}\n{}"
@@ -343,6 +349,9 @@ class ThumbSrv(object):
343
349
  except:
344
350
  pass
345
351
 
352
+ if abspath != ap_unpk:
353
+ wunlink(self.log, ap_unpk, vn.flags)
354
+
346
355
  try:
347
356
  wrename(self.log, ttpath, tpath, vn.flags)
348
357
  except:
@@ -581,6 +590,25 @@ class ThumbSrv(object):
581
590
  cmd += [fsenc(tpath)]
582
591
  self._run_ff(cmd, vn)
583
592
 
593
+ if "pngquant" in vn.flags:
594
+ wtpath = tpath + ".png"
595
+ cmd = [
596
+ b"pngquant",
597
+ b"--strip",
598
+ b"--nofs",
599
+ b"--output",
600
+ fsenc(wtpath),
601
+ fsenc(tpath),
602
+ ]
603
+ ret = runcmd(cmd, timeout=vn.flags["convt"], nice=True, oom=400)[0]
604
+ if ret:
605
+ try:
606
+ wunlink(self.log, wtpath, vn.flags)
607
+ except:
608
+ pass
609
+ else:
610
+ wrename(self.log, wtpath, tpath, vn.flags)
611
+
584
612
  def conv_spec(self, abspath , tpath , fmt , vn ) :
585
613
  ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
586
614
  if "ac" not in ret:
@@ -643,8 +671,8 @@ class ThumbSrv(object):
643
671
  raise Exception("disabled in server config")
644
672
 
645
673
  self.wait4ram(0.2, tpath)
646
- ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
647
- if "ac" not in ret:
674
+ tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2))
675
+ if "ac" not in tags:
648
676
  raise Exception("not audio")
649
677
 
650
678
  if quality.endswith("k"):
@@ -665,7 +693,7 @@ class ThumbSrv(object):
665
693
  b"-v", b"error",
666
694
  b"-hide_banner",
667
695
  b"-i", fsenc(abspath),
668
- b"-map_metadata", b"-1",
696
+ ] + self.big_tags(rawtags) + [
669
697
  b"-map", b"0:a:0",
670
698
  b"-ar", b"44100",
671
699
  b"-ac", b"2",
@@ -681,16 +709,16 @@ class ThumbSrv(object):
681
709
  raise Exception("disabled in server config")
682
710
 
683
711
  self.wait4ram(0.2, tpath)
684
- ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
685
- if "ac" not in ret:
712
+ tags, rawtags = ffprobe(abspath, int(vn.flags["convt"] / 2))
713
+ if "ac" not in tags:
686
714
  raise Exception("not audio")
687
715
 
688
716
  try:
689
- dur = ret[".dur"][1]
717
+ dur = tags[".dur"][1]
690
718
  except:
691
719
  dur = 0
692
720
 
693
- src_opus = abspath.lower().endswith(".opus") or ret["ac"][1] == "opus"
721
+ src_opus = abspath.lower().endswith(".opus") or tags["ac"][1] == "opus"
694
722
  want_caf = tpath.endswith(".caf")
695
723
  tmp_opus = tpath
696
724
  if want_caf:
@@ -711,7 +739,7 @@ class ThumbSrv(object):
711
739
  b"-v", b"error",
712
740
  b"-hide_banner",
713
741
  b"-i", fsenc(abspath),
714
- b"-map_metadata", b"-1",
742
+ ] + self.big_tags(rawtags) + [
715
743
  b"-map", b"0:a:0",
716
744
  b"-c:a", b"libopus",
717
745
  b"-b:a", bq,
@@ -768,6 +796,16 @@ class ThumbSrv(object):
768
796
  except:
769
797
  pass
770
798
 
799
+ def big_tags(self, raw_tags ) :
800
+ ret = []
801
+ for k, vs in raw_tags.items():
802
+ for v in vs:
803
+ if len(str(v)) >= 1024:
804
+ bv = k.encode("utf-8", "replace")
805
+ ret += [b"-metadata", bv + b"="]
806
+ break
807
+ return ret
808
+
771
809
  def poke(self, tdir ) :
772
810
  if not self.poke_cd.poke(tdir):
773
811
  return
copyparty/up2k.py CHANGED
@@ -10,7 +10,6 @@ import math
10
10
  import os
11
11
  import re
12
12
  import shutil
13
- import signal
14
13
  import stat
15
14
  import subprocess as sp
16
15
  import tempfile
@@ -29,6 +28,7 @@ from .fsutil import Fstab
29
28
  from .mtag import MParser, MTag
30
29
  from .util import (
31
30
  HAVE_SQLITE3,
31
+ VF_CAREFUL,
32
32
  SYMTIME,
33
33
  Daemon,
34
34
  MTHash,
@@ -88,9 +88,6 @@ CV_EXTS = set(zsg.split(","))
88
88
  HINT_HISTPATH = "you could try moving the database to another location (preferably an SSD or NVME drive) using either the --hist argument (global option for all volumes), or the hist volflag (just for this volume)"
89
89
 
90
90
 
91
- VF_CAREFUL = {"mv_re_t": 5, "rm_re_t": 5, "mv_re_r": 0.1, "rm_re_r": 0.1}
92
-
93
-
94
91
  class Dbw(object):
95
92
  def __init__(self, c , n , t ) :
96
93
  self.c = c
@@ -458,7 +455,13 @@ class Up2k(object):
458
455
  # important; not deferred by db_act
459
456
  timeout = self._check_lifetimes()
460
457
 
461
- timeout = min(timeout, now + self._check_xiu())
458
+ try:
459
+ timeout = min(timeout, now + self._check_xiu())
460
+ except Exception as ex:
461
+ if "closed cursor" in str(ex):
462
+ self.log("sched_rescan: lost db")
463
+ return
464
+ raise
462
465
 
463
466
  with self.mutex:
464
467
  for vp, vol in sorted(self.asrv.vfs.all_vols.items()):
@@ -703,9 +706,9 @@ class Up2k(object):
703
706
  try:
704
707
  bos.makedirs(vol.realpath) # gonna happen at snap anyways
705
708
  dir_is_empty(self.log_func, not self.args.no_scandir, vol.realpath)
706
- except:
709
+ except Exception as ex:
707
710
  self.volstate[vol.vpath] = "OFFLINE (cannot access folder)"
708
- self.log("cannot access " + vol.realpath, c=1)
711
+ self.log("cannot access %s: %r" % (vol.realpath, ex), c=1)
709
712
  continue
710
713
 
711
714
  if scan_vols and vol.vpath not in scan_vols:
@@ -1036,8 +1039,11 @@ class Up2k(object):
1036
1039
  return None
1037
1040
 
1038
1041
  def _verify_db_cache(self, cur , vpath ) :
1039
- # check if volume config changed since last use; drop caches if so
1040
- zsl = [vpath] + list(sorted(self.asrv.vfs.all_vols.keys()))
1042
+ # check if list of intersecting volumes changed since last use; drop caches if so
1043
+ prefix = (vpath + "/").lstrip("/")
1044
+ zsl = [x for x in self.asrv.vfs.all_vols if x.startswith(prefix)]
1045
+ zsl = [x[len(prefix) :] for x in zsl]
1046
+ zsl.sort()
1041
1047
  zb = hashlib.sha1("\n".join(zsl).encode("utf-8", "replace")).digest()
1042
1048
  vcfg = base64.urlsafe_b64encode(zb[:18]).decode("ascii")
1043
1049
 
@@ -1647,7 +1653,7 @@ class Up2k(object):
1647
1653
 
1648
1654
  if e2vp and rewark:
1649
1655
  self.hub.retcode = 1
1650
- os.kill(os.getpid(), signal.SIGTERM)
1656
+ Daemon(self.hub.sigterm)
1651
1657
  raise Exception("{} files have incorrect hashes".format(len(rewark)))
1652
1658
 
1653
1659
  if not e2vu or not rewark:
copyparty/util.py CHANGED
@@ -337,6 +337,21 @@ APPLESAN_TXT = r"/(__MACOS|Icon\r\r)|/\.(_|DS_Store|AppleDouble|LSOverride|Docum
337
337
  APPLESAN_RE = re.compile(APPLESAN_TXT)
338
338
 
339
339
 
340
+ HUMANSIZE_UNITS = ("B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB")
341
+
342
+ UNHUMANIZE_UNITS = {
343
+ "b": 1,
344
+ "k": 1024,
345
+ "m": 1024 * 1024,
346
+ "g": 1024 * 1024 * 1024,
347
+ "t": 1024 * 1024 * 1024 * 1024,
348
+ "p": 1024 * 1024 * 1024 * 1024 * 1024,
349
+ "e": 1024 * 1024 * 1024 * 1024 * 1024 * 1024,
350
+ }
351
+
352
+ VF_CAREFUL = {"mv_re_t": 5, "rm_re_t": 5, "mv_re_r": 0.1, "rm_re_r": 0.1}
353
+
354
+
340
355
  pybin = sys.executable or ""
341
356
  if EXE:
342
357
  pybin = ""
@@ -442,13 +457,22 @@ class Daemon(threading.Thread):
442
457
  r = True,
443
458
  ka = None,
444
459
  ) :
445
- threading.Thread.__init__(
446
- self, target=target, name=name, args=a or (), kwargs=ka
447
- )
460
+ threading.Thread.__init__(self, name=name)
461
+ self.a = a or ()
462
+ self.ka = ka or {}
463
+ self.fun = target
448
464
  self.daemon = True
449
465
  if r:
450
466
  self.start()
451
467
 
468
+ def run(self):
469
+ if not ANYWIN and not PY2:
470
+ signal.pthread_sigmask(
471
+ signal.SIG_BLOCK, [signal.SIGINT, signal.SIGTERM, signal.SIGUSR1]
472
+ )
473
+
474
+ self.fun(*self.a, **self.ka)
475
+
452
476
 
453
477
  class Netdev(object):
454
478
  def __init__(self, ip , idx , name , desc ):
@@ -843,6 +867,7 @@ class ProgressPrinter(threading.Thread):
843
867
  self.start()
844
868
 
845
869
  def run(self) :
870
+ sigblock()
846
871
  tp = 0
847
872
  msg = None
848
873
  no_stdout = self.args.q
@@ -1287,6 +1312,15 @@ def log_thrs(log , ival , name ) :
1287
1312
  log(name, "\033[0m \033[33m".join(tv), 3)
1288
1313
 
1289
1314
 
1315
+ def sigblock():
1316
+ if ANYWIN or PY2:
1317
+ return
1318
+
1319
+ signal.pthread_sigmask(
1320
+ signal.SIG_BLOCK, [signal.SIGINT, signal.SIGTERM, signal.SIGUSR1]
1321
+ )
1322
+
1323
+
1290
1324
  def vol_san(vols , txt ) :
1291
1325
  txt0 = txt
1292
1326
  for vol in vols:
@@ -1308,10 +1342,11 @@ def vol_san(vols , txt ) :
1308
1342
 
1309
1343
  def min_ex(max_lines = 8, reverse = False) :
1310
1344
  et, ev, tb = sys.exc_info()
1311
- stb = traceback.extract_tb(tb)
1345
+ stb = traceback.extract_tb(tb) if tb else traceback.extract_stack()[:-1]
1312
1346
  fmt = "%s @ %d <%s>: %s"
1313
1347
  ex = [fmt % (fp.split(os.sep)[-1], ln, fun, txt) for fp, ln, fun, txt in stb]
1314
- ex.append("[%s] %s" % (et.__name__ if et else "(anonymous)", ev))
1348
+ if et or ev or tb:
1349
+ ex.append("[%s] %s" % (et.__name__ if et else "(anonymous)", ev))
1315
1350
  return "\n".join(ex[-max_lines:][:: -1 if reverse else 1])
1316
1351
 
1317
1352
 
@@ -1764,7 +1799,7 @@ def gencookie(k , v , r , tls , dur = 0, txt = "") :
1764
1799
 
1765
1800
 
1766
1801
  def humansize(sz , terse = False) :
1767
- for unit in ["B", "KiB", "MiB", "GiB", "TiB"]:
1802
+ for unit in HUMANSIZE_UNITS:
1768
1803
  if sz < 1024:
1769
1804
  break
1770
1805
 
@@ -1785,12 +1820,7 @@ def unhumanize(sz ) :
1785
1820
  pass
1786
1821
 
1787
1822
  mc = sz[-1:].lower()
1788
- mi = {
1789
- "k": 1024,
1790
- "m": 1024 * 1024,
1791
- "g": 1024 * 1024 * 1024,
1792
- "t": 1024 * 1024 * 1024 * 1024,
1793
- }.get(mc, 1)
1823
+ mi = UNHUMANIZE_UNITS.get(mc, 1)
1794
1824
  return int(float(sz[:-1]) * mi)
1795
1825
 
1796
1826
 
@@ -2473,6 +2503,7 @@ def sendfile_py(
2473
2503
  s ,
2474
2504
  bufsz ,
2475
2505
  slp ,
2506
+ use_poll ,
2476
2507
  ) :
2477
2508
  remains = upper - lower
2478
2509
  f.seek(lower)
@@ -2501,22 +2532,31 @@ def sendfile_kern(
2501
2532
  s ,
2502
2533
  bufsz ,
2503
2534
  slp ,
2535
+ use_poll ,
2504
2536
  ) :
2505
2537
  out_fd = s.fileno()
2506
2538
  in_fd = f.fileno()
2507
2539
  ofs = lower
2508
2540
  stuck = 0.0
2541
+ if use_poll:
2542
+ poll = select.poll()
2543
+ poll.register(out_fd, select.POLLOUT)
2544
+
2509
2545
  while ofs < upper:
2510
2546
  stuck = stuck or time.time()
2511
2547
  try:
2512
2548
  req = min(2 ** 30, upper - ofs)
2513
- select.select([], [out_fd], [], 10)
2549
+ if use_poll:
2550
+ poll.poll(10000)
2551
+ else:
2552
+ select.select([], [out_fd], [], 10)
2514
2553
  n = os.sendfile(out_fd, in_fd, ofs, req)
2515
2554
  stuck = 0
2516
2555
  except OSError as ex:
2517
2556
  # client stopped reading; do another select
2518
2557
  d = time.time() - stuck
2519
2558
  if d < 3600 and ex.errno == errno.EWOULDBLOCK:
2559
+ time.sleep(0.02)
2520
2560
  continue
2521
2561
 
2522
2562
  n = 0
@@ -2660,7 +2700,7 @@ def unescape_cookie(orig ) :
2660
2700
 
2661
2701
  def guess_mime(url , fallback = "application/octet-stream") :
2662
2702
  try:
2663
- _, ext = url.rsplit(".", 1)
2703
+ ext = url.rsplit(".", 1)[1].lower()
2664
2704
  except:
2665
2705
  return fallback
2666
2706
 
copyparty/web/a/u2c.py CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env python3
2
2
  from __future__ import print_function, unicode_literals
3
3
 
4
- S_VERSION = "1.16"
5
- S_BUILD_DT = "2024-04-20"
4
+ S_VERSION = "1.18"
5
+ S_BUILD_DT = "2024-06-01"
6
6
 
7
7
  """
8
8
  u2c.py: upload to copyparty
@@ -79,12 +79,21 @@ req_ses = requests.Session()
79
79
 
80
80
 
81
81
  class Daemon(threading.Thread):
82
- def __init__(self, target, name=None, a=None):
83
- # type: (Any, Any, Any) -> None
84
- threading.Thread.__init__(self, target=target, args=a or (), name=name)
82
+ def __init__(self, target, name = None, a = None):
83
+ threading.Thread.__init__(self, name=name)
84
+ self.a = a or ()
85
+ self.fun = target
85
86
  self.daemon = True
86
87
  self.start()
87
88
 
89
+ def run(self):
90
+ try:
91
+ signal.pthread_sigmask(signal.SIG_BLOCK, [signal.SIGINT, signal.SIGTERM])
92
+ except:
93
+ pass
94
+
95
+ self.fun(*self.a)
96
+
88
97
 
89
98
  class File(object):
90
99
  """an up2k upload task; represents a single file"""
@@ -1135,7 +1144,7 @@ source file/folder selection uses rsync syntax, meaning that:
1135
1144
  ap.add_argument("url", type=unicode, help="server url, including destination folder")
1136
1145
  ap.add_argument("files", type=unicode, nargs="+", help="files and/or folders to process")
1137
1146
  ap.add_argument("-v", action="store_true", help="verbose")
1138
- ap.add_argument("-a", metavar="PASSWORD", help="password or $filepath")
1147
+ ap.add_argument("-a", metavar="PASSWD", help="password or $filepath")
1139
1148
  ap.add_argument("-s", action="store_true", help="file-search (disables upload)")
1140
1149
  ap.add_argument("-x", type=unicode, metavar="REGEX", default="", help="skip file if filesystem-abspath matches REGEX, example: '.*/\\.hist/.*'")
1141
1150
  ap.add_argument("--ok", action="store_true", help="continue even if some local files are inaccessible")
@@ -1153,8 +1162,8 @@ source file/folder selection uses rsync syntax, meaning that:
1153
1162
  ap.add_argument("--drd", action="store_true", help="delete remote files during upload instead of afterwards; reduces peak disk space usage, but will reupload instead of detecting renames")
1154
1163
 
1155
1164
  ap = app.add_argument_group("performance tweaks")
1156
- ap.add_argument("-j", type=int, metavar="THREADS", default=4, help="parallel connections")
1157
- ap.add_argument("-J", type=int, metavar="THREADS", default=hcores, help="num cpu-cores to use for hashing; set 0 or 1 for single-core hashing")
1165
+ ap.add_argument("-j", type=int, metavar="CONNS", default=2, help="parallel connections")
1166
+ ap.add_argument("-J", type=int, metavar="CORES", default=hcores, help="num cpu-cores to use for hashing; set 0 or 1 for single-core hashing")
1158
1167
  ap.add_argument("-nh", action="store_true", help="disable hashing while uploading")
1159
1168
  ap.add_argument("-ns", action="store_true", help="no status panel (for slow consoles and macos)")
1160
1169
  ap.add_argument("--cd", type=float, metavar="SEC", default=5, help="delay before reattempting a failed handshake/upload")
@@ -1162,7 +1171,7 @@ source file/folder selection uses rsync syntax, meaning that:
1162
1171
  ap.add_argument("-z", action="store_true", help="ZOOMIN' (skip uploading files if they exist at the destination with the ~same last-modified timestamp, so same as yolo / turbo with date-chk but even faster)")
1163
1172
 
1164
1173
  ap = app.add_argument_group("tls")
1165
- ap.add_argument("-te", metavar="PEM_FILE", help="certificate to expect/verify")
1174
+ ap.add_argument("-te", metavar="PATH", help="path to ca.pem or cert.pem to expect/verify")
1166
1175
  ap.add_argument("-td", action="store_true", help="disable certificate check")
1167
1176
  # fmt: on
1168
1177
 
@@ -1199,6 +1208,14 @@ source file/folder selection uses rsync syntax, meaning that:
1199
1208
  ar.url = ar.url.rstrip("/") + "/"
1200
1209
  if "://" not in ar.url:
1201
1210
  ar.url = "http://" + ar.url
1211
+
1212
+ if "https://" in ar.url.lower():
1213
+ try:
1214
+ import ssl, zipfile
1215
+ except:
1216
+ t = "ERROR: https is not available for some reason; please use http"
1217
+ print("\n\n %s\n\n" % (t,))
1218
+ raise
1202
1219
 
1203
1220
  if ar.a and ar.a.startswith("$"):
1204
1221
  fn = ar.a[1:]
Binary file
Binary file
Binary file
Binary file
copyparty/web/md2.js.gz CHANGED
Binary file
Binary file
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: copyparty
3
- Version: 1.13.1
3
+ Version: 1.13.3
4
4
  Summary: Portable file server with accelerated resumable uploads, deduplication, WebDAV, FTP, zeroconf, media indexer, video thumbnails, audio transcoding, and write-only folders
5
5
  Author-email: ed <copyparty@ocv.me>
6
6
  License: MIT
@@ -284,6 +284,7 @@ also see [comparison to similar software](./docs/versus.md)
284
284
  * ☑ ...of videos using FFmpeg
285
285
  * ☑ ...of audio (spectrograms) using FFmpeg
286
286
  * ☑ cache eviction (max-age; maybe max-size eventually)
287
+ * ☑ multilingual UI (english, norwegian, [add your own](./docs/rice/#translations)))
287
288
  * ☑ SPA (browse while uploading)
288
289
  * server indexing
289
290
  * ☑ [locate files by contents](#file-search)
@@ -466,7 +467,7 @@ configuring accounts/volumes with arguments:
466
467
  `-v .::r,usr1,usr2:rw,usr3,usr4` = usr1/2 read-only, 3/4 read-write
467
468
 
468
469
  permissions:
469
- * `r` (read): browse folder contents, download files, download as zip/tar
470
+ * `r` (read): browse folder contents, download files, download as zip/tar, see filekeys/dirkeys
470
471
  * `w` (write): upload files, move files *into* this folder
471
472
  * `m` (move): move files/folders *from* this folder
472
473
  * `d` (delete): delete files/folders
@@ -668,7 +669,7 @@ you can also zip a selection of files or folders by clicking them in the browser
668
669
 
669
670
  cool trick: download a folder by appending url-params `?tar&opus` or `?tar&mp3` to transcode all audio files (except aac|m4a|mp3|ogg|opus|wma) to opus/mp3 before they're added to the archive
670
671
  * super useful if you're 5 minutes away from takeoff and realize you don't have any music on your phone but your server only has flac files and downloading those will burn through all your data + there wouldn't be enough time anyways
671
- * and url-params `&j` / `&w` produce jpeg/webm thumbnails/spectrograms instead of the original audio/video/images
672
+ * and url-params `&j` / `&w` produce jpeg/webm thumbnails/spectrograms instead of the original audio/video/images (`&p` for audio waveforms)
672
673
  * can also be used to pregenerate thumbnails; combine with `--th-maxage=9999999` or `--th-clean=0`
673
674
 
674
675
 
@@ -964,6 +965,8 @@ using arguments or config files, or a mix of both:
964
965
 
965
966
  **NB:** as humongous as this readme is, there is also a lot of undocumented features. Run copyparty with `--help` to see all available global options; all of those can be used in the `[global]` section of config files, and everything listed in `--help-flags` can be used in volumes as volflags.
966
967
  * if running in docker/podman, try this: `docker run --rm -it copyparty/ac --help`
968
+ * or see this (probably outdated): https://ocv.me/copyparty/helptext.html
969
+ * or if you prefer plaintext, https://ocv.me/copyparty/helptext.txt
967
970
 
968
971
 
969
972
  ## zeroconf
@@ -1140,7 +1143,7 @@ tweaking the ui
1140
1143
  * to sort in music order (album, track, artist, title) with filename as fallback, you could `--sort tags/Cirle,tags/.tn,tags/Artist,tags/Title,href`
1141
1144
  * to sort by upload date, first enable showing the upload date in the listing with `-e2d -mte +.up_at` and then `--sort tags/.up_at`
1142
1145
 
1143
- see [./docs/rice](./docs/rice) for more, including how to add stuff (css/`<meta>`/...) to the html `<head>` tag
1146
+ see [./docs/rice](./docs/rice) for more, including how to add stuff (css/`<meta>`/...) to the html `<head>` tag, or to add your own translation
1144
1147
 
1145
1148
 
1146
1149
  ## opengraph