Compare commits

...

37 Commits

Author SHA1 Message Date
ed
95ea6d5f78 v0.12.11 2021-08-06 00:53:44 +02:00
ed
7d290f6b8f fix volflag syntax in examples 2021-08-06 00:50:29 +02:00
ed
9db617ed5a new mtp: media-hash 2021-08-06 00:49:42 +02:00
ed
514456940a tooltips, examples, fwd ng in lpad 2021-08-05 23:56:09 +02:00
ed
33feefd9cd sup merge conflict 2021-08-05 23:14:19 +02:00
ed
65e14cf348 batch-rename: add functions and presets 2021-08-05 23:11:06 +02:00
ed
1d61bcc4f3 every time 2021-08-05 21:56:52 +02:00
ed
c38bbaca3c mention batch-rename in readme 2021-08-05 21:53:51 +02:00
ed
246d245ebc make it better 2021-08-05 21:53:08 +02:00
ed
f269a710e2 suspiciously working first attempt at batch-rename 2021-08-05 20:49:49 +02:00
ed
051998429c fix argv compat on windows paths 2021-08-05 20:46:08 +02:00
ed
432cdd640f video-thumbs: take first video stream + better errors 2021-08-05 20:44:04 +02:00
ed
9ed9b0964e nice race 2021-08-03 22:53:13 +00:00
ed
6a97b3526d why was that there 2021-08-03 21:16:26 +00:00
ed
451d757996 fix renaming single symlinks 2021-08-03 20:12:51 +02:00
ed
f9e9eba3b1 sfx-repack: fix no-fnt, no-dd 2021-08-03 20:12:21 +02:00
ed
2a9a6aebd9 systemd fun 2021-08-03 09:22:16 +02:00
ed
adbb6c449e v0.12.10 2021-08-02 00:49:31 +02:00
ed
3993605324 add -mth (deafult-hidden columns) 2021-08-02 00:47:07 +02:00
ed
0ae574ec2c better mutagen codec detection 2021-08-02 00:40:40 +02:00
ed
c56ded828c v0.12.9 2021-08-01 00:40:15 +02:00
ed
02c7061945 v0.12.8 2021-08-01 00:17:05 +02:00
ed
9209e44cd3 heh 2021-08-01 00:08:50 +02:00
ed
ebed37394e better rename ui 2021-08-01 00:04:53 +02:00
ed
4c7a2a7ec3 uridec alerts 2021-07-31 22:05:31 +02:00
ed
0a25a88a34 add mojibake fixer 2021-07-31 14:31:39 +02:00
ed
6aa9025347 v0.12.7 2021-07-31 13:21:43 +02:00
ed
a918cc67eb only drop tags when its safe 2021-07-31 13:19:02 +02:00
ed
08f4695283 v0.12.6 2021-07-31 12:38:53 +02:00
ed
44e76d5eeb optimize make-sfx 2021-07-31 12:38:17 +02:00
ed
cfa36fd279 phone-friendly toast positioning 2021-07-31 10:56:03 +02:00
ed
3d4166e006 dont thumbnail thumbnails 2021-07-31 10:51:18 +02:00
ed
07bac1c592 add option to show dotfiles 2021-07-31 10:44:35 +02:00
ed
755f2ce1ba more url encoding fun 2021-07-31 10:24:34 +02:00
ed
cca2844deb fix mode display for move 2021-07-31 07:19:10 +00:00
ed
24a2f760b7 v0.12.5 2021-07-30 19:28:14 +02:00
ed
79bbd8fe38 systemd: line-buffered logging 2021-07-30 10:39:46 +02:00
25 changed files with 800 additions and 146 deletions

View File

@@ -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) 📷 **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 ## readme toc
* top * top
@@ -41,6 +36,7 @@ this is the readme for v0.12 which has a different expression for volume permiss
* [uploading](#uploading) * [uploading](#uploading)
* [file-search](#file-search) * [file-search](#file-search)
* [file manager](#file-manager) * [file manager](#file-manager)
* [batch rename](#batch-rename)
* [markdown viewer](#markdown-viewer) * [markdown viewer](#markdown-viewer)
* [other tricks](#other-tricks) * [other tricks](#other-tricks)
* [searching](#searching) * [searching](#searching)
@@ -135,12 +131,13 @@ summary: all planned features work! now please enjoy the bloatening
* ☑ FUSE client (read-only) * ☑ FUSE client (read-only)
* browser * browser
* ☑ navpane (directory tree sidebar) * ☑ navpane (directory tree sidebar)
* ☑ file manager (cut/paste, delete, batch-rename)
* ☑ audio player (with OS media controls) * ☑ audio player (with OS media controls)
* ☑ image gallery with webm player
* ☑ thumbnails * ☑ thumbnails
* ☑ ...of images using Pillow * ☑ ...of images using Pillow
* ☑ ...of videos using FFmpeg * ☑ ...of videos using FFmpeg
* ☑ cache eviction (max-age; maybe max-size eventually) * ☑ cache eviction (max-age; maybe max-size eventually)
* ☑ image gallery with webm player
* ☑ SPA (browse while uploading) * ☑ SPA (browse while uploading)
* if you use the navpane to navigate, not folders in the file list * if you use the navpane to navigate, not folders in the file list
* server indexing * server indexing
@@ -181,7 +178,7 @@ small collection of user feedback
* this is an msys2 bug, the regular windows edition of python is fine * 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 * 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 # accounts and volumes
@@ -373,6 +370,51 @@ 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) 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 ## markdown viewer
![copyparty-md-read-fs8](https://user-images.githubusercontent.com/241032/115978057-66419080-a57d-11eb-8539-d2be843991aa.png) ![copyparty-md-read-fs8](https://user-images.githubusercontent.com/241032/115978057-66419080-a57d-11eb-8539-d2be843991aa.png)
@@ -415,9 +457,9 @@ through arguments:
* `-e2tsr` deletes all existing tags, does a full reindex * `-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: 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:c,e2dsa:c,e2tsr` does a full reindex of everything on startup
* `-v ~/music::r:cd2d` disables **all** indexing, even if any `-e2*` are on * `-v ~/music::r:c,d2d` 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,d2t` disables all `-e2t*` (tags), does not affect `-e2d*`
note: 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 * `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 +478,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 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: 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: note:
* markdown edits are always stored in a local `.hist` subdirectory * markdown edits are always stored in a local `.hist` subdirectory
@@ -447,10 +489,12 @@ note:
## metadata from audio files ## 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: `-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 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` `-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 tags that start with a `.` such as `.bpm` and `.dur`(ation) indicate numeric value
@@ -471,7 +515,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 .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,`) * `-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) *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)

View File

@@ -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-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 * [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 # dependencies
@@ -18,7 +19,10 @@ run [`install-deps.sh`](install-deps.sh) to build/install most dependencies requ
# usage from copyparty # 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 * `f,` makes the detected value replace any existing values
* the `.` in `.bpm` indicates numeric value * 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 ## usage with volume-flags
instead of affecting all volumes, you can set the options for just one volume like so: 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
View 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()

View File

@@ -13,6 +13,13 @@
# But note that journalctl will get the timestamps wrong due to # But note that journalctl will get the timestamps wrong due to
# python disabling line-buffering, so messages are out-of-order: # python disabling line-buffering, so messages are out-of-order:
# https://user-images.githubusercontent.com/241032/126040249-cb535cc7-c599-4931-a796-a5d9af691bad.png # 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 `/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] [Unit]
Description=copyparty file server Description=copyparty file server

View File

@@ -24,6 +24,7 @@ from .__init__ import E, WINDOWS, VT100, PY2, unicode
from .__version__ import S_VERSION, S_BUILD_DT, CODENAME from .__version__ import S_VERSION, S_BUILD_DT, CODENAME
from .svchub import SvcHub from .svchub import SvcHub
from .util import py_desc, align_tab, IMPLICATIONS from .util import py_desc, align_tab, IMPLICATIONS
from .authsrv import re_vol
HAVE_SSL = True HAVE_SSL = True
try: try:
@@ -326,7 +327,7 @@ def run_argparse(argv, formatter):
ap2.add_argument("-e2d", action="store_true", help="enable up2k database") 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("-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("-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("--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-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)") 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("--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("-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.)", 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.add_argument("-mtp", metavar="M=[f,]bin", type=u, action="append", help="read tag M using bin")
ap2 = ap.add_argument_group('appearance options') ap2 = ap.add_argument_group('appearance options')
@@ -396,10 +399,16 @@ def main(argv=None):
nstrs = [] nstrs = []
anymod = False anymod = False
for ostr in al.v or []: 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 mod = False
oa = ostr.split(":") for opt in perms.split(":"):
na = oa[:2]
for opt in oa[2:]:
if re.match("c[^,]", opt): if re.match("c[^,]", opt):
mod = True mod = True
na.append("c," + opt[1:]) na.append("c," + opt[1:])

View File

@@ -1,8 +1,8 @@
# coding: utf-8 # coding: utf-8
VERSION = (0, 12, 4) VERSION = (0, 12, 11)
CODENAME = "fil\033[33med" CODENAME = "fil\033[33med"
BUILD_DT = (2021, 7, 30) BUILD_DT = (2021, 8, 6)
S_VERSION = ".".join(map(str, VERSION)) S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)

View File

@@ -310,6 +310,12 @@ class VFS(object):
yield f yield f
if WINDOWS:
re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):(.*)$")
else:
re_vol = re.compile(r"^([^:]*):([^:]*):(.*)$")
class AuthSrv(object): class AuthSrv(object):
"""verifies users against given paths""" """verifies users against given paths"""
@@ -319,11 +325,6 @@ class AuthSrv(object):
self.warn_anonwrite = warn_anonwrite self.warn_anonwrite = warn_anonwrite
self.line_ctr = 0 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.mutex = threading.Lock()
self.reload() self.reload()
@@ -453,7 +454,7 @@ class AuthSrv(object):
# list of src:dst:permset:permset:... # list of src:dst:permset:permset:...
# permset is <rwmd>[,username][,username] or <c>,<flag>[=args] # permset is <rwmd>[,username][,username] or <c>,<flag>[=args]
for v_str in self.args.v: for v_str in self.args.v:
m = self.re_vol.match(v_str) m = re_vol.match(v_str)
if not m: if not m:
raise Exception("invalid -v argument: [{}]".format(v_str)) raise Exception("invalid -v argument: [{}]".format(v_str))
@@ -624,9 +625,11 @@ class AuthSrv(object):
if k1 in vol.flags: if k1 in vol.flags:
vol.flags[k2] = True vol.flags[k2] = True
# default tag-list if unset # default tag cfgs if unset
if "mte" not in vol.flags: if "mte" not in vol.flags:
vol.flags["mte"] = self.args.mte 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 # append parsers from argv to volume-flags
self._read_volflag(vol.flags, "mtp", self.args.mtp, True) self._read_volflag(vol.flags, "mtp", self.args.mtp, True)

View File

@@ -1,6 +1,5 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
from copyparty.authsrv import AuthSrv
import sys import sys
import signal import signal
@@ -9,6 +8,7 @@ import threading
from .broker_util import ExceptionalQueue from .broker_util import ExceptionalQueue
from .httpsrv import HttpSrv from .httpsrv import HttpSrv
from .util import FAKE_MP from .util import FAKE_MP
from copyparty.authsrv import AuthSrv
class MpWorker(object): class MpWorker(object):

View File

@@ -182,7 +182,7 @@ class HttpCli(object):
self.uparam = uparam self.uparam = uparam
self.cookies = cookies self.cookies = cookies
self.vpath = unquotep(vpath) self.vpath = unquotep(vpath) # not query, so + means +
pwd = uparam.get("pw") pwd = uparam.get("pw")
self.uname = self.asrv.iacct.get(pwd, "*") self.uname = self.asrv.iacct.get(pwd, "*")
@@ -1310,11 +1310,9 @@ class HttpCli(object):
else: else:
fn = self.headers.get("host", "hey") fn = self.headers.get("host", "hey")
afn = "".join( safe = (string.ascii_letters + string.digits).replace("%", "")
[x if x in (string.ascii_letters + string.digits) else "_" for x in fn] afn = "".join([x if x in safe.replace('"', "") else "_" for x in fn])
) bascii = unicode(safe).encode("utf-8")
bascii = unicode(string.ascii_letters + string.digits).encode("utf-8")
ufn = fn.encode("utf-8", "xmlcharrefreplace") ufn = fn.encode("utf-8", "xmlcharrefreplace")
if PY2: if PY2:
ufn = [unicode(x) if x in bascii else "%{:02x}".format(ord(x)) for x in ufn] ufn = [unicode(x) if x in bascii else "%{:02x}".format(ord(x)) for x in ufn]
@@ -1329,6 +1327,7 @@ class HttpCli(object):
cdis = "attachment; filename=\"{}.{}\"; filename*=UTF-8''{}.{}" cdis = "attachment; filename=\"{}.{}\"; filename*=UTF-8''{}.{}"
cdis = cdis.format(afn, fmt, ufn, fmt) cdis = cdis.format(afn, fmt, ufn, fmt)
self.log(cdis)
self.send_headers(None, mime=mime, headers={"Content-Disposition": cdis}) self.send_headers(None, mime=mime, headers={"Content-Disposition": cdis})
fgen = vn.zipgen(rem, items, self.uname, dots, not self.args.no_scandir) fgen = vn.zipgen(rem, items, self.uname, dots, not self.args.no_scandir)
@@ -1621,6 +1620,9 @@ class HttpCli(object):
if not dst: if not dst:
raise Pebkac(400, "need dst vpath") raise Pebkac(400, "need dst vpath")
# x-www-form-urlencoded (url query part) uses
# either + or %20 for 0x20 so handle both
dst = unquotep(dst.replace("+", " "))
x = self.conn.hsrv.broker.put( x = self.conn.hsrv.broker.put(
True, "up2k.handle_mv", self.uname, self.vpath, dst True, "up2k.handle_mv", self.uname, self.vpath, dst
) )
@@ -1753,7 +1755,7 @@ class HttpCli(object):
"acct": self.uname, "acct": self.uname,
"perms": json.dumps(perms), "perms": json.dumps(perms),
"taglist": [], "taglist": [],
"tag_order": [], "def_hcols": [],
"have_up2k_idx": ("e2d" in vn.flags), "have_up2k_idx": ("e2d" in vn.flags),
"have_tags_idx": ("e2t" in vn.flags), "have_tags_idx": ("e2t" in vn.flags),
"have_mv": (not self.args.no_mv), "have_mv": (not self.args.no_mv),
@@ -1950,8 +1952,8 @@ class HttpCli(object):
j2a["logues"] = logues j2a["logues"] = logues
j2a["taglist"] = taglist j2a["taglist"] = taglist
if "mte" in vn.flags: if "mth" in vn.flags:
j2a["tag_order"] = json.dumps(vn.flags["mte"].split(",")) j2a["def_hcols"] = vn.flags["mth"].split(",")
if self.args.css_browser: if self.args.css_browser:
j2a["css"] = self.args.css_browser j2a["css"] = self.args.css_browser

View File

@@ -174,25 +174,26 @@ class HttpSrv(object):
now = time.time() now = time.time()
if now - (self.tp_time or now) > 300: 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 self.tp_q = None
if self.tp_q:
self.tp_q.put((sck, addr))
with self.mutex: with self.mutex:
self.ncli += 1 self.ncli += 1
if self.tp_q:
self.tp_time = self.tp_time or now 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: if self.tp_nthr < self.ncli + 4:
self.start_threads(8) self.start_threads(8)
self.tp_q.put((sck, addr))
return return
if not self.args.no_htp: 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" 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) self.log(self.name, m, 1)
with self.mutex:
self.ncli += 1
thr = threading.Thread( thr = threading.Thread(
target=self.thr_client, target=self.thr_client,
args=(sck, addr), args=(sck, addr),

View File

@@ -433,6 +433,14 @@ class MTag(object):
]: ]:
try: try:
v = getattr(md.info, attr) v = getattr(md.info, attr)
except:
if k != "ac":
continue
try:
v = str(md.info).split(".")[1]
if v.startswith("ogg"):
v = v[3:]
except: except:
continue continue

View File

@@ -26,6 +26,9 @@ class ThumbCli(object):
if is_vid and self.args.no_vthumb: if is_vid and self.args.no_vthumb:
return None return None
if rem.startswith(".hist/th/") and rem.split(".")[-1] in ["webp", "jpg"]:
return os.path.join(ptop, rem)
if fmt == "j" and self.args.th_no_jpg: if fmt == "j" and self.args.th_no_jpg:
fmt = "w" fmt = "w"

View File

@@ -205,8 +205,8 @@ class ThumbSrv(object):
try: try:
fun(abspath, tpath) fun(abspath, tpath)
except: except:
msg = "{} failed on {}\n{}" msg = "{} could not create thumbnail of {}\n{}"
self.log(msg.format(fun.__name__, abspath, min_ex()), 3) self.log(msg.format(fun.__name__, abspath, min_ex()), "1;30")
with open(tpath, "wb") as _: with open(tpath, "wb") as _:
pass pass
@@ -286,8 +286,9 @@ class ThumbSrv(object):
cmd += seek cmd += seek
cmd += [ cmd += [
b"-i", fsenc(abspath), b"-i", fsenc(abspath),
b"-map", b"0:v:0",
b"-vf", scale, b"-vf", scale,
b"-vframes", b"1", b"-frames:v", b"1",
] ]
# fmt: on # fmt: on
@@ -308,8 +309,9 @@ class ThumbSrv(object):
ret, sout, serr = runcmd(cmd) ret, sout, serr = runcmd(cmd)
if ret != 0: if ret != 0:
msg = ["ff: {}".format(x) for x in serr.split("\n")] m = "FFmpeg failed (probably a corrupt video file):\n"
self.log("FFmpeg failed:\n" + "\n".join(msg), c="1;30") 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])) raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
def poke(self, tdir): def poke(self, tdir):

View File

@@ -1405,7 +1405,7 @@ class Up2k(object):
try: try:
ptop = dbv.realpath ptop = dbv.realpath
cur, wark, _, _, _, _ = self._find_from_vpath(ptop, volpath) cur, wark, _, _, _, _ = self._find_from_vpath(ptop, volpath)
self._forget_file(ptop, volpath, cur, wark) self._forget_file(ptop, volpath, cur, wark, True)
finally: finally:
cur.connection.commit() cur.connection.commit()
@@ -1422,8 +1422,8 @@ class Up2k(object):
if not srem: if not srem:
raise Pebkac(400, "mv: cannot move a mountpoint") raise Pebkac(400, "mv: cannot move a mountpoint")
st = bos.stat(sabs) st = bos.lstat(sabs)
if stat.S_ISREG(st.st_mode): if stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode):
return self._mv_file(uname, svp, dvp) return self._mv_file(uname, svp, dvp)
jail = svn.get_dbv(srem)[0] jail = svn.get_dbv(srem)[0]
@@ -1491,10 +1491,10 @@ class Up2k(object):
fsize = st.st_size fsize = st.st_size
if w: if w:
if c2: if c2 and c2 != c1:
self._copy_tags(c1, c2, w) self._copy_tags(c1, c2, w)
self._forget_file(svn.realpath, srem, c1, w) self._forget_file(svn.realpath, srem, c1, w, c1 != c2)
self._relink(w, svn.realpath, srem, dabs) self._relink(w, svn.realpath, srem, dabs)
c1.connection.commit() c1.connection.commit()
@@ -1535,14 +1535,16 @@ class Up2k(object):
return cur, wark, ftime, fsize, ip, at return cur, wark, ftime, fsize, ip, at
return cur, None, None, None, None, None return cur, None, None, None, None, None
def _forget_file(self, ptop, vrem, cur, wark): def _forget_file(self, ptop, vrem, cur, wark, drop_tags):
"""forgets file in db, fixes symlinks, does not delete""" """forgets file in db, fixes symlinks, does not delete"""
srd, sfn = vsplit(vrem) srd, sfn = vsplit(vrem)
self.log("forgetting {}".format(vrem)) self.log("forgetting {}".format(vrem))
if wark: if wark:
self.log("found {} in db".format(wark)) self.log("found {} in db".format(wark))
self._relink(wark, ptop, vrem, None) if self._relink(wark, ptop, vrem, None):
drop_tags = False
if drop_tags:
q = "delete from mt where w=?" q = "delete from mt where w=?"
cur.execute(q, (wark[:16],)) cur.execute(q, (wark[:16],))
self.db_rm(cur, srd, sfn) self.db_rm(cur, srd, sfn)
@@ -1581,7 +1583,7 @@ class Up2k(object):
self.log("found {} dupe: [{}] {}".format(wark, ptop, dvrem)) self.log("found {} dupe: [{}] {}".format(wark, ptop, dvrem))
if not dupes: if not dupes:
return return 0
full = {} full = {}
links = {} links = {}
@@ -1618,6 +1620,8 @@ class Up2k(object):
self._symlink(dabs, alink, False) self._symlink(dabs, alink, False)
return len(full) + len(links)
def _get_wark(self, cj): def _get_wark(self, cj):
if len(cj["name"]) > 1024 or len(cj["hash"]) > 512 * 1024: # 16TiB if len(cj["name"]) > 1024 or len(cj["hash"]) > 512 * 1024: # 16TiB
raise Pebkac(400, "name or numchunks not according to spec") raise Pebkac(400, "name or numchunks not according to spec")

View File

@@ -22,9 +22,6 @@ html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
body {
padding-bottom: 5em;
}
pre, code, tt { pre, code, tt {
font-family: monospace, monospace; font-family: monospace, monospace;
} }
@@ -49,7 +46,7 @@ pre, code, tt {
transition: opacity 0.14s, height 0.14s, padding 0.14s; transition: opacity 0.14s, height 0.14s, padding 0.14s;
} }
#toast { #toast {
top: 1.4em; bottom: 5em;
right: -1em; right: -1em;
line-height: 1.5em; line-height: 1.5em;
padding: 1em 1.3em; padding: 1em 1.3em;
@@ -810,6 +807,7 @@ input.eq_gain {
#wrap { #wrap {
margin-top: 2em; margin-top: 2em;
min-height: 90vh; min-height: 90vh;
padding-bottom: 5em;
} }
#tree { #tree {
display: none; display: none;
@@ -1068,10 +1066,68 @@ html.light #ggrid a:hover {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
#rui {
position: fixed;
top: 0;
left: 0;
width: calc(100% - 2em);
height: auto;
overflow: auto;
max-height: calc(100% - 2em);
border-bottom: .5em solid #999;
background: #333;
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+td {
padding: .2em 0 .2em .5em;
}
#rn_vadv input {
font-family: monospace, monospace;
}
#rui td+td,
#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;
border: 1px solid #777;
padding: .2em .25em;
}
#rui h1 {
margin: 0 0 .3em 0;
padding: 0;
font-size: 1.5em;
}
#pvol, #pvol,
#barbuf, #barbuf,
#barpos, #barpos,
#u2conf label, #u2conf label,
#rui label,
#ops { #ops {
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;

View File

@@ -125,7 +125,7 @@
<script> <script>
var acct = "{{ acct }}", var acct = "{{ acct }}",
perms = {{ perms }}, perms = {{ perms }},
tag_order_cfg = {{ tag_order }}, def_hcols = {{ def_hcols|tojson }},
have_up2k_idx = {{ have_up2k_idx|tojson }}, have_up2k_idx = {{ have_up2k_idx|tojson }},
have_tags_idx = {{ have_tags_idx|tojson }}, have_tags_idx = {{ have_tags_idx|tojson }},
have_mv = {{ have_mv|tojson }}, have_mv = {{ have_mv|tojson }},

View File

@@ -133,6 +133,7 @@ ebi('op_cfg').innerHTML = (
' <div>\n' + ' <div>\n' +
' <a id="tooltips" class="tgl btn" href="#" tt="◔ ◡ ◔"> tooltips</a>\n' + ' <a id="tooltips" class="tgl btn" href="#" tt="◔ ◡ ◔"> tooltips</a>\n' +
' <a id="lightmode" class="tgl btn" href="#">☀️ lightmode</a>\n' + ' <a id="lightmode" class="tgl btn" href="#">☀️ lightmode</a>\n' +
' <a id="dotfiles" class="tgl btn" href="#" tt="show hidden files (if server permits)">dotfiles</a>\n' +
' <a id="griden" class="tgl btn" href="#" tt="toggle icons or list-view$NHotkey: G">田 the grid</a>\n' + ' <a id="griden" class="tgl btn" href="#" tt="toggle icons or list-view$NHotkey: G">田 the grid</a>\n' +
' <a id="thumbs" class="tgl btn" href="#" tt="in icon view, toggle icons or thumbnails$NHotkey: T">🖼️ thumbs</a>\n' + ' <a id="thumbs" class="tgl btn" href="#" tt="in icon view, toggle icons or thumbnails$NHotkey: T">🖼️ thumbs</a>\n' +
' </div>\n' + ' </div>\n' +
@@ -521,15 +522,14 @@ var mp = new MPlayer();
makeSortable(ebi('files'), mp.read_order.bind(mp)); makeSortable(ebi('files'), mp.read_order.bind(mp));
function get_np() { function ft2dict(tr) {
var th = ebi('files').tHead.rows[0].cells, var th = ebi('files').tHead.rows[0].cells,
tr = QS('#files tr.play').cells,
rv = [], rv = [],
ra = [], ra = [],
rt = {}; rt = {};
for (var a = 1, aa = th.length; a < aa; a++) { for (var a = 1, aa = th.length; a < aa; a++) {
var tv = tr[a].textContent, var tv = tr.cells[a].textContent,
tk = a == 1 ? 'file' : th[a].getAttribute('name').split('/').slice(-1)[0], tk = a == 1 ? 'file' : th[a].getAttribute('name').split('/').slice(-1)[0],
vis = th[a].className.indexOf('min') === -1; vis = th[a].className.indexOf('min') === -1;
@@ -540,6 +540,12 @@ function get_np() {
rt[tk] = tv; rt[tk] = tv;
} }
return [rt, rv, ra]; return [rt, rv, ra];
}
function get_np() {
var tr = QS('#files tr.play');
return ft2dict(tr);
}; };
@@ -1454,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 fileman = (function () {
var bren = ebi('fren'), var bren = ebi('fren'),
bdel = ebi('fdel'), bdel = ebi('fdel'),
@@ -1468,10 +1573,10 @@ var fileman = (function () {
if (r.clip === null) if (r.clip === null)
r.clip = jread('fman_clip', []); r.clip = jread('fman_clip', []);
var sel = msel.getsel(); var nsel = msel.getsel().length;
clmod(bren, 'en', sel.length == 1); clmod(bren, 'en', nsel == 1);
clmod(bdel, 'en', sel.length); clmod(bdel, 'en', nsel);
clmod(bcut, 'en', sel.length); clmod(bcut, 'en', nsel);
clmod(bpst, 'en', r.clip && r.clip.length); clmod(bpst, 'en', r.clip && r.clip.length);
bren.style.display = have_mv && has(perms, 'write') && has(perms, 'move') ? '' : 'none'; bren.style.display = have_mv && has(perms, 'write') && has(perms, 'move') ? '' : 'none';
bdel.style.display = have_del && has(perms, 'delete') ? '' : 'none'; bdel.style.display = have_del && has(perms, 'delete') ? '' : 'none';
@@ -1487,22 +1592,269 @@ var fileman = (function () {
return toast.err(3, 'cannot rename:\nyou do not have “move” permission in this folder'); return toast.err(3, 'cannot rename:\nyou do not have “move” permission in this folder');
var sel = msel.getsel(); var sel = msel.getsel();
if (sel.length !== 1) if (!sel.length)
return toast.err(3, 'select exactly 1 item to rename'); return toast.err(3, 'select at least one item to rename');
var src = sel[0].vp; var f = [],
if (src.endsWith('/')) base = vsplit(sel[0].vp)[0],
src = src.slice(0, -1); mkeys;
var vsp = vsplit(src), for (var a = 0; a < sel.length; a++) {
base = vsp[0], var vp = sel[a].vp;
ofn = vsp[1]; if (vp.endsWith('/'))
vp = vp.slice(0, -1);
var fn = prompt('new filename:', ofn); var vsp = vsplit(vp);
if (!fn || fn == ofn) if (base != vsp[0])
return toast.warn(1, 'rename aborted'); return toast.err(0, 'bug:\n' + base + '\n' + vsp[0]);
var dst = base + fn; 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) {
rui = mknod('div');
rui.setAttribute('id', 'rui');
document.body.appendChild(rui);
}
var html = sel.length > 1 ? ['<div>'] : [
'<div>',
'<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>'
];
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 &lt;code&gt;(1)&lt;/code&gt; and &lt;code&gt;(2)&lt;/code&gt; and so on" /></td></tr>',
'<tr><td>format</td><td><input type="text" id="rn_fmt" tt="inspired by foobar2000:$N&lt;code&gt;(title)&lt;/code&gt; is replaced by song title,$N&lt;code&gt;[(artist) - ](title)&lt;/code&gt; skips the first part if artist is blank$N&lt;code&gt;$lpad((tn),2,0)&lt;/code&gt; 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>');
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);
}
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();
};
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;
}
};
function rn_apply() {
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() { function rename_cb() {
if (this.readyState != XMLHttpRequest.DONE) if (this.readyState != XMLHttpRequest.DONE)
@@ -1513,13 +1865,16 @@ var fileman = (function () {
toast.err(9, 'rename failed:\n' + msg); toast.err(9, 'rename failed:\n' + msg);
return; return;
} }
toast.ok(2, 'rename OK');
treectl.goto(get_evpath()); f.shift().inew.value = '( OK )';
return rn_apply();
} }
var xhr = new XMLHttpRequest(); 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.onreadystatechange = rename_cb;
xhr.send(); xhr.send();
}
}; };
r.delete = function (e) { r.delete = function (e) {
@@ -1611,7 +1966,7 @@ var fileman = (function () {
links = QSA('#files tbody td:nth-child(2) a'); links = QSA('#files tbody td:nth-child(2) a');
for (var a = 0, aa = links.length; a < aa; a++) for (var a = 0, aa = links.length; a < aa; a++)
indir.push(links[a].getAttribute('name')); indir.push(vsplit(links[a].getAttribute('href'))[1]);
for (var a = 0; a < r.clip.length; a++) { for (var a = 0; a < r.clip.length; a++) {
var found = false; var found = false;
@@ -1626,12 +1981,12 @@ var fileman = (function () {
} }
if (exists.length) if (exists.length)
alert('these ' + exists.length + ' items cannot be pasted here (names already exist):\n\n' + exists.join('\n')); alert('these ' + exists.length + ' items cannot be pasted here (names already exist):\n\n' + uricom_adec(exists).join('\n'));
if (!req.length) if (!req.length)
return; return;
if (!confirm('paste these ' + req.length + ' items here?\n\n' + req.join('\n'))) if (!confirm('paste these ' + req.length + ' items here?\n\n' + uricom_adec(req).join('\n')))
return; return;
function paster() { function paster() {
@@ -1644,7 +1999,7 @@ var fileman = (function () {
r.tx(srcdir); r.tx(srcdir);
return; return;
} }
toast.inf(0, 'pasting ' + (req.length + 1) + ' items\n\n' + vp); toast.inf(0, 'pasting ' + (req.length + 1) + ' items\n\n' + uricom_dec(vp)[0]);
var dst = get_evpath() + vp.split('/').slice(-1)[0]; var dst = get_evpath() + vp.split('/').slice(-1)[0];
@@ -2415,6 +2770,7 @@ var treectl = (function () {
prev_atop = null, prev_atop = null,
prev_winh = null, prev_winh = null,
dyn = bcfg_get('dyntree', true), dyn = bcfg_get('dyntree', true),
dots = bcfg_get('dotfiles', false),
treesz = icfg_get('treesz', 16); treesz = icfg_get('treesz', 16);
treesz = Math.min(Math.max(treesz, 4), 50); treesz = Math.min(Math.max(treesz, 4), 50);
@@ -2533,7 +2889,7 @@ var treectl = (function () {
xhr.dst = dst; xhr.dst = dst;
xhr.rst = rst; xhr.rst = rst;
xhr.ts = Date.now(); xhr.ts = Date.now();
xhr.open('GET', dst + '?tree=' + top, true); xhr.open('GET', dst + '?tree=' + top + (dots ? '&dots' : ''), true);
xhr.onreadystatechange = recvtree; xhr.onreadystatechange = recvtree;
xhr.send(); xhr.send();
enspin('#tree'); enspin('#tree');
@@ -2637,7 +2993,7 @@ var treectl = (function () {
xhr.top = url; xhr.top = url;
xhr.hpush = hpush; xhr.hpush = hpush;
xhr.ts = Date.now(); xhr.ts = Date.now();
xhr.open('GET', xhr.top + '?ls', true); xhr.open('GET', xhr.top + '?ls' + (dots ? '&dots' : ''), true);
xhr.onreadystatechange = recvls; xhr.onreadystatechange = recvls;
xhr.send(); xhr.send();
if (hpush) if (hpush)
@@ -2774,6 +3130,13 @@ var treectl = (function () {
return ret; return ret;
} }
function tdots(e) {
ev(e);
dots = !dots;
bcfg_set('dotfiles', dots);
treectl.goto(get_evpath());
}
function dyntree(e) { function dyntree(e) {
ev(e); ev(e);
dyn = !dyn; dyn = !dyn;
@@ -2793,6 +3156,7 @@ var treectl = (function () {
ebi('entree').onclick = treectl.entree; ebi('entree').onclick = treectl.entree;
ebi('detree').onclick = treectl.detree; ebi('detree').onclick = treectl.detree;
ebi('dotfiles').onclick = tdots;
ebi('dyntree').onclick = dyntree; ebi('dyntree').onclick = dyntree;
ebi('twig').onclick = scaletree; ebi('twig').onclick = scaletree;
ebi('twobytwo').onclick = scaletree; ebi('twobytwo').onclick = scaletree;
@@ -2839,7 +3203,7 @@ function apply_perms(newperms) {
var axs = [], var axs = [],
aclass = '>', aclass = '>',
chk = ['read', 'write', 'rename', 'delete']; chk = ['read', 'write', 'move', 'delete'];
for (var a = 0; a < chk.length; a++) for (var a = 0; a < chk.length; a++)
if (has(perms, chk[a])) if (has(perms, chk[a]))
@@ -2955,6 +3319,8 @@ var filecols = (function () {
"q": "quality / bitrate", "q": "quality / bitrate",
"Ac": "audio codec", "Ac": "audio codec",
"Vc": "video codec", "Vc": "video codec",
"Ahash": "audio checksum",
"Vhash": "video checksum",
"Res": "resolution", "Res": "resolution",
"T": "filetype", "T": "filetype",
"aq": "audio quality / bitrate", "aq": "audio quality / bitrate",
@@ -2966,6 +3332,21 @@ var filecols = (function () {
"hz": "sample rate" "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 add_btns = function () {
var ths = QSA('#files th>span'); var ths = QSA('#files th>span');
for (var a = 0, aa = ths.length; a < aa; a++) { for (var a = 0, aa = ths.length; a < aa; a++) {
@@ -3319,13 +3700,11 @@ var msel = (function () {
item.id = links[a].getAttribute('id'); item.id = links[a].getAttribute('id');
item.sel = links[a].closest('tr').classList.contains('sel'); item.sel = links[a].closest('tr').classList.contains('sel');
item.vp = href.indexOf('/') !== -1 ? href : vbase + href; item.vp = href.indexOf('/') !== -1 ? href : vbase + href;
item.name = href.split('/').slice(-1);
r.all.push(item); r.all.push(item);
if (item.sel) if (item.sel)
r.sel.push(item); r.sel.push(item);
links[a].setAttribute('name', item.name);
links[a].closest('tr').setAttribute('tabindex', '0'); links[a].closest('tr').setAttribute('tabindex', '0');
} }
}; };
@@ -3365,10 +3744,15 @@ var msel = (function () {
}; };
ebi('selzip').onclick = function (e) { ebi('selzip').onclick = function (e) {
ev(e); ev(e);
var names = r.getsel(), var sel = r.getsel(),
arg = ebi('selzip').getAttribute('fmt'), arg = ebi('selzip').getAttribute('fmt'),
txt = names.join('\n'), frm = mknod('form'),
frm = mknod('form'); txt = [];
for (var a = 0; a < sel.length; a++)
txt.push(vsplit(sel[a].vp)[1]);
txt = txt.join('\n');
frm.setAttribute('action', '?' + arg); frm.setAttribute('action', '?' + arg);
frm.setAttribute('method', 'post'); frm.setAttribute('method', 'post');

View File

@@ -16,9 +16,6 @@ html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
body {
padding-bottom: 5em;
}
#box { #box {
padding: .5em 1em; padding: .5em 1em;
background: #2c2c2c; background: #2c2c2c;

View File

@@ -398,6 +398,15 @@ function uricom_dec(txt) {
} }
function uricom_adec(arr) {
var ret = [];
for (var a = 0; a < arr.length; a++)
ret.push(uricom_dec(arr[a])[0]);
return ret;
}
function get_evpath() { function get_evpath() {
var ret = document.location.pathname; var ret = document.location.pathname;

View File

@@ -44,7 +44,7 @@ avg() { awk 'function pr(ncsz) {if (nsmp>0) {printf "%3s %s\n", csz, sum/nsmp} c
dirs=("$HOME/vfs/ほげ" "$HOME/vfs/ほげ/ぴよ" "$HOME/vfs/$(printf \\xed\\x91)" "$HOME/vfs/$(printf \\xed\\x91/\\xed\\x92)") dirs=("$HOME/vfs/ほげ" "$HOME/vfs/ほげ/ぴよ" "$HOME/vfs/$(printf \\xed\\x91)" "$HOME/vfs/$(printf \\xed\\x91/\\xed\\x92)")
mkdir -p "${dirs[@]}" mkdir -p "${dirs[@]}"
for dir in "${dirs[@]}"; do for fn in ふが "$(printf \\xed\\x93)" 'qwe,rty;asd fgh+jkl%zxc&vbn <qwe>"rty'"'"'uio&asd&nbsp;fgh'; do echo "$dir" > "$dir/$fn.html"; done; done for dir in "${dirs[@]}"; do for fn in ふが "$(printf \\xed\\x93)" 'qwe,rty;asd fgh+jkl%zxc&vbn <qwe>"rty'"'"'uio&asd&nbsp;fgh'; do echo "$dir" > "$dir/$fn.html"; done; done
# qw er+ty%20ui%%20op<as>df&gh&amp;jk#zx'cv"bn`m=qw*er^ty?ui@op,as.df-gh_jk
## ##
## upload mojibake ## upload mojibake
@@ -79,6 +79,10 @@ command -v gdate && date() { gdate "$@"; }; while true; do t=$(date +%s.%N); (ti
# get all up2k search result URLs # get all up2k search result URLs
var t=[]; var b=document.location.href.split('#')[0].slice(0, -1); document.querySelectorAll('#u2tab .prog a').forEach((x) => {t.push(b+encodeURI(x.getAttribute("href")))}); console.log(t.join("\n")); var t=[]; var b=document.location.href.split('#')[0].slice(0, -1); document.querySelectorAll('#u2tab .prog a').forEach((x) => {t.push(b+encodeURI(x.getAttribute("href")))}); console.log(t.join("\n"));
# rename all selected songs to <leading-track-number> + <Title> + <extension>
var sel=msel.getsel(), ci=find_file_col('Title')[0], re=[]; for (var a=0; a<sel.length; a++) { var url=sel[a].vp, tag=ebi(sel[a].id).closest('tr').querySelectorAll('td')[ci].textContent, name=uricom_dec(vsplit(url)[1])[0], m=/^([0-9]+[\. -]+)?.*(\.[^\.]+$)/.exec(name), name2=(m[1]||'')+tag+m[2], url2=vsplit(url)[0]+uricom_enc(name2,false); if (url!=url2) re.push([url, url2]); }
console.log(JSON.stringify(re, null, ' '));
function f() { if (!re.length) return treectl.goto(get_evpath()); var [u1,u2] = re.shift(); fetch(u1+'?move='+u2).then((rsp) => {if (rsp.ok) f(); }); }; f();
## ##
## bash oneliners ## bash oneliners

View File

@@ -2,6 +2,7 @@
set -e set -e
echo echo
help() { exec cat <<'EOF'
# optional args: # optional args:
# #
@@ -26,6 +27,8 @@ echo
# #
# `no-dd` saves ~2k by removing the mouse cursor # `no-dd` saves ~2k by removing the mouse cursor
EOF
}
# port install gnutar findutils gsed coreutils # port install gnutar findutils gsed coreutils
gtar=$(command -v gtar || command -v gnutar) || true gtar=$(command -v gtar || command -v gnutar) || true
@@ -34,6 +37,7 @@ gtar=$(command -v gtar || command -v gnutar) || true
sed() { gsed "$@"; } sed() { gsed "$@"; }
find() { gfind "$@"; } find() { gfind "$@"; }
sort() { gsort "$@"; } sort() { gsort "$@"; }
sha1sum() { shasum "$@"; }
unexpand() { gunexpand "$@"; } unexpand() { gunexpand "$@"; }
command -v grealpath >/dev/null && command -v grealpath >/dev/null &&
realpath() { grealpath "$@"; } realpath() { grealpath "$@"; }
@@ -72,6 +76,7 @@ while [ ! -z "$1" ]; do
no-cm) no_cm=1 ; ;; no-cm) no_cm=1 ; ;;
no-sh) do_sh= ; ;; no-sh) do_sh= ; ;;
no-py) do_py= ; ;; no-py) do_py= ; ;;
*) help ; ;;
esac esac
shift shift
done done
@@ -81,16 +86,23 @@ tmv() {
mv t "$1" mv t "$1"
} }
stamp=$(
for d in copyparty scripts; do
find $d -type f -printf '%TY-%Tm-%Td %TH:%TM:%TS %p\n'
done | sort | tail -n 1 | sha1sum | cut -c-16
)
rm -rf sfx/* rm -rf sfx/*
mkdir -p sfx build mkdir -p sfx build
cd sfx cd sfx
[ $repack ] && { tmpdir="$(
old="$(
printf '%s\n' "$TMPDIR" /tmp | printf '%s\n' "$TMPDIR" /tmp |
awk '/./ {print; exit}' awk '/./ {print; exit}'
)/pe-copyparty" )"
[ $repack ] && {
old="$tmpdir/pe-copyparty"
echo "repack of files in $old" echo "repack of files in $old"
cp -pR "$old/"*{dep-j2,copyparty} . cp -pR "$old/"*{dep-j2,copyparty} .
} }
@@ -172,12 +184,12 @@ mkdir -p ../dist
sfx_out=../dist/copyparty-sfx sfx_out=../dist/copyparty-sfx
echo cleanup echo cleanup
find .. -name '*.pyc' -delete find -name '*.pyc' -delete
find .. -name __pycache__ -delete find -name __pycache__ -delete
# especially prevent osx from leaking your lan ip (wtf apple) # especially prevent osx from leaking your lan ip (wtf apple)
find .. -type f \( -name .DS_Store -or -name ._.DS_Store \) -delete find -type f \( -name .DS_Store -or -name ._.DS_Store \) -delete
find .. -type f -name ._\* | while IFS= read -r f; do cmp <(printf '\x00\x05\x16') <(head -c 3 -- "$f") && rm -f -- "$f"; done find -type f -name ._\* | while IFS= read -r f; do cmp <(printf '\x00\x05\x16') <(head -c 3 -- "$f") && rm -f -- "$f"; done
echo use smol web deps echo use smol web deps
rm -f copyparty/web/deps/*.full.* copyparty/web/dbg-* copyparty/web/Makefile rm -f copyparty/web/deps/*.full.* copyparty/web/dbg-* copyparty/web/Makefile
@@ -196,19 +208,24 @@ done
rm -rf copyparty/web/mde.* copyparty/web/deps/easymde* rm -rf copyparty/web/mde.* copyparty/web/deps/easymde*
echo h > copyparty/web/mde.html echo h > copyparty/web/mde.html
f=copyparty/web/md.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 ] && { [ $no_fnt ] && {
rm -f copyparty/web/deps/scp.woff2 rm -f copyparty/web/deps/scp.woff2
f=copyparty/web/md.css 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 ] && { [ $no_dd ] && {
rm -rf copyparty/web/dd rm -rf copyparty/web/dd
f=copyparty/web/browser.css 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 ] || [ $repack ] ||
@@ -241,10 +258,9 @@ find | grep -E '\.(js|html)$' | while IFS= read -r f; do
tmv "$f" tmv "$f"
done done
gzres() { gzres() {
command -v pigz && command -v pigz &&
pk='pigz -11 -J 34 -I 100' || pk='pigz -11 -I 256' ||
pk='gzip' pk='gzip'
echo "$pk" echo "$pk"
@@ -254,7 +270,30 @@ find | grep -E '\.(js|css)$' | grep -vF /deps/ | while IFS= read -r f; do
done done
echo echo
} }
zdir="$tmpdir/cpp-mksfx"
[ -e "$zdir/$stamp" ] || rm -rf "$zdir"
mkdir -p "$zdir"
echo a > "$zdir/$stamp"
nf=$(ls -1 "$zdir"/arc.* | wc -l)
[ $nf -ge 2 ] && [ ! $repack ] && use_zdir=1 || use_zdir=
[ $use_zdir ] || {
echo "$nf alts += 1"
gzres gzres
[ $repack ] ||
tar -cf "$zdir/arc.$(date +%s)" copyparty/web/*.gz
}
[ $use_zdir ] && {
arcs=("$zdir"/arc.*)
arc="${arcs[$RANDOM % ${#arcs[@]} ] }"
echo "using $arc"
tar -xf "$arc"
for f in copyparty/web/*.gz; do
rm "${f%.*}"
done
}
echo gen tarlist echo gen tarlist

View File

@@ -124,7 +124,7 @@ def tc1():
arg = "{}:{}:{}".format(pd, ud, p, hp) arg = "{}:{}:{}".format(pd, ud, p, hp)
if hp: if hp:
arg += ":chist=" + hp arg += ":c,hist=" + hp
args += ["-v", arg] args += ["-v", arg]

View File

@@ -65,9 +65,9 @@ def uncomment(fpath):
def main(): def main():
print("uncommenting", end="") print("uncommenting", end="", flush=True)
for f in sys.argv[1:]: for f in sys.argv[1:]:
print(".", end="") print(".", end="", flush=True)
uncomment(f) uncomment(f)
print("k") print("k")

View File

@@ -43,6 +43,7 @@ class Cfg(Namespace):
nih=True, nih=True,
mtp=[], mtp=[],
mte="a", mte="a",
mth="",
hist=None, hist=None,
no_hash=False, no_hash=False,
css_browser=None, css_browser=None,

View File

@@ -21,6 +21,7 @@ class Cfg(Namespace):
ex2 = { ex2 = {
"mtp": [], "mtp": [],
"mte": "a", "mte": "a",
"mth": "",
"hist": None, "hist": None,
"no_hash": False, "no_hash": False,
"css_browser": None, "css_browser": None,