1
Fork 0

Figured out plotting!

Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
prescientmoon 2024-06-27 21:22:44 +02:00
parent b2e88e703b
commit 49d50bf88b
Signed by: prescientmoon
SSH key fingerprint: SHA256:UUF9JT2s8Xfyv76b8ZuVL7XrmimH4o49p4b+iexbVH4
12 changed files with 1297 additions and 777 deletions

378
Cargo.lock generated
View file

@ -401,6 +401,42 @@ version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "core-graphics"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-graphics-types",
"foreign-types",
"libc",
]
[[package]]
name = "core-graphics-types"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"libc",
]
[[package]]
name = "core-text"
version = "20.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5"
dependencies = [
"core-foundation",
"core-graphics",
"foreign-types",
"libc",
]
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.12" version = "0.2.12"
@ -494,24 +530,13 @@ dependencies = [
] ]
[[package]] [[package]]
name = "csv" name = "cstr"
version = "1.3.0" version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" checksum = "68523903c8ae5aacfa32a0d9ae60cadeb764e1da14ee0d26b1f3089f13a54636"
dependencies = [ dependencies = [
"csv-core", "proc-macro2",
"itoa", "quote",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70"
dependencies = [
"memchr",
] ]
[[package]] [[package]]
@ -634,12 +659,33 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "dlib"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [
"libloading",
]
[[package]] [[package]]
name = "dotenvy" name = "dotenvy"
version = "0.15.7" version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "dwrote"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439a1c2ba5611ad3ed731280541d36d2e9c4ac5e7fb818a27b604bdc5a6aa65b"
dependencies = [
"lazy_static",
"libc",
"winapi",
"wio",
]
[[package]] [[package]]
name = "edit-distance" name = "edit-distance"
version = "2.1.0" version = "2.1.0"
@ -655,12 +701,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.34" version = "0.8.34"
@ -728,28 +768,6 @@ dependencies = [
"zune-inflate", "zune-inflate",
] ]
[[package]]
name = "failure"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86"
dependencies = [
"backtrace",
"failure_derive",
]
[[package]]
name = "failure_derive"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"synstructure",
]
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.1.0" version = "2.1.0"
@ -775,6 +793,12 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "float-ord"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d"
[[package]] [[package]]
name = "flume" name = "flume"
version = "0.11.0" version = "0.11.0"
@ -792,6 +816,58 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "font-kit"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2845a73bbd781e691ab7c2a028c579727cd254942e8ced57ff73e0eafd60de87"
dependencies = [
"bitflags 2.5.0",
"byteorder",
"core-foundation",
"core-graphics",
"core-text",
"dirs-next",
"dwrote",
"float-ord",
"freetype-sys",
"lazy_static",
"libc",
"log",
"pathfinder_geometry",
"pathfinder_simd",
"walkdir",
"winapi",
"yeslogic-fontconfig-sys",
]
[[package]]
name = "foreign-types"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
dependencies = [
"foreign-types-macros",
"foreign-types-shared",
]
[[package]]
name = "foreign-types-macros"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.66",
]
[[package]]
name = "foreign-types-shared"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.1" version = "1.2.1"
@ -801,6 +877,17 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "freetype-sys"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.3.30" version = "0.3.30"
@ -930,6 +1017,16 @@ dependencies = [
"wasi", "wasi",
] ]
[[package]]
name = "gif"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06"
dependencies = [
"color_quant",
"weezl",
]
[[package]] [[package]]
name = "gif" name = "gif"
version = "0.13.1" version = "0.13.1"
@ -1176,6 +1273,20 @@ dependencies = [
"unicode-normalization", "unicode-normalization",
] ]
[[package]]
name = "image"
version = "0.24.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d"
dependencies = [
"bytemuck",
"byteorder",
"color_quant",
"jpeg-decoder",
"num-traits",
"png",
]
[[package]] [[package]]
name = "image" name = "image"
version = "0.25.1" version = "0.25.1"
@ -1186,7 +1297,7 @@ dependencies = [
"byteorder", "byteorder",
"color_quant", "color_quant",
"exr", "exr",
"gif", "gif 0.13.1",
"image-webp", "image-webp",
"num-traits", "num-traits",
"png", "png",
@ -1242,17 +1353,6 @@ version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
[[package]]
name = "is-terminal"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.12.1" version = "0.12.1"
@ -1709,6 +1809,25 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pathfinder_geometry"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3"
dependencies = [
"log",
"pathfinder_simd",
]
[[package]]
name = "pathfinder_simd"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebf45976c56919841273f2a0fc684c28437e2f304e264557d9c72be5d5a718be"
dependencies = [
"rustc_version",
]
[[package]] [[package]]
name = "peeking_take_while" name = "peeking_take_while"
version = "0.1.2" version = "0.1.2"
@ -1770,13 +1889,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]] [[package]]
name = "plotlib" name = "plotters"
version = "0.5.1" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/starlitcanopy/plotters.git?rev=986cd959362a2dbec8d1b25670fd083b904d7b8c#986cd959362a2dbec8d1b25670fd083b904d7b8c"
checksum = "9462104f987d8d0f6625f0c7764f1c8b890bd1dc8584d8293e031f25c5a0d242"
dependencies = [ dependencies = [
"failure", "chrono",
"svg", "font-kit",
"image 0.24.9",
"lazy_static",
"num-traits",
"pathfinder_geometry",
"plotters-backend",
"plotters-bitmap",
"plotters-svg",
"ttf-parser",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "plotters-backend"
version = "0.4.0"
source = "git+https://github.com/starlitcanopy/plotters.git?rev=986cd959362a2dbec8d1b25670fd083b904d7b8c#986cd959362a2dbec8d1b25670fd083b904d7b8c"
[[package]]
name = "plotters-bitmap"
version = "0.4.0"
source = "git+https://github.com/starlitcanopy/plotters.git?rev=986cd959362a2dbec8d1b25670fd083b904d7b8c#986cd959362a2dbec8d1b25670fd083b904d7b8c"
dependencies = [
"gif 0.11.4",
"image 0.24.9",
"plotters-backend",
]
[[package]]
name = "plotters-svg"
version = "0.4.0"
source = "git+https://github.com/starlitcanopy/plotters.git?rev=986cd959362a2dbec8d1b25670fd083b904d7b8c#986cd959362a2dbec8d1b25670fd083b904d7b8c"
dependencies = [
"plotters-backend",
] ]
[[package]] [[package]]
@ -1833,20 +1984,6 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "prettytable-rs"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a"
dependencies = [
"csv",
"encode_unicode",
"is-terminal",
"lazy_static",
"term",
"unicode-width",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.85" version = "1.0.85"
@ -2168,6 +2305,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc_version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver",
]
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.34" version = "0.38.34"
@ -2243,12 +2389,6 @@ dependencies = [
"untrusted", "untrusted",
] ]
[[package]]
name = "rustversion"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.18" version = "1.0.18"
@ -2420,14 +2560,12 @@ name = "shimmeringmoon"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"csv",
"edit-distance", "edit-distance",
"image", "image 0.25.1",
"kd-tree", "kd-tree",
"num", "num",
"plotlib", "plotters",
"poise", "poise",
"prettytable-rs",
"sqlx", "sqlx",
"tesseract", "tesseract",
"tokio", "tokio",
@ -2761,12 +2899,6 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]]
name = "svg"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3685c82a045a6af0c488f0550b0f52b4c77d2a52b0ca8aba719f9d268fa96965"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.109" version = "1.0.109"
@ -2795,18 +2927,6 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "synstructure"
version = "0.12.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"unicode-xid",
]
[[package]] [[package]]
name = "system-configuration" name = "system-configuration"
version = "0.5.1" version = "0.5.1"
@ -2865,17 +2985,6 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "term"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f"
dependencies = [
"dirs-next",
"rustversion",
"winapi",
]
[[package]] [[package]]
name = "tesseract" name = "tesseract"
version = "0.15.1" version = "0.15.1"
@ -3160,6 +3269,12 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "ttf-parser"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b3e06c9b9d80ed6b745c7159c40b311ad2916abb34a49e9be2653b90db0d8dd"
[[package]] [[package]]
name = "tungstenite" name = "tungstenite"
version = "0.21.0" version = "0.21.0"
@ -3264,18 +3379,6 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "unicode-width"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
[[package]]
name = "unicode-xid"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
[[package]] [[package]]
name = "unicode_categories" name = "unicode_categories"
version = "0.1.1" version = "0.1.1"
@ -3702,6 +3805,27 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "wio"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5"
dependencies = [
"winapi",
]
[[package]]
name = "yeslogic-fontconfig-sys"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffb6b23999a8b1a997bf47c7bb4d19ad4029c3327bb3386ebe0a5ff584b33c7a"
dependencies = [
"cstr",
"dlib",
"once_cell",
"pkg-config",
]
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.7.34" version = "0.7.34"

View file

@ -5,14 +5,12 @@ edition = "2021"
[dependencies] [dependencies]
chrono = "0.4.38" chrono = "0.4.38"
csv = "1.3.0"
edit-distance = "2.1.0" edit-distance = "2.1.0"
image = "0.25.1" image = "0.25.1"
kd-tree = "0.6.0" kd-tree = "0.6.0"
num = "0.4.3" num = "0.4.3"
plotlib = "0.5.1" plotters = { git="https://github.com/starlitcanopy/plotters.git", rev="986cd959362a2dbec8d1b25670fd083b904d7b8c" }
poise = "0.6.1" poise = "0.6.1"
prettytable-rs = "0.10.0"
sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio", "chrono"] } sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio", "chrono"] }
tesseract = "0.15.1" tesseract = "0.15.1"
tokio = {version="1.38.0", features=["rt-multi-thread"]} tokio = {version="1.38.0", features=["rt-multi-thread"]}

View file

@ -28,6 +28,8 @@
rust-analyzer-nightly rust-analyzer-nightly
ruff ruff
imagemagick imagemagick
fontconfig
freetype
clang clang
llvmPackages.clang llvmPackages.clang

View file

@ -43,7 +43,7 @@ CREATE TABLE IF NOT EXISTS plays (
creation_zeta_ptt INTEGER, creation_zeta_ptt INTEGER,
score INTEGER NOT NULL, score INTEGER NOT NULL,
zeta_score INTEGER, zeta_score INTEGER NOT NULL,
max_recall INTEGER, max_recall INTEGER,
far_notes INTEGER, far_notes INTEGER,

View file

@ -5,7 +5,7 @@ use sqlx::{prelude::FromRow, SqlitePool};
use crate::context::Error; use crate::context::Error;
// {{{ Difficuly // {{{ Difficuly
#[derive(Debug, Clone, Copy, sqlx::Type)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, sqlx::Type)]
pub enum Difficulty { pub enum Difficulty {
PST, PST,
PRS, PRS,
@ -85,17 +85,31 @@ impl CachedSong {
} }
#[inline] #[inline]
pub fn lookup(&self, difficulty: Difficulty) -> Option<&Chart> { pub fn lookup(&self, difficulty: Difficulty) -> Result<&Chart, Error> {
self.charts self.charts
.get(difficulty.to_index()) .get(difficulty.to_index())
.and_then(|c| c.as_ref()) .and_then(|c| c.as_ref())
.ok_or_else(|| {
format!(
"Could not find difficulty {:?} for song {}",
difficulty, self.song.title
)
.into()
})
} }
#[inline] #[inline]
pub fn lookup_mut(&mut self, difficulty: Difficulty) -> Option<&mut Chart> { pub fn lookup_mut(&mut self, difficulty: Difficulty) -> Result<&mut Chart, Error> {
self.charts self.charts
.get_mut(difficulty.to_index()) .get_mut(difficulty.to_index())
.and_then(|c| c.as_mut()) .and_then(|c| c.as_mut())
.ok_or_else(|| {
format!(
"Could not find difficulty {:?} for song {}",
difficulty, self.song.title
)
.into()
})
} }
#[inline] #[inline]
@ -117,8 +131,26 @@ impl SongCache {
} }
#[inline] #[inline]
pub fn lookup_mut(&mut self, id: u32) -> Option<&mut CachedSong> { pub fn lookup_chart(&self, chart_id: u32) -> Result<(&Song, &Chart), Error> {
self.songs.get_mut(id as usize).and_then(|i| i.as_mut()) self.songs()
.find_map(|item| {
item.charts().find_map(|chart| {
if chart.id == chart_id {
Some((&item.song, chart))
} else {
None
}
})
})
.ok_or_else(|| format!("Could not find chart with id {}", chart_id).into())
}
#[inline]
pub fn lookup_mut(&mut self, id: u32) -> Result<&mut CachedSong, Error> {
self.songs
.get_mut(id as usize)
.and_then(|i| i.as_mut())
.ok_or_else(|| format!("Could not find song with id {}", id).into())
} }
#[inline] #[inline]

2
src/commands/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod score;
pub mod stats;

View file

@ -1,8 +1,10 @@
use std::fmt::Display; use std::fmt::Display;
use crate::context::{Context, Error}; use crate::context::{Context, Error};
use crate::score::{jacket_rects, CreatePlay, ImageCropper, ImageDimensions, RelativeRect, Score}; use crate::score::{
use crate::user::User; jacket_rects, CreatePlay, ImageCropper, ImageDimensions, Play, RelativeRect, Score,
};
use crate::user::{discord_it_to_discord_user, User};
use image::imageops::FilterType; use image::imageops::FilterType;
use image::ImageFormat; use image::ImageFormat;
use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateMessage}; use poise::serenity_prelude::{CreateAttachment, CreateEmbed, CreateMessage};
@ -36,7 +38,7 @@ pub async fn help(
#[poise::command( #[poise::command(
prefix_command, prefix_command,
slash_command, slash_command,
subcommands("magic", "delete"), subcommands("magic", "delete", "show"),
subcommand_required subcommand_required
)] )]
pub async fn score(_ctx: Context<'_>) -> Result<(), Error> { pub async fn score(_ctx: Context<'_>) -> Result<(), Error> {
@ -88,8 +90,8 @@ pub async fn magic(
if files.len() == 0 { if files.len() == 0 {
ctx.reply("No images found attached to message").await?; ctx.reply("No images found attached to message").await?;
} else { } else {
let mut embeds: Vec<CreateEmbed> = vec![]; let mut embeds = Vec::with_capacity(files.len());
let mut attachments: Vec<CreateAttachment> = vec![]; let mut attachments = Vec::with_capacity(files.len());
let handle = ctx let handle = ctx
.reply(format!("Processed 0/{} scores", files.len())) .reply(format!("Processed 0/{} scores", files.len()))
.await?; .await?;
@ -116,7 +118,7 @@ pub async fn magic(
.content(format!("Image {}: reading jacket", i + 1)); .content(format!("Image {}: reading jacket", i + 1));
handle.edit(ctx, edited).await?; handle.edit(ctx, edited).await?;
let song_by_jacket = cropper.read_jacket(ctx.data(), &image); let song_by_jacket = cropper.read_jacket(ctx.data(), &image).await;
// This makes OCR more likely to work // This makes OCR more likely to work
let mut ocr_image = image.grayscale().blur(1.); let mut ocr_image = image.grayscale().blur(1.);
@ -151,8 +153,10 @@ pub async fn magic(
.content(format!("Image {}: reading title", i + 1)); .content(format!("Image {}: reading title", i + 1));
handle.edit(ctx, edited).await?; handle.edit(ctx, edited).await?;
let song_by_name = cropper.read_song(&ocr_image, &ctx.data().song_cache); let song_by_name = cropper
let cached_song = match (song_by_jacket, song_by_name) { .read_song(&ocr_image, &ctx.data().song_cache, difficulty)
.await;
let (song, chart) = match (song_by_jacket, song_by_name) {
// {{{ Both errors // {{{ Both errors
(Err(err_jacket), Err(err_name)) => { (Err(err_jacket), Err(err_name)) => {
error_with_image( error_with_image(
@ -196,16 +200,8 @@ Title error: {}
.ok_or_else(|| "Could not find jacket area in picture")? .ok_or_else(|| "Could not find jacket area in picture")?
.to_absolute(); .to_absolute();
// }}} // }}}
// {{{ Find chart
let chart = by_name.lookup(difficulty).ok_or_else(|| {
format!(
"Cannot find difficulty {:?} for chart {:?}",
difficulty, by_name.song.title
)
})?;
// }}}
// {{{ Build path // {{{ Build path
let filename = format!("{}-{}", by_name.song.id, chart.id); let filename = format!("{}-{}", by_name.0.id, by_name.1.id);
let jacket = format!("user/{}", filename); let jacket = format!("user/{}", filename);
let jacket_dir = ctx.data().data_dir.join("jackets/user"); let jacket_dir = ctx.data().data_dir.join("jackets/user");
@ -221,42 +217,27 @@ Title error: {}
sqlx::query!( sqlx::query!(
"UPDATE charts SET jacket=? WHERE song_id=? AND difficulty=?", "UPDATE charts SET jacket=? WHERE song_id=? AND difficulty=?",
jacket, jacket,
chart.song_id, by_name.1.song_id,
chart.difficulty, by_name.1.difficulty,
) )
.execute(&ctx.data().db) .execute(&ctx.data().db)
.await?; .await?;
// }}} // }}}
// {{{ Aquire and use song cache lock // {{{ Aquire and use song cache lock
{ {
let mut song_cache = ctx let mut song_cache = ctx.data().song_cache.lock().await;
.data()
.song_cache
.lock()
.map_err(|_| "Poisoned song cache")?;
let chart = song_cache let chart = song_cache
.lookup_mut(by_name.song.id) .lookup_mut(by_name.0.id)?
.ok_or_else(|| { .lookup_mut(difficulty)?;
format!("Could not find song for id {}", by_name.song.id)
})?
.lookup_mut(difficulty)
.ok_or_else(|| {
format!(
"Could not find difficulty {:?} for song {}",
difficulty, by_name.song.title
)
})?;
if chart.jacket.is_none() { if chart.jacket.is_none() {
if let Some(chart) = by_name.lookup_mut(difficulty) { by_name.1.jacket = Some(jacket_path.clone());
chart.jacket = Some(jacket_path.clone());
};
chart.jacket = Some(jacket_path); chart.jacket = Some(jacket_path);
} else { } else {
println!( println!(
"Jacket not detected for chart {} [{:?}]", "Jacket not detected for chart {} [{:?}]",
by_name.song.id, difficulty by_name.0.id, difficulty
) )
}; };
} }
@ -267,10 +248,10 @@ Title error: {}
// }}} // }}}
// {{{ Both succeeded // {{{ Both succeeded
(Ok(by_jacket), Ok(by_name)) => { (Ok(by_jacket), Ok(by_name)) => {
if by_name.song.id != by_jacket.song.id { if by_name.0.id != by_jacket.0.id {
println!( println!(
"Got diverging choices between '{:?}' and '{:?}'", "Got diverging choices between '{:?}' and '{:?}'",
by_jacket.song.id, by_name.song.id by_jacket.0.id, by_name.0.id
); );
}; };
@ -278,16 +259,6 @@ Title error: {}
} // }}} } // }}}
}; };
// {{{ Build chart
let song = &cached_song.song;
let chart = cached_song.lookup(difficulty).ok_or_else(|| {
format!(
"Could not find difficulty {:?} for song {}",
difficulty, song.title
)
})?;
// }}}
let edited = CreateReply::default() let edited = CreateReply::default()
.reply(true) .reply(true)
.content(format!("Image {}: reading score", i + 1)); .content(format!("Image {}: reading score", i + 1));
@ -315,7 +286,7 @@ Title error: {}
// {{{ Build play // {{{ Build play
let (score, maybe_fars, score_warning) = let (score, maybe_fars, score_warning) =
Score::resolve_ambiguities(score_possibilities, None, chart.note_count)?; Score::resolve_ambiguities(score_possibilities, None, chart.note_count)?;
let play = CreatePlay::new(score, chart, &user) let play = CreatePlay::new(score, &chart, &user)
.with_attachment(file) .with_attachment(file)
.with_fars(maybe_fars) .with_fars(maybe_fars)
.save(&ctx.data()) .save(&ctx.data())
@ -323,15 +294,13 @@ Title error: {}
// }}} // }}}
// }}} // }}}
// {{{ Deliver embed // {{{ Deliver embed
let (mut embed, attachment) = play.to_embed(&song, &chart, i).await?; let (mut embed, attachment) = play.to_embed(&song, &chart, i, None).await?;
if let Some(warning) = score_warning { if let Some(warning) = score_warning {
embed = embed.description(warning); embed = embed.description(warning);
} }
embeds.push(embed); embeds.push(embed);
if let Some(attachment) = attachment { attachments.extend(attachment);
attachments.push(attachment);
}
// }}} // }}}
} else { } else {
ctx.reply("One of the attached files is not an image!") ctx.reply("One of the attached files is not an image!")
@ -350,10 +319,8 @@ Title error: {}
handle.delete(ctx).await?; handle.delete(ctx).await?;
let msg = CreateMessage::new().embeds(embeds);
ctx.channel_id() ctx.channel_id()
.send_files(ctx.http(), attachments, msg) .send_files(ctx.http(), attachments, CreateMessage::new().embeds(embeds))
.await?; .await?;
} }
@ -403,3 +370,66 @@ pub async fn delete(
Ok(()) Ok(())
} }
// }}} // }}}
// {{{ Score show
/// Show scores given their ides
#[poise::command(prefix_command, slash_command)]
pub async fn show(
ctx: Context<'_>,
#[description = "Ids of score to show"] ids: Vec<u32>,
) -> Result<(), Error> {
if ids.len() == 0 {
ctx.reply("Empty ID list provided").await?;
return Ok(());
}
let lock = ctx.data().song_cache.lock().await;
let mut embeds = Vec::with_capacity(ids.len());
let mut attachments = Vec::with_capacity(ids.len());
for (i, id) in ids.iter().enumerate() {
let res = query!(
"
SELECT
p.id,p.chart_id,p.user_id,p.score,p.zeta_score,
p.max_recall,p.created_at,p.far_notes,
u.discord_id
FROM plays p
JOIN users u ON p.user_id = u.id
WHERE p.id=?
",
id
)
.fetch_one(&ctx.data().db)
.await
.map_err(|_| format!("Could not find play with id {}", id))?;
let play = Play {
id: res.id as u32,
chart_id: res.chart_id as u32,
user_id: res.user_id as u32,
score: Score(res.score as u32),
zeta_score: Score(res.zeta_score as u32),
max_recall: res.max_recall.map(|r| r as u32),
far_notes: res.far_notes.map(|r| r as u32),
created_at: res.created_at,
discord_attachment_id: None,
creation_ptt: None,
creation_zeta_ptt: None,
};
let user = discord_it_to_discord_user(&ctx, &res.discord_id).await?;
let (song, chart) = lock.lookup_chart(play.chart_id)?;
let (embed, attachment) = play.to_embed(song, chart, i, Some(&user)).await?;
embeds.push(embed);
attachments.extend(attachment);
}
ctx.channel_id()
.send_files(ctx.http(), attachments, CreateMessage::new().embeds(embeds))
.await?;
Ok(())
}
// }}}

245
src/commands/stats.rs Normal file
View file

@ -0,0 +1,245 @@
use std::io::Cursor;
use chrono::{DateTime, NaiveDateTime};
use image::{ImageBuffer, Rgb};
use plotters::{
backend::{BitMapBackend, PixelFormat, RGBPixel},
chart::{ChartBuilder, LabelAreaPosition},
drawing::IntoDrawingArea,
element::Circle,
series::LineSeries,
style::{
text_anchor::{HPos, Pos, VPos},
Color, FontTransform, IntoFont, TextStyle, BLUE, WHITE,
},
};
use poise::{
serenity_prelude::{CreateAttachment, CreateMessage},
CreateReply,
};
use sqlx::query_as;
use crate::{
chart::Difficulty,
context::{Context, Error},
score::{guess_chart_name, DbPlay, Score},
user::{discord_it_to_discord_user, User},
};
// {{{ Stats
/// Stats display
#[poise::command(
prefix_command,
slash_command,
subcommands("chart"),
subcommand_required
)]
pub async fn stats(_ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
// }}}
// {{{ Chart
/// Chart-related stats
#[poise::command(
prefix_command,
slash_command,
subcommands("best", "plot"),
subcommand_required
)]
pub async fn chart(_ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
// }}}
// {{{ Best score
/// Show the best score on a given chart
#[poise::command(prefix_command, slash_command)]
pub async fn best(
ctx: Context<'_>,
#[rest]
#[description = "Name of chart to show (difficulty at the end)"]
name: String,
) -> Result<(), Error> {
let user = match User::from_context(&ctx).await {
Ok(user) => user,
Err(_) => {
ctx.say("You are not an user in my database, sorry!")
.await?;
return Ok(());
}
};
let name = name.trim();
let (name, difficulty) = name
.strip_suffix("PST")
.zip(Some(Difficulty::PST))
.or_else(|| name.strip_suffix("PRS").zip(Some(Difficulty::PRS)))
.or_else(|| name.strip_suffix("FTR").zip(Some(Difficulty::FTR)))
.or_else(|| name.strip_suffix("ETR").zip(Some(Difficulty::ETR)))
.or_else(|| name.strip_suffix("BYD").zip(Some(Difficulty::BYD)))
.unwrap_or((&name, Difficulty::FTR));
let (song, chart) = guess_chart_name(name, &ctx.data().song_cache, difficulty).await?;
let play = query_as!(
DbPlay,
"
SELECT * FROM plays
WHERE user_id=?
AND chart_id=?
ORDER BY score DESC
",
user.id,
chart.id
)
.fetch_one(&ctx.data().db)
.await
.map_err(|_| format!("Could not find any scores for chart"))?
.to_play();
let (embed, attachment) = play
.to_embed(
&song,
&chart,
0,
Some(&discord_it_to_discord_user(&ctx, &user.discord_id).await?),
)
.await?;
ctx.channel_id()
.send_files(ctx.http(), attachment, CreateMessage::new().embed(embed))
.await?;
Ok(())
}
// }}}
// Score plot
/// Show the best score on a given chart
#[poise::command(prefix_command, slash_command)]
pub async fn plot(
ctx: Context<'_>,
#[rest]
#[description = "Name of chart to show (difficulty at the end)"]
name: String,
) -> Result<(), Error> {
let user = match User::from_context(&ctx).await {
Ok(user) => user,
Err(_) => {
ctx.say("You are not an user in my database, sorry!")
.await?;
return Ok(());
}
};
let name = name.trim();
let (name, difficulty) = name
.strip_suffix("PST")
.zip(Some(Difficulty::PST))
.or_else(|| name.strip_suffix("PRS").zip(Some(Difficulty::PRS)))
.or_else(|| name.strip_suffix("FTR").zip(Some(Difficulty::FTR)))
.or_else(|| name.strip_suffix("ETR").zip(Some(Difficulty::ETR)))
.or_else(|| name.strip_suffix("BYD").zip(Some(Difficulty::BYD)))
.unwrap_or((&name, Difficulty::FTR));
let (song, chart) = guess_chart_name(name, &ctx.data().song_cache, difficulty).await?;
let plays = query_as!(
DbPlay,
"
SELECT * FROM plays
WHERE user_id=?
AND chart_id=?
ORDER BY created_at ASC
",
user.id,
chart.id
)
.fetch_all(&ctx.data().db)
.await?;
if plays.len() == 0 {
ctx.reply("No plays found").await?;
return Ok(());
}
let min_time = plays.iter().map(|p| p.created_at).min().unwrap();
let max_time = plays.iter().map(|p| p.created_at).max().unwrap();
let mut min_score = plays.iter().map(|p| p.score).min().unwrap();
if min_score > 9_900_000 {
min_score = 9_800_000;
} else if min_score > 9_800_000 {
min_score = 9_800_000;
} else if min_score > 9_500_000 {
min_score = 9_500_000;
} else {
min_score = 9_000_000
};
let max_score = 10_010_000;
let width = 1024;
let height = 768;
let mut buffer = vec![u8::MAX; RGBPixel::PIXEL_SIZE * (width * height) as usize];
{
let mut root = BitMapBackend::with_buffer(&mut buffer, (width, height)).into_drawing_area();
let mut chart = ChartBuilder::on(&root)
.margin(25)
.caption(
format!("{} [{:?}]", song.title, chart.difficulty),
("sans-serif", 40),
)
.set_label_area_size(LabelAreaPosition::Left, 100)
.set_label_area_size(LabelAreaPosition::Bottom, 40)
.build_cartesian_2d(
min_time.and_utc().timestamp_millis()..max_time.and_utc().timestamp_millis(),
min_score..max_score,
)?;
chart
.configure_mesh()
.light_line_style(WHITE)
.y_label_formatter(&|s| format!("{}", Score(*s as u32)))
.y_desc("Score")
.x_label_formatter(&|d| {
format!(
"{}",
DateTime::from_timestamp_millis(*d).unwrap().date_naive()
)
})
.y_label_style(TextStyle::from(("sans-serif", 20).into_font()))
.x_label_style(TextStyle::from(("sans-serif", 20).into_font()))
.draw()?;
let mut points: Vec<_> = plays
.iter()
.map(|play| (play.created_at.and_utc().timestamp_millis(), play.score))
.collect();
points.sort();
points.dedup();
chart.draw_series(LineSeries::new(points.iter().map(|(t, s)| (*t, *s)), &BLUE))?;
chart.draw_series(
points
.iter()
.map(|(t, s)| Circle::new((*t, *s), 3, BLUE.filled())),
)?;
root.present()?;
}
let image: ImageBuffer<Rgb<u8>, _> = ImageBuffer::from_raw(width, height, buffer).unwrap();
let mut buffer = Vec::new();
let mut cursor = Cursor::new(&mut buffer);
image.write_to(&mut cursor, image::ImageFormat::Png)?;
let reply = CreateReply::default().attachment(CreateAttachment::bytes(buffer, "plot.png"));
ctx.send(reply).await?;
Ok(())
}
//

View file

@ -1,9 +1,7 @@
use std::{ use std::{path::PathBuf, sync::Arc};
path::PathBuf,
sync::{Arc, Mutex},
};
use sqlx::SqlitePool; use sqlx::SqlitePool;
use tokio::sync::Mutex;
use crate::{chart::SongCache, jacket::JacketCache}; use crate::{chart::SongCache, jacket::JacketCache};

View file

@ -1,6 +1,7 @@
#![warn(clippy::str_to_string)] #![warn(clippy::str_to_string)]
#![feature(iter_map_windows)] #![feature(iter_map_windows)]
#![feature(let_chains)] #![feature(let_chains)]
#![feature(async_closure)]
mod chart; mod chart;
mod commands; mod commands;
@ -9,7 +10,6 @@ mod jacket;
mod score; mod score;
mod user; mod user;
use chart::Difficulty;
use context::{Error, UserContext}; use context::{Error, UserContext};
use poise::serenity_prelude::{self as serenity}; use poise::serenity_prelude::{self as serenity};
use sqlx::sqlite::SqlitePoolOptions; use sqlx::sqlite::SqlitePoolOptions;
@ -38,7 +38,11 @@ async fn main() {
// {{{ Poise options // {{{ Poise options
let options = poise::FrameworkOptions { let options = poise::FrameworkOptions {
commands: vec![commands::help(), commands::score()], commands: vec![
commands::score::help(),
commands::score::score(),
commands::stats::stats(),
],
prefix_options: poise::PrefixFrameworkOptions { prefix_options: poise::PrefixFrameworkOptions {
stripped_dynamic_prefix: Some(|_ctx, message, _user_ctx| { stripped_dynamic_prefix: Some(|_ctx, message, _user_ctx| {
Box::pin(async { Box::pin(async {

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,7 @@
use std::str::FromStr;
use poise::serenity_prelude::UserId;
use crate::context::{Context, Error}; use crate::context::{Context, Error};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -21,3 +25,14 @@ impl User {
}) })
} }
} }
#[inline]
pub async fn discord_it_to_discord_user(
&ctx: &Context<'_>,
discord_id: &str,
) -> Result<poise::serenity_prelude::User, Error> {
UserId::from_str(discord_id)?
.to_user(ctx.http())
.await
.map_err(|e| e.into())
}