Compare commits

...

68 Commits

Author SHA1 Message Date
ed
7f8d7d6006 v0.9.9 2021-03-21 17:15:47 +01:00
ed
49e4fb7e12 finally time to undefault this 2021-03-21 16:19:45 +01:00
ed
8dbbea473f mtp incoming files too 2021-03-21 15:21:07 +01:00
ed
3d375d5114 assert mtm/mtp is used by mte 2021-03-21 14:15:55 +01:00
ed
f3eae67d97 readme 2021-03-21 09:41:05 +01:00
ed
40c1b19235 detrimental to search results 2021-03-21 08:16:17 +01:00
ed
ccaf0ab159 gj 2021-03-21 07:49:05 +01:00
ed
d07f147423 fixes 2021-03-21 06:00:21 +01:00
ed
f5cb9f92b9 better task recovery on restart 2021-03-21 05:57:24 +01:00
ed
f991f74983 hotkeys for directory traversal 2021-03-21 04:04:30 +01:00
ed
6b3295059e add time markers in player 2021-03-21 03:21:05 +01:00
ed
b18a07ae6b fix file srch 2021-03-21 02:46:40 +01:00
ed
8ab03dabda ok 2021-03-21 02:41:15 +01:00
ed
5e760e35dc togglebutton for tooltips to save iphone users 2021-03-21 02:33:53 +01:00
ed
afbfa04514 fixes 2021-03-21 01:55:12 +01:00
ed
7aace470c5 preserve file refs on sort (crc32 instead of idx) 2021-03-21 01:41:18 +01:00
ed
b4acb24f6a remember sort order 2021-03-20 10:56:35 +01:00
ed
bcee8a4934 Merge branch 'master' of gh:9001/copyparty 2021-03-20 09:20:12 +01:00
ed
36b0718542 media plauer hoetkeys 2021-03-20 09:20:08 +01:00
ed
9a92bca45d Merge branch 'master' of github:9001/copyparty
idk forgot to pull
2021-03-20 07:32:28 +00:00
ed
b07445a363 search ratecontrol and timeouts cause it can get bad 2021-03-20 07:32:01 +00:00
ed
a62ec0c27e fixes 2021-03-20 05:58:34 +01:00
ed
57e3a2d382 normalize keys to rekobo on index 2021-03-20 05:45:34 +01:00
ed
b61022b374 fixes 2021-03-20 03:08:16 +00:00
ed
a3e2b2ec87 mm:ss durations on initial html too 2021-03-20 01:27:51 +01:00
ed
a83d3f8801 prevent dupe tags from mtp (replace id3 tags) 2021-03-20 01:00:57 +01:00
ed
90c5f2b9d2 spread search interface horizontally 2021-03-20 01:00:44 +01:00
ed
4885653c07 advanced search (key/bpm/...)
man i hope sqlite is good at opimizing
2021-03-20 00:06:11 +01:00
ed
21e1cd87ca nice on windows 2021-03-19 23:43:34 +01:00
ed
81f82e8e9f nice them too 2021-03-19 21:28:10 +01:00
ed
c0e31851da add timeouts for mtp calls 2021-03-19 21:22:56 +01:00
ed
6599c3eced no racing pls 2021-03-19 20:42:33 +01:00
ed
5d6c61a861 add mtp eta 2021-03-19 01:20:01 +01:00
ed
1a5c66edd3 build beatroot from source if need be 2021-03-19 00:43:23 +01:00
ed
deae9fe95a vscode is entirely too helpful 2021-03-19 00:14:42 +01:00
ed
abd65c6334 support metadata plugins 2021-03-19 00:08:31 +01:00
ed
8137a99904 mtag-bin: support alpine + misc health checks 2021-03-18 01:01:57 +01:00
ed
6f6f9c1f74 mtag-bin: support macos (macports) 2021-03-17 23:57:18 +01:00
ed
7b575f716f mtag-bin: support windows (mingw64) 2021-03-17 23:17:02 +01:00
ed
6ba6ea3572 linkify 2021-03-17 01:42:59 +01:00
ed
9a22ad5ea3 this makes more sense 2021-03-17 01:37:59 +01:00
ed
beaab9778e make mistakes 2021-03-17 00:55:27 +01:00
ed
f327bdb6b4 never trust tags かしら 2021-03-15 23:12:13 +01:00
ed
ae180e0f5f save bpm/tempo notes from the bitbucket 2021-03-15 03:10:14 +01:00
ed
e3f1d19756 v0.9.8 2021-03-15 01:13:46 +01:00
ed
93c2bd6ef6 fix tree trying to make surprise appearances 2021-03-13 02:29:13 +01:00
ed
4d0e5ff6db turns out whitespace compresses better than tabs 2021-03-13 00:16:07 +01:00
ed
0893f06919 browser: reload music player on column-sort
so tracks play in the right order
2021-03-13 00:15:53 +01:00
ed
46b6abde3f fuse-client: password from file 2021-03-13 00:14:22 +01:00
ed
0696610dee give up, just try both and see what sticks 2021-03-13 00:14:07 +01:00
ed
edf0d3684c sfx: improvements from r0c 2021-03-13 00:13:10 +01:00
ed
7af159f5f6 heh 2021-03-09 21:36:14 +01:00
ed
7f2cb6764a v0.9.7 2021-03-08 03:51:26 +01:00
ed
96495a9bf1 v0.9.6 2021-03-07 21:44:25 +01:00
ed
b2fafec5fc handle key-normalization errors 2021-03-07 21:41:36 +01:00
ed
0850b8ae2b v0.9.5 2021-03-07 19:25:24 +01:00
ed
8a68a96c57 css tweaks 2021-03-07 19:15:19 +01:00
ed
d3aae8ed6a more mojibake fixes 2021-03-07 18:58:26 +01:00
ed
c62ebadda8 separate tree scrollbar 2021-03-07 18:26:57 +01:00
ed
ffcee6d390 add tooltips and more mojibake compat 2021-03-07 04:14:55 +01:00
ed
de32838346 key notation normalization (why tho) 2021-03-07 02:46:17 +01:00
ed
b9a4e47ea2 mojibake support for the spa stuff 2021-03-06 22:48:49 +01:00
ed
57d994422d logging cleanup 2021-03-06 17:38:56 +01:00
ed
6ecd745323 so much for sessionStorage 2021-03-06 16:34:55 +01:00
ed
bd769f5bdb fix py2 + encourage py3 2021-03-06 02:42:17 +01:00
ed
2381692aba js cfg 2021-03-06 02:30:36 +01:00
ed
24fdada0a0 did you know rhel 7 has an sqlite3 from 2015 2021-03-06 02:28:49 +01:00
ed
bb5169710a warn people when they're gonna have a bad time 2021-03-06 00:30:05 +01:00
38 changed files with 2485 additions and 571 deletions

6
.vscode/tasks.json vendored
View File

@@ -8,8 +8,10 @@
}, },
{ {
"label": "no_dbg", "label": "no_dbg",
"command": "${config:python.pythonPath} -m copyparty -ed -emp -e2dsa -e2ts -a ed:wark -v srv::r:aed:cnodupe -v dist:dist:r ;exit 1", "type": "shell",
"type": "shell" "command": "${config:python.pythonPath} -m copyparty -ed -emp -e2dsa -e2ts -a ed:wark -v srv::r:aed:cnodupe -v dist:dist:r ;exit 1"
// -v ~/Music/mt:mt:r:cmtp=.bpm=~/dev/copyparty/bin/mtag/audio-bpm.py:cmtp=key=~/dev/copyparty/bin/mtag/audio-key.py:ce2tsr
// -v ~/Music/mt:mt:r:cmtp=.bpm=~/dev/copyparty/bin/mtag/audio-bpm.py:ce2tsr
} }
] ]
} }

View File

@@ -13,6 +13,29 @@ turn your phone or raspi into a portable file server with resumable uploads/down
* code standard: `black` * code standard: `black`
## readme toc
* top
* [quickstart](#quickstart)
* [notes](#notes)
* [status](#status)
* [bugs](#bugs)
* [usage](#usage)
* [searching](#searching)
* [search configuration](#search-configuration)
* [metadata from audio files](#metadata-from-audio-files)
* [file parser plugins](#file-parser-plugins)
* [client examples](#client-examples)
* [dependencies](#dependencies)
* [optional gpl stuff](#optional-gpl-stuff)
* [sfx](#sfx)
* [sfx repack](#sfx-repack)
* [install on android](#install-on-android)
* [dev env setup](#dev-env-setup)
* [how to release](#how-to-release)
* [todo](#todo)
## quickstart ## quickstart
download [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) and you're all set! download [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) and you're all set!
@@ -69,7 +92,19 @@ summary: it works! you can use it! (but technically not even close to beta)
# bugs # bugs
* probably, pls let me know * Windows: python 3.7 and older cannot read tags with ffprobe, so use mutagen or upgrade
* Windows: python 2.7 cannot index non-ascii filenames with `-e2d`
* probably more, pls let me know
# usage
the browser has the following hotkeys
* `0..9` jump to 10%..90%
* `U/O` skip 10sec back/forward
* `J/L` prev/next song
* `I/K` prev/next folder
* `P` parent folder
# searching # searching
@@ -98,19 +133,24 @@ through arguments:
* `-e2tsr` deletes all existing tags, so a full reindex * `-e2tsr` deletes all existing tags, so 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::ce2dsa:ce2tsr` does a full reindex of everything on startup * `-v ~/music::r:ce2dsa:ce2tsr` does a full reindex of everything on startup
* `-v ~/music::cd2d` disables **all** indexing, even if any `-e2*` are on * `-v ~/music::r:cd2d` disables **all** indexing, even if any `-e2*` are on
* `-v ~/music::cd2t` disables all `-e2t*` (tags), does not affect `-e2d*` * `-v ~/music::r:cd2t` disables all `-e2t*` (tags), does not affect `-e2d*`
`e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and cause `e2ts` to reindex those `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and cause `e2ts` to reindex those
## 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::cmte=title,artist` indexes and displays *title* followed by *artist* * `-v ~/music::r:cmte=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
`-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
see the beautiful mess of a dictionary in [mtag.py](https://github.com/9001/copyparty/blob/master/copyparty/mtag.py) for the default mappings (should cover mp3,opus,flac,m4a,wav,aif,) see the beautiful mess of a dictionary in [mtag.py](https://github.com/9001/copyparty/blob/master/copyparty/mtag.py) for the default mappings (should cover mp3,opus,flac,m4a,wav,aif,)
`--no-mutagen` disables mutagen and uses ffprobe instead, which... `--no-mutagen` disables mutagen and uses ffprobe instead, which...
@@ -120,6 +160,21 @@ see the beautiful mess of a dictionary in [mtag.py](https://github.com/9001/copy
* more importantly runs ffprobe on incoming files which is bad if your ffmpeg has a cve * more importantly runs ffprobe on incoming files which is bad if your ffmpeg has a cve
## file parser plugins
copyparty can invoke external programs to collect additional metadata for files using `mtp` (as argument or volume flag), there is a default timeout of 30sec
* `-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
## complete examples
* read-only music server with bpm and key scanning
* `python copyparty-sfx.py -v /mnt/nas/music:/music:r -e2dsa -e2ts -mtp .bpm=f,~/bin/audio-bpm.py -mtp key=f,~/bin/audio-key.py`
# client examples # client examples
* javascript: dump some state into a file (two separate examples) * javascript: dump some state into a file (two separate examples)
@@ -156,6 +211,13 @@ copyparty returns a truncated sha512sum of your PUT/POST as base64; you can gene
* `Pillow` (requires py2.7 or py3.5+) * `Pillow` (requires py2.7 or py3.5+)
## optional gpl stuff
some bundled tools have copyleft dependencies, see [./bin/#mtag](bin/#mtag)
these are standalone and will never be imported / evaluated by copyparty
# sfx # sfx
currently there are two self-contained binaries: currently there are two self-contained binaries:
@@ -210,6 +272,7 @@ pip install black bandit pylint flake8 # vscode tooling
in the `scripts` folder: in the `scripts` folder:
* run `make -C deps-docker` to build all dependencies * run `make -C deps-docker` to build all dependencies
* `git tag v1.2.3 && git push origin --tags`
* create github release with `make-tgz-release.sh` * create github release with `make-tgz-release.sh`
* upload to pypi with `make-pypi-release.(sh|bat)` * upload to pypi with `make-pypi-release.(sh|bat)`
* create sfx with `make-sfx.sh` * create sfx with `make-sfx.sh`

View File

@@ -1,4 +1,4 @@
# copyparty-fuse.py # [`copyparty-fuse.py`](copyparty-fuse.py)
* mount a copyparty server as a local filesystem (read-only) * mount a copyparty server as a local filesystem (read-only)
* **supports Windows!** -- expect `194 MiB/s` sequential read * **supports Windows!** -- expect `194 MiB/s` sequential read
* **supports Linux** -- expect `117 MiB/s` sequential read * **supports Linux** -- expect `117 MiB/s` sequential read
@@ -29,7 +29,7 @@ you could replace winfsp with [dokan](https://github.com/dokan-dev/dokany/releas
# copyparty-fuse🅱.py # [`copyparty-fuse🅱.py`](copyparty-fuseb.py)
* mount a copyparty server as a local filesystem (read-only) * mount a copyparty server as a local filesystem (read-only)
* does the same thing except more correct, `samba` approves * does the same thing except more correct, `samba` approves
* **supports Linux** -- expect `18 MiB/s` (wait what) * **supports Linux** -- expect `18 MiB/s` (wait what)
@@ -37,5 +37,11 @@ you could replace winfsp with [dokan](https://github.com/dokan-dev/dokany/releas
# copyparty-fuse-streaming.py # [`copyparty-fuse-streaming.py`](copyparty-fuse-streaming.py)
* pretend this doesn't exist * pretend this doesn't exist
# [`mtag/`](mtag/)
* standalone programs which perform misc. file analysis
* copyparty can Popen programs like these during file indexing to collect additional metadata

View File

@@ -1008,6 +1008,12 @@ def main():
log = null_log log = null_log
dbg = null_log dbg = null_log
if ar.a and ar.a.startswith("$"):
fn = ar.a[1:]
log("reading password from file [{}]".format(fn))
with open(fn, "rb") as f:
ar.a = f.read().decode("utf-8").strip()
if WINDOWS: if WINDOWS:
os.system("rem") os.system("rem")

8
bin/mtag/README.md Normal file
View File

@@ -0,0 +1,8 @@
standalone programs which take an audio file as argument
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
run [`install-deps.sh`](install-deps.sh) to build/install most dependencies required by these programs (supports windows/linux/macos)

69
bin/mtag/audio-bpm.py Executable file
View File

@@ -0,0 +1,69 @@
#!/usr/bin/env python
import os
import sys
import vamp
import tempfile
import numpy as np
import subprocess as sp
from copyparty.util import fsenc
"""
dep: vamp
dep: beatroot-vamp
dep: ffmpeg
"""
def det(tf):
# fmt: off
sp.check_call([
"ffmpeg",
"-nostdin",
"-hide_banner",
"-v", "fatal",
"-ss", "13",
"-y", "-i", fsenc(sys.argv[1]),
"-ac", "1",
"-ar", "22050",
"-t", "300",
"-f", "f32le",
tf
])
# fmt: on
with open(tf, "rb") as f:
d = np.fromfile(f, dtype=np.float32)
try:
# 98% accuracy on jcore
c = vamp.collect(d, 22050, "beatroot-vamp:beatroot")
cl = c["list"]
except:
# fallback; 73% accuracy
plug = "vamp-example-plugins:fixedtempo"
c = vamp.collect(d, 22050, plug, parameters={"maxdflen": 40})
print(c["list"][0]["label"].split(" ")[0])
return
# throws if detection failed:
bpm = float(cl[-1]["timestamp"] - cl[1]["timestamp"])
bpm = round(60 * ((len(cl) - 1) / bpm), 2)
print(f"{bpm:.2f}")
def main():
with tempfile.NamedTemporaryFile(suffix=".pcm", delete=False) as f:
f.write(b"h")
tf = f.name
try:
det(tf)
except:
pass
finally:
os.unlink(tf)
if __name__ == "__main__":
main()

18
bin/mtag/audio-key.py Executable file
View File

@@ -0,0 +1,18 @@
#!/usr/bin/env python
import sys
import keyfinder
"""
dep: github/mixxxdj/libkeyfinder
dep: pypi/keyfinder
dep: ffmpeg
note: cannot fsenc
"""
try:
print(keyfinder.key(sys.argv[1]).camelot())
except:
pass

265
bin/mtag/install-deps.sh Executable file
View File

@@ -0,0 +1,265 @@
#!/bin/bash
set -e
# install dependencies for audio-*.py
#
# linux: requires {python3,ffmpeg,fftw}-dev py3-{wheel,pip} py3-numpy{,-dev} vamp-sdk-dev patchelf
# win64: requires msys2-mingw64 environment
# macos: requires macports
#
# has the following manual dependencies, especially on mac:
# https://www.vamp-plugins.org/pack.html
#
# installs stuff to the following locations:
# ~/pe/
# whatever your python uses for --user packages
#
# does the following terrible things:
# modifies the keyfinder python lib to load the .so in ~/pe
linux=1
win=
[ ! -z "$MSYSTEM" ] || [ -e /msys2.exe ] && {
[ "$MSYSTEM" = MINGW64 ] || {
echo windows detected, msys2-mingw64 required
exit 1
}
pacman -S --needed mingw-w64-x86_64-{ffmpeg,python,python-pip,vamp-plugin-sdk}
win=1
linux=
}
mac=
[ $(uname -s) = Darwin ] && {
#pybin="$(printf '%s\n' /opt/local/bin/python* | (sed -E 's/(.*\/[^/0-9]+)([0-9]?[^/]*)$/\2 \1/' || cat) | (sort -nr || cat) | (sed -E 's/([^ ]*) (.*)/\2\1/' || cat) | grep -E '/(python|pypy)[0-9\.-]*$' | head -n 1)"
pybin=/opt/local/bin/python3.9
[ -e "$pybin" ] || {
echo mac detected, python3 from macports required
exit 1
}
pkgs='ffmpeg python39 py39-wheel'
ninst=$(port installed | awk '/^ /{print$1}' | sort | uniq | grep -E '^('"$(echo "$pkgs" | tr ' ' '|')"')$' | wc -l)
[ $ninst -eq 3 ] || {
sudo port install $pkgs
}
mac=1
linux=
}
hash -r
[ $mac ] || {
command -v python3 && pybin=python3 || pybin=python
}
$pybin -m pip install --user numpy
command -v gnutar && tar() { gnutar "$@"; }
command -v gtar && tar() { gtar "$@"; }
command -v gsed && sed() { gsed "$@"; }
need() {
command -v $1 >/dev/null || {
echo need $1
exit 1
}
}
need cmake
need ffmpeg
need $pybin
#need patchelf
td="$(mktemp -d)"
cln() {
rm -rf "$td"
}
trap cln EXIT
cd "$td"
pwd
dl_text() {
command -v curl >/dev/null && exec curl "$@"
exec wget -O- "$@"
}
dl_files() {
local yolo= ex=
[ $1 = "yolo" ] && yolo=1 && ex=k && shift
command -v curl >/dev/null && exec curl -${ex}JOL "$@"
[ $yolo ] && ex=--no-check-certificate
exec wget --trust-server-names $ex "$@"
}
export -f dl_files
github_tarball() {
dl_text "$1" |
tee json |
(
# prefer jq if available
jq -r '.tarball_url' ||
# fallback to awk (sorry)
awk -F\" '/"tarball_url": "/ {print$4}'
) |
tee /dev/stderr |
tr -d '\r' | tr '\n' '\0' |
xargs -0 bash -c 'dl_files "$@"' _
}
gitlab_tarball() {
dl_text "$1" |
tee json |
(
# prefer jq if available
jq -r '.[0].assets.sources[]|select(.format|test("tar.gz")).url' ||
# fallback to abomination
tr \" '\n' | grep -E '\.tar\.gz$' | head -n 1
) |
tee /dev/stderr |
tr -d '\r' | tr '\n' '\0' |
tee links |
xargs -0 bash -c 'dl_files "$@"' _
}
install_keyfinder() {
# windows support:
# use msys2 in mingw-w64 mode
# pacman -S --needed mingw-w64-x86_64-{ffmpeg,python}
github_tarball https://api.github.com/repos/mixxxdj/libkeyfinder/releases/latest
tar -xf mixxxdj-libkeyfinder-*
rm -- *.tar.gz
cd mixxxdj-libkeyfinder*
h="$HOME"
so="lib/libkeyfinder.so"
memes=()
[ $win ] &&
so="bin/libkeyfinder.dll" &&
h="$(printf '%s\n' "$USERPROFILE" | tr '\\' '/')" &&
memes+=(-G "MinGW Makefiles" -DBUILD_TESTING=OFF)
[ $mac ] &&
so="lib/libkeyfinder.dylib"
cmake -DCMAKE_INSTALL_PREFIX="$h/pe/keyfinder" "${memes[@]}" -S . -B build
cmake --build build --parallel $(nproc || echo 4)
cmake --install build
libpath="$h/pe/keyfinder/$so"
[ $linux ] && [ ! -e "$libpath" ] &&
so=lib64/libkeyfinder.so
libpath="$h/pe/keyfinder/$so"
[ -e "$libpath" ] || {
echo "so not found at $sop"
exit 1
}
# rm -rf /Users/ed/Library/Python/3.9/lib/python/site-packages/*keyfinder*
CFLAGS="-I$h/pe/keyfinder/include -I/opt/local/include" \
LDFLAGS="-L$h/pe/keyfinder/lib -L$h/pe/keyfinder/lib64 -L/opt/local/lib" \
PKG_CONFIG_PATH=/c/msys64/mingw64/lib/pkgconfig \
$pybin -m pip install --user keyfinder
pypath="$($pybin -c 'import keyfinder; print(keyfinder.__file__)')"
for pyso in "${pypath%/*}"/*.so; do
[ -e "$pyso" ] || break
patchelf --set-rpath "${libpath%/*}" "$pyso" ||
echo "WARNING: patchelf failed (only fatal on musl-based distros)"
done
mv "$pypath"{,.bak}
(
printf 'import ctypes\nctypes.cdll.LoadLibrary("%s")\n' "$libpath"
cat "$pypath.bak"
) >"$pypath"
echo
echo libkeyfinder successfully installed to the following locations:
echo " $libpath"
echo " $pypath"
}
have_beatroot() {
$pybin -c 'import vampyhost, sys; plugs = vampyhost.list_plugins(); sys.exit(0 if "beatroot-vamp:beatroot" in plugs else 1)'
}
install_vamp() {
# windows support:
# use msys2 in mingw-w64 mode
# pacman -S --needed mingw-w64-x86_64-{ffmpeg,python,python-pip,vamp-plugin-sdk}
$pybin -m pip install --user vamp
have_beatroot || {
printf '\033[33mcould not find the vamp beatroot plugin, building from source\033[0m\n'
(dl_files yolo https://code.soundsoftware.ac.uk/attachments/download/885/beatroot-vamp-v1.0.tar.gz)
sha512sum -c <(
echo "1f444d1d58ccf565c0adfe99f1a1aa62789e19f5071e46857e2adfbc9d453037bc1c4dcb039b02c16240e9b97f444aaff3afb625c86aa2470233e711f55b6874 -"
) <beatroot-vamp-v1.0.tar.gz
tar -xf beatroot-vamp-v1.0.tar.gz
cd beatroot-vamp-v1.0
make -f Makefile.linux -j4
# /home/ed/vamp /home/ed/.vamp /usr/local/lib/vamp
mkdir ~/vamp
cp -pv beatroot-vamp.* ~/vamp/
}
have_beatroot &&
printf '\033[32mfound the vamp beatroot plugin, nice\033[0m\n' ||
printf '\033[31mWARNING: could not find the vamp beatroot plugin, please install it for optimal results\033[0m\n'
}
# not in use because it kinda segfaults, also no windows support
install_soundtouch() {
gitlab_tarball https://gitlab.com/api/v4/projects/soundtouch%2Fsoundtouch/releases
tar -xvf soundtouch-*
rm -- *.tar.gz
cd soundtouch-*
# https://github.com/jrising/pysoundtouch
./bootstrap
./configure --enable-integer-samples CXXFLAGS="-fPIC" --prefix="$HOME/pe/soundtouch"
make -j$(nproc || echo 4)
make install
CFLAGS=-I$HOME/pe/soundtouch/include/ \
LDFLAGS=-L$HOME/pe/soundtouch/lib \
$pybin -m pip install --user git+https://github.com/snowxmas/pysoundtouch.git
pypath="$($pybin -c 'import importlib; print(importlib.util.find_spec("soundtouch").origin)')"
libpath="$(echo "$HOME/pe/soundtouch/lib/")"
patchelf --set-rpath "$libpath" "$pypath"
echo
echo soundtouch successfully installed to the following locations:
echo " $libpath"
echo " $pypath"
}
[ "$1" = keyfinder ] && { install_keyfinder; exit $?; }
[ "$1" = soundtouch ] && { install_soundtouch; exit $?; }
[ "$1" = vamp ] && { install_vamp; exit $?; }
echo no args provided, installing keyfinder and vamp
install_keyfinder
install_vamp

8
bin/mtag/sleep.py Normal file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env python
import time
import random
v = random.random() * 6
time.sleep(v)
print(f"{v:.2f}")

View File

@@ -12,7 +12,7 @@
Description=copyparty file server Description=copyparty file server
[Service] [Service]
ExecStart=/usr/bin/python /usr/local/bin/copyparty-sfx.py -q -v /mnt::a ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::a
ExecStartPre=/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf' ExecStartPre=/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
[Install] [Install]

View File

@@ -12,16 +12,19 @@ import re
import os import os
import sys import sys
import time import time
import signal
import shutil import shutil
import filecmp import filecmp
import locale import locale
import argparse import argparse
import threading
import traceback
from textwrap import dedent from textwrap import dedent
from .__init__ import E, WINDOWS, VT100 from .__init__ import E, WINDOWS, VT100, PY2
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 from .util import py_desc, align_tab, IMPLICATIONS
HAVE_SSL = True HAVE_SSL = True
try: try:
@@ -53,6 +56,10 @@ class RiceFormatter(argparse.HelpFormatter):
return "".join(indent + line + "\n" for line in text.splitlines()) return "".join(indent + line + "\n" for line in text.splitlines())
def warn(msg):
print("\033[1mwarning:\033[0;33m {}\033[0m\n".format(msg))
def ensure_locale(): def ensure_locale():
for x in [ for x in [
"en_US.UTF-8", "en_US.UTF-8",
@@ -160,6 +167,16 @@ def configure_ssl_ciphers(al):
sys.exit(0) sys.exit(0)
def sighandler(signal=None, frame=None):
msg = [""] * 5
for th in threading.enumerate():
msg.append(str(th))
msg.extend(traceback.format_stack(sys._current_frames()[th.ident]))
msg.append("\n")
print("\n".join(msg))
def main(): def main():
time.strptime("19970815", "%Y%m%d") # python#7980 time.strptime("19970815", "%Y%m%d") # python#7980
if WINDOWS: if WINDOWS:
@@ -237,6 +254,7 @@ def main():
ap.add_argument("-a", metavar="ACCT", type=str, action="append", help="add account") ap.add_argument("-a", metavar="ACCT", type=str, action="append", help="add account")
ap.add_argument("-v", metavar="VOL", type=str, action="append", help="add volume") ap.add_argument("-v", metavar="VOL", type=str, action="append", help="add volume")
ap.add_argument("-q", action="store_true", help="quiet") ap.add_argument("-q", action="store_true", help="quiet")
ap.add_argument("--log-conn", action="store_true", help="print tcp-server msgs")
ap.add_argument("-ed", action="store_true", help="enable ?dots") ap.add_argument("-ed", action="store_true", help="enable ?dots")
ap.add_argument("-emp", action="store_true", help="enable markdown plugins") ap.add_argument("-emp", action="store_true", help="enable markdown plugins")
ap.add_argument("-mcr", metavar="SEC", type=int, default=60, help="md-editor mod-chk rate") ap.add_argument("-mcr", metavar="SEC", type=int, default=60, help="md-editor mod-chk rate")
@@ -245,7 +263,7 @@ def main():
ap.add_argument("-nid", action="store_true", help="no info disk-usage") ap.add_argument("-nid", action="store_true", help="no info disk-usage")
ap.add_argument("--no-sendfile", action="store_true", help="disable sendfile (for debugging)") ap.add_argument("--no-sendfile", action="store_true", help="disable sendfile (for debugging)")
ap.add_argument("--no-scandir", action="store_true", help="disable scandir (for debugging)") ap.add_argument("--no-scandir", action="store_true", help="disable scandir (for debugging)")
ap.add_argument("--urlform", type=str, default="print,get", help="how to handle url-forms") ap.add_argument("--urlform", metavar="MODE", type=str, default="print,get", help="how to handle url-forms")
ap.add_argument("--salt", type=str, default="hunter2", help="up2k file-hash salt") ap.add_argument("--salt", type=str, default="hunter2", help="up2k file-hash salt")
ap2 = ap.add_argument_group('database options') ap2 = ap.add_argument_group('database options')
@@ -260,11 +278,13 @@ def main():
ap2.add_argument("-mtm", metavar="M=t,t,t", action="append", type=str, help="add/replace metadata mapping") ap2.add_argument("-mtm", metavar="M=t,t,t", action="append", type=str, help="add/replace metadata mapping")
ap2.add_argument("-mte", metavar="M,M,M", type=str, help="tags to index/display (comma-sep.)", ap2.add_argument("-mte", metavar="M,M,M", type=str, help="tags to index/display (comma-sep.)",
default="circle,album,.tn,artist,title,.bpm,key,.dur,.q") default="circle,album,.tn,artist,title,.bpm,key,.dur,.q")
ap2.add_argument("-mtp", metavar="M=[f,]bin", action="append", type=str, help="read tag M using bin")
ap2.add_argument("--srch-time", metavar="SEC", type=int, default=30, help="search deadline")
ap2 = ap.add_argument_group('SSL/TLS options') ap2 = ap.add_argument_group('SSL/TLS options')
ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls") ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls")
ap2.add_argument("--https-only", action="store_true", help="disable plaintext") ap2.add_argument("--https-only", action="store_true", help="disable plaintext")
ap2.add_argument("--ssl-ver", type=str, help="ssl/tls versions to allow") ap2.add_argument("--ssl-ver", metavar="LIST", type=str, help="ssl/tls versions to allow")
ap2.add_argument("--ciphers", metavar="LIST", help="set allowed ciphers") ap2.add_argument("--ciphers", metavar="LIST", help="set allowed ciphers")
ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info") ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info")
ap2.add_argument("--ssl-log", metavar="PATH", help="log master secrets") ap2.add_argument("--ssl-log", metavar="PATH", help="log master secrets")
@@ -273,13 +293,7 @@ def main():
# fmt: on # fmt: on
# propagate implications # propagate implications
for k1, k2 in [ for k1, k2 in IMPLICATIONS:
["e2dsa", "e2ds"],
["e2ds", "e2d"],
["e2tsr", "e2ts"],
["e2ts", "e2t"],
["e2t", "e2d"],
]:
if getattr(al, k1): if getattr(al, k1):
setattr(al, k2, True) setattr(al, k2, True)
@@ -300,7 +314,15 @@ def main():
if al.ciphers: if al.ciphers:
configure_ssl_ciphers(al) configure_ssl_ciphers(al)
else: else:
print("\033[33m ssl module does not exist; cannot enable https\033[0m\n") warn("ssl module does not exist; cannot enable https")
if PY2 and WINDOWS and al.e2d:
warn(
"windows py2 cannot do unicode filenames with -e2d\n"
+ " (if you crash with codec errors then that is why)"
)
# signal.signal(signal.SIGINT, sighandler)
SvcHub(al).run() SvcHub(al).run()

View File

@@ -1,8 +1,8 @@
# coding: utf-8 # coding: utf-8
VERSION = (0, 9, 4) VERSION = (0, 9, 9)
CODENAME = "the strongest music server" CODENAME = "the strongest music server"
BUILD_DT = (2021, 3, 5) BUILD_DT = (2021, 3, 21)
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

@@ -1,12 +1,13 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import os
import re import re
import os
import sys
import threading import threading
from .__init__ import PY2, WINDOWS from .__init__ import PY2, WINDOWS
from .util import undot, Pebkac, fsdec, fsenc, statdir from .util import IMPLICATIONS, undot, Pebkac, fsdec, fsenc, statdir, nuprint
class VFS(object): class VFS(object):
@@ -106,7 +107,7 @@ class VFS(object):
"""return user-readable [fsdir,real,virt] items at vpath""" """return user-readable [fsdir,real,virt] items at vpath"""
virt_vis = {} # nodes readable by user virt_vis = {} # nodes readable by user
abspath = self.canonical(rem) abspath = self.canonical(rem)
real = list(statdir(print, scandir, lstat, abspath)) real = list(statdir(nuprint, scandir, lstat, abspath))
real.sort() real.sort()
if not rem: if not rem:
for name, vn2 in sorted(self.nodes.items()): for name, vn2 in sorted(self.nodes.items()):
@@ -147,8 +148,8 @@ class AuthSrv(object):
self.mutex = threading.Lock() self.mutex = threading.Lock()
self.reload() self.reload()
def log(self, msg): def log(self, msg, c=0):
self.log_func("auth", msg) self.log_func("auth", msg, c)
def invert(self, orig): def invert(self, orig):
if PY2: if PY2:
@@ -200,16 +201,39 @@ class AuthSrv(object):
continue continue
lvl, uname = ln.split(" ") lvl, uname = ln.split(" ")
if lvl in "ra": self._read_vol_str(
mread[vol_dst].append(uname) lvl, uname, mread[vol_dst], mwrite[vol_dst], mflags[vol_dst]
if lvl in "wa": )
mwrite[vol_dst].append(uname)
def _read_vol_str(self, lvl, uname, mr, mw, mf):
if lvl == "c": if lvl == "c":
cval = True cval = True
if "=" in uname: if "=" in uname:
uname, cval = uname.split("=", 1) uname, cval = uname.split("=", 1)
mflags[vol_dst][uname] = cval self._read_volflag(mf, uname, cval, False)
return
if uname == "":
uname = "*"
if lvl in "ra":
mr.append(uname)
if lvl in "wa":
mw.append(uname)
def _read_volflag(self, flags, name, value, is_list):
if name not in ["mtp"]:
flags[name] = value
return
if not is_list:
value = [value]
elif not value:
return
flags[name] = flags.get(name, []) + value
def reload(self): def reload(self):
""" """
@@ -232,7 +256,7 @@ class AuthSrv(object):
if self.args.v: if self.args.v:
# list of src:dst:permset:permset:... # list of src:dst:permset:permset:...
# permset is [rwa]username # permset is [rwa]username or [c]flag
for v_str in self.args.v: for v_str in self.args.v:
m = self.re_vol.match(v_str) m = self.re_vol.match(v_str)
if not m: if not m:
@@ -249,22 +273,7 @@ class AuthSrv(object):
perms = perms.split(":") perms = perms.split(":")
for (lvl, uname) in [[x[0], x[1:]] for x in perms]: for (lvl, uname) in [[x[0], x[1:]] for x in perms]:
if lvl == "c": self._read_vol_str(lvl, uname, mread[dst], mwrite[dst], mflags[dst])
cval = True
if "=" in uname:
uname, cval = uname.split("=", 1)
mflags[dst][uname] = cval
continue
if uname == "":
uname = "*"
if lvl in "ra":
mread[dst].append(uname)
if lvl in "wa":
mwrite[dst].append(uname)
if self.args.c: if self.args.c:
for cfg_fn in self.args.c: for cfg_fn in self.args.c:
@@ -304,12 +313,14 @@ class AuthSrv(object):
if missing_users: if missing_users:
self.log( self.log(
"\033[31myou must -a the following users: " "you must -a the following users: "
+ ", ".join(k for k in sorted(missing_users)) + ", ".join(k for k in sorted(missing_users)),
+ "\033[0m" c=1,
) )
raise Exception("invalid config") raise Exception("invalid config")
all_mte = {}
errors = False
for vol in vfs.all_vols.values(): for vol in vfs.all_vols.values():
if (self.args.e2ds and vol.uwrite) or self.args.e2dsa: if (self.args.e2ds and vol.uwrite) or self.args.e2dsa:
vol.flags["e2ds"] = True vol.flags["e2ds"] = True
@@ -321,16 +332,66 @@ class AuthSrv(object):
if getattr(self.args, k): if getattr(self.args, k):
vol.flags[k] = True vol.flags[k] = True
for k1, k2 in IMPLICATIONS:
if k1 in vol.flags:
vol.flags[k2] = True
# default tag-list if unset # default tag-list 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
# append parsers from argv to volume-flags
self._read_volflag(vol.flags, "mtp", self.args.mtp, True)
# verify tags mentioned by -mt[mp] are used by -mte
local_mtp = {}
local_only_mtp = {}
for a in vol.flags.get("mtp", []) + vol.flags.get("mtm", []):
a = a.split("=")[0]
local_mtp[a] = True
local = True
for b in self.args.mtp or []:
b = b.split("=")[0]
if a == b:
local = False
if local:
local_only_mtp[a] = True
local_mte = {}
for a in vol.flags.get("mte", "").split(","):
local = True
all_mte[a] = True
local_mte[a] = True
for b in self.args.mte.split(","):
if not a or not b:
continue
if a == b:
local = False
for mtp in local_only_mtp.keys():
if mtp not in local_mte:
m = 'volume "/{}" defines metadata tag "{}", but doesnt use it in "-mte" (or with "cmte" in its volume-flags)'
self.log(m.format(vol.vpath, mtp), 1)
errors = True
for mtp in self.args.mtp or []:
mtp = mtp.split("=")[0]
if mtp not in all_mte:
m = 'metadata tag "{}" is defined by "-mtm" or "-mtp", but is not used by "-mte" (or by any "cmte" volume-flag)'
self.log(m.format(mtp), 1)
errors = True
if errors:
sys.exit(1)
try: try:
v, _ = vfs.get("/", "*", False, True) v, _ = vfs.get("/", "*", False, True)
if self.warn_anonwrite and os.getcwd() == v.realpath: if self.warn_anonwrite and os.getcwd() == v.realpath:
self.warn_anonwrite = False self.warn_anonwrite = False
msg = "\033[31manyone can read/write the current directory: {}\033[0m" msg = "anyone can read/write the current directory: {}"
self.log(msg.format(v.realpath)) self.log(msg.format(v.realpath), c=1)
except Pebkac: except Pebkac:
self.warn_anonwrite = True self.warn_anonwrite = True

View File

@@ -49,11 +49,11 @@ class MpWorker(object):
# print('k') # print('k')
pass pass
def log(self, src, msg): def log(self, src, msg, c=0):
self.q_yield.put([0, "log", [src, msg]]) self.q_yield.put([0, "log", [src, msg, c]])
def logw(self, msg): def logw(self, msg, c=0):
self.log("mp{}".format(self.n), msg) self.log("mp{}".format(self.n), msg, c)
def httpdrop(self, addr): def httpdrop(self, addr):
self.q_yield.put([0, "httpdrop", [addr]]) self.q_yield.put([0, "httpdrop", [addr]])
@@ -73,7 +73,9 @@ class MpWorker(object):
if PY2: if PY2:
sck = pickle.loads(sck) # nosec sck = pickle.loads(sck) # nosec
self.log("%s %s" % addr, "\033[1;30m|%sC-qpop\033[0m" % ("-" * 4,)) if self.args.log_conn:
self.log("%s %s" % addr, "|%sC-qpop" % ("-" * 4,), c="1;30")
self.httpsrv.accept(sck, addr) self.httpsrv.accept(sck, addr)
with self.mutex: with self.mutex:

View File

@@ -28,7 +28,9 @@ class BrokerThr(object):
def put(self, want_retval, dest, *args): def put(self, want_retval, dest, *args):
if dest == "httpconn": if dest == "httpconn":
sck, addr = args sck, addr = args
self.log("%s %s" % addr, "\033[1;30m|%sC-qpop\033[0m" % ("-" * 4,)) if self.args.log_conn:
self.log("%s %s" % addr, "|%sC-qpop" % ("-" * 4,), c="1;30")
self.httpsrv.accept(sck, addr) self.httpsrv.accept(sck, addr)
else: else:

View File

@@ -41,11 +41,11 @@ class HttpCli(object):
self.absolute_urls = False self.absolute_urls = False
self.out_headers = {"Access-Control-Allow-Origin": "*"} self.out_headers = {"Access-Control-Allow-Origin": "*"}
def log(self, msg): def log(self, msg, c=0):
self.log_func(self.log_src, msg) self.log_func(self.log_src, msg, c)
def _check_nonfatal(self, ex): def _check_nonfatal(self, ex):
return ex.code < 400 or ex.code == 404 return ex.code < 400 or ex.code in [404, 429]
def _assert_safe_rem(self, rem): def _assert_safe_rem(self, rem):
# sanity check to prevent any disasters # sanity check to prevent any disasters
@@ -63,7 +63,7 @@ class HttpCli(object):
if not headerlines[0]: if not headerlines[0]:
# seen after login with IE6.0.2900.5512.xpsp.080413-2111 (xp-sp3) # seen after login with IE6.0.2900.5512.xpsp.080413-2111 (xp-sp3)
self.log("\033[1;31mBUG: trailing newline from previous request\033[0m") self.log("BUG: trailing newline from previous request", c="1;31")
headerlines.pop(0) headerlines.pop(0)
try: try:
@@ -74,7 +74,7 @@ class HttpCli(object):
except Pebkac as ex: except Pebkac as ex:
# self.log("pebkac at httpcli.run #1: " + repr(ex)) # self.log("pebkac at httpcli.run #1: " + repr(ex))
self.keepalive = self._check_nonfatal(ex) self.keepalive = self._check_nonfatal(ex)
self.loud_reply(str(ex), status=ex.code) self.loud_reply(unicode(ex), status=ex.code)
return self.keepalive return self.keepalive
# time.sleep(0.4) # time.sleep(0.4)
@@ -163,7 +163,7 @@ class HttpCli(object):
response = ["HTTP/1.1 {} {}".format(status, HTTPCODE[status])] response = ["HTTP/1.1 {} {}".format(status, HTTPCODE[status])]
if length is not None: if length is not None:
response.append("Content-Length: " + str(length)) response.append("Content-Length: " + unicode(length))
# close if unknown length, otherwise take client's preference # close if unknown length, otherwise take client's preference
response.append("Connection: " + ("Keep-Alive" if self.keepalive else "Close")) response.append("Connection: " + ("Keep-Alive" if self.keepalive else "Close"))
@@ -450,19 +450,30 @@ class HttpCli(object):
idx = self.conn.get_u2idx() idx = self.conn.get_u2idx()
t0 = time.time() t0 = time.time()
if idx.p_end:
penalty = 0.7
t_idle = t0 - idx.p_end
if idx.p_dur > 0.7 and t_idle < penalty:
m = "rate-limit ({:.1f} sec), cost {:.2f}, idle {:.2f}"
raise Pebkac(429, m.format(penalty, idx.p_dur, t_idle))
if "srch" in body: if "srch" in body:
# search by up2k hashlist # search by up2k hashlist
vbody = copy.deepcopy(body) vbody = copy.deepcopy(body)
vbody["hash"] = len(vbody["hash"]) vbody["hash"] = len(vbody["hash"])
self.log("qj: " + repr(vbody)) self.log("qj: " + repr(vbody))
hits = idx.fsearch(vols, body) hits = idx.fsearch(vols, body)
self.log("q#: {} ({:.2f}s)".format(repr(hits), time.time() - t0)) msg = repr(hits)
taglist = [] taglist = []
else: else:
# search by query params # search by query params
self.log("qj: " + repr(body)) self.log("qj: " + repr(body))
hits, taglist = idx.search(vols, body) hits, taglist = idx.search(vols, body)
self.log("q#: {} ({:.2f}s)".format(len(hits), time.time() - t0)) msg = len(hits)
idx.p_end = time.time()
idx.p_dur = idx.p_end - t0
self.log("q#: {} ({:.2f}s)".format(msg, idx.p_dur))
order = [] order = []
cfg = self.args.mte.split(",") cfg = self.args.mte.split(",")
@@ -521,7 +532,7 @@ class HttpCli(object):
if len(cstart) > 1 and path != os.devnull: if len(cstart) > 1 and path != os.devnull:
self.log( self.log(
"clone {} to {}".format( "clone {} to {}".format(
cstart[0], " & ".join(str(x) for x in cstart[1:]) cstart[0], " & ".join(unicode(x) for x in cstart[1:])
) )
) )
ofs = 0 ofs = 0
@@ -697,7 +708,7 @@ class HttpCli(object):
raise raise
except Pebkac as ex: except Pebkac as ex:
errmsg = str(ex) errmsg = unicode(ex)
td = max(0.1, time.time() - t0) td = max(0.1, time.time() - t0)
sz_total = sum(x[0] for x in files) sz_total = sum(x[0] for x in files)
@@ -1006,7 +1017,7 @@ class HttpCli(object):
mime=guess_mime(req_path)[0] or "application/octet-stream", mime=guess_mime(req_path)[0] or "application/octet-stream",
) )
logmsg += str(status) + logtail logmsg += unicode(status) + logtail
if self.mode == "HEAD" or not do_send: if self.mode == "HEAD" or not do_send:
self.log(logmsg) self.log(logmsg)
@@ -1020,7 +1031,7 @@ class HttpCli(object):
remains = sendfile_py(lower, upper, f, self.s) remains = sendfile_py(lower, upper, f, self.s)
if remains > 0: if remains > 0:
logmsg += " \033[31m" + str(upper - remains) + "\033[0m" logmsg += " \033[31m" + unicode(upper - remains) + "\033[0m"
spd = self._spd((upper - lower) - remains) spd = self._spd((upper - lower) - remains)
self.log("{}, {}".format(logmsg, spd)) self.log("{}, {}".format(logmsg, spd))
@@ -1067,7 +1078,7 @@ class HttpCli(object):
sz_html = len(template.render(**targs).encode("utf-8")) sz_html = len(template.render(**targs).encode("utf-8"))
self.send_headers(sz_html + sz_md, status) self.send_headers(sz_html + sz_md, status)
logmsg += str(status) logmsg += unicode(status)
if self.mode == "HEAD" or not do_send: if self.mode == "HEAD" or not do_send:
self.log(logmsg) self.log(logmsg)
return True return True
@@ -1081,7 +1092,7 @@ class HttpCli(object):
self.log(logmsg + " \033[31md/c\033[0m") self.log(logmsg + " \033[31md/c\033[0m")
return False return False
self.log(logmsg + " " + str(len(html))) self.log(logmsg + " " + unicode(len(html)))
return True return True
def tx_mounts(self): def tx_mounts(self):
@@ -1115,7 +1126,8 @@ class HttpCli(object):
excl = None excl = None
if target: if target:
excl, target = (target.split("/", 1) + [""])[:2] excl, target = (target.split("/", 1) + [""])[:2]
ret["k" + excl] = self.gen_tree("/".join([top, excl]).strip("/"), target) sub = self.gen_tree("/".join([top, excl]).strip("/"), target)
ret["k" + quotep(excl)] = sub
try: try:
vn, rem = self.auth.vfs.get(top, self.uname, True, False) vn, rem = self.auth.vfs.get(top, self.uname, True, False)
@@ -1136,7 +1148,7 @@ class HttpCli(object):
vfs_ls = exclude_dotfiles(vfs_ls) vfs_ls = exclude_dotfiles(vfs_ls)
for fn in [x for x in vfs_ls if x != excl]: for fn in [x for x in vfs_ls if x != excl]:
dirs.append(fn) dirs.append(quotep(fn))
for x in vfs_virt.keys(): for x in vfs_virt.keys():
if x != excl: if x != excl:
@@ -1275,21 +1287,26 @@ class HttpCli(object):
del f["rd"] del f["rd"]
if icur: if icur:
q = "select w from up where rd = ? and fn = ?" q = "select w from up where rd = ? and fn = ?"
try:
r = icur.execute(q, (rd, fn)).fetchone() r = icur.execute(q, (rd, fn)).fetchone()
except:
args = s3enc(idx.mem_cur, rd, fn)
r = icur.execute(q, args).fetchone()
tags = {}
f["tags"] = tags
if not r: if not r:
continue continue
w = r[0][:16] w = r[0][:16]
tags = {}
q = "select k, v from mt where w = ? and k != 'x'" q = "select k, v from mt where w = ? and k != 'x'"
for k, v in icur.execute(q, (w,)): for k, v in icur.execute(q, (w,)):
taglist[k] = True taglist[k] = True
tags[k] = v tags[k] = v
f["tags"] = tags
if icur: if icur:
taglist = [k for k in self.args.mte.split(",") if k in taglist] taglist = [k for k in vn.flags["mte"].split(",") if k in taglist]
for f in dirs: for f in dirs:
f["tags"] = {} f["tags"] = {}
@@ -1297,7 +1314,7 @@ class HttpCli(object):
try: try:
if not self.args.nih: if not self.args.nih:
srv_info.append(str(socket.gethostname()).split(".")[0]) srv_info.append(unicode(socket.gethostname()).split(".")[0])
except: except:
self.log("#wow #whoa") self.log("#wow #whoa")
pass pass
@@ -1362,7 +1379,7 @@ class HttpCli(object):
ts=ts, ts=ts,
perms=json.dumps(perms), perms=json.dumps(perms),
taglist=taglist, taglist=taglist,
tag_order=json.dumps(self.args.mte.split(",")), tag_order=json.dumps(vn.flags["mte"].split(",")),
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),
logues=logues, logues=logues,

View File

@@ -81,8 +81,8 @@ class HttpConn(object):
def respath(self, res_name): def respath(self, res_name):
return os.path.join(E.mod, "web", res_name) return os.path.join(E.mod, "web", res_name)
def log(self, msg): def log(self, msg, c=0):
self.log_func(self.log_src, msg) self.log_func(self.log_src, msg, c)
def get_u2idx(self): def get_u2idx(self):
if not self.u2idx: if not self.u2idx:
@@ -129,7 +129,7 @@ class HttpConn(object):
if is_https: if is_https:
if self.sr: if self.sr:
self.log("\033[1;31mTODO: cannot do https in jython\033[0m") self.log("TODO: cannot do https in jython", c="1;31")
return return
self.log_src = self.log_src.replace("[36m", "[35m") self.log_src = self.log_src.replace("[36m", "[35m")
@@ -180,7 +180,7 @@ class HttpConn(object):
pass pass
else: else:
self.log("\033[35mhandshake\033[0m " + em) self.log("handshake\033[0m " + em, c=5)
return return

View File

@@ -38,7 +38,9 @@ class HttpSrv(object):
def accept(self, sck, addr): def accept(self, sck, addr):
"""takes an incoming tcp connection and creates a thread to handle it""" """takes an incoming tcp connection and creates a thread to handle it"""
self.log("%s %s" % addr, "\033[1;30m|%sC-cthr\033[0m" % ("-" * 5,)) if self.args.log_conn:
self.log("%s %s" % addr, "|%sC-cthr" % ("-" * 5,), c="1;30")
thr = threading.Thread(target=self.thr_client, args=(sck, addr)) thr = threading.Thread(target=self.thr_client, args=(sck, addr))
thr.daemon = True thr.daemon = True
thr.start() thr.start()
@@ -66,11 +68,15 @@ class HttpSrv(object):
thr.start() thr.start()
try: try:
self.log("%s %s" % addr, "\033[1;30m|%sC-crun\033[0m" % ("-" * 6,)) if self.args.log_conn:
self.log("%s %s" % addr, "|%sC-crun" % ("-" * 6,), c="1;30")
cli.run() cli.run()
finally: finally:
self.log("%s %s" % addr, "\033[1;30m|%sC-cdone\033[0m" % ("-" * 7,)) if self.args.log_conn:
self.log("%s %s" % addr, "|%sC-cdone" % ("-" * 7,), c="1;30")
try: try:
sck.shutdown(socket.SHUT_RDWR) sck.shutdown(socket.SHUT_RDWR)
sck.close() sck.close()
@@ -78,7 +84,8 @@ class HttpSrv(object):
if not MACOS: if not MACOS:
self.log( self.log(
"%s %s" % addr, "%s %s" % addr,
"\033[1;30mshut({}): {}\033[0m".format(sck.fileno(), ex), "shut({}): {}".format(sck.fileno(), ex),
c="1;30",
) )
if ex.errno not in [10038, 10054, 107, 57, 9]: if ex.errno not in [10038, 10054, 107, 57, 9]:
# 10038 No longer considered a socket # 10038 No longer considered a socket

View File

@@ -8,7 +8,10 @@ import shutil
import subprocess as sp import subprocess as sp
from .__init__ import PY2, WINDOWS from .__init__ import PY2, WINDOWS
from .util import fsenc, fsdec from .util import fsenc, fsdec, REKOBO_LKEY
if not PY2:
unicode = str
class MTag(object): class MTag(object):
@@ -18,13 +21,14 @@ class MTag(object):
self.prefer_mt = False self.prefer_mt = False
mappings = args.mtm mappings = args.mtm
self.backend = "ffprobe" if args.no_mutagen else "mutagen" self.backend = "ffprobe" if args.no_mutagen else "mutagen"
or_ffprobe = " or ffprobe"
if self.backend == "mutagen": if self.backend == "mutagen":
self.get = self.get_mutagen self.get = self.get_mutagen
try: try:
import mutagen import mutagen
except: except:
self.log("\033[33mcould not load mutagen, trying ffprobe instead") self.log("could not load mutagen, trying ffprobe instead", c=3)
self.backend = "ffprobe" self.backend = "ffprobe"
if self.backend == "ffprobe": if self.backend == "ffprobe":
@@ -32,7 +36,7 @@ class MTag(object):
self.prefer_mt = True self.prefer_mt = True
# about 20x slower # about 20x slower
if PY2: if PY2:
cmd = ["ffprobe", "-version"] cmd = [b"ffprobe", b"-version"]
try: try:
sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE) sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE)
except: except:
@@ -41,9 +45,15 @@ class MTag(object):
if not shutil.which("ffprobe"): if not shutil.which("ffprobe"):
self.usable = False self.usable = False
if self.usable and WINDOWS and sys.version_info < (3, 8):
self.usable = False
or_ffprobe = " or python >= 3.8"
msg = "found ffprobe but your python is too old; need 3.8 or newer"
self.log(msg, c=1)
if not self.usable: if not self.usable:
msg = "\033[31mneed mutagen or ffprobe to read media tags so please run this:\n {} -m pip install --user mutagen \033[0m" msg = "need mutagen{} to read media tags so please run this:\n {} -m pip install --user mutagen"
self.log(msg.format(os.path.basename(sys.executable))) self.log(msg.format(or_ffprobe, os.path.basename(sys.executable)), c=1)
return return
# https://picard-docs.musicbrainz.org/downloads/MusicBrainz_Picard_Tag_Map.html # https://picard-docs.musicbrainz.org/downloads/MusicBrainz_Picard_Tag_Map.html
@@ -115,8 +125,8 @@ class MTag(object):
} }
# self.get = self.compare # self.get = self.compare
def log(self, msg): def log(self, msg, c=0):
self.log_func("mtag", msg) self.log_func("mtag", msg, c)
def normalize_tags(self, ret, md): def normalize_tags(self, ret, md):
for k, v in dict(md).items(): for k, v in dict(md).items():
@@ -133,7 +143,7 @@ class MTag(object):
ret[mk] = [pref, v[0]] ret[mk] = [pref, v[0]]
# take first value # take first value
ret = {k: str(v[1]).strip() for k, v in ret.items()} ret = {k: unicode(v[1]).strip() for k, v in ret.items()}
# track 3/7 => track 3 # track 3/7 => track 3
for k, v in ret.items(): for k, v in ret.items():
@@ -141,6 +151,12 @@ class MTag(object):
v = v.split("/")[0].strip().lstrip("0") v = v.split("/")[0].strip().lstrip("0")
ret[k] = v or 0 ret[k] = v or 0
# normalize key notation to rkeobo
okey = ret.get("key")
if okey:
key = okey.replace(" ", "").replace("maj", "").replace("min", "m")
ret["key"] = REKOBO_LKEY.get(key.lower(), okey)
return ret return ret
def compare(self, abspath): def compare(self, abspath):
@@ -206,7 +222,7 @@ class MTag(object):
return self.normalize_tags(ret, md) return self.normalize_tags(ret, md)
def get_ffprobe(self, abspath): def get_ffprobe(self, abspath):
cmd = ["ffprobe", "-hide_banner", "--", fsenc(abspath)] cmd = [b"ffprobe", b"-hide_banner", b"--", fsenc(abspath)]
p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE) p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE)
r = p.communicate() r = p.communicate()
txt = r[1].decode("utf-8", "replace") txt = r[1].decode("utf-8", "replace")
@@ -215,7 +231,7 @@ class MTag(object):
""" """
note: note:
tags which contain newline will be truncated on first \n, tags which contain newline will be truncated on first \n,
ffmpeg emits \n and spacepads the : to align visually ffprobe emits \n and spacepads the : to align visually
note: note:
the Stream ln always mentions Audio: if audio the Stream ln always mentions Audio: if audio
the Stream ln usually has kb/s, is more accurate the Stream ln usually has kb/s, is more accurate
@@ -285,9 +301,7 @@ class MTag(object):
sec *= 60 sec *= 60
sec += int(f) sec += int(f)
except: except:
self.log( self.log("invalid timestr from ffprobe: [{}]".format(tstr), c=3)
"\033[33minvalid timestr from ffmpeg: [{}]".format(tstr)
)
ret[".dur"] = sec ret[".dur"] = sec
m = ptn_br1.search(ln) m = ptn_br1.search(ln)
@@ -304,3 +318,30 @@ class MTag(object):
ret = {k: [0, v] for k, v in ret.items()} ret = {k: [0, v] for k, v in ret.items()}
return self.normalize_tags(ret, md) return self.normalize_tags(ret, md)
def get_bin(self, parsers, abspath):
pypath = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
pypath = [str(pypath)] + [str(x) for x in sys.path if x]
pypath = str(os.pathsep.join(pypath))
env = os.environ.copy()
env["PYTHONPATH"] = pypath
ret = {}
for tagname, (binpath, timeout) in parsers.items():
try:
cmd = [sys.executable, binpath, abspath]
args = {"env": env, "timeout": timeout}
if WINDOWS:
args["creationflags"] = 0x4000
else:
cmd = ["nice"] + cmd
cmd = [fsenc(x) for x in cmd]
v = sp.check_output(cmd, **args).strip()
if v:
ret[tagname] = v.decode("utf-8")
except:
pass
return ret

View File

@@ -9,7 +9,6 @@ from datetime import datetime, timedelta
import calendar import calendar
from .__init__ import PY2, WINDOWS, MACOS, VT100 from .__init__ import PY2, WINDOWS, MACOS, VT100
from .authsrv import AuthSrv
from .tcpsrv import TcpSrv from .tcpsrv import TcpSrv
from .up2k import Up2k from .up2k import Up2k
from .util import mp from .util import mp
@@ -66,10 +65,10 @@ class SvcHub(object):
self.broker.shutdown() self.broker.shutdown()
print("nailed it") print("nailed it")
def _log_disabled(self, src, msg): def _log_disabled(self, src, msg, c=0):
pass pass
def _log_enabled(self, src, msg): def _log_enabled(self, src, msg, c=0):
"""handles logging from all components""" """handles logging from all components"""
with self.log_mutex: with self.log_mutex:
now = time.time() now = time.time()
@@ -92,6 +91,13 @@ class SvcHub(object):
msg = self.ansi_re.sub("", msg) msg = self.ansi_re.sub("", msg)
if "\033" in src: if "\033" in src:
src = self.ansi_re.sub("", src) src = self.ansi_re.sub("", src)
elif c:
if isinstance(c, int):
msg = "\033[3{}m{}".format(c, msg)
elif "\033" not in c:
msg = "\033[{}m{}\033[0m".format(c, msg)
else:
msg = "{}{}\033[0m".format(c, msg)
ts = datetime.utcfromtimestamp(now).strftime("%H:%M:%S.%f")[:-3] ts = datetime.utcfromtimestamp(now).strftime("%H:%M:%S.%f")[:-3]
msg = fmt.format(ts, src, msg) msg = fmt.format(ts, src, msg)

View File

@@ -68,22 +68,29 @@ class TcpSrv(object):
self.log("tcpsrv", "listening @ {0}:{1}".format(ip, port)) self.log("tcpsrv", "listening @ {0}:{1}".format(ip, port))
while True: while True:
self.log("tcpsrv", "\033[1;30m|%sC-ncli\033[0m" % ("-" * 1,)) if self.args.log_conn:
self.log("tcpsrv", "|%sC-ncli" % ("-" * 1,), c="1;30")
if self.num_clients.v >= self.args.nc: if self.num_clients.v >= self.args.nc:
time.sleep(0.1) time.sleep(0.1)
continue continue
self.log("tcpsrv", "\033[1;30m|%sC-acc1\033[0m" % ("-" * 2,)) if self.args.log_conn:
self.log("tcpsrv", "|%sC-acc1" % ("-" * 2,), c="1;30")
ready, _, _ = select.select(self.srv, [], []) ready, _, _ = select.select(self.srv, [], [])
for srv in ready: for srv in ready:
sck, addr = srv.accept() sck, addr = srv.accept()
sip, sport = srv.getsockname() sip, sport = srv.getsockname()
if self.args.log_conn:
self.log( self.log(
"%s %s" % addr, "%s %s" % addr,
"\033[1;30m|{}C-acc2 \033[0;36m{} \033[3{}m{}".format( "|{}C-acc2 \033[0;36m{} \033[3{}m{}".format(
"-" * 3, sip, sport % 8, sport "-" * 3, sip, sport % 8, sport
), ),
c="1;30",
) )
self.num_clients.add() self.num_clients.add()
self.hub.broker.put(False, "httpconn", sck, addr) self.hub.broker.put(False, "httpconn", sck, addr)

View File

@@ -1,10 +1,13 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import re
import os import os
import time
import threading
from datetime import datetime from datetime import datetime
from .util import u8safe from .util import u8safe, s3dec, html_escape, Pebkac
from .up2k import up2k_wark_from_hashlist from .up2k import up2k_wark_from_hashlist
@@ -19,15 +22,21 @@ class U2idx(object):
def __init__(self, args, log_func): def __init__(self, args, log_func):
self.args = args self.args = args
self.log_func = log_func self.log_func = log_func
self.timeout = args.srch_time
if not HAVE_SQLITE3: if not HAVE_SQLITE3:
self.log("could not load sqlite3; searchign wqill be disabled") self.log("could not load sqlite3; searchign wqill be disabled")
return return
self.cur = {} self.cur = {}
self.mem_cur = sqlite3.connect(":memory:")
self.mem_cur.execute(r"create table a (b text)")
def log(self, msg): self.p_end = None
self.log_func("u2idx", msg) self.p_dur = 0
def log(self, msg, c=0):
self.log_func("u2idx", msg, c)
def fsearch(self, vols, body): def fsearch(self, vols, body):
"""search by up2k hashlist""" """search by up2k hashlist"""
@@ -37,7 +46,14 @@ class U2idx(object):
fsize = body["size"] fsize = body["size"]
fhash = body["hash"] fhash = body["hash"]
wark = up2k_wark_from_hashlist(self.args.salt, fsize, fhash) wark = up2k_wark_from_hashlist(self.args.salt, fsize, fhash)
return self.run_query(vols, "w = ?", [wark], "", [])[0]
uq = "substr(w,1,16) = ? and w = ?"
uv = [wark[:16], wark]
try:
return self.run_query(vols, uq, uv, {})[0]
except Exception as ex:
raise Pebkac(500, repr(ex))
def get_cur(self, ptop): def get_cur(self, ptop):
cur = self.cur.get(ptop) cur = self.cur.get(ptop)
@@ -67,17 +83,64 @@ class U2idx(object):
uq, uv = _sqlize(qobj) uq, uv = _sqlize(qobj)
tq = ""
tv = []
qobj = {} qobj = {}
if "tags" in body: if "tags" in body:
_conv_txt(qobj, body, "tags", "mt.v") _conv_txt(qobj, body, "tags", "mt.v")
tq, tv = _sqlize(qobj)
return self.run_query(vols, uq, uv, tq, tv) if "adv" in body:
_conv_adv(qobj, body, "adv")
def run_query(self, vols, uq, uv, tq, tv): try:
self.log("qs: {} {} , {} {}".format(uq, repr(uv), tq, repr(tv))) return self.run_query(vols, uq, uv, qobj)
except Exception as ex:
raise Pebkac(500, repr(ex))
def run_query(self, vols, uq, uv, targs):
self.log("qs: {} {} , {}".format(uq, repr(uv), repr(targs)))
done_flag = []
self.active_id = "{:.6f}_{}".format(
time.time(), threading.current_thread().ident
)
thr = threading.Thread(
target=self.terminator,
args=(
self.active_id,
done_flag,
),
)
thr.daemon = True
thr.start()
if not targs:
if not uq:
q = "select * from up"
v = ()
else:
q = "select * from up where " + uq
v = tuple(uv)
else:
q = "select up.* from up"
keycmp = "substr(up.w,1,16)"
where = []
v = []
ctr = 0
for tq, tv in sorted(targs.items()):
ctr += 1
tq = tq.split("\n")[0]
keycmp2 = "mt{}.w".format(ctr)
q += " inner join mt mt{} on {} = {}".format(ctr, keycmp, keycmp2)
keycmp = keycmp2
where.append(tq.replace("mt.", keycmp[:-1]))
v.append(tv)
if uq:
where.append(uq)
v.extend(uv)
q += " where " + (" and ".join(where))
# self.log("q2: {} {}".format(q, repr(v)))
ret = [] ret = []
lim = 1000 lim = 1000
@@ -87,18 +150,7 @@ class U2idx(object):
if not cur: if not cur:
continue continue
if not tq: self.active_cur = cur
if not uq:
q = "select * from up"
v = ()
else:
q = "select * from up where " + uq
v = tuple(uv)
else:
# naive assumption: tags first
q = "select up.* from up inner join mt on substr(up.w,1,16) = mt.w where {}"
q = q.format(" and ".join([tq, uq]) if uq else tq)
v = tuple(tv + uv)
sret = [] sret = []
c = cur.execute(q, v) c = cur.execute(q, v)
@@ -108,6 +160,9 @@ class U2idx(object):
if lim <= 0: if lim <= 0:
break break
if rd.startswith("//") or fn.startswith("//"):
rd, fn = s3dec(rd, fn)
rp = os.path.join(vtop, rd, fn).replace("\\", "/") rp = os.path.join(vtop, rd, fn).replace("\\", "/")
sret.append({"ts": int(ts), "sz": sz, "rp": rp, "w": w[:16]}) sret.append({"ts": int(ts), "sz": sz, "rp": rp, "w": w[:16]})
@@ -115,17 +170,35 @@ class U2idx(object):
w = hit["w"] w = hit["w"]
del hit["w"] del hit["w"]
tags = {} tags = {}
q = "select k, v from mt where w = ? and k != 'x'" q2 = "select k, v from mt where w = ? and k != 'x'"
for k, v in cur.execute(q, (w,)): for k, v2 in cur.execute(q2, (w,)):
taglist[k] = True taglist[k] = True
tags[k] = v tags[k] = v2
hit["tags"] = tags hit["tags"] = tags
ret.extend(sret) ret.extend(sret)
done_flag.append(True)
self.active_id = None
# undupe hits from multiple metadata keys
if len(ret) > 1:
ret = [ret[0]] + [
y for x, y in zip(ret[:-1], ret[1:]) if x["rp"] != y["rp"]
]
return ret, list(taglist.keys()) return ret, list(taglist.keys())
def terminator(self, identifier, done_flag):
for _ in range(self.timeout):
time.sleep(1)
if done_flag:
return
if identifier == self.active_id:
self.active_cur.connection.interrupt()
def _open(ptop): def _open(ptop):
db_path = os.path.join(ptop, ".hist", "up2k.db") db_path = os.path.join(ptop, ".hist", "up2k.db")
@@ -181,6 +254,23 @@ def _conv_txt(q, body, k, sql):
q[qk + "\n" + v] = u8safe(v) q[qk + "\n" + v] = u8safe(v)
def _conv_adv(q, body, k):
ptn = re.compile(r"^(\.?[a-z]+) *(==?|!=|<=?|>=?) *(.*)$")
parts = body[k].split(" ")
parts = [x.strip() for x in parts if x.strip()]
for part in parts:
m = ptn.match(part)
if not m:
p = html_escape(part)
raise Pebkac(400, "invalid argument [" + p + "]")
k, op, v = m.groups()
qk = "mt.k = '{}' and mt.v {} ?".format(k, op)
q[qk + "\n" + v] = u8safe(v)
def _sqlize(qobj): def _sqlize(qobj):
keys = [] keys = []
values = [] values = []

View File

@@ -13,6 +13,7 @@ import base64
import hashlib import hashlib
import threading import threading
import traceback import traceback
import subprocess as sp
from copy import deepcopy from copy import deepcopy
from .__init__ import WINDOWS from .__init__ import WINDOWS
@@ -25,9 +26,10 @@ from .util import (
sanitize_fn, sanitize_fn,
ren_open, ren_open,
atomic_move, atomic_move,
w8b64enc, s3enc,
w8b64dec, s3dec,
statdir, statdir,
s2hms,
) )
from .mtag import MTag from .mtag import MTag
from .authsrv import AuthSrv from .authsrv import AuthSrv
@@ -64,14 +66,18 @@ class Up2k(object):
self.flags = {} self.flags = {}
self.cur = {} self.cur = {}
self.mtag = None self.mtag = None
self.n_mtag_thr_alive = 0 self.pending_tags = None
self.n_mtag_tags_added = 0
self.mem_cur = None self.mem_cur = None
self.sqlite_ver = None
self.no_expr_idx = False
if HAVE_SQLITE3: if HAVE_SQLITE3:
# mojibake detector # mojibake detector
self.mem_cur = self._orz(":memory:") self.mem_cur = self._orz(":memory:")
self.mem_cur.execute(r"create table a (b text)") self.mem_cur.execute(r"create table a (b text)")
self.sqlite_ver = tuple([int(x) for x in sqlite3.sqlite_version.split(".")])
if self.sqlite_ver < (3, 9):
self.no_expr_idx = True
if WINDOWS: if WINDOWS:
# usually fails to set lastmod too quickly # usually fails to set lastmod too quickly
@@ -87,7 +93,7 @@ class Up2k(object):
self.log("could not initialize sqlite3, will use in-memory registry only") self.log("could not initialize sqlite3, will use in-memory registry only")
# this is kinda jank # this is kinda jank
auth = AuthSrv(self.args, self.log, False) auth = AuthSrv(self.args, self.log_func, False)
have_e2d = self.init_indexes(auth) have_e2d = self.init_indexes(auth)
if have_e2d: if have_e2d:
@@ -103,31 +109,12 @@ class Up2k(object):
thr.daemon = True thr.daemon = True
thr.start() thr.start()
def log(self, msg): thr = threading.Thread(target=self._run_all_mtp)
self.log_func("up2k", msg + "\033[K") thr.daemon = True
thr.start()
def w8enc(self, rd, fn): def log(self, msg, c=0):
ret = [] self.log_func("up2k", msg + "\033[K", c)
for v in [rd, fn]:
try:
self.mem_cur.execute("select * from a where b = ?", (v,))
ret.append(v)
except:
ret.append("//" + w8b64enc(v))
# self.log("mojien/{} [{}] {}".format(k, v, ret[-1][2:]))
return tuple(ret)
def w8dec(self, rd, fn):
ret = []
for k, v in [["d", rd], ["f", fn]]:
if v.startswith("//"):
ret.append(w8b64dec(v[2:]))
# self.log("mojide/{} [{}] {}".format(k, ret[-1], v[2:]))
else:
ret.append(v)
return tuple(ret)
def _vis_job_progress(self, job): def _vis_job_progress(self, job):
perc = 100 - (len(job["need"]) * 100.0 / len(job["hash"])) perc = 100 - (len(job["need"]) * 100.0 / len(job["hash"]))
@@ -141,24 +128,46 @@ class Up2k(object):
return ret return ret
def _expr_idx_filter(self, flags):
if not self.no_expr_idx:
return False, flags
ret = {k: v for k, v in flags.items() if not k.startswith("e2t")}
if ret.keys() == flags.keys():
return False, flags
return True, ret
def init_indexes(self, auth): def init_indexes(self, auth):
self.pp = ProgressPrinter() self.pp = ProgressPrinter()
vols = auth.vfs.all_vols.values() vols = auth.vfs.all_vols.values()
t0 = time.time() t0 = time.time()
have_e2d = False have_e2d = False
if self.no_expr_idx:
modified = False
for vol in vols:
m, f = self._expr_idx_filter(vol.flags)
if m:
vol.flags = f
modified = True
if modified:
msg = "disabling -e2t because your sqlite belongs in a museum"
self.log(msg, c=3)
live_vols = [] live_vols = []
for vol in vols: for vol in vols:
try: try:
os.listdir(vol.realpath) os.listdir(vol.realpath)
live_vols.append(vol) live_vols.append(vol)
except: except:
self.log("\033[31mcannot access " + vol.realpath) self.log("cannot access " + vol.realpath, c=1)
vols = live_vols vols = live_vols
need_mtag = False need_mtag = False
for vol in auth.vfs.all_vols.values(): for vol in vols:
if "e2t" in vol.flags: if "e2t" in vol.flags:
need_mtag = True need_mtag = True
@@ -204,8 +213,8 @@ class Up2k(object):
self.log(msg.format(len(vols), time.time() - t0)) self.log(msg.format(len(vols), time.time() - t0))
if needed_mutagen: if needed_mutagen:
msg = "\033[31mcould not read tags because no backends are available (mutagen or ffprobe)\033[0m" msg = "could not read tags because no backends are available (mutagen or ffprobe)"
self.log(msg) self.log(msg, c=1)
return have_e2d return have_e2d
@@ -214,6 +223,12 @@ class Up2k(object):
if ptop in self.registry: if ptop in self.registry:
return None return None
_, flags = self._expr_idx_filter(flags)
a = "\033[0;36m{}:\033[1;30m{}"
a = [a.format(k, v) for k, v in sorted(flags.items())]
self.log(" ".join(a) + "\033[0m")
reg = {} reg = {}
path = os.path.join(ptop, ".hist", "up2k.snap") path = os.path.join(ptop, ".hist", "up2k.snap")
if "e2d" in flags and os.path.exists(path): if "e2d" in flags and os.path.exists(path):
@@ -312,7 +327,7 @@ class Up2k(object):
try: try:
c = dbw[0].execute(sql, (rd, fn)) c = dbw[0].execute(sql, (rd, fn))
except: except:
c = dbw[0].execute(sql, self.w8enc(rd, fn)) c = dbw[0].execute(sql, s3enc(self.mem_cur, rd, fn))
in_db = list(c.fetchall()) in_db = list(c.fetchall())
if in_db: if in_db:
@@ -366,7 +381,7 @@ class Up2k(object):
for dwark, dts, dsz, drd, dfn in c: for dwark, dts, dsz, drd, dfn in c:
nchecked += 1 nchecked += 1
if drd.startswith("//") or dfn.startswith("//"): if drd.startswith("//") or dfn.startswith("//"):
drd, dfn = self.w8dec(drd, dfn) drd, dfn = s3dec(drd, dfn)
abspath = os.path.join(top, drd, dfn) abspath = os.path.join(top, drd, dfn)
# almost zero overhead dw # almost zero overhead dw
@@ -430,19 +445,7 @@ class Up2k(object):
mpool = False mpool = False
if self.mtag.prefer_mt and not self.args.no_mtag_mt: if self.mtag.prefer_mt and not self.args.no_mtag_mt:
# mp.pool.ThreadPool and concurrent.futures.ThreadPoolExecutor mpool = self._start_mpool()
# both do crazy runahead so lets reinvent another wheel
nw = os.cpu_count()
if not self.n_mtag_thr_alive:
msg = 'using {} cores for tag reader "{}"'
self.log(msg.format(nw, self.mtag.backend))
self.n_mtag_thr_alive = nw
mpool = Queue(nw)
for _ in range(nw):
thr = threading.Thread(target=self._tag_thr, args=(mpool,))
thr.daemon = True
thr.start()
c2 = cur.connection.cursor() c2 = cur.connection.cursor()
c3 = cur.connection.cursor() c3 = cur.connection.cursor()
@@ -453,16 +456,21 @@ class Up2k(object):
if c2.execute(q, (w[:16],)).fetchone(): if c2.execute(q, (w[:16],)).fetchone():
continue continue
if "mtp" in flags:
q = "insert into mt values (?,'t:mtp','a')"
c2.execute(q, (w[:16],))
if rd.startswith("//") or fn.startswith("//"):
rd, fn = s3dec(rd, fn)
abspath = os.path.join(ptop, rd, fn) abspath = os.path.join(ptop, rd, fn)
self.pp.msg = "c{} {}".format(n_left, abspath) self.pp.msg = "c{} {}".format(n_left, abspath)
args = c3, entags, w, abspath args = [entags, w, abspath]
if not mpool: if not mpool:
n_tags = self._tag_file(*args) n_tags = self._tag_file(c3, *args)
else: else:
mpool.put(args) mpool.put(["mtag"] + args)
with self.mutex: n_tags = len(self._flush_mpool(c3))
n_tags = self.n_mtag_tags_added
self.n_mtag_tags_added = 0
n_add += n_tags n_add += n_tags
n_buf += n_tags n_buf += n_tags
@@ -474,42 +482,270 @@ class Up2k(object):
last_write = time.time() last_write = time.time()
n_buf = 0 n_buf = 0
if self.n_mtag_thr_alive: self._stop_mpool(mpool, c3)
mpool.join()
for _ in range(self.n_mtag_thr_alive):
mpool.put(None)
c3.close() c3.close()
c2.close() c2.close()
return n_add, n_rm, True return n_add, n_rm, True
def _flush_mpool(self, wcur):
with self.mutex:
ret = []
for x in self.pending_tags:
self._tag_file(wcur, *x)
ret.append(x[1])
self.pending_tags = []
return ret
def _run_all_mtp(self):
t0 = time.time()
self.mtp_force = {}
self.mtp_parsers = {}
for ptop, flags in self.flags.items():
if "mtp" in flags:
self._run_one_mtp(ptop)
td = time.time() - t0
msg = "mtp finished in {:.2f} sec ({})"
self.log(msg.format(td, s2hms(td, True)))
def _run_one_mtp(self, ptop):
db_path = os.path.join(ptop, ".hist", "up2k.db")
sz0 = os.path.getsize(db_path) // 1024
entags = self.entags[ptop]
force = {}
timeout = {}
parsers = {}
for parser in self.flags[ptop]["mtp"]:
orig = parser
tag, parser = parser.split("=", 1)
if tag not in entags:
continue
while True:
try:
bp = os.path.expanduser(parser)
if os.path.exists(bp):
parsers[tag] = [bp, timeout.get(tag, 30)]
break
except:
pass
try:
arg, parser = parser.split(",", 1)
arg = arg.lower()
if arg == "f":
force[tag] = True
continue
if arg.startswith("t"):
timeout[tag] = int(arg[1:])
continue
raise Exception()
except:
self.log("invalid argument: " + orig, 1)
return
self.mtp_force[ptop] = force
self.mtp_parsers[ptop] = parsers
q = "select count(w) from mt where k = 't:mtp'"
with self.mutex:
cur = self.cur[ptop]
cur = cur.connection.cursor()
wcur = cur.connection.cursor()
n_left = cur.execute(q).fetchone()[0]
mpool = self._start_mpool()
batch_sz = mpool.maxsize * 3
t_prev = time.time()
n_prev = n_left
n_done = 0
to_delete = {}
in_progress = {}
while True:
with self.mutex:
q = "select w from mt where k = 't:mtp' limit ?"
warks = cur.execute(q, (batch_sz,)).fetchall()
warks = [x[0] for x in warks]
jobs = []
for w in warks:
q = "select rd, fn from up where substr(w,1,16)=? limit 1"
rd, fn = cur.execute(q, (w,)).fetchone()
rd, fn = s3dec(rd, fn)
abspath = os.path.join(ptop, rd, fn)
q = "select k from mt where w = ?"
have = cur.execute(q, (w,)).fetchall()
have = [x[0] for x in have]
if ".dur" not in have and ".dur" in entags:
# skip non-audio
to_delete[w] = True
n_left -= 1
continue
if w in in_progress:
continue
task_parsers = {
k: v for k, v in parsers.items() if k in force or k not in have
}
jobs.append([task_parsers, None, w, abspath])
in_progress[w] = True
done = self._flush_mpool(wcur)
with self.mutex:
for w in done:
to_delete[w] = True
in_progress.pop(w)
n_done += 1
for w in to_delete.keys():
q = "delete from mt where w = ? and k = 't:mtp'"
cur.execute(q, (w,))
to_delete = {}
if not warks:
break
if not jobs:
continue
try:
now = time.time()
s = ((now - t_prev) / (n_prev - n_left)) * n_left
h, s = divmod(s, 3600)
m, s = divmod(s, 60)
n_prev = n_left
t_prev = now
except:
h = 1
m = 1
msg = "mtp: {} done, {} left, eta {}h {:02d}m"
with self.mutex:
msg = msg.format(n_done, n_left, int(h), int(m))
self.log(msg, c=6)
for j in jobs:
n_left -= 1
mpool.put(j)
with self.mutex:
cur.connection.commit()
done = self._stop_mpool(mpool, wcur)
with self.mutex:
for w in done:
q = "delete from mt where w = ? and k = 't:mtp'"
cur.execute(q, (w,))
cur.connection.commit()
if n_done:
self.vac(cur, db_path, n_done, 0, sz0)
wcur.close()
cur.close()
def _start_mpool(self):
if WINDOWS and False:
nah = open(os.devnull, "wb")
wmic = "processid={}".format(os.getpid())
wmic = ["wmic", "process", "where", wmic, "call", "setpriority"]
sp.call(wmic + ["below normal"], stdout=nah, stderr=nah)
# mp.pool.ThreadPool and concurrent.futures.ThreadPoolExecutor
# both do crazy runahead so lets reinvent another wheel
nw = os.cpu_count() if hasattr(os, "cpu_count") else 4
if self.pending_tags is None:
self.log("using {}x {}".format(nw, self.mtag.backend))
self.pending_tags = []
mpool = Queue(nw)
for _ in range(nw):
thr = threading.Thread(target=self._tag_thr, args=(mpool,))
thr.daemon = True
thr.start()
return mpool
def _stop_mpool(self, mpool, wcur):
if not mpool:
return
for _ in range(mpool.maxsize):
mpool.put(None)
mpool.join()
done = self._flush_mpool(wcur)
if WINDOWS and False:
nah = open(os.devnull, "wb")
wmic = "processid={}".format(os.getpid())
wmic = ["wmic", "process", "where", wmic, "call", "setpriority"]
sp.call(wmic + ["below normal"], stdout=nah, stderr=nah)
return done
def _tag_thr(self, q): def _tag_thr(self, q):
while True: while True:
task = q.get() task = q.get()
if not task: if not task:
break q.task_done()
return
try: try:
write_cur, entags, wark, abspath = task parser, entags, wark, abspath = task
if parser == "mtag":
tags = self.mtag.get(abspath) tags = self.mtag.get(abspath)
else:
tags = self.mtag.get_bin(parser, abspath)
vtags = [
"\033[36m{} \033[33m{}".format(k, v) for k, v in tags.items()
]
self.log("{}\033[0m [{}]".format(" ".join(vtags), abspath))
with self.mutex: with self.mutex:
n = self._tag_file(write_cur, entags, wark, abspath, tags) self.pending_tags.append([entags, wark, abspath, tags])
self.n_mtag_tags_added += n
except: except:
with self.mutex: ex = traceback.format_exc()
self.n_mtag_thr_alive -= 1 if parser == "mtag":
raise parser = self.mtag.backend
finally:
msg = "{} failed to read tags from {}:\n{}"
self.log(msg.format(parser, abspath, ex), c=3)
q.task_done() q.task_done()
def _tag_file(self, write_cur, entags, wark, abspath, tags=None): def _tag_file(self, write_cur, entags, wark, abspath, tags=None):
tags = tags or self.mtag.get(abspath) if tags is None:
tags = self.mtag.get(abspath)
if entags:
tags = {k: v for k, v in tags.items() if k in entags} tags = {k: v for k, v in tags.items() if k in entags}
if not tags: if not tags:
# indicate scanned without tags # indicate scanned without tags
tags = {"x": 0} tags = {"x": 0}
if not tags:
return 0
for k in tags.keys():
q = "delete from mt where w = ? and ({})".format(
" or ".join(["k = ?"] * len(tags))
)
args = [wark[:16]] + list(tags.keys())
write_cur.execute(q, tuple(args))
ret = 0 ret = 0
for k, v in tags.items(): for k, v in tags.items():
q = "insert into mt values (?,?,?)" q = "insert into mt values (?,?,?)"
@@ -520,6 +756,7 @@ class Up2k(object):
def _orz(self, db_path): def _orz(self, db_path):
return sqlite3.connect(db_path, check_same_thread=False).cursor() return sqlite3.connect(db_path, check_same_thread=False).cursor()
# x.set_trace_callback(trace)
def _open_db(self, db_path): def _open_db(self, db_path):
existed = os.path.exists(db_path) existed = os.path.exists(db_path)
@@ -615,8 +852,12 @@ class Up2k(object):
except: except:
pass pass
idx = r"create index up_w on up(substr(w,1,16))"
if self.no_expr_idx:
idx = r"create index up_w on up(w)"
for cmd in [ for cmd in [
r"create index up_w on up(substr(w,1,16))", idx,
r"create table mt (w text, k text, v int)", r"create table mt (w text, k text, v int)",
r"create index mt_w on mt(w)", r"create index mt_w on mt(w)",
r"create index mt_k on mt(k)", r"create index mt_k on mt(k)",
@@ -661,12 +902,17 @@ class Up2k(object):
cur = self.cur.get(cj["ptop"], None) cur = self.cur.get(cj["ptop"], None)
reg = self.registry[cj["ptop"]] reg = self.registry[cj["ptop"]]
if cur: if cur:
if self.no_expr_idx:
q = r"select * from up where w = ?"
argv = (wark,)
else:
q = r"select * from up where substr(w,1,16) = ? and w = ?" q = r"select * from up where substr(w,1,16) = ? and w = ?"
argv = (wark[:16], wark) argv = (wark[:16], wark)
cur = cur.execute(q, argv) cur = cur.execute(q, argv)
for _, dtime, dsize, dp_dir, dp_fn in cur: for _, dtime, dsize, dp_dir, dp_fn in cur:
if dp_dir.startswith("//") or dp_fn.startswith("//"): if dp_dir.startswith("//") or dp_fn.startswith("//"):
dp_dir, dp_fn = self.w8dec(dp_dir, dp_fn) dp_dir, dp_fn = s3dec(dp_dir, dp_fn)
dp_abs = os.path.join(cj["ptop"], dp_dir, dp_fn).replace("\\", "/") dp_abs = os.path.join(cj["ptop"], dp_dir, dp_fn).replace("\\", "/")
# relying on path.exists to return false on broken symlinks # relying on path.exists to return false on broken symlinks
@@ -788,8 +1034,13 @@ class Up2k(object):
raise OSError() raise OSError()
elif fs1 == fs2: elif fs1 == fs2:
# same fs; make symlink as relative as possible # same fs; make symlink as relative as possible
nsrc = src.replace("\\", "/").split("/") v = []
ndst = dst.replace("\\", "/").split("/") for p in [src, dst]:
if WINDOWS:
p = p.replace("\\", "/")
v.append(p.split("/"))
nsrc, ndst = v
nc = 0 nc = 0
for a, b in zip(nsrc, ndst): for a, b in zip(nsrc, ndst):
if a != b: if a != b:
@@ -797,7 +1048,8 @@ class Up2k(object):
nc += 1 nc += 1
if nc > 1: if nc > 1:
lsrc = nsrc[nc:] lsrc = nsrc[nc:]
lsrc = "../" * (len(lsrc) - 1) + "/".join(lsrc) hops = len(ndst[nc:]) - 1
lsrc = "../" * hops + "/".join(lsrc)
os.symlink(fsenc(lsrc), fsenc(ldst)) os.symlink(fsenc(lsrc), fsenc(ldst))
except (AttributeError, OSError) as ex: except (AttributeError, OSError) as ex:
self.log("cannot symlink; creating copy: " + repr(ex)) self.log("cannot symlink; creating copy: " + repr(ex))
@@ -882,7 +1134,7 @@ class Up2k(object):
try: try:
db.execute(sql, (rd, fn)) db.execute(sql, (rd, fn))
except: except:
db.execute(sql, self.w8enc(rd, fn)) db.execute(sql, s3enc(self.mem_cur, rd, fn))
def db_add(self, db, wark, rd, fn, ts, sz): def db_add(self, db, wark, rd, fn, ts, sz):
sql = "insert into up values (?,?,?,?,?)" sql = "insert into up values (?,?,?,?,?)"
@@ -890,7 +1142,7 @@ class Up2k(object):
try: try:
db.execute(sql, v) db.execute(sql, v)
except: except:
rd, fn = self.w8enc(rd, fn) rd, fn = s3enc(self.mem_cur, rd, fn)
v = (wark, ts, sz, rd, fn) v = (wark, ts, sz, rd, fn)
db.execute(sql, v) db.execute(sql, v)
@@ -919,7 +1171,7 @@ class Up2k(object):
ret = [] ret = []
with open(path, "rb", 512 * 1024) as f: with open(path, "rb", 512 * 1024) as f:
while fsz > 0: while fsz > 0:
self.pp.msg = "{} MB".format(int(fsz / 1024 / 1024)) self.pp.msg = "{} MB, {}".format(int(fsz / 1024 / 1024), path)
hashobj = hashlib.sha512() hashobj = hashlib.sha512()
rem = min(csz, fsz) rem = min(csz, fsz)
fsz -= rem fsz -= rem
@@ -1029,24 +1281,36 @@ class Up2k(object):
def _tagger(self): def _tagger(self):
while True: while True:
ptop, wark, rd, fn = self.tagq.get() ptop, wark, rd, fn = self.tagq.get()
if "e2t" not in self.flags[ptop]:
continue
abspath = os.path.join(ptop, rd, fn) abspath = os.path.join(ptop, rd, fn)
self.log("tagging " + abspath) tags = self.mtag.get(abspath)
ntags1 = len(tags)
if self.mtp_parsers.get(ptop, {}):
parser = {
k: v
for k, v in self.mtp_parsers[ptop].items()
if k in self.mtp_force[ptop] or k not in tags
}
tags.update(self.mtag.get_bin(parser, abspath))
with self.mutex: with self.mutex:
cur = self.cur[ptop] cur = self.cur[ptop]
if not cur: if not cur:
self.log("\033[31mno cursor to write tags with??") self.log("no cursor to write tags with??", c=1)
continue continue
entags = self.entags[ptop] entags = self.entags[ptop]
if not entags: if not entags:
self.log("\033[33mno entags okay.jpg") self.log("no entags okay.jpg", c=3)
continue continue
if "e2t" in self.flags[ptop]: self._tag_file(cur, entags, wark, abspath, tags)
self._tag_file(cur, entags, wark, abspath)
cur.connection.commit() cur.connection.commit()
self.log("tagged {} ({}+{})".format(abspath, ntags1, len(tags) - ntags1))
def _hasher(self): def _hasher(self):
while True: while True:
ptop, rd, fn = self.hashq.get() ptop, rd, fn = self.hashq.get()

View File

@@ -10,6 +10,7 @@ import select
import struct import struct
import hashlib import hashlib
import platform import platform
import traceback
import threading import threading
import mimetypes import mimetypes
import contextlib import contextlib
@@ -56,11 +57,58 @@ HTTPCODE = {
413: "Payload Too Large", 413: "Payload Too Large",
416: "Requested Range Not Satisfiable", 416: "Requested Range Not Satisfiable",
422: "Unprocessable Entity", 422: "Unprocessable Entity",
429: "Too Many Requests",
500: "Internal Server Error", 500: "Internal Server Error",
501: "Not Implemented", 501: "Not Implemented",
} }
IMPLICATIONS = [
["e2dsa", "e2ds"],
["e2ds", "e2d"],
["e2tsr", "e2ts"],
["e2ts", "e2t"],
["e2t", "e2d"],
]
REKOBO_KEY = {
v: ln.split(" ", 1)[0]
for ln in """
1B 6d B
2B 7d Gb F#
3B 8d Db C#
4B 9d Ab G#
5B 10d Eb D#
6B 11d Bb A#
7B 12d F
8B 1d C
9B 2d G
10B 3d D
11B 4d A
12B 5d E
1A 6m Abm G#m
2A 7m Ebm D#m
3A 8m Bbm A#m
4A 9m Fm
5A 10m Cm
6A 11m Gm
7A 12m Dm
8A 1m Am
9A 2m Em
10A 3m Bm
11A 4m Gbm F#m
12A 5m Dbm C#m
""".strip().split(
"\n"
)
for v in ln.strip().split(" ")[1:]
if v
}
REKOBO_LKEY = {k.lower(): v for k, v in REKOBO_KEY.items()}
class Counter(object): class Counter(object):
def __init__(self, v=0): def __init__(self, v=0):
self.v = v self.v = v
@@ -119,19 +167,51 @@ class ProgressPrinter(threading.Thread):
continue continue
msg = self.msg msg = self.msg
m = " {}\033[K\r".format(msg) uprint(" {}\033[K\r".format(msg))
try:
print(m, end="")
except UnicodeEncodeError:
try:
print(m.encode("utf-8", "replace").decode(), end="")
except:
print(m.encode("ascii", "replace").decode(), end="")
print("\033[K", end="") print("\033[K", end="")
sys.stdout.flush() # necessary on win10 even w/ stderr btw sys.stdout.flush() # necessary on win10 even w/ stderr btw
def uprint(msg):
try:
print(msg, end="")
except UnicodeEncodeError:
try:
print(msg.encode("utf-8", "replace").decode(), end="")
except:
print(msg.encode("ascii", "replace").decode(), end="")
def nuprint(msg):
uprint("{}\n".format(msg))
def rice_tid():
tid = threading.current_thread().ident
c = struct.unpack(b"B" * 5, struct.pack(b">Q", tid)[-5:])
return "".join("\033[1;37;48;5;{}m{:02x}".format(x, x) for x in c) + "\033[0m"
def trace(*args, **kwargs):
t = time.time()
stack = "".join(
"\033[36m{}\033[33m{}".format(x[0].split(os.sep)[-1][:-3], x[1])
for x in traceback.extract_stack()[3:-1]
)
parts = ["{:.6f}".format(t), rice_tid(), stack]
if args:
parts.append(repr(args))
if kwargs:
parts.append(repr(kwargs))
msg = "\033[0m ".join(parts)
# _tracebuf.append(msg)
nuprint(msg)
@contextlib.contextmanager @contextlib.contextmanager
def ren_open(fname, *args, **kwargs): def ren_open(fname, *args, **kwargs):
fdir = kwargs.pop("fdir", None) fdir = kwargs.pop("fdir", None)
@@ -470,6 +550,16 @@ def get_spd(nbyte, t0, t=None):
return "{} \033[0m{}/s\033[0m".format(s1, s2) return "{} \033[0m{}/s\033[0m".format(s1, s2)
def s2hms(s, optional_h=False):
s = int(s)
h, s = divmod(s, 3600)
m, s = divmod(s, 60)
if not h and optional_h:
return "{}:{:02}".format(m, s)
return "{}:{:02}:{:02}".format(h, m, s)
def undot(path): def undot(path):
ret = [] ret = []
for node in path.split("/"): for node in path.split("/"):
@@ -597,6 +687,31 @@ else:
fsdec = w8dec fsdec = w8dec
def s3enc(mem_cur, rd, fn):
ret = []
for v in [rd, fn]:
try:
mem_cur.execute("select * from a where b = ?", (v,))
ret.append(v)
except:
ret.append("//" + w8b64enc(v))
# self.log("mojien/{} [{}] {}".format(k, v, ret[-1][2:]))
return tuple(ret)
def s3dec(rd, fn):
ret = []
for k, v in [["d", rd], ["f", fn]]:
if v.startswith("//"):
ret.append(w8b64dec(v[2:]))
# self.log("mojide/{} [{}] {}".format(k, ret[-1], v[2:]))
else:
ret.append(v)
return tuple(ret)
def atomic_move(src, dst): def atomic_move(src, dst):
if not PY2: if not PY2:
os.replace(src, dst) os.replace(src, dst)
@@ -734,7 +849,8 @@ def statdir(logger, scandir, lstat, top):
try: try:
yield [fsdec(fh.name), fh.stat(follow_symlinks=not lstat)] yield [fsdec(fh.name), fh.stat(follow_symlinks=not lstat)]
except Exception as ex: except Exception as ex:
logger("scan-stat: {} @ {}".format(repr(ex), fsdec(fh.path))) msg = "scan-stat: \033[36m{} @ {}"
logger(msg.format(repr(ex), fsdec(fh.path)))
else: else:
src = "listdir" src = "listdir"
fun = os.lstat if lstat else os.stat fun = os.lstat if lstat else os.stat
@@ -743,9 +859,11 @@ def statdir(logger, scandir, lstat, top):
try: try:
yield [fsdec(name), fun(abspath)] yield [fsdec(name), fun(abspath)]
except Exception as ex: except Exception as ex:
logger("list-stat: {} @ {}".format(repr(ex), fsdec(abspath))) msg = "list-stat: \033[36m{} @ {}"
logger(msg.format(repr(ex), fsdec(abspath)))
except Exception as ex: except Exception as ex:
logger("{}: {} @ {}".format(src, repr(ex), top)) logger("{}: \033[31m{} @ {}".format(src, repr(ex), top))
def unescape_cookie(orig): def unescape_cookie(orig):
@@ -802,7 +920,11 @@ def chkcmd(*argv):
def gzip_orig_sz(fn): def gzip_orig_sz(fn):
with open(fsenc(fn), "rb") as f: with open(fsenc(fn), "rb") as f:
f.seek(-4, 2) f.seek(-4, 2)
return struct.unpack(b"I", f.read(4))[0] rv = f.read(4)
try:
return struct.unpack(b"I", rv)[0]
except:
return struct.unpack("I", rv)[0]
def py_desc(): def py_desc():
@@ -812,7 +934,11 @@ def py_desc():
if ofs > 0: if ofs > 0:
py_ver = py_ver[:ofs] py_ver = py_ver[:ofs]
try:
bitness = struct.calcsize(b"P") * 8 bitness = struct.calcsize(b"P") * 8
except:
bitness = struct.calcsize("P") * 8
host_os = platform.system() host_os = platform.system()
compiler = platform.python_compiler() compiler = platform.python_compiler()

View File

@@ -42,12 +42,8 @@ body {
#path #entree { #path #entree {
margin-left: -.7em; margin-left: -.7em;
} }
#treetab {
display: none;
}
#files { #files {
border-spacing: 0; border-spacing: 0;
margin-top: 2em;
z-index: 1; z-index: 1;
position: relative; position: relative;
} }
@@ -55,11 +51,10 @@ body {
display: block; display: block;
padding: .3em 0; padding: .3em 0;
} }
#files[ts] tbody div a { #files tbody div a {
color: #f5a; color: #f5a;
} }
a, a, #files tbody div a:last-child {
#files[ts] tbody div a:last-child {
color: #fc5; color: #fc5;
padding: .2em; padding: .2em;
text-decoration: none; text-decoration: none;
@@ -158,6 +153,15 @@ a,
.logue { .logue {
padding: .2em 1.5em; padding: .2em 1.5em;
} }
.logue:empty {
display: none;
}
#pro.logue {
margin-bottom: .8em;
}
#epi.logue {
margin: .8em 0;
}
#srv_info { #srv_info {
opacity: .5; opacity: .5;
font-size: .8em; font-size: .8em;
@@ -401,7 +405,7 @@ input[type="checkbox"]:checked+label {
#op_search table { #srch_form {
border: 1px solid #3a3a3a; border: 1px solid #3a3a3a;
box-shadow: 0 0 1em #222 inset; box-shadow: 0 0 1em #222 inset;
background: #2d2d2d; background: #2d2d2d;
@@ -410,14 +414,25 @@ input[type="checkbox"]:checked+label {
margin-bottom: 0; margin-bottom: 0;
padding: 0 .5em .5em 0; padding: 0 .5em .5em 0;
} }
#srch_form table {
display: inline-block;
}
#srch_form td { #srch_form td {
padding: .6em .6em; padding: .6em .6em;
} }
#srch_form td:first-child {
width: 3em;
padding-right: .2em;
text-align: right;
}
#op_search input { #op_search input {
margin: 0; margin: 0;
} }
#srch_q { #srch_q {
white-space: pre; white-space: pre;
color: #f80;
height: 1em;
margin: .2em 0 -1em 1.6em;
} }
#files td div span { #files td div span {
color: #fff; color: #fff;
@@ -443,13 +458,42 @@ input[type="checkbox"]:checked+label {
#files td div a:last-child { #files td div a:last-child {
width: 100%; width: 100%;
} }
#tree, #wrap {
#treefiles { margin-top: 2em;
vertical-align: top;
} }
#tree { #tree {
padding-top: 2em; display: none;
position: fixed;
left: 0;
bottom: 0;
top: 7em;
padding-top: .2em;
overflow-y: auto;
-ms-scroll-chaining: none;
overscroll-behavior-y: none;
scrollbar-color: #eb0 #333;
} }
#thx_ff {
padding: 5em 0;
}
#tree::-webkit-scrollbar-track {
background: #333;
}
#tree::-webkit-scrollbar {
background: #333;
}
#tree::-webkit-scrollbar-thumb {
background: #eb0;
}
#tree:hover {
z-index: 2;
}
#treeul {
position: relative;
left: -1.7em;
width: calc(100% + 1.3em);
}
.tglbtn,
#tree>a+a { #tree>a+a {
padding: .2em .4em; padding: .2em .4em;
font-size: 1.2em; font-size: 1.2em;
@@ -460,9 +504,11 @@ input[type="checkbox"]:checked+label {
position: relative; position: relative;
top: -.2em; top: -.2em;
} }
.tglbtn:hover,
#tree>a+a:hover { #tree>a+a:hover {
background: #805; background: #805;
} }
.tglbtn.on,
#tree>a+a.on { #tree>a+a.on {
background: #fc4; background: #fc4;
color: #400; color: #400;
@@ -472,24 +518,22 @@ input[type="checkbox"]:checked+label {
padding: .3em .5em; padding: .3em .5em;
font-size: 1.5em; font-size: 1.5em;
} }
#treefiles #files tbody {
border-radius: 0 .7em 0 .7em;
}
#treefiles #files thead th:nth-child(1) {
border-radius: .7em 0 0 0;
}
#tree ul, #tree ul,
#tree li { #tree li {
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
#tree ul { #tree ul {
border-left: .2em solid #444; border-left: .2em solid #555;
} }
#tree li { #tree li {
margin-left: 1em; margin-left: 1em;
list-style: none; list-style: none;
white-space: nowrap; border-top: 1px solid #4c4c4c;
border-bottom: 1px solid #222;
}
#tree li:last-child {
border-bottom: none;
} }
#treeul a.hl { #treeul a.hl {
color: #400; color: #400;
@@ -503,24 +547,12 @@ input[type="checkbox"]:checked+label {
#treeul a+a { #treeul a+a {
width: calc(100% - 2em); width: calc(100% - 2em);
background: #333; background: #333;
line-height: 1em;
} }
#treeul a+a:hover { #treeul a+a:hover {
background: #222; background: #222;
color: #fff; color: #fff;
} }
#treeul {
position: relative;
overflow: hidden;
left: -1.7em;
}
#treeul:hover {
z-index: 2;
overflow: visible;
}
#treeul:hover a+a {
width: auto;
min-width: calc(100% - 2em);
}
#treeul a:first-child { #treeul a:first-child {
font-family: monospace, monospace; font-family: monospace, monospace;
} }
@@ -579,3 +611,44 @@ input[type="checkbox"]:checked+label {
color: #300; color: #300;
background: #fea; background: #fea;
} }
#op_cfg {
max-width: none;
margin-right: 1.5em;
}
#op_cfg>div>a {
line-height: 2em;
}
#op_cfg>div>span {
display: inline-block;
padding: .2em .4em;
}
#op_cfg h3 {
margin: .8em 0 0 .6em;
padding: 0;
border-bottom: 1px solid #555;
}
#opdesc {
display: none;
}
#ops:hover #opdesc {
display: block;
background: linear-gradient(0deg,#555, #4c4c4c 80%, #444);
box-shadow: 0 .3em 1em #222;
padding: 1em;
border-radius: .3em;
position: absolute;
z-index: 3;
top: 6em;
right: 1.5em;
}
#ops:hover #opdesc.off {
display: none;
}
#opdesc code {
background: #3c3c3c;
padding: .2em .3em;
border-top: 1px solid #777;
border-radius: .3em;
font-family: monospace, monospace;
line-height: 2em;
}

View File

@@ -12,29 +12,41 @@
<body> <body>
<div id="ops"> <div id="ops">
<a href="#" data-dest="">---</a> <a href="#" data-dest="" data-desc="close submenu">---</a>
<a href="#" data-perm="read" data-dest="search">🔎</a> <a href="#" data-perm="read" data-dest="search" data-desc="search for files by attributes, path/name, music tags, or any combination of those.&lt;br /&gt;&lt;br /&gt;&lt;code&gt;foo bar&lt;/code&gt; = must contain both foo and bar,&lt;br /&gt;&lt;code&gt;foo -bar&lt;/code&gt; = must contain foo but not bar,&lt;br /&gt;&lt;code&gt;^yana .opus$&lt;/code&gt; = must start with yana and have the opus extension">🔎</a>
{%- if have_up2k_idx %} {%- if have_up2k_idx %}
<a href="#" data-dest="up2k">🚀</a> <a href="#" data-dest="up2k" data-desc="up2k: upload files (if you have write-access) or toggle into the search-mode and drag files onto the search button to see if they exist somewhere on the server">🚀</a>
{%- else %} {%- else %}
<a href="#" data-perm="write" data-dest="up2k">🚀</a> <a href="#" data-perm="write" data-dest="up2k" data-desc="up2k: upload files with resume support (close your browser and drop the same files in later)">🚀</a>
{%- endif %} {%- endif %}
<a href="#" data-perm="write" data-dest="bup">🎈</a> <a href="#" data-perm="write" data-dest="bup" data-desc="bup: basic uploader, even supports netscape 4.0">🎈</a>
<a href="#" data-perm="write" data-dest="mkdir">📂</a> <a href="#" data-perm="write" data-dest="mkdir" data-desc="mkdir: create a new directory">📂</a>
<a href="#" data-perm="write" data-dest="new_md">📝</a> <a href="#" data-perm="write" data-dest="new_md" data-desc="new-md: create a new markdown document">📝</a>
<a href="#" data-perm="write" data-dest="msg">📟</a> <a href="#" data-perm="write" data-dest="msg" data-desc="msg: send a message to the server log">📟</a>
<a href="#" data-dest="cfg" data-desc="configuration options">⚙️</a>
<div id="opdesc"></div>
</div> </div>
<div id="op_search" class="opview"> <div id="op_search" class="opview">
{%- if have_tags_idx %} {%- if have_tags_idx %}
<table id="srch_form" class="tags"></table> <div id="srch_form" class="tags"></div>
{%- else %} {%- else %}
<table id="srch_form"></table> <div id="srch_form"></div>
{%- endif %} {%- endif %}
<div id="srch_q"></div> <div id="srch_q"></div>
</div> </div>
{%- include 'upload.html' %} {%- include 'upload.html' %}
<div id="op_cfg" class="opview opbox">
<h3>key notation</h3>
<div id="key_notation"></div>
<h3>tooltips</h3>
<div>
<a id="tooltips" class="tglbtn" href="#">enable</a>
</div>
</div>
<h1 id="path"> <h1 id="path">
<a href="#" id="entree">🌲</a> <a href="#" id="entree">🌲</a>
{%- for n in vpnodes %} {%- for n in vpnodes %}
@@ -42,36 +54,34 @@
{%- endfor %} {%- endfor %}
</h1> </h1>
<div id="pro" class="logue">{{ logues[0] }}</div> <div id="tree">
<table id="treetab">
<tr>
<td id="tree">
<a href="#" id="detree">🍞...</a> <a href="#" id="detree">🍞...</a>
<a href="#" step="2" id="twobytwo">+</a> <a href="#" step="2" id="twobytwo">+</a>
<a href="#" step="-2" id="twig">&ndash;</a> <a href="#" step="-2" id="twig">&ndash;</a>
<a href="#" id="dyntree">a</a> <a href="#" class="tglbtn" id="dyntree">a</a>
<ul id="treeul"></ul> <ul id="treeul"></ul>
</td> <div id="thx_ff">&nbsp;</div>
<td id="treefiles"></td> </div>
</tr>
</table> <div id="wrap">
<div id="pro" class="logue">{{ logues[0] }}</div>
<table id="files"> <table id="files">
<thead> <thead>
<tr> <tr>
<th></th> <th></th>
<th><span>File Name</span></th> <th name="href"><span>File Name</span></th>
<th sort="int"><span>Size</span></th> <th name="sz" sort="int"><span>Size</span></th>
{%- for k in taglist %} {%- for k in taglist %}
{%- if k.startswith('.') %} {%- if k.startswith('.') %}
<th sort="int"><span>{{ k[1:] }}</span></th> <th name="tags/{{ k }}" sort="int"><span>{{ k[1:] }}</span></th>
{%- else %} {%- else %}
<th><span>{{ k[0]|upper }}{{ k[1:] }}</span></th> <th name="tags/{{ k }}"><span>{{ k[0]|upper }}{{ k[1:] }}</span></th>
{%- endif %} {%- endif %}
{%- endfor %} {%- endfor %}
<th><span>T</span></th> <th name="ext"><span>T</span></th>
<th><span>Date</span></th> <th name="ts"><span>Date</span></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -93,6 +103,8 @@
<h2><a href="?h">control-panel</a></h2> <h2><a href="?h">control-panel</a></h2>
</div>
{%- if srv_info %} {%- if srv_info %}
<div id="srv_info"><span>{{ srv_info }}</span></div> <div id="srv_info"><span>{{ srv_info }}</span></div>
{%- endif %} {%- endif %}

File diff suppressed because it is too large Load Diff

View File

@@ -65,7 +65,7 @@ function statify(obj) {
if (a > 0) if (a > 0)
loc.push(n[a]); loc.push(n[a]);
var dec = hesc(decodeURIComponent(n[a])); var dec = hesc(uricom_dec(n[a])[0]);
nav.push('<a href="/' + loc.join('/') + '">' + dec + '</a>'); nav.push('<a href="/' + loc.join('/') + '">' + dec + '</a>');
} }

View File

@@ -15,7 +15,7 @@ var dom_md = ebi('mt');
if (a > 0) if (a > 0)
loc.push(n[a]); loc.push(n[a]);
var dec = decodeURIComponent(n[a]).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); var dec = uricom_dec(n[a])[0].replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
nav.push('<a href="/' + loc.join('/') + '">' + dec + '</a>'); nav.push('<a href="/' + loc.join('/') + '">' + dec + '</a>');
} }

View File

@@ -46,9 +46,9 @@ function up2k_flagbus() {
var dbg = function (who, msg) { var dbg = function (who, msg) {
console.log('flagbus(' + flag.id + '): [' + who + '] ' + msg); console.log('flagbus(' + flag.id + '): [' + who + '] ' + msg);
}; };
flag.ch.onmessage = function (ev) { flag.ch.onmessage = function (e) {
var who = ev.data[0], var who = e.data[0],
what = ev.data[1]; what = e.data[1];
if (who == flag.id) { if (who == flag.id) {
dbg(who, 'hi me (??)'); dbg(who, 'hi me (??)');
@@ -83,7 +83,7 @@ function up2k_flagbus() {
flag.ch.postMessage([flag.id, "hey"]); flag.ch.postMessage([flag.id, "hey"]);
} }
else { else {
dbg('?', ev.data); dbg('?', e.data);
} }
}; };
var tx = function (now, msg) { var tx = function (now, msg) {
@@ -194,7 +194,7 @@ function up2k_init(have_crypto) {
// handle user intent to use the basic uploader instead // handle user intent to use the basic uploader instead
ebi('u2nope').onclick = function (e) { ebi('u2nope').onclick = function (e) {
e.preventDefault(); ev(e);
setmsg(); setmsg();
goto('bup'); goto('bup');
}; };
@@ -254,29 +254,29 @@ function up2k_init(have_crypto) {
} }
ebi('u2btn').addEventListener('click', nav, false); ebi('u2btn').addEventListener('click', nav, false);
function ondrag(ev) { function ondrag(e) {
ev.stopPropagation(); e.stopPropagation();
ev.preventDefault(); e.preventDefault();
ev.dataTransfer.dropEffect = 'copy'; e.dataTransfer.dropEffect = 'copy';
ev.dataTransfer.effectAllowed = 'copy'; e.dataTransfer.effectAllowed = 'copy';
} }
ebi('u2btn').addEventListener('dragover', ondrag, false); ebi('u2btn').addEventListener('dragover', ondrag, false);
ebi('u2btn').addEventListener('dragenter', ondrag, false); ebi('u2btn').addEventListener('dragenter', ondrag, false);
function gotfile(ev) { function gotfile(e) {
ev.stopPropagation(); e.stopPropagation();
ev.preventDefault(); e.preventDefault();
var files; var files;
var is_itemlist = false; var is_itemlist = false;
if (ev.dataTransfer) { if (e.dataTransfer) {
if (ev.dataTransfer.items) { if (e.dataTransfer.items) {
files = ev.dataTransfer.items; // DataTransferItemList files = e.dataTransfer.items; // DataTransferItemList
is_itemlist = true; is_itemlist = true;
} }
else files = ev.dataTransfer.files; // FileList else files = e.dataTransfer.files; // FileList
} }
else files = ev.target.files; else files = e.target.files;
if (files.length == 0) if (files.length == 0)
return alert('no files selected??'); return alert('no files selected??');
@@ -332,7 +332,7 @@ function up2k_init(have_crypto) {
"name": fobj.name, "name": fobj.name,
"size": fobj.size, "size": fobj.size,
"lmod": lmod / 1000, "lmod": lmod / 1000,
"purl": get_vpath(), "purl": get_evpath(),
"done": false, "done": false,
"hash": [] "hash": []
}; };
@@ -655,8 +655,8 @@ function up2k_init(have_crypto) {
prog(t.n, nchunk, col_hashing); prog(t.n, nchunk, col_hashing);
}; };
var segm_load = function (ev) { var segm_load = function (e) {
cache_buf = ev.target.result; cache_buf = e.target.result;
cache_ofs = 0; cache_ofs = 0;
hash_calc(); hash_calc();
}; };
@@ -730,7 +730,7 @@ function up2k_init(have_crypto) {
st.busy.handshake.push(t); st.busy.handshake.push(t);
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.onload = function (ev) { xhr.onload = function (e) {
if (xhr.status == 200) { if (xhr.status == 200) {
var response = JSON.parse(xhr.responseText); var response = JSON.parse(xhr.responseText);
@@ -881,7 +881,7 @@ function up2k_init(have_crypto) {
alert('y o u b r o k e i t\n\n(was that a folder? just files please)'); alert('y o u b r o k e i t\n\n(was that a folder? just files please)');
}; };
reader.onload = function (ev) { reader.onload = function (e) {
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.upload.onprogress = function (xev) { xhr.upload.onprogress = function (xev) {
var perc = xev.loaded / (cdr - car) * 100; var perc = xev.loaded / (cdr - car) * 100;
@@ -915,7 +915,7 @@ function up2k_init(have_crypto) {
xhr.setRequestHeader('Content-Type', 'application/octet-stream'); xhr.setRequestHeader('Content-Type', 'application/octet-stream');
xhr.overrideMimeType('Content-Type', 'application/octet-stream'); xhr.overrideMimeType('Content-Type', 'application/octet-stream');
xhr.responseType = 'text'; xhr.responseType = 'text';
xhr.send(ev.target.result); xhr.send(e.target.result);
}; };
reader.readAsArrayBuffer(bobslice.call(t.fobj, car, cdr)); reader.readAsArrayBuffer(bobslice.call(t.fobj, car, cdr));
@@ -944,7 +944,7 @@ function up2k_init(have_crypto) {
/// config ui /// config ui
// //
function onresize(ev) { function onresize(e) {
var bar = ebi('ops'), var bar = ebi('ops'),
wpx = innerWidth, wpx = innerWidth,
fpx = parseInt(getComputedStyle(bar)['font-size']), fpx = parseInt(getComputedStyle(bar)['font-size']),
@@ -959,17 +959,17 @@ function up2k_init(have_crypto) {
ebi('u2conf').setAttribute('class', wide ? 'has_btn' : ''); ebi('u2conf').setAttribute('class', wide ? 'has_btn' : '');
} }
} }
window.onresize = onresize; window.addEventListener('resize', onresize);
onresize(); onresize();
function desc_show(ev) { function desc_show(e) {
var msg = this.getAttribute('alt'); var msg = this.getAttribute('alt');
msg = msg.replace(/\$N/g, "<br />"); msg = msg.replace(/\$N/g, "<br />");
var cdesc = ebi('u2cdesc'); var cdesc = ebi('u2cdesc');
cdesc.innerHTML = msg; cdesc.innerHTML = msg;
cdesc.setAttribute('class', 'show'); cdesc.setAttribute('class', 'show');
} }
function desc_hide(ev) { function desc_hide(e) {
ebi('u2cdesc').setAttribute('class', ''); ebi('u2cdesc').setAttribute('class', '');
} }
var o = document.querySelectorAll('#u2conf *[alt]'); var o = document.querySelectorAll('#u2conf *[alt]');
@@ -1084,17 +1084,17 @@ function up2k_init(have_crypto) {
} }
} }
function nop(ev) { function nop(e) {
ev.preventDefault(); ev(e);
this.click(); this.click();
} }
ebi('nthread_add').onclick = function (ev) { ebi('nthread_add').onclick = function (e) {
ev.preventDefault(); ev(e);
bumpthread(1); bumpthread(1);
}; };
ebi('nthread_sub').onclick = function (ev) { ebi('nthread_sub').onclick = function (e) {
ev.preventDefault(); ev(e);
bumpthread(-1); bumpthread(-1);
}; };

View File

@@ -62,7 +62,7 @@
width: calc(100% - 2em); width: calc(100% - 2em);
max-width: 100em; max-width: 100em;
} }
#u2form.srch #u2tab { #op_up2k.srch #u2tab {
max-width: none; max-width: none;
} }
#u2tab td { #u2tab td {
@@ -76,7 +76,7 @@
#u2tab td:nth-child(3) { #u2tab td:nth-child(3) {
width: 40%; width: 40%;
} }
#u2form.srch #u2tab td:nth-child(3) { #op_up2k.srch #u2tab td:nth-child(3) {
font-family: sans-serif; font-family: sans-serif;
width: auto; width: auto;
} }

View File

@@ -23,6 +23,7 @@ function esc(txt) {
} }
function vis_exh(msg, url, lineNo, columnNo, error) { function vis_exh(msg, url, lineNo, columnNo, error) {
window.onerror = undefined; window.onerror = undefined;
window['vis_exh'] = null;
var html = ['<h1>you hit a bug!</h1><p>please screenshot this error and send me a copy arigathanks gozaimuch (ed/irc.rizon.net or ed#2644)</p><p>', var html = ['<h1>you hit a bug!</h1><p>please screenshot this error and send me a copy arigathanks gozaimuch (ed/irc.rizon.net or ed#2644)</p><p>',
esc(String(msg)), '</p><p>', esc(url + ' @' + lineNo + ':' + columnNo), '</p>']; esc(String(msg)), '</p><p>', esc(url + ' @' + lineNo + ':' + columnNo), '</p>'];
@@ -90,7 +91,29 @@ function import_js(url, cb) {
} }
function sortTable(table, col) { var crctab = (function () {
var c, tab = [];
for (var n = 0; n < 256; n++) {
c = n;
for (var k = 0; k < 8; k++) {
c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
}
tab[n] = c;
}
return tab;
})();
function crc32(str) {
var crc = 0 ^ (-1);
for (var i = 0; i < str.length; i++) {
crc = (crc >>> 8) ^ crctab[(crc ^ str.charCodeAt(i)) & 0xFF];
}
return ((crc ^ (-1)) >>> 0).toString(16);
};
function sortTable(table, col, cb) {
var tb = table.tBodies[0], var tb = table.tBodies[0],
th = table.tHead.rows[0].cells, th = table.tHead.rows[0].cells,
tr = Array.prototype.slice.call(tb.rows, 0), tr = Array.prototype.slice.call(tb.rows, 0),
@@ -99,6 +122,27 @@ function sortTable(table, col) {
th[a].className = th[a].className.replace(/ *sort-?1 */, " "); th[a].className = th[a].className.replace(/ *sort-?1 */, " ");
th[col].className += ' sort' + reverse; th[col].className += ' sort' + reverse;
var stype = th[col].getAttribute('sort'); var stype = th[col].getAttribute('sort');
try {
var nrules = [], rules = jread("fsort", []);
rules.unshift([th[col].getAttribute('name'), reverse, stype || '']);
for (var a = 0; a < rules.length; a++) {
var add = true;
for (var b = 0; b < a; b++)
if (rules[a][0] == rules[b][0])
add = false;
if (add)
nrules.push(rules[a]);
if (nrules.length >= 10)
break;
}
jwrite("fsort", nrules);
}
catch (ex) {
console.log("failed to persist sort rules, resetting: " + ex);
jwrite("fsort", null);
}
var vl = []; var vl = [];
for (var a = 0; a < tr.length; a++) { for (var a = 0; a < tr.length; a++) {
var cell = tr[a].cells[col]; var cell = tr[a].cells[col];
@@ -126,8 +170,9 @@ function sortTable(table, col) {
return reverse * (a.localeCompare(b)); return reverse * (a.localeCompare(b));
}); });
for (i = 0; i < tr.length; ++i) tb.appendChild(tr[vl[i][1]]); for (i = 0; i < tr.length; ++i) tb.appendChild(tr[vl[i][1]]);
if (cb) cb();
} }
function makeSortable(table) { function makeSortable(table, cb) {
var th = table.tHead, i; var th = table.tHead, i;
th && (th = th.rows[0]) && (th = th.cells); th && (th = th.rows[0]) && (th = th.cells);
if (th) i = th.length; if (th) i = th.length;
@@ -135,7 +180,7 @@ function makeSortable(table) {
while (--i >= 0) (function (i) { while (--i >= 0) (function (i) {
th[i].onclick = function (e) { th[i].onclick = function (e) {
ev(e); ev(e);
sortTable(table, i); sortTable(table, i, cb);
}; };
}(i)); }(i));
} }
@@ -156,7 +201,7 @@ function opclick(e) {
var dest = this.getAttribute('data-dest'); var dest = this.getAttribute('data-dest');
goto(dest); goto(dest);
swrite('opmode', dest || undefined); swrite('opmode', dest || null);
var input = document.querySelector('.opview.act input:not([type="hidden"])') var input = document.querySelector('.opview.act input:not([type="hidden"])')
if (input) if (input)
@@ -182,6 +227,9 @@ function goto(dest) {
if (fn) if (fn)
fn(); fn();
} }
if (window['treectl'])
treectl.onscroll();
} }
@@ -220,6 +268,31 @@ function linksplit(rp) {
} }
function uricom_enc(txt, do_fb_enc) {
try {
return encodeURIComponent(txt);
}
catch (ex) {
console.log("uce-err [" + txt + "]");
if (do_fb_enc)
return esc(txt);
return txt;
}
}
function uricom_dec(txt) {
try {
return [decodeURIComponent(txt), true];
}
catch (ex) {
console.log("ucd-err [" + txt + "]");
return [txt, false];
}
}
function get_evpath() { function get_evpath() {
var ret = document.location.pathname; var ret = document.location.pathname;
@@ -234,7 +307,7 @@ function get_evpath() {
function get_vpath() { function get_vpath() {
return decodeURIComponent(get_evpath()); return uricom_dec(get_evpath())[0];
} }
@@ -244,6 +317,7 @@ function unix2iso(ts) {
function s2ms(s) { function s2ms(s) {
s = Math.floor(s);
var m = Math.floor(s / 60); var m = Math.floor(s / 60);
return m + ":" + ("0" + (s - m * 60)).slice(-2); return m + ":" + ("0" + (s - m * 60)).slice(-2);
} }
@@ -262,12 +336,12 @@ function sread(key) {
if (window.localStorage) if (window.localStorage)
return localStorage.getItem(key); return localStorage.getItem(key);
return ''; return null;
} }
function swrite(key, val) { function swrite(key, val) {
if (window.localStorage) { if (window.localStorage) {
if (val === undefined) if (val === undefined || val === null)
localStorage.removeItem(key); localStorage.removeItem(key);
else else
localStorage.setItem(key, val); localStorage.setItem(key, val);
@@ -293,7 +367,7 @@ function icfg_get(name, defval) {
var o = ebi(name); var o = ebi(name);
var val = parseInt(sread(name)); var val = parseInt(sread(name));
if (val === null) if (isNaN(val))
return parseInt(o ? o.value : defval); return parseInt(o ? o.value : defval);
if (o) if (o)
@@ -330,19 +404,19 @@ function bcfg_upd_ui(name, val) {
if (o.getAttribute('type') == 'checkbox') if (o.getAttribute('type') == 'checkbox')
o.checked = val; o.checked = val;
else if (o) else if (o) {
o.setAttribute('class', val ? 'on' : ''); var fun = val ? 'add' : 'remove';
o.classList[fun]('on');
}
} }
function hist_push(html, url) { function hist_push(url) {
var key = new Date().getTime(); console.log("h-push " + url);
sessionStorage.setItem(key, html); history.pushState(url, url, url);
history.pushState(key, url, url);
} }
function hist_replace(html, url) { function hist_replace(url) {
var key = new Date().getTime(); console.log("h-repl " + url);
sessionStorage.setItem(key, html); history.replaceState(url, url, url);
history.replaceState(key, url, url);
} }

242
docs/music-analysis.sh Normal file
View File

@@ -0,0 +1,242 @@
#!/bin/bash
echo please dont actually run this as a scriopt
exit 1
# dependency-heavy, not particularly good fit
pacman -S llvm10
python3 -m pip install --user librosa
git clone https://github.com/librosa/librosa.git
# correct bpm for tracks with bad tags
br='
/Trip Trip Trip\(Hardcore Edit\).mp3/ {v=176}
/World!!.BIG_SOS/ {v=175}
/\/08\..*\(BIG_SOS Bootleg\)\.mp3/ {v=175}
/もってけ!セーラ服.Asterisk DnB/ {v=175}
/Rondo\(Asterisk DnB Re.mp3/ {v=175}
/Ray Nautica 175 Edit/ {v=175;x="thunk"}
/TOKIMEKI Language.Jauz/ {v=174}
/YUPPUN Hardcore Remix\).mp3/ {v=174;x="keeps drifting"}
/(èâAâï.î╧ûδ|バーチャリアル.狐耶)J-Core Remix\).mp3/ {v=172;x="hard"}
/lucky train..Freezer/ {v=170}
/Alf zero Bootleg ReMix/ {v=170}
/Prisoner of Love.Kacky/ {v=170}
/火炎 .Qota/ {v=170}
/\(hu-zin Bootleg\)\.mp3/ {v=170}
/15. STRAIGHT BET\(Milynn Bootleg\)\.mp3/ {v=170}
/\/13.*\(Milynn Bootleg\)\.mp3/ {v=167;x="way hard"}
/COLOR PLANET .10SAI . nijikon Remix\)\.mp3/ {v=165}
/11\. (朝はご飯派|Æ⌐é═é▓ö╤öh)\.mp3/ {v=162}
/09\. Where.s the core/ {v=160}
/PLANET\(Koushif Jersey Club Bootleg\)remaster.mp3/ {v=160;x="starts ez turns bs"}
/kened Soul - Madeon x Angel Beats!.mp3/ {v=160}
/Dear Moments\(Mother Harlot Bootleg\)\.mp3/ {v=150}
/POWER.Ringos UKG/ {v=140}
/ブルー・フィールド\(Ringos UKG Remix\).mp3/ {v=135}
/プラチナジェット.Ringo Remix..mp3/ {v=131.2}
/Mirrorball Love \(TKM Bootleg Mix\).mp3/ {v=130}
/Photon Melodies \(TKM Bootleg Mix\).mp3/ {v=128}
/Trap of Love \(TKM Bootleg Mix\).mp3/ {v=128}
/One Step \(TKM Bootleg Mix\)\.mp3/ {v=126}
/04 (トリカムイ岩|âgâèâJâÇâCèΓ).mp3/ {v=125}
/Get your Wish \(NAWN REMIX\)\.mp3/ {v=95}
/Flicker .Nitro Fun/ {v=92}
/\/14\..*suicat Remix/ {v=85.5;x="tricky"}
/Yanagi Nagi - Harumodoki \(EO Remix\)\.mp3/ {v=150}
/Azure - Nicology\.mp3/ {v=128;x="off by 5 how"}
'
# afun host, collects/grades the results
runfun() { cores=8; touch run; rm -f /dev/shm/mres.*; t00=$(date +%s); tbc() { bc | sed -r 's/(\.[0-9]{2}).*/\1/'; }; for ((core=0; core<$cores; core++)); do sqlite3 /mnt/Users/ed/Music/.hist/up2k.db 'select dur.w, dur.v, bpm.v from mt bpm join mt dur on bpm.w = dur.w where bpm.k = ".bpm" and dur.k = ".dur" order by dur.w' | uniq -w16 | while IFS=\| read w dur bpm; do sqlite3 /mnt/Users/ed/Music/.hist/up2k.db "select rd, fn from up where substr(w,1,16) = '$w'" | sed -r "s/^/$bpm /"; done | grep mir/cr | tr \| / | awk '{v=$1;sub(/[^ ]+ /,"")} '"$br"' {printf "%s %s\n",v,$0}' | while read bpm fn; do [ -e run ] || break; n=$((n+1)); ncore=$((n%cores)); [ $ncore -eq $core ] || continue; t0=$(date +%s.%N); (afun || exit 1; t=$(date +%s.%N); td=$(echo "scale=3; $t - $t0" | tbc); bd=$(echo "scale=3; $bpm / $py" | tbc); printf '%4s sec, %4s orig, %6s py, %4s div, %s\n' $td $bpm $py $bd "$fn") | tee -a /dev/shm/mres.$ncore; rv=${PIPESTATUS[0]}; [ $rv -eq 0 ] || { echo "FAULT($rv): $fn"; }; done & done; wait 2>/dev/null; cat /dev/shm/mres.* | awk 'function prt(c) {printf "\033[3%sm%s\033[0m\n",c,$0} $8!="div,"{next} $5!~/^[0-9\.]+/{next} {meta=$3;det=$5;div=meta/det} div<0.7{det/=2} div>1.3{det*=2} {idet=sprintf("%.0f",det)} {idiff=idet-meta} meta>idet{idiff=meta-idet} idiff==0{n0++;prt(6);next} idiff==1{n1++;prt(3);next} idiff>10{nx++;prt(1);next} {n10++;prt(5)} END {printf "ok: %d 1off: %2s (%3s) 10off: %2s (%3s) fail: %2s\n",n0,n1,n0+n1,n10,n0+n1+n10,nx}'; te=$(date +%s); echo $((te-t00)) sec spent; }
# ok: 8 1off: 62 ( 70) 10off: 86 (156) fail: 25 # 105 sec, librosa @ 8c archvm on 3700x w10
# ok: 4 1off: 59 ( 63) 10off: 65 (128) fail: 53 # using original tags (bad)
afun() { ffmpeg -hide_banner -v fatal -nostdin -ss $((dur/3)) -y -i /mnt/Users/ed/Music/"$fn" -t 60 /dev/shm/$core.wav || return 1; py="$(/home/ed/src/librosa/examples/beat_tracker.py /dev/shm/$core.wav x 2>&1 | awk 'BEGIN {v=1} /^Estimated tempo: /{v=$3} END {print v}')"; } runfun
# ok: 119 1off: 5 (124) 10off: 8 (132) fail: 49 # 51 sec, vamp-example-fixedtempo
# ok: 109 1off: 4 (113) 10off: 9 (122) fail: 59 # bad-tags
afun() { ffmpeg -hide_banner -v fatal -nostdin -ss $((dur/3)) -y -i /mnt/Users/ed/Music/"$fn" -ac 1 -ar 22050 -f f32le /dev/shm/$core.pcm || return 1; py="$(python3 -c 'import vamp; import numpy as np; f = open("/dev/shm/'$core'.pcm", "rb"); d = np.fromfile(f, dtype=np.float32); c = vamp.collect(d, 22050, "vamp-example-plugins:fixedtempo", parameters={"maxdflen":40}); print(c["list"][0]["label"].split(" ")[0])')"; }; runfun
# ok: 102 1off: 61 (163) 10off: 12 (175) fail: 6 # 61 sec, vamp-qm-tempotracker
# ok: 80 1off: 48 (128) 10off: 11 (139) fail: 42 # bad-tags
afun() { ffmpeg -hide_banner -v fatal -nostdin -ss $((dur/3)) -y -i /mnt/Users/ed/Music/"$fn" -ac 1 -ar 22050 -f f32le /dev/shm/$core.pcm || return 1; py="$(python3 -c 'import vamp; import numpy as np; f = open("/dev/shm/'$core'.pcm", "rb"); d = np.fromfile(f, dtype=np.float32); c = vamp.collect(d, 22050, "qm-vamp-plugins:qm-tempotracker", parameters={"inputtempo":150}); v = [float(x["label"].split(" ")[0]) for x in c["list"] if x["label"]]; v = list(sorted(v))[len(v)//4:-len(v)//4]; print(round(sum(v) / len(v), 1))')"; }; runfun
# ok: 133 1off: 32 (165) 10off: 12 (177) fail: 3 # 51 sec, vamp-beatroot
# ok: 101 1off: 22 (123) 10off: 16 (139) fail: 39 # bad-tags
# note: some tracks fully fail to analyze (unlike the others which always provide a guess)
afun() { ffmpeg -hide_banner -v fatal -nostdin -ss $((dur/3)) -y -i /mnt/Users/ed/Music/"$fn" -ac 1 -ar 22050 -f f32le /dev/shm/$core.pcm || return 1; py="$(python3 -c 'import vamp; import numpy as np; f = open("/dev/shm/'$core'.pcm", "rb"); d = np.fromfile(f, dtype=np.float32); c = vamp.collect(d, 22050, "beatroot-vamp:beatroot"); cl=c["list"]; print(round(60*((len(cl)-1)/(float(cl[-1]["timestamp"]-cl[1]["timestamp"]))), 2))')"; }; runfun
# ok: 124 1off: 9 (133) 10off: 40 (173) fail: 8 # 231 sec, essentia/full
# ok: 109 1off: 8 (117) 10off: 22 (139) fail: 42 # bad-tags
afun() { ffmpeg -hide_banner -v fatal -nostdin -ss $((dur/3)) -y -i /mnt/Users/ed/Music/"$fn" -ac 1 -ar 44100 /dev/shm/$core.wav || return 1; py="$(python3 -c 'import essentia; import essentia.standard as es; fe, fef = es.MusicExtractor(lowlevelStats=["mean", "stdev"], rhythmStats=["mean", "stdev"], tonalStats=["mean", "stdev"])("/dev/shm/'$core'.wav"); print("{:.2f}".format(fe["rhythm.bpm"]))')"; }; runfun
# ok: 113 1off: 18 (131) 10off: 46 (177) fail: 4 # 134 sec, essentia/re2013
# ok: 101 1off: 15 (116) 10off: 26 (142) fail: 39 # bad-tags
afun() { ffmpeg -hide_banner -v fatal -nostdin -ss $((dur/3)) -y -i /mnt/Users/ed/Music/"$fn" -ac 1 -ar 44100 /dev/shm/$core.wav || return 1; py="$(python3 -c 'from essentia.standard import *; a=MonoLoader(filename="/dev/shm/'$core'.wav")(); bpm,beats,confidence,_,intervals=RhythmExtractor2013(method="multifeature")(a); print("{:.2f}".format(bpm))')"; }; runfun
########################################################################
##
## key detectyion
##
########################################################################
# console scriptlet reusing keytabs from browser.js
var m=''; for (var a=0; a<24; a++) m += 's/\\|(' + maps["traktor_sharps"][a].trim() + "|" + maps["rekobo_classic"][a].trim() + "|" + maps["traktor_musical"][a].trim() + "|" + maps["traktor_open"][a].trim() + ')$/|' + maps["rekobo_alnum"][a].trim() + '/;'; console.log(m);
# translate to camelot
re='s/\|(B|B|B|6d)$/|1B/;s/\|(F#|F#|Gb|7d)$/|2B/;s/\|(C#|Db|Db|8d)$/|3B/;s/\|(G#|Ab|Ab|9d)$/|4B/;s/\|(D#|Eb|Eb|10d)$/|5B/;s/\|(A#|Bb|Bb|11d)$/|6B/;s/\|(F|F|F|12d)$/|7B/;s/\|(C|C|C|1d)$/|8B/;s/\|(G|G|G|2d)$/|9B/;s/\|(D|D|D|3d)$/|10B/;s/\|(A|A|A|4d)$/|11B/;s/\|(E|E|E|5d)$/|12B/;s/\|(G#m|Abm|Abm|6m)$/|1A/;s/\|(D#m|Ebm|Ebm|7m)$/|2A/;s/\|(A#m|Bbm|Bbm|8m)$/|3A/;s/\|(Fm|Fm|Fm|9m)$/|4A/;s/\|(Cm|Cm|Cm|10m)$/|5A/;s/\|(Gm|Gm|Gm|11m)$/|6A/;s/\|(Dm|Dm|Dm|12m)$/|7A/;s/\|(Am|Am|Am|1m)$/|8A/;s/\|(Em|Em|Em|2m)$/|9A/;s/\|(Bm|Bm|Bm|3m)$/|10A/;s/\|(F#m|F#m|Gbm|4m)$/|11A/;s/\|(C#m|Dbm|Dbm|5m)$/|12A/;'
# runner/wrapper
runfun() { cores=8; touch run; tbc() { bc | sed -r 's/(\.[0-9]{2}).*/\1/'; }; for ((core=0; core<$cores; core++)); do sqlite3 /mnt/Users/ed/Music/.hist/up2k.db 'select dur.w, dur.v, key.v from mt key join mt dur on key.w = dur.w where key.k = "key" and dur.k = ".dur" order by dur.w' | uniq -w16 | grep -vE '(Off-Key|None)$' | sed -r "s/ //g;$re" | uniq -w16 | while IFS=\| read w dur bpm; do sqlite3 /mnt/Users/ed/Music/.hist/up2k.db "select rd, fn from up where substr(w,1,16) = '$w'" | sed -r "s/^/$bpm /"; done| grep mir/cr | tr \| / | while read key fn; do [ -e run ] || break; n=$((n+1)); ncore=$((n%cores)); [ $ncore -eq $core ] || continue; t0=$(date +%s.%N); (afun || exit 1; t=$(date +%s.%N); td=$(echo "scale=3; $t - $t0" | tbc); [ "$key" = "$py" ] && c=2 || c=5; printf '%4s sec, %4s orig, \033[3%dm%4s py,\033[0m %s\n' $td "$key" $c "$py" "$fn") || break; done & done; time wait 2>/dev/null; }
# ok: 26 1off: 10 2off: 1 fail: 3 # 15 sec, keyfinder
afun() { ffmpeg -hide_banner -v fatal -nostdin -ss $((dur/3)) -y -i /mnt/Users/ed/Music/"$fn" -ac 1 -ar 44100 -t 60 /dev/shm/$core.wav || break; py="$(python3 -c 'import sys; import keyfinder; print(keyfinder.key(sys.argv[1]).camelot())' "/dev/shm/$core.wav")"; }; runfun
# https://github.com/MTG/essentia/raw/master/src/examples/tutorial/example_key_by_steps_streaming.py
# https://essentia.upf.edu/reference/std_Key.html # edma edmm braw bgate
sed -ri 's/^(key = Key\().*/\1profileType="bgate")/' example_key_by_steps_streaming.py
afun() { ffmpeg -hide_banner -v fatal -nostdin -ss $((dur/3)) -y -i /mnt/Users/ed/Music/"$fn" -ac 1 -ar 44100 -t 60 /dev/shm/$core.wav || break; py="$(python3 example_key_by_steps_streaming.py /dev/shm/$core.{wav,yml} 2>/dev/null | sed -r "s/ major//;s/ minor/m/;s/^/|/;$re;s/.//")"; }; runfun
########################################################################
##
## misc
##
########################################################################
python3 -m pip install --user vamp
import librosa
d, r = librosa.load('/dev/shm/0.wav')
d.dtype
# dtype('float32')
d.shape
# (1323000,)
d
# array([-1.9614939e-08, 1.8037968e-08, -1.4106059e-08, ...,
# 1.2024145e-01, 2.7462116e-01, 1.6202132e-01], dtype=float32)
import vamp
c = vamp.collect(d, r, "vamp-example-plugins:fixedtempo")
c
# {'list': [{'timestamp': 0.005804988, 'duration': 9.999092971, 'label': '110.0 bpm', 'values': array([109.98116], dtype=float32)}]}
ffmpeg -ss 48 -i /mnt/Users/ed/Music/mir/cr-a/'I Beg You(ths Bootleg).wav' -ac 1 -ar 22050 -f f32le -t 60 /dev/shm/f32.pcm
import numpy as np
f = open('/dev/shm/f32.pcm', 'rb')
d = np.fromfile(f, dtype=np.float32)
d
array([-0.17803933, -0.27206388, -0.41586545, ..., -0.04940119,
-0.0267825 , -0.03564296], dtype=float32)
d = np.reshape(d, [1, -1])
d
array([[-0.17803933, -0.27206388, -0.41586545, ..., -0.04940119,
-0.0267825 , -0.03564296]], dtype=float32)
import vampyhost
print("\n".join(vampyhost.list_plugins()))
mvamp:marsyas_bextract_centroid
mvamp:marsyas_bextract_lpcc
mvamp:marsyas_bextract_lsp
mvamp:marsyas_bextract_mfcc
mvamp:marsyas_bextract_rolloff
mvamp:marsyas_bextract_scf
mvamp:marsyas_bextract_sfm
mvamp:marsyas_bextract_zero_crossings
mvamp:marsyas_ibt
mvamp:zerocrossing
qm-vamp-plugins:qm-adaptivespectrogram
qm-vamp-plugins:qm-barbeattracker
qm-vamp-plugins:qm-chromagram
qm-vamp-plugins:qm-constantq
qm-vamp-plugins:qm-dwt
qm-vamp-plugins:qm-keydetector
qm-vamp-plugins:qm-mfcc
qm-vamp-plugins:qm-onsetdetector
qm-vamp-plugins:qm-segmenter
qm-vamp-plugins:qm-similarity
qm-vamp-plugins:qm-tempotracker
qm-vamp-plugins:qm-tonalchange
qm-vamp-plugins:qm-transcription
vamp-aubio:aubiomelenergy
vamp-aubio:aubiomfcc
vamp-aubio:aubionotes
vamp-aubio:aubioonset
vamp-aubio:aubiopitch
vamp-aubio:aubiosilence
vamp-aubio:aubiospecdesc
vamp-aubio:aubiotempo
vamp-example-plugins:amplitudefollower
vamp-example-plugins:fixedtempo
vamp-example-plugins:percussiononsets
vamp-example-plugins:powerspectrum
vamp-example-plugins:spectralcentroid
vamp-example-plugins:zerocrossing
vamp-rubberband:rubberband
plug = vampyhost.load_plugin("vamp-example-plugins:fixedtempo", 22050, 0)
plug.info
{'apiVersion': 2, 'pluginVersion': 1, 'identifier': 'fixedtempo', 'name': 'Simple Fixed Tempo Estimator', 'description': 'Study a short section of audio and estimate its tempo, assuming the tempo is constant', 'maker': 'Vamp SDK Example Plugins', 'copyright': 'Code copyright 2008 Queen Mary, University of London. Freely redistributable (BSD license)'}
plug = vampyhost.load_plugin("qm-vamp-plugins:qm-tempotracker", 22050, 0)
from pprint import pprint; pprint(plug.parameters)
for c in plug.parameters: print("{} \033[36m{} [\033[33m{}\033[36m] = {}\033[0m".format(c["identifier"], c["name"], "\033[36m, \033[33m".join(c["valueNames"]), c["valueNames"][int(c["defaultValue"])])) if "valueNames" in c else print("{} \033[36m{} [\033[33m{}..{}\033[36m] = {}\033[0m".format(c["identifier"], c["name"], c["minValue"], c["maxValue"], c["defaultValue"]))
beatroot-vamp:beatroot
cl=c["list"]; 60*((len(cl)-1)/(float(cl[-1]["timestamp"]-cl[1]["timestamp"])))
ffmpeg -ss 48 -i /mnt/Users/ed/Music/mir/cr-a/'I Beg You(ths Bootleg).wav' -ac 1 -ar 22050 -f f32le -t 60 /dev/shm/f32.pcm
# 128 bpm, key 5A Cm
import vamp
import numpy as np
f = open('/dev/shm/f32.pcm', 'rb')
d = np.fromfile(f, dtype=np.float32)
c = vamp.collect(d, 22050, "vamp-example-plugins:fixedtempo", parameters={"maxdflen":40})
c["list"][0]["label"]
# 127.6 bpm
c = vamp.collect(d, 22050, "qm-vamp-plugins:qm-tempotracker", parameters={"inputtempo":150})
print("\n".join([v["label"] for v in c["list"] if v["label"]]))
v = [float(x["label"].split(' ')[0]) for x in c["list"] if x["label"]]
v = list(sorted(v))[len(v)//4:-len(v)//4]
v = sum(v) / len(v)
# 128.1 bpm

View File

@@ -11,6 +11,13 @@ gzip -d < .hist/up2k.snap | jq -r '.[].tnam' | while IFS= read -r f; do rm -f --
gzip -d < .hist/up2k.snap | jq -r '.[].name' | while IFS= read -r f; do wc -c -- "$f" | grep -qiE '^[^0-9a-z]*0' && rm -f -- "$f"; done gzip -d < .hist/up2k.snap | jq -r '.[].name' | while IFS= read -r f; do wc -c -- "$f" | grep -qiE '^[^0-9a-z]*0' && rm -f -- "$f"; done
##
## detect partial uploads based on file contents
## (in case of context loss or old copyparties)
echo; find -type f | while IFS= read -r x; do printf '\033[A\033[36m%s\033[K\033[0m\n' "$x"; tail -c$((1024*1024)) <"$x" | xxd -a | awk 'NR==1&&/^[0: ]+.{16}$/{next} NR==2&&/^\*$/{next} NR==3&&/^[0f]+: [0 ]+65 +.{16}$/{next} {e=1} END {exit e}' || continue; printf '\033[A\033[31msus:\033[33m %s \033[0m\n\n' "$x"; done
## ##
## create a test payload ## create a test payload
@@ -60,6 +67,33 @@ wget -S --header='Accept-Encoding: gzip' -U 'MSIE 6.0; SV1' http://127.0.0.1:392
shab64() { sp=$1; f="$2"; v=0; sz=$(stat -c%s "$f"); while true; do w=$((v+sp*1024*1024)); printf $(tail -c +$((v+1)) "$f" | head -c $((w-v)) | sha512sum | cut -c-64 | sed -r 's/ .*//;s/(..)/\\x\1/g') | base64 -w0 | cut -c-43 | tr '+/' '-_'; v=$w; [ $v -lt $sz ] || break; done; } shab64() { sp=$1; f="$2"; v=0; sz=$(stat -c%s "$f"); while true; do w=$((v+sp*1024*1024)); printf $(tail -c +$((v+1)) "$f" | head -c $((w-v)) | sha512sum | cut -c-64 | sed -r 's/ .*//;s/(..)/\\x\1/g') | base64 -w0 | cut -c-43 | tr '+/' '-_'; v=$w; [ $v -lt $sz ] || break; done; }
##
## poll url for performance issues
command -v gdate && date() { gdate "$@"; }; while true; do t=$(date +%s.%N); (time wget http://127.0.0.1:3923/?ls -qO- | jq -C '.files[]|{sz:.sz,ta:.tags.artist,tb:.tags.".bpm"}|del(.[]|select(.==null))' | awk -F\" '/"/{t[$2]++} END {for (k in t){v=t[k];p=sprintf("%" (v+1) "s",v);gsub(/ /,"#",p);printf "\033[36m%s\033[33m%s ",k,p}}') 2>&1 | awk -v ts=$t 'NR==1{t1=$0} NR==2{sub(/.*0m/,"");sub(/s$/,"");t2=$0;c=2; if(t2>0.3){c=3} if(t2>0.8){c=1} } END{sub(/[0-9]{6}$/,"",ts);printf "%s \033[3%dm%s %s\033[0m\n",ts,c,t2,t1}'; sleep 0.1 || break; done
##
## sqlite3 stuff
# find dupe metadata keys
sqlite3 up2k.db 'select mt1.w, mt1.k, mt1.v, mt2.v from mt mt1 inner join mt mt2 on mt1.w = mt2.w where mt1.k = mt2.k and mt1.rowid != mt2.rowid'
# partial reindex by deleting all tags for a list of files
time sqlite3 up2k.db 'select mt1.w from mt mt1 inner join mt mt2 on mt1.w = mt2.w where mt1.k = +mt2.k and mt1.rowid != mt2.rowid' > warks
cat warks | while IFS= read -r x; do sqlite3 up2k.db "delete from mt where w = '$x'"; done
##
## media
# split track into test files
e=6; s=10; d=~/dev/copyparty/srv/aus; n=1; p=0; e=$((e*60)); rm -rf $d; mkdir $d; while true; do ffmpeg -hide_banner -ss $p -i 'nervous_testpilot - office.mp3' -c copy -t $s $d/$(printf %04d $n).mp3; n=$((n+1)); p=$((p+s)); [ $p -gt $e ] && break; done
-v srv/aus:aus:r:ce2dsa:ce2ts:cmtp=fgsfds=bin/mtag/sleep.py
sqlite3 .hist/up2k.db 'select * from mt where k="fgsfds" or k="t:mtp"' | tee /dev/stderr | wc -l
## ##
## vscode ## vscode
@@ -89,6 +123,9 @@ for d in /usr /var; do find $d -type f -size +30M 2>/dev/null; done | while IFS=
brew install python@2 brew install python@2
pip install virtualenv pip install virtualenv
# readme toc
cat README.md | awk '!/^#/{next} {lv=length($1);sub(/[^ ]+ /,"");bab=$0;gsub(/ /,"-",bab)} {printf "%" ((lv-1)*4+1) "s [%s](#%s)\n", "*",$0,bab}'
## ##
## http 206 ## http 206

View File

@@ -20,6 +20,7 @@ set -e
# -rwxr-xr-x 0 ed ed 183808 Nov 19 00:43 copyparty-extras/sfx-lite/copyparty-sfx.py # -rwxr-xr-x 0 ed ed 183808 Nov 19 00:43 copyparty-extras/sfx-lite/copyparty-sfx.py
command -v gnutar && tar() { gnutar "$@"; }
command -v gtar && tar() { gtar "$@"; } command -v gtar && tar() { gtar "$@"; }
command -v gsed && sed() { gsed "$@"; } command -v gsed && sed() { gsed "$@"; }
td="$(mktemp -d)" td="$(mktemp -d)"
@@ -29,11 +30,11 @@ pwd
dl_text() { dl_text() {
command -v curl && exec curl "$@" command -v curl >/dev/null && exec curl "$@"
exec wget -O- "$@" exec wget -O- "$@"
} }
dl_files() { dl_files() {
command -v curl && exec curl -L --remote-name-all "$@" command -v curl >/dev/null && exec curl -L --remote-name-all "$@"
exec wget "$@" exec wget "$@"
} }
export -f dl_files export -f dl_files

View File

@@ -169,6 +169,7 @@ done
sed -r '/edit2">edit \(fancy/d' <$f >t && tmv "$f" sed -r '/edit2">edit \(fancy/d' <$f >t && tmv "$f"
} }
[ $repack ] ||
find | grep -E '\.py$' | find | grep -E '\.py$' |
grep -vE '__version__' | grep -vE '__version__' |
tr '\n' '\0' | tr '\n' '\0' |
@@ -180,7 +181,7 @@ tmv "$f"
# up2k goes from 28k to 22k laff # up2k goes from 28k to 22k laff
echo entabbening echo entabbening
find | grep -E '\.(js|css|html|py)$' | while IFS= read -r f; do find | grep -E '\.(js|css|html)$' | while IFS= read -r f; do
unexpand -t 4 --first-only <"$f" >t unexpand -t 4 --first-only <"$f" >t
tmv "$f" tmv "$f"
done done
@@ -214,5 +215,4 @@ printf "done:\n"
printf " %s\n" "$(realpath $sfx_out)."{sh,py} printf " %s\n" "$(realpath $sfx_out)."{sh,py}
# rm -rf * # rm -rf *
# tar -tvf ../sfx/tar | sed -r 's/(.* ....-..-.. ..:.. )(.*)/\2 `` \1/' | sort | sed -r 's/(.*) `` (.*)/\2 \1/'| less # while true; do ./make-sfx.sh; for f in ..//dist/copyparty-sfx.{sh,py}; do mv $f $f.$(wc -c <$f | awk '{print$1}'); done; done
# for n in {1..9}; do tar -tf tar | grep -vE '/$' | sed -r 's/(.*)\.(.*)/\2.\1/' | sort | sed -r 's/([^\.]+)\.(.*)/\2.\1/' | tar -cT- | bzip2 -c$n | wc -c; done

View File

@@ -1,9 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
# coding: utf-8 # coding: latin-1
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import os, sys, time, shutil, signal, tarfile, hashlib, platform, tempfile import os, sys, time, shutil, runpy, tarfile, hashlib, platform, tempfile, traceback
import subprocess as sp
""" """
run me with any version of python, i will unpack and run copyparty run me with any version of python, i will unpack and run copyparty
@@ -344,20 +343,24 @@ def get_payload():
break break
def confirm(): def confirm(rv):
msg() msg()
msg(traceback.format_exc())
msg("*** hit enter to exit ***") msg("*** hit enter to exit ***")
try: try:
raw_input() if PY2 else input() raw_input() if PY2 else input()
except: except:
pass pass
sys.exit(rv)
def run(tmp, j2ver): def run(tmp, j2ver):
global cpp global cpp
msg("jinja2:", j2ver or "bundled") msg("jinja2:", j2ver or "bundled")
msg("sfxdir:", tmp) msg("sfxdir:", tmp)
msg()
# "systemd-tmpfiles-clean.timer"?? HOW do you even come up with this shit # "systemd-tmpfiles-clean.timer"?? HOW do you even come up with this shit
try: try:
@@ -373,30 +376,16 @@ def run(tmp, j2ver):
if j2ver: if j2ver:
del ld[-1] del ld[-1]
cmd = ( for x in ld:
"import sys, runpy; " sys.path.insert(0, x)
+ "".join(['sys.path.insert(0, r"' + x + '"); ' for x in ld])
+ 'runpy.run_module("copyparty", run_name="__main__")'
)
cmd = [sys.executable, "-c", cmd] + list(sys.argv[1:])
cmd = [str(x) for x in cmd]
msg("\n", cmd, "\n")
cpp = sp.Popen(cmd)
try: try:
cpp.wait() runpy.run_module(str("copyparty"), run_name=str("__main__"))
except SystemExit as ex:
if ex.code:
confirm(ex.code)
except: except:
cpp.wait() confirm(1)
if cpp.returncode != 0:
confirm()
sys.exit(cpp.returncode)
def bye(sig, frame):
if cpp is not None:
cpp.terminate()
def main(): def main():
@@ -430,8 +419,6 @@ def main():
# skip 0 # skip 0
signal.signal(signal.SIGTERM, bye)
tmp = unpack() tmp = unpack()
try: try:
@@ -439,7 +426,7 @@ def main():
except: except:
j2ver = None j2ver = None
return run(tmp, j2ver) run(tmp, j2ver)
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -90,7 +90,7 @@ class TestVFS(unittest.TestCase):
finally: finally:
return ret return ret
def log(self, src, msg): def log(self, src, msg, c=0):
pass pass
def test(self): def test(self):