diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f04cb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +.DS_STORE +node_modules \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2195c1a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "yaml.schemas": { + "https://json.schemastore.org/github-workflow.json": "file:///Users/bryce.dorn/Projects/apod-color-search/.github/workflows/deploy.yml" + } +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..b32332a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1876 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e" +dependencies = [ + "libc", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "apod-color-search" +version = "0.1.0" +dependencies = [ + "bytes", + "chrono", + "colorsys", + "hex", + "howlong", + "image", + "postgrest", + "reqwest", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bindgen" +version = "0.58.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f8523b410d7187a43085e7e064416ea32ded16bd0a4e6fc025e21616d01258f" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "clap", + "env_logger", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "which", +] + +[[package]] +name = "bit_field" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb6dd1c2376d2e096796e234a70e17e94cc2d5d54ff8ce42b28cef1d0d359a4" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bumpalo" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" + +[[package]] +name = "bytemuck" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cexpr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "clang-sys" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a050e2153c5be08febd6734e29298e844fdb0fa21aeddd63b4eb7baa106c69b" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorsys" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2ad453c82bd637e3969dc52f06676610db0b20c607bf0634c7e9d840789e8" + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "once_cell", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "deflate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f" +dependencies = [ + "adler32", +] + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "encoding_rs" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "exr" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c26a90d9dd411a3d119d6f55752fb4c134ca243250c32fb9cab7b2561638d2" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "smallvec", + "threadpool", +] + +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + +[[package]] +name = "flate2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin 0.9.4", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfc52cbddcfd745bf1740338492bb0bd83d76c67b445f91c5fb29fae29ecaa1" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2acedae88d38235936c3922476b10fced7b2b68136f5e3c03c2d5be348a1115" + +[[package]] +name = "futures-io" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93a66fc6d035a26a3ae255a6d2bca35eda63ae4c5512bef54449113f7a1228e5" + +[[package]] +name = "futures-sink" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0bae1fe9752cf7fd9b0064c674ae63f97b37bc714d745cbde0afb7ec4e6765" + +[[package]] +name = "futures-task" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "842fc63b931f4056a24d59de13fb1272134ce261816e063e634ad0c15cdc5306" + +[[package]] +name = "futures-util" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0828a5471e340229c11c77ca80017937ce3c58cb788a17e5f1c2d5c485a9577" +dependencies = [ + "futures-core", + "futures-io", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "h2" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "howlong" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54389bee324631cb91ad980b79c0912bb1e2523749c6d8ccec648f2629721c41" +dependencies = [ + "bindgen", + "cc", + "cfg-if", + "errno", + "libc", + "thiserror", + "winapi", +] + +[[package]] +name = "http" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" +dependencies = [ + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad2bfd338099682614d3ee3fe0cd72e0b6a41ca6a87f6a74a3bd593c91650501" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "image" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e30ca2ecf7666107ff827a8e481de6a132a9b687ed3bb20bb1c144a36c00964" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-rational", + "num-traits", + "png", + "scoped_threadpool", + "tiff", +] + +[[package]] +name = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipnet" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" + +[[package]] +name = "itoa" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" + +[[package]] +name = "jpeg-decoder" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9478aa10f73e7528198d75109c8be5cd7d15fb530238040148d5f9a22d4c5b3b" +dependencies = [ + "rayon", +] + +[[package]] +name = "js-sys" +version = "0.3.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "libc" +version = "0.2.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" + +[[package]] +name = "libloading" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "miniz_oxide" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + +[[package]] +name = "native-tls" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "memchr", + "version_check", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" + +[[package]] +name = "openssl" +version = "0.10.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + +[[package]] +name = "png" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc38c0ad57efb786dd57b9864e5b18bae478c00c824dc55a38bbc9da95dde3ba" +dependencies = [ + "bitflags", + "crc32fast", + "deflate", + "miniz_oxide", +] + +[[package]] +name = "postgrest" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91790151eb757e5498713aab1db2f24d0cafbff6be43fdb355a1ada49c63c290" +dependencies = [ + "reqwest", +] + +[[package]] +name = "proc-macro2" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" +dependencies = [ + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "reqwest" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustls" +version = "0.20.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" +dependencies = [ + "base64", +] + +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "schannel" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +dependencies = [ + "lazy_static", + "windows-sys", +] + +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e8e5d5b70924f74ff5c6d64d9a5acd91422117c60f48c4e07855238a254553" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d8e8de557aee63c26b85b947f5e59b690d0454c753f3adeb5cd7835ab88391" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" + +[[package]] +name = "socket2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09" +dependencies = [ + "lock_api", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "syn" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "tiff" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7259662e32d1e219321eb309d5f9d898b779769d81b76e762c07c8e5d38fcb65" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-util" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" + +[[package]] +name = "unicode-normalization" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa76fb221a1f8acddf5b54ace85912606980ad661ac7a503b4570ffd3a624dad" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" + +[[package]] +name = "web-sys" +version = "0.3.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf" +dependencies = [ + "webpki", +] + +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + +[[package]] +name = "which" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" +dependencies = [ + "libc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a299912 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "apod-color-search" +version = "0.1.0" +edition = "2021" + +[dependencies] +reqwest = { version = "0.11", features = ["json", "blocking"] } +tokio = { version = "1", features = ["full"] } +postgrest = "1.0" +serde = { version = "1", features = ["derive"] } +serde_json = "1.0" +image = "0.24.3" +bytes = "1.2.1" +colorsys = "0.6.6" +hex = "0.4.3" +howlong = "0.1" +chrono = "0.4.22" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d515ff --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# apod color search 🪐 + +Search for [APOD](https://apod.nasa.gov/apod/astropix.html) photos by color! Consists of four functional parts: + +## web + +A [Svelte](https://svelte.dev/) app to provide search interface. Uses [vanilla-colorful](https://github.com/web-padawan/vanilla-colorful) for color picker. + +## api + +[Deno](https://deno.land/)-based API that retrieves APOD information from database for images that match the given hex value. + +## cache + +Node server acting as reverse cache proxy to maintain persistent [Redis](https://redis.com/) connection. Aside from connection limits, [benchmarks](https://github.com/brycedorn/deno-node-redis-postgres-benchmarks) seem to indicate this is more performant than connecting directly to Redis via Deno for each request. + +## src + +[Rust](https://www.rust-lang.org/) utility to analyze and process images. Used to populate database with historical data, now scheduled to run monthly to process new APODs. + + 1. Fetches APOD metatada via [apod-api](https://github.com/nasa/apod-api). + 1. Analyzes image to get highest-frequency non-grayscale colors. + 1. Saves result to Postgres. + +Runs via GitHub Actions UI to then trigger additional workflows. This was used to leverage GitHub Actions to batch process a large amount of images concurrently & remotely. \ No newline at end of file diff --git a/api/.vscode/extensions.json b/api/.vscode/extensions.json new file mode 100644 index 0000000..09cf720 --- /dev/null +++ b/api/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "denoland.vscode-deno" + ] +} diff --git a/api/.vscode/settings.json b/api/.vscode/settings.json new file mode 100644 index 0000000..6f4f84f --- /dev/null +++ b/api/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "deno.enable": true, + "deno.lint": true, + "editor.defaultFormatter": "denoland.vscode-deno" +} diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..ac895e2 --- /dev/null +++ b/api/README.md @@ -0,0 +1,19 @@ +# apod color search API + +Powers `/search/:hex` endpoint that returns/sets result from/in database/cache. + +Why Deno for API and separate Node-based cache? +[Benchmarks](https://github.com/brycedorn/deno-node-redis-postgres-benchmarks) +seem to indicate this is the most performant approach. + +Uses [fresh](https://fresh.deno.dev/) for simple project structure. + +### Usage + +Start the project: + +``` +deno task start +``` + +This will watch the project directory and restart as necessary. diff --git a/api/deno.json b/api/deno.json new file mode 100644 index 0000000..8d9ac33 --- /dev/null +++ b/api/deno.json @@ -0,0 +1,10 @@ +{ + "tasks": { + "start": "deno run -A --watch=static/,routes/ dev.ts" + }, + "importMap": "./import_map.json", + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "preact" + } +} diff --git a/api/deno.lock b/api/deno.lock new file mode 100644 index 0000000..3f2125f --- /dev/null +++ b/api/deno.lock @@ -0,0 +1,212 @@ +{ + "version": "2", + "remote": { + "https://deno.land/std@0.140.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74", + "https://deno.land/std@0.140.0/_util/os.ts": "3b4c6e27febd119d36a416d7a97bd3b0251b77c88942c8f16ee5953ea13e2e49", + "https://deno.land/std@0.140.0/fs/_util.ts": "0fb24eb4bfebc2c194fb1afdb42b9c3dda12e368f43e8f2321f84fc77d42cb0f", + "https://deno.land/std@0.140.0/fs/ensure_dir.ts": "9dc109c27df4098b9fc12d949612ae5c9c7169507660dcf9ad90631833209d9d", + "https://deno.land/std@0.140.0/fs/expand_glob.ts": "0c10130d67c9b02164b03df8e43c6d6defbf8e395cb69d09e84a8586e6d72ac3", + "https://deno.land/std@0.140.0/fs/walk.ts": "117403ccd21fd322febe56ba06053b1ad5064c802170f19b1ea43214088fe95f", + "https://deno.land/std@0.140.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3", + "https://deno.land/std@0.140.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09", + "https://deno.land/std@0.140.0/path/_util.ts": "c1e9686d0164e29f7d880b2158971d805b6e0efc3110d0b3e24e4b8af2190d2b", + "https://deno.land/std@0.140.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633", + "https://deno.land/std@0.140.0/path/glob.ts": "cb5255638de1048973c3e69e420c77dc04f75755524cb3b2e160fe9277d939ee", + "https://deno.land/std@0.140.0/path/mod.ts": "d3e68d0abb393fb0bf94a6d07c46ec31dc755b544b13144dee931d8d5f06a52d", + "https://deno.land/std@0.140.0/path/posix.ts": "293cdaec3ecccec0a9cc2b534302dfe308adb6f10861fa183275d6695faace44", + "https://deno.land/std@0.140.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9", + "https://deno.land/std@0.140.0/path/win32.ts": "31811536855e19ba37a999cd8d1b62078235548d67902ece4aa6b814596dd757", + "https://deno.land/std@0.142.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74", + "https://deno.land/std@0.142.0/async/deferred.ts": "bc18e28108252c9f67dfca2bbc4587c3cbf3aeb6e155f8c864ca8ecff992b98a", + "https://deno.land/std@0.142.0/async/delay.ts": "cbbdf1c87d1aed8edc7bae13592fb3e27e3106e0748f089c263390d4f49e5f6c", + "https://deno.land/std@0.142.0/bytes/bytes_list.ts": "aba5e2369e77d426b10af1de0dcc4531acecec27f9b9056f4f7bfbf8ac147ab4", + "https://deno.land/std@0.142.0/bytes/equals.ts": "fc16dff2090cced02497f16483de123dfa91e591029f985029193dfaa9d894c9", + "https://deno.land/std@0.142.0/bytes/mod.ts": "763f97d33051cc3f28af1a688dfe2830841192a9fea0cbaa55f927b49d49d0bf", + "https://deno.land/std@0.142.0/io/buffer.ts": "bd0c4bf53db4b4be916ca5963e454bddfd3fcd45039041ea161dbf826817822b", + "https://deno.land/std@0.142.0/io/types.d.ts": "01f60ae7ec02675b5dbed150d258fc184a78dfe5c209ef53ba4422b46b58822c", + "https://deno.land/std@0.150.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74", + "https://deno.land/std@0.150.0/_util/os.ts": "3b4c6e27febd119d36a416d7a97bd3b0251b77c88942c8f16ee5953ea13e2e49", + "https://deno.land/std@0.150.0/async/abortable.ts": "87aa7230be8360c24ad437212311c9e8d4328854baec27b4c7abb26e85515c06", + "https://deno.land/std@0.150.0/async/deadline.ts": "48ac998d7564969f3e6ec6b6f9bf0217ebd00239b1b2292feba61272d5dd58d0", + "https://deno.land/std@0.150.0/async/debounce.ts": "564273ef242bcfcda19a439132f940db8694173abffc159ea34f07d18fc42620", + "https://deno.land/std@0.150.0/async/deferred.ts": "bc18e28108252c9f67dfca2bbc4587c3cbf3aeb6e155f8c864ca8ecff992b98a", + "https://deno.land/std@0.150.0/async/delay.ts": "cbbdf1c87d1aed8edc7bae13592fb3e27e3106e0748f089c263390d4f49e5f6c", + "https://deno.land/std@0.150.0/async/mod.ts": "9852cd8ed897ab2d41a8fbee611d574e97898327db5c19d9d58e41126473f02c", + "https://deno.land/std@0.150.0/async/mux_async_iterator.ts": "5b4aca6781ad0f2e19ccdf1d1a1c092ccd3e00d52050d9c27c772658c8367256", + "https://deno.land/std@0.150.0/async/pool.ts": "ef9eb97b388543acbf0ac32647121e4dbe629236899586c4d4311a8770fbb239", + "https://deno.land/std@0.150.0/async/tee.ts": "bcfae0017ebb718cf4eef9e2420e8675d91cb1bcc0ed9b668681af6e6caad846", + "https://deno.land/std@0.150.0/flags/mod.ts": "594472736e24b2f2afd3451cf7ccd58a21706ce91006478a544fdfa056c69697", + "https://deno.land/std@0.150.0/fs/_util.ts": "2cf50bfb1081c2d5f2efec10ac19abbc2baf478e51cd1b057d0da2f30585b6ba", + "https://deno.land/std@0.150.0/fs/walk.ts": "6ce8d87fbaeda23383e979599ad27f3f94b3e5ff0c0cd976b5fc5c2aa0df7d92", + "https://deno.land/std@0.150.0/http/http_status.ts": "897575a7d6bc2b9123f6a38ecbc0f03d95a532c5d92029315dc9f508e12526b8", + "https://deno.land/std@0.150.0/http/server.ts": "0b0a9f3abfcfecead944b31ee9098a0c11a59b0495bf873ee200eb80e7441483", + "https://deno.land/std@0.150.0/media_types/_util.ts": "ce9b4fc4ba1c447dafab619055e20fd88236ca6bdd7834a21f98bd193c3fbfa1", + "https://deno.land/std@0.150.0/media_types/mod.ts": "2d4b6f32a087029272dc59e0a55ae3cc4d1b27b794ccf528e94b1925795b3118", + "https://deno.land/std@0.150.0/media_types/vendor/mime-db.v1.52.0.ts": "724cee25fa40f1a52d3937d6b4fbbfdd7791ff55e1b7ac08d9319d5632c7f5af", + "https://deno.land/std@0.150.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3", + "https://deno.land/std@0.150.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09", + "https://deno.land/std@0.150.0/path/_util.ts": "c1e9686d0164e29f7d880b2158971d805b6e0efc3110d0b3e24e4b8af2190d2b", + "https://deno.land/std@0.150.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633", + "https://deno.land/std@0.150.0/path/glob.ts": "cb5255638de1048973c3e69e420c77dc04f75755524cb3b2e160fe9277d939ee", + "https://deno.land/std@0.150.0/path/mod.ts": "4945b430b759b0b3d98f2a278542cbcf95e0ad2bd8eaaed3c67322b306b2b346", + "https://deno.land/std@0.150.0/path/posix.ts": "c1f7afe274290ea0b51da07ee205653b2964bd74909a82deb07b69a6cc383aaa", + "https://deno.land/std@0.150.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9", + "https://deno.land/std@0.150.0/path/win32.ts": "bd7549042e37879c68ff2f8576a25950abbfca1d696d41d82c7bca0b7e6f452c", + "https://deno.land/std@0.150.0/semver/mod.ts": "4a5195fa81b4aede8875a386550a1119f01fb58d74aea899b2cfb136c05a7310", + "https://deno.land/std@0.152.0/async/abortable.ts": "87aa7230be8360c24ad437212311c9e8d4328854baec27b4c7abb26e85515c06", + "https://deno.land/std@0.152.0/async/deadline.ts": "48ac998d7564969f3e6ec6b6f9bf0217ebd00239b1b2292feba61272d5dd58d0", + "https://deno.land/std@0.152.0/async/debounce.ts": "564273ef242bcfcda19a439132f940db8694173abffc159ea34f07d18fc42620", + "https://deno.land/std@0.152.0/async/deferred.ts": "bc18e28108252c9f67dfca2bbc4587c3cbf3aeb6e155f8c864ca8ecff992b98a", + "https://deno.land/std@0.152.0/async/delay.ts": "cbbdf1c87d1aed8edc7bae13592fb3e27e3106e0748f089c263390d4f49e5f6c", + "https://deno.land/std@0.152.0/async/mod.ts": "dd0a8ed4f3984ffabe2fcca7c9f466b7932d57b1864ffee148a5d5388316db6b", + "https://deno.land/std@0.152.0/async/mux_async_iterator.ts": "5b4aca6781ad0f2e19ccdf1d1a1c092ccd3e00d52050d9c27c772658c8367256", + "https://deno.land/std@0.152.0/async/pool.ts": "ef9eb97b388543acbf0ac32647121e4dbe629236899586c4d4311a8770fbb239", + "https://deno.land/std@0.152.0/async/tee.ts": "bcfae0017ebb718cf4eef9e2420e8675d91cb1bcc0ed9b668681af6e6caad846", + "https://deno.land/std@0.152.0/http/server.ts": "0b0a9f3abfcfecead944b31ee9098a0c11a59b0495bf873ee200eb80e7441483", + "https://deno.land/x/code_block_writer@11.0.3/mod.ts": "2c3448060e47c9d08604c8f40dee34343f553f33edcdfebbf648442be33205e5", + "https://deno.land/x/code_block_writer@11.0.3/utils/string_utils.ts": "60cb4ec8bd335bf241ef785ccec51e809d576ff8e8d29da43d2273b69ce2a6ff", + "https://deno.land/x/denoflate@1.2.1/mod.ts": "f5628e44b80b3d80ed525afa2ba0f12408e3849db817d47a883b801f9ce69dd6", + "https://deno.land/x/denoflate@1.2.1/pkg/denoflate.js": "b9f9ad9457d3f12f28b1fb35c555f57443427f74decb403113d67364e4f2caf4", + "https://deno.land/x/denoflate@1.2.1/pkg/denoflate_bg.wasm.js": "d581956245407a2115a3d7e8d85a9641c032940a8e810acbd59ca86afd34d44d", + "https://deno.land/x/dotenv@v3.2.0/load.ts": "cbd76a0aee01aad8d09222afaa1dd04b84d9d3e44637503b01bf77a91df9e041", + "https://deno.land/x/dotenv@v3.2.0/mod.ts": "077b48773de9205266a0b44c3c3a3c3083449ed64bb0b6cc461b95720678d38e", + "https://deno.land/x/dotenv@v3.2.0/util.ts": "693730877b13f8ead2b79b2aa31e2a0652862f7dc0c5f6d2f313f4d39c7b7670", + "https://deno.land/x/esbuild@v0.14.51/mod.d.ts": "c142324d0383c39de0d7660cd207a7f7f52c7198a13d7d3281c0d636a070f441", + "https://deno.land/x/esbuild@v0.14.51/mod.js": "7432566c71fac77637822dc230319c7392a2d2fef51204c9d12c956d7093c279", + "https://deno.land/x/esbuild@v0.14.51/wasm.d.ts": "c142324d0383c39de0d7660cd207a7f7f52c7198a13d7d3281c0d636a070f441", + "https://deno.land/x/esbuild@v0.14.51/wasm.js": "afc1b6927543b664af60ce452c4929e5dc2bb9a0f4ed47b446a6431847c7598e", + "https://deno.land/x/esbuild_deno_loader@0.5.2/deps.ts": "bf83c27b7787b2f245fa0bc0b99f5041aa949c000a81c016cfe828d06b476d37", + "https://deno.land/x/esbuild_deno_loader@0.5.2/mod.ts": "bc111a68f323dbdb6edec68dd558ab732b27866d2ef304708872d763387b65d7", + "https://deno.land/x/esbuild_deno_loader@0.5.2/src/deno.ts": "0e83ccabbe2b004389288e38df2031b79eb347df2d139fce9394d8e88a11f259", + "https://deno.land/x/esbuild_deno_loader@0.5.2/src/native_loader.ts": "343854a566cf510cf25144f7c09fc0c1097780a31830305142a075d12bb697ba", + "https://deno.land/x/esbuild_deno_loader@0.5.2/src/portable_loader.ts": "35b6c526eed8c2c781a3256b23c30aa7cce69c0ef1d583c15528663287ba18a3", + "https://deno.land/x/esbuild_deno_loader@0.5.2/src/shared.ts": "b64749cd8c0f6252a11498bd8758ef1220003e46b2c9b68e16da63fd7e92b13a", + "https://deno.land/x/fresh@1.1.2/dev.ts": "a66c7d64be35bcd6a8e12eec9c27ae335044c70363a241f2e36ee776db468622", + "https://deno.land/x/fresh@1.1.2/server.ts": "f379c9aad24471a71e58fb887fa57e5cc27ad9df035987eb260541c78df38e84", + "https://deno.land/x/fresh@1.1.2/src/dev/deps.ts": "de5470828c17839c0b52c328e6709f3477740b9800deaf724d6569b64b1d3872", + "https://deno.land/x/fresh@1.1.2/src/dev/error.ts": "21a38d240c00279662e6adde41367f1da0ae7e2836d993f818ea94aabab53e7b", + "https://deno.land/x/fresh@1.1.2/src/dev/mod.ts": "f5836b2eccd0efd7c0a726a121f174a974daefc22058f759f07d4df56c46e978", + "https://deno.land/x/fresh@1.1.2/src/runtime/csp.ts": "9ee900e9b0b786057b1009da5976298c202d1b86d1f1e4d2510bde5f06530ac9", + "https://deno.land/x/fresh@1.1.2/src/runtime/head.ts": "0f9932874497ab6e57ed1ba01d549e843523df4a5d36ef97460e7a43e3132fdc", + "https://deno.land/x/fresh@1.1.2/src/runtime/utils.ts": "8320a874a44bdd5905c7d4b87a0e7a14a6c50a2ed133800e72ae57341e4d4faa", + "https://deno.land/x/fresh@1.1.2/src/server/bundle.ts": "f529a54ea5e078b9bd94c64c2ad345a3ba763b39c25b6e61f90629bdfa39c2ff", + "https://deno.land/x/fresh@1.1.2/src/server/constants.ts": "ad10dda1bc20c25c2926f6a8cfd79ef4368d70b4b03a645f65c04b3fa7d93a8c", + "https://deno.land/x/fresh@1.1.2/src/server/context.ts": "b385b982de2e5ee6e543dd56aad0e88f8f8fe5d9497909cf16caaf23b4d88b62", + "https://deno.land/x/fresh@1.1.2/src/server/default_error_page.ts": "1155ac98b3729bf2e51be730af507e431db0ee43ac84c86f1c022a831b310106", + "https://deno.land/x/fresh@1.1.2/src/server/deps.ts": "1c4e05b7d6e4eecc0012c9ec30ee587d8f533ab67c2bcb59048ed5f76841a770", + "https://deno.land/x/fresh@1.1.2/src/server/htmlescape.ts": "834ac7d0caa9fc38dffd9b8613fb47aeecd4f22d5d70c51d4b20a310c085835c", + "https://deno.land/x/fresh@1.1.2/src/server/mod.ts": "72d213444334dd2e94c757a0eee0fc486c0919399ea9184d07ad042f34edd00d", + "https://deno.land/x/fresh@1.1.2/src/server/render.ts": "6f50707bd1f6e33ed84bb71ae3b0996d202b953cefc4285f5356524c7b21f01f", + "https://deno.land/x/fresh@1.1.2/src/server/types.ts": "dde992ab4ee635df71a7fc96fe4cd85943c1a9286ea8eb586563d5f5ca154955", + "https://deno.land/x/fresh@1.1.3/dev.ts": "a66c7d64be35bcd6a8e12eec9c27ae335044c70363a241f2e36ee776db468622", + "https://deno.land/x/fresh@1.1.3/server.ts": "f379c9aad24471a71e58fb887fa57e5cc27ad9df035987eb260541c78df38e84", + "https://deno.land/x/fresh@1.1.3/src/dev/deps.ts": "de5470828c17839c0b52c328e6709f3477740b9800deaf724d6569b64b1d3872", + "https://deno.land/x/fresh@1.1.3/src/dev/error.ts": "21a38d240c00279662e6adde41367f1da0ae7e2836d993f818ea94aabab53e7b", + "https://deno.land/x/fresh@1.1.3/src/dev/mod.ts": "f5836b2eccd0efd7c0a726a121f174a974daefc22058f759f07d4df56c46e978", + "https://deno.land/x/fresh@1.1.3/src/runtime/csp.ts": "9ee900e9b0b786057b1009da5976298c202d1b86d1f1e4d2510bde5f06530ac9", + "https://deno.land/x/fresh@1.1.3/src/runtime/head.ts": "0f9932874497ab6e57ed1ba01d549e843523df4a5d36ef97460e7a43e3132fdc", + "https://deno.land/x/fresh@1.1.3/src/runtime/utils.ts": "8320a874a44bdd5905c7d4b87a0e7a14a6c50a2ed133800e72ae57341e4d4faa", + "https://deno.land/x/fresh@1.1.3/src/server/bundle.ts": "7fb20c084948894b8eca90984ef92c3f6d12a194910ecb9adee21ace9f1607bb", + "https://deno.land/x/fresh@1.1.3/src/server/constants.ts": "ad10dda1bc20c25c2926f6a8cfd79ef4368d70b4b03a645f65c04b3fa7d93a8c", + "https://deno.land/x/fresh@1.1.3/src/server/context.ts": "bf16ec4b6deccf0b588a294814d636bc1648647860ade31dbfdb8dc3e3435634", + "https://deno.land/x/fresh@1.1.3/src/server/default_error_page.ts": "9a1a595a1a2b31c9b724b04db82b8af256285536db272658d831ac9ef1d3d448", + "https://deno.land/x/fresh@1.1.3/src/server/deps.ts": "1b467e4d00109356b06117a9f2e5a21a2b48353a0e573b25cdbbfe7e9cfccb34", + "https://deno.land/x/fresh@1.1.3/src/server/htmlescape.ts": "834ac7d0caa9fc38dffd9b8613fb47aeecd4f22d5d70c51d4b20a310c085835c", + "https://deno.land/x/fresh@1.1.3/src/server/mod.ts": "72d213444334dd2e94c757a0eee0fc486c0919399ea9184d07ad042f34edd00d", + "https://deno.land/x/fresh@1.1.3/src/server/render.ts": "6f50707bd1f6e33ed84bb71ae3b0996d202b953cefc4285f5356524c7b21f01f", + "https://deno.land/x/fresh@1.1.3/src/server/types.ts": "dde992ab4ee635df71a7fc96fe4cd85943c1a9286ea8eb586563d5f5ca154955", + "https://deno.land/x/importmap@0.2.1/_util.ts": "ada9a9618b537e6c0316c048a898352396c882b9f2de38aba18fd3f2950ede89", + "https://deno.land/x/importmap@0.2.1/mod.ts": "ae3d1cd7eabd18c01a4960d57db471126b020f23b37ef14e1359bbb949227ade", + "https://deno.land/x/redis@v0.26.0/backoff.ts": "33e4a6e245f8743fbae0ce583993a671a3ac2ecee433a3e7f0bd77b5dd541d84", + "https://deno.land/x/redis@v0.26.0/command.ts": "1b49e8215941e53c61c7b9d408a57f54e8774263170f9a179c96e98f6e200453", + "https://deno.land/x/redis@v0.26.0/connection.ts": "c31d2e0cb360bc641e7286f1d53cf58790fbcda025c06887f84a821f39d0fdff", + "https://deno.land/x/redis@v0.26.0/errors.ts": "bc8f7091cb9f36cdd31229660e0139350b02c26851e3ac69d592c066745feb27", + "https://deno.land/x/redis@v0.26.0/executor.ts": "03e5f43df4e0c9c62b0e1be778811d45b6a1966ddf406e21ed5a227af70b7183", + "https://deno.land/x/redis@v0.26.0/mod.ts": "c6bc7a62be64b671a434eb79f33b467bd0528ca07bcb48cc7891bd2df45f7f92", + "https://deno.land/x/redis@v0.26.0/pipeline.ts": "80cc26a881149264d51dd019f1044c4ec9012399eca9f516057dc81c9b439370", + "https://deno.land/x/redis@v0.26.0/protocol/_util.ts": "0525f7f444a96b92cd36423abdfe221f8d8de4a018dc5cb6750a428a5fc897c2", + "https://deno.land/x/redis@v0.26.0/protocol/command.ts": "b1efd3b62fe5d1230e6d96b5c65ba7de1592a1eda2cc927161e5997a15f404ac", + "https://deno.land/x/redis@v0.26.0/protocol/mod.ts": "f2601df31d8adc71785b5d19f6a7e43dfce94adbb6735c4dafc1fb129169d11a", + "https://deno.land/x/redis@v0.26.0/protocol/reply.ts": "0876abb08f5740f5f8d62b5b6b96d80d49f54938afff1558615e5f1a05b8783e", + "https://deno.land/x/redis@v0.26.0/protocol/types.ts": "baadd6841901dda08d3e357b375d4c25d80b30fbdd81f43fd11089f8a23832ff", + "https://deno.land/x/redis@v0.26.0/pubsub.ts": "03d9624a8bc071891fb21b29624be0d60875fbd6ef06d5c71d96d778c9ab44ae", + "https://deno.land/x/redis@v0.26.0/redis.ts": "53f9ada525a36ec487232f912789ffa181068d8a9209958bb6dc0b521fee9b1f", + "https://deno.land/x/redis@v0.26.0/stream.ts": "fcbb29932755cdeb30e7f451bf3aa8d49b5df2c3897e1a4aaa25b210f0e865a2", + "https://deno.land/x/redis@v0.26.0/vendor/https/deno.land/std/async/deferred.ts": "d8a492d7b141ef1721b1ed0745e51c867b572e6bba1238011130d07ff973582b", + "https://deno.land/x/redis@v0.26.0/vendor/https/deno.land/std/async/delay.ts": "e324e793554896ff649731073b5d580c90ccd3baa88497d92c5ac2485967e32f", + "https://deno.land/x/redis@v0.26.0/vendor/https/deno.land/std/io/buffer.ts": "6f36c74f0fce64250c85c0b18621a9316efd70e6dfa910b905c3347d0ee6dd02", + "https://deno.land/x/rutt@0.0.13/mod.ts": "af981cfb95131152bf50fc9140fc00cb3efb6563df2eded1d408361d8577df20", + "https://deno.land/x/rutt@0.0.14/mod.ts": "5027b8e8b12acca48b396a25aee74ad7ee94a25c24cda75571d7839cbd41113c", + "https://deno.land/x/ts_morph@16.0.0/common/DenoRuntime.ts": "537800e840d0994f9055164e11bf33eadf96419246af0d3c453793c3ae67bdb3", + "https://deno.land/x/ts_morph@16.0.0/common/mod.ts": "01985d2ee7da8d1caee318a9d07664774fbee4e31602bc2bb6bb62c3489555ed", + "https://deno.land/x/ts_morph@16.0.0/common/ts_morph_common.d.ts": "39f2ddefd4995e4344236c44c2bf296069149f45ef6f00440b56e7b32cb2b3bd", + "https://deno.land/x/ts_morph@16.0.0/common/ts_morph_common.js": "7d908bf4f416aa96de956dc11ecc83b585bed297e16418d496ca04a3481067e0", + "https://deno.land/x/ts_morph@16.0.0/common/typescript.d.ts": "df7dd83543f14081ca74918d5a80ff60f634f465746cf2aff8924b28bcc3b152", + "https://deno.land/x/ts_morph@16.0.0/common/typescript.js": "5c59651248a4c41b25fa7beee8e0d0d0fab5f439fa72d478e65abd8241aa533c", + "https://deno.land/x/ts_morph@16.0.0/mod.ts": "adba9b82f24865d15d2c78ef6074b9a7457011719056c9928c800f130a617c93", + "https://deno.land/x/ts_morph@16.0.0/ts_morph.d.ts": "38668b0e3780282a56a805425494490b0045d1928bd040c47a94095749dab8c3", + "https://deno.land/x/ts_morph@16.0.0/ts_morph.js": "9fc0f3d6a3997c2df023fabc4e529d2117d214ffd4fd04247ca2f56c4e9cd470", + "https://esm.sh/*preact-render-to-string@5.2.4": "b5ea1be1c1c40c87e04f7f29921e504032e0afa618ca8941aad479ea0f4d8751", + "https://esm.sh/@supabase/supabase-js@1.35.4": "a6889a95301fed79a0b42b434b6249be0c82a57c0d28b5e7ece4e27833bfb74a", + "https://esm.sh/preact@10.11.0": "e888b244446037c56f1881173fb51d1f5fa7aae5599e6c5154619346a6a5094e", + "https://esm.sh/preact@10.11.0/hooks": "2b8ec155eb8b87501663f074acff1d55a9114fa7d88f0b39da06c940af1ff736", + "https://esm.sh/preact@10.11.0/jsx-runtime": "5c123264f19799ab243211132dded45f6d42d594b5c78dd585f947d07bf20eae", + "https://esm.sh/stable/preact@10.11.0/deno/hooks.js": "48b7674c1f0c2a0f8a0f758b786f5bc15ba0f7a4f3a356ecc783848f1e4a1c55", + "https://esm.sh/stable/preact@10.11.0/deno/jsx-runtime.js": "b5a8e96758c20b5dea05802c6f6962b8a95bfdbd476eb4ea51cf3234f6e09271", + "https://esm.sh/stable/preact@10.11.0/deno/preact.js": "071b515099e5dff2fe56768be62644e32fab702b194171357ccc4d7d1210144a", + "https://esm.sh/v106/preact-render-to-string@5.2.4/X-ZS8q/deno/preact-render-to-string.js": "75aad97b00d5ad63383de7f729a3f1fea59e69a9b876db5551d9c746ed372f82", + "https://esm.sh/v106/preact-render-to-string@5.2.4/X-ZS8q/src/index.d.ts": "b1d73703252c8570fdf2952475805f5808ba3511fefbd93a3e7bd8406de7dcd0", + "https://esm.sh/v106/preact@10.11.0/hooks/src/index.d.ts": "5c29febb624fc25d71cb0e125848c9b711e233337a08f7eacfade38fd4c14cc3", + "https://esm.sh/v106/preact@10.11.0/jsx-runtime/src/index.d.ts": "e153460ed2b3fe2ad8b93696ecd48fbf73cd628b0b0ea6692b71804a3af69dfd", + "https://esm.sh/v106/preact@10.11.0/src/index.d.ts": "1a5c331227be54be6515b0c92a469d352834fa413963ae84a39a05a3177111f6", + "https://esm.sh/v106/preact@10.11.0/src/jsx.d.ts": "c423715fd7992b2e1446fea11d2d04e8adbd66c1edca1ce5e85f90e0d26a2eb2", + "https://esm.sh/v87/@supabase/functions-js@1.3.4/deno/functions-js.js": "aec59baabe0d80f83abd20c8af974d566090d9be4a14f4170f8bc7a63284733e", + "https://esm.sh/v87/@supabase/functions-js@1.3.4/dist/module/index.d.ts": "88d9d5c482fd00057e47afb95b204059b817763c481ad8fd76b976a5fdad5181", + "https://esm.sh/v87/@supabase/functions-js@1.3.4/dist/module/types.d.ts": "97ea2a052b4f33565295e7b6d1418d26b61e802d4515155ff4de734fb73a932a", + "https://esm.sh/v87/@supabase/gotrue-js@1.22.19/deno/gotrue-js.js": "89481a98de68088ac7980e97019419f424b1934328364633dd9a8146c226010e", + "https://esm.sh/v87/@supabase/gotrue-js@1.22.19/dist/module/GoTrueApi.d.ts": "ebf54f33b81292ea8d39ef6b9ebd4b51cef8ebcdc0774922e51dda355243510a", + "https://esm.sh/v87/@supabase/gotrue-js@1.22.19/dist/module/GoTrueClient.d.ts": "97ff7c779d936403d30f4f8bf290672020269cf2043d1ce754e2c09bfb527a2d", + "https://esm.sh/v87/@supabase/gotrue-js@1.22.19/dist/module/index.d.ts": "a81c5527a5e21da98bd821db07950ab5c1e2ca499e84d3af3134232d15cea8ee", + "https://esm.sh/v87/@supabase/gotrue-js@1.22.19/dist/module/lib/fetch.d.ts": "68adc6de94a2dd86a09ee6dc5e292fa0611813b0ab21cde39cb557aacf301d4d", + "https://esm.sh/v87/@supabase/gotrue-js@1.22.19/dist/module/lib/types.d.ts": "153f43d531da4bf5ab87ff0d3245b7175e2bbf6be9bf4c2419c50d5cde35b0a2", + "https://esm.sh/v87/@supabase/postgrest-js@0.37.4/deno/postgrest-js.js": "8202dd997dd7476195b0ddad9636446176e868407e37a4e1587ab7968e3c6682", + "https://esm.sh/v87/@supabase/postgrest-js@0.37.4/dist/module/PostgrestClient.d.ts": "1f6c8fd94ad55208c9dbb6e8669adc4fe5db4c0c23b81c77ed7d0af764ea6857", + "https://esm.sh/v87/@supabase/postgrest-js@0.37.4/dist/module/index.d.ts": "ec8892c676ae5497b44f94e26e862e30e1419abf872728d94c08b781cba07a90", + "https://esm.sh/v87/@supabase/postgrest-js@0.37.4/dist/module/lib/PostgrestFilterBuilder.d.ts": "0ef020b3e17ae0a25c187fc55990c2b36210827fd2e38b9205d1d4ead8201a02", + "https://esm.sh/v87/@supabase/postgrest-js@0.37.4/dist/module/lib/PostgrestQueryBuilder.d.ts": "da3c017706eec6683d27fa21ec5722b195e323f53fbbbcac75e340752f9ab3dc", + "https://esm.sh/v87/@supabase/postgrest-js@0.37.4/dist/module/lib/PostgrestTransformBuilder.d.ts": "9c23d606830fc5f207af24b07700f862bf825ed4eb2456d79e0f1d97eced2042", + "https://esm.sh/v87/@supabase/postgrest-js@0.37.4/dist/module/lib/types.d.ts": "d5be981c5ad0336999d86a9c438ab9ea76f5f8be8c5103b6d55af2fed85c205c", + "https://esm.sh/v87/@supabase/realtime-js@1.7.3/deno/realtime-js.js": "adc3c59d18d2a5ae645e329d1f0d6df72759faf9c7c3337b63e122ae6a98af6c", + "https://esm.sh/v87/@supabase/realtime-js@1.7.3/dist/module/RealtimeChannel.d.ts": "427d149b691544f05f60f5a61e2b695df0491f7c8154bfb27ee6075e34a2d071", + "https://esm.sh/v87/@supabase/realtime-js@1.7.3/dist/module/RealtimeClient.d.ts": "3420af01cbcf053c099ad728d592a42e2c6ba5238c51eb6bb570c00eb82bdb42", + "https://esm.sh/v87/@supabase/realtime-js@1.7.3/dist/module/RealtimePresence.d.ts": "533295073209c9c93c303d70ec85c75ba412653a93922418830a81256133f159", + "https://esm.sh/v87/@supabase/realtime-js@1.7.3/dist/module/RealtimeSubscription.d.ts": "bf660e2de63a34d3a841b60065f96eb479ba3b3e81d036daf194c50207a2a89e", + "https://esm.sh/v87/@supabase/realtime-js@1.7.3/dist/module/index.d.ts": "654010dccdd8d7a8bd400c8e13b85100ac35d1a6cb3935b0f9bbb7c085358842", + "https://esm.sh/v87/@supabase/realtime-js@1.7.3/dist/module/lib/constants.d.ts": "b8392f2eb7cf41c3479c142875adbbe4b48bfafdced532165abc1cc68aa20cb5", + "https://esm.sh/v87/@supabase/realtime-js@1.7.3/dist/module/lib/push.d.ts": "d55942d47f5d6bf49e4ed2f6e35541efc404ef91421f09936271f668863ed772", + "https://esm.sh/v87/@supabase/realtime-js@1.7.3/dist/module/lib/serializer.d.ts": "4990f875f6e9a1898655fdba7b822222f8b930bfb7c9253b4f047aa4b05d4ca1", + "https://esm.sh/v87/@supabase/realtime-js@1.7.3/dist/module/lib/timer.d.ts": "ddf170afd193dc372316ed1f4a087f0e9732a5b8bd9cb9e5c3ac74f1cc676e31", + "https://esm.sh/v87/@supabase/realtime-js@1.7.3/dist/module/lib/transformers.d.ts": "7d48cf9114d770158dac276a93382f6e6b2d0c1fe563f463dccfd05b798db54f", + "https://esm.sh/v87/@supabase/storage-js@1.7.2/deno/storage-js.js": "657d7d32bf6a661b31aa58e7b7df3ea096734eeef3e716b00157f769956aaac8", + "https://esm.sh/v87/@supabase/storage-js@1.7.2/dist/module/StorageClient.d.ts": "bf8d39df125f56198444d5b086a8e2129586e0ec36cf60edc532084e4f76baa0", + "https://esm.sh/v87/@supabase/storage-js@1.7.2/dist/module/index.d.ts": "b2c4803f5f61e839e3d09cc1a17b5812389e17ef68f7040e92199e89f7bc0d5c", + "https://esm.sh/v87/@supabase/storage-js@1.7.2/dist/module/lib/StorageBucketApi.d.ts": "9908ab345d21bd854a598888303d2361e24f0299c5477fe0d072b043d55751b9", + "https://esm.sh/v87/@supabase/storage-js@1.7.2/dist/module/lib/StorageFileApi.d.ts": "836bd203dd84114a4a3fe3ff50fb3e6bd36d11d2e505f115059eebe0869ef349", + "https://esm.sh/v87/@supabase/storage-js@1.7.2/dist/module/lib/constants.d.ts": "c521b227c3bf3bf60807c36424ac2c575ac088cd1b50da880f1fca8ab18198ea", + "https://esm.sh/v87/@supabase/storage-js@1.7.2/dist/module/lib/fetch.d.ts": "e813237e484f3bccc439f070e710cb8a7411b8716ad3284897e9091e3580b6af", + "https://esm.sh/v87/@supabase/storage-js@1.7.2/dist/module/lib/index.d.ts": "1847fc2baf867156426311507838760146177aae022b0a7a522076b8318dc27b", + "https://esm.sh/v87/@supabase/storage-js@1.7.2/dist/module/lib/types.d.ts": "5145954f43385a10ad61b7de05c90d551b84af054d8d6c04a6c343f1afd9640c", + "https://esm.sh/v87/@supabase/supabase-js@1.35.4/deno/supabase-js.js": "5b5839ac543af8ddffc604ea140294e96cdf858b1b98038a617a122efdc35551", + "https://esm.sh/v87/@supabase/supabase-js@1.35.4/dist/module/SupabaseClient.d.ts": "4179218c6529756d7ca9f5f1d1c066c484b629eff593f97ee6582826080b4357", + "https://esm.sh/v87/@supabase/supabase-js@1.35.4/dist/module/index.d.ts": "3cede88ead4fe965d5aa175966406b67652e217c520fddf881275eb1da6c3755", + "https://esm.sh/v87/@supabase/supabase-js@1.35.4/dist/module/lib/SupabaseAuthClient.d.ts": "aa870fb8e56a7754fed63f6d751177bc571867b4e17b8d60c9b45b7289ea585b", + "https://esm.sh/v87/@supabase/supabase-js@1.35.4/dist/module/lib/SupabaseQueryBuilder.d.ts": "efb4ab4b520931704ffb49e67e7e897c144b5b79d185ca682adf0fc7e3689c7e", + "https://esm.sh/v87/@supabase/supabase-js@1.35.4/dist/module/lib/SupabaseRealtimeClient.d.ts": "9746f0c7f4987cd97e23973fc7319255a8d148e90ec47ee16a6c30c95542e912", + "https://esm.sh/v87/@supabase/supabase-js@1.35.4/dist/module/lib/types.d.ts": "8e42cd7d5ae450d11cb4eec3563cdbde2fdc93017edbef773647623149bdd674", + "https://esm.sh/v87/@types/phoenix@1.5.4/index.d.ts": "5bedfe185f19a13c7df212e1eec2584b710b9db5fd5e73b767df2e6116313565", + "https://esm.sh/v87/cross-fetch@3.1.5/deno/cross-fetch.js": "99e629f3e83aa4c4a4ef6a2932330ada74498e9dca8eea01fb2d952984245fcb", + "https://esm.sh/v87/es5-ext@0.10.61/deno/global.js": "79ab223f828d873f3b5b9988034661da729423cf8a250d859f90c446b8b5eb09", + "https://esm.sh/v87/node.ns.d.ts": "0fb081f0cd2150931bd54baa04f31466abd9ca701fd9060d07931402cf8367ba", + "https://esm.sh/v87/websocket@1.0.34/deno/websocket.js": "2d8198389bbadce81e2f3c1f4a62b72dbbc7341d216a6fe3c87c166471307da1" + } +} diff --git a/api/dev.ts b/api/dev.ts new file mode 100755 index 0000000..2d85d6c --- /dev/null +++ b/api/dev.ts @@ -0,0 +1,5 @@ +#!/usr/bin/env -S deno run -A --watch=static/,routes/ + +import dev from "$fresh/dev.ts"; + +await dev(import.meta.url, "./main.ts"); diff --git a/api/fresh.gen.ts b/api/fresh.gen.ts new file mode 100644 index 0000000..8df24b0 --- /dev/null +++ b/api/fresh.gen.ts @@ -0,0 +1,25 @@ +// DO NOT EDIT. This file is generated by fresh. +// This file SHOULD be checked into source version control. +// This file is automatically updated during development when running `dev.ts`. + +import config from "./deno.json" assert { type: "json" }; +import * as $0 from "./routes/_middleware.ts"; +import * as $1 from "./routes/index.tsx"; +import * as $2 from "./routes/search/[hex].ts"; +import * as $3 from "./routes/types.ts"; +import * as $4 from "./routes/utils.ts"; + +const manifest = { + routes: { + "./routes/_middleware.ts": $0, + "./routes/index.tsx": $1, + "./routes/search/[hex].ts": $2, + "./routes/types.ts": $3, + "./routes/utils.ts": $4, + }, + islands: {}, + baseUrl: import.meta.url, + config, +}; + +export default manifest; diff --git a/api/import_map.json b/api/import_map.json new file mode 100644 index 0000000..2b84d3d --- /dev/null +++ b/api/import_map.json @@ -0,0 +1,13 @@ +{ + "imports": { + "$fresh/": "https://deno.land/x/fresh@1.1.3/", + "preact": "https://esm.sh/preact@10.11.0", + "preact/": "https://esm.sh/preact@10.11.0/", + "preact-render-to-string": "https://esm.sh/*preact-render-to-string@5.2.4", + "url": "https://deno.land/std@0.148.0/node/url.ts", + "supabase": "https://esm.sh/@supabase/supabase-js@1.35.4", + "dotenv": "https://deno.land/x/dotenv@v3.2.0/load.ts", + "@preact/signals": "https://esm.sh/*@preact/signals@1.0.3", + "@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.0.1" + } +} diff --git a/api/main.ts b/api/main.ts new file mode 100644 index 0000000..6c5ff9e --- /dev/null +++ b/api/main.ts @@ -0,0 +1,18 @@ +/// +/// +/// +/// +/// + +import { start } from "$fresh/server.ts"; +import manifest from "./fresh.gen.ts"; + +import "dotenv"; +import { createClient } from "supabase"; + +export const supabase = createClient( + Deno.env.get("SUPABASE_URL") as string, + Deno.env.get("SUPABASE_PUBLIC_API_KEY") as string, +); + +await start(manifest); diff --git a/api/routes/_middleware.ts b/api/routes/_middleware.ts new file mode 100644 index 0000000..e89d979 --- /dev/null +++ b/api/routes/_middleware.ts @@ -0,0 +1,15 @@ +import { MiddlewareHandlerContext } from "$fresh/server.ts"; + +export async function handler( + _req: Request, + ctx: MiddlewareHandlerContext, +) { + const resp = await ctx.next(); + + const allowedOrigin = Deno.env.get("ACS_WEB_ORIGIN") as string; + + resp.headers.set("Access-Control-Allow-Origin", allowedOrigin); + resp.headers.set("Access-Control-Allow-Methods", "GET"); + + return resp; +} diff --git a/api/routes/index.tsx b/api/routes/index.tsx new file mode 100644 index 0000000..fefa2f5 --- /dev/null +++ b/api/routes/index.tsx @@ -0,0 +1,3 @@ +export default function Index() { + return APOD Color Search API; +} diff --git a/api/routes/search/[hex].ts b/api/routes/search/[hex].ts new file mode 100644 index 0000000..7281581 --- /dev/null +++ b/api/routes/search/[hex].ts @@ -0,0 +1,118 @@ +import { HandlerContext } from "$fresh/server.ts"; +import { supabase } from "../../main.ts"; +import { + hexToRgb, + minimizeDistance, + queryCache, + SuccessResponse, + updateCache, +} from "../utils.ts"; + +const ENABLE_LOGS = Deno.env.get("ENABLE_LOGS"); + +if (!ENABLE_LOGS) { + console.info = () => {}; +} + +export const handler = async ( + _req: Request, + ctx: HandlerContext, +): Promise => { + const { hex } = ctx.params; + + const cachedResult = await queryCache(hex); + + if (cachedResult.isCached) { + console.info(`Returning cached result for #${hex}`); + + return SuccessResponse(cachedResult.result); + } + + console.log("Getting RGB..."); + + const rgb = hexToRgb(`#${hex}`); + const [r, g, b] = rgb; + const COLOR_MATCH_THRESHOLD = 30; + + console.log(rgb); + + // Find all color matches within threshold + const { data: colors } = await supabase + .from("colors") + .select() + .gt("r", r - COLOR_MATCH_THRESHOLD) + .lt("r", r + COLOR_MATCH_THRESHOLD) + .gt("g", g - COLOR_MATCH_THRESHOLD) + .lt("g", g + COLOR_MATCH_THRESHOLD) + .gt("b", b - COLOR_MATCH_THRESHOLD) + .lt("b", b + COLOR_MATCH_THRESHOLD); + + // No color matches + if (!colors || colors?.length == 0) { + console.info(`Caching empty result for #${hex}`); + + await updateCache(hex, "[]"); + + return SuccessResponse([]); + } + + console.info( + `Closest colors to #${hex}`, + ":", + colors.map(({ hex }) => hex), + ); + + // TODO: gather closest colors since this can be long list + + // Find all clusters that contain color matches + const { data: clusters } = await supabase + .from("clusters") + .select() + .in( + "color_id", + colors?.map(({ id }) => id), + ) + // .order('percent', { ascending: false }) + .limit(15); + + // No cluster matches + if (!clusters || clusters?.length == 0) { + console.info(`Caching empty result for #${hex}`); + + await updateCache(hex, "[]"); + + return SuccessResponse([]); + } + + // Sort clusters by Euclidean distance to search color (smallest COLOR_MATCH_THRESHOLD) + clusters.sort((a, b) => minimizeDistance(a, b, rgb, colors)); + + console.info( + "Closest cluster to", + hex, + ":", + colors.find((color) => color.id === clusters[0].color_id), + ); + + const { data: days } = await supabase + .from("days") + .select() + .in( + "id", + clusters?.map(({ day_id }) => day_id), + ) + .is("exclude_from_results", null) + .limit(5); + + if (!days) { + console.info(`No matching days found for #${hex}`); + + return SuccessResponse([]); + } + + console.info(`Caching result for #${hex}`); + + await updateCache(hex, JSON.stringify(days)); + + return SuccessResponse(days); +}; diff --git a/api/routes/types.ts b/api/routes/types.ts new file mode 100644 index 0000000..e0b5625 --- /dev/null +++ b/api/routes/types.ts @@ -0,0 +1,35 @@ +export type ColorType = { + id: number; + created_at: string; + hex: string; + r: number; + g: number; + b: number; +}; + +export type ClusterType = { + id: number; + created_at: string; + color_id: number; + day_id: number; + freq: number; + percent: number; +}; + +export type DayType = { + id: number; + created_at: string; + date: string; + hdurl: string; + media_type: string; + title: string; + url: string; + exclude_from_results: boolean; +}; + +export type CacheResult = { + isCached: boolean; + result: DayType[]; +}; + +export type SearchResult = DayType[]; diff --git a/api/routes/utils.ts b/api/routes/utils.ts new file mode 100644 index 0000000..9c2cc78 --- /dev/null +++ b/api/routes/utils.ts @@ -0,0 +1,94 @@ +import { CacheResult, ClusterType, ColorType, SearchResult } from "./types.ts"; + +const DISABLE_CACHE = Deno.env.get("DISABLE_CACHE"); +const CACHE_URL = Deno.env.get("CACHE_URL"); + +export function SuccessResponse(data: SearchResult) { + return new Response(JSON.stringify({ data }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +} + +export function ErrorResponse(error: Response) { + return new Response(JSON.stringify(error), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); +} + +export function hexToRgb(hex: string): number[] { + const normal = hex.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i); + if (normal) return normal.slice(1).map((e) => parseInt(e, 16)); + + const shorthand = hex.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/i); + if (shorthand) return shorthand.slice(1).map((e) => 0x11 * parseInt(e, 16)); + + return []; +} + +export function getDistance(searchColor: number[], matchColor: ColorType) { + const matchColors = [matchColor.r, matchColor.g, matchColor.b]; + + return matchColors.map((matchColor, i) => + Math.abs(searchColor[i] - matchColor) + ) + .reduce((a, b) => a + b, 0); +} + +export function minimizeDistance( + a: ClusterType, + b: ClusterType, + searchColor: number[], + colors: ColorType[], +) { + const colorA = colors.find(({ id }: { id: number }) => id === a.color_id); + const colorB = colors.find(({ id }: { id: number }) => id === b.color_id); + + if (!colorA || !colorB) { + return Number.MAX_SAFE_INTEGER; + } + + return getDistance(searchColor, colorA) - getDistance(searchColor, colorB); +} + +export async function queryCache(hex: string): Promise { + if (DISABLE_CACHE) { + return { isCached: false, result: [] }; + } + + const response = await fetch(`${CACHE_URL}/get/${hex}`); + + if (!response.ok) { + throw Error(); + } + + const result = await response.json(); + + return result; +} + +export async function updateCache( + hex: string, + value: string, +): Promise { + if (DISABLE_CACHE) { + return { isCached: false, result: [] }; + } + + const response = await fetch(`${CACHE_URL}/set/${hex}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: value, + }); + + if (!response.ok) { + throw Error(); + } + + const result = await response.json(); + + return result; +} diff --git a/api/static/favicon.ico b/api/static/favicon.ico new file mode 100644 index 0000000..6af6fda Binary files /dev/null and b/api/static/favicon.ico differ diff --git a/cache/.dockerignore b/cache/.dockerignore new file mode 100644 index 0000000..5171c54 --- /dev/null +++ b/cache/.dockerignore @@ -0,0 +1,2 @@ +node_modules +npm-debug.log \ No newline at end of file diff --git a/cache/Dockerfile b/cache/Dockerfile new file mode 100644 index 0000000..b4dbbf0 --- /dev/null +++ b/cache/Dockerfile @@ -0,0 +1,19 @@ +FROM node:16 + +# Create app directory +WORKDIR /usr/src/app + +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+) +COPY package*.json ./ + +RUN npm install +# If you are building your code for production +# RUN npm ci --only=production + +# Bundle app source +COPY . . + +EXPOSE 4000 +CMD [ "node", "server.js" ] \ No newline at end of file diff --git a/cache/README.md b/cache/README.md new file mode 100644 index 0000000..e7e2c1c --- /dev/null +++ b/cache/README.md @@ -0,0 +1,16 @@ +# apod color search cache + +Powers `/get/:hex` and `/set/:hex` endpoints for interacting with Redis. + +Why Deno for API and separate Node-based cache? [Benchmarks](https://github.com/brycedorn/deno-node-redis-postgres-benchmarks) seem to indicate this is the most performant approach. + +### Usage + +Install dependencies and start the project: + +``` +npm i +npm start +``` + +This will watch the project directory and restart as necessary. diff --git a/cache/fly.toml b/cache/fly.toml new file mode 100644 index 0000000..d8f2a8d --- /dev/null +++ b/cache/fly.toml @@ -0,0 +1,37 @@ +# fly.toml file generated for apod-color-search-cache on 2023-02-18T15:18:25Z + +app = "apod-color-search-cache" +kill_signal = "SIGINT" +kill_timeout = 5 +processes = [] + +[env] + +[experimental] + auto_rollback = true + +[[services]] + http_checks = [] + internal_port = 4000 + processes = ["app"] + protocol = "tcp" + script_checks = [] + [services.concurrency] + hard_limit = 25 + soft_limit = 20 + type = "connections" + + [[services.ports]] + force_https = true + handlers = ["http"] + port = 80 + + [[services.ports]] + handlers = ["tls", "http"] + port = 443 + + [[services.tcp_checks]] + grace_period = "1s" + interval = "15s" + restart_limit = 0 + timeout = "2s" diff --git a/cache/package-lock.json b/cache/package-lock.json new file mode 100644 index 0000000..cd9a268 --- /dev/null +++ b/cache/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "apod-color-search-cache", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "apod-color-search-cache", + "version": "1.0.0", + "license": "MIT" + } + } +} diff --git a/cache/package.json b/cache/package.json new file mode 100644 index 0000000..5462e0c --- /dev/null +++ b/cache/package.json @@ -0,0 +1,15 @@ +{ + "name": "apod-color-search-cache", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "scripts": { + "start": "nodemon -r dotenv/config server.js dotenv_config_path=../.env" + }, + "dependencies": { + "dotenv": "^16.0.3", + "express": "^4.18.2", + "nodemon": "^2.0.20", + "redis": "^4.6.4" + } +} diff --git a/cache/server.js b/cache/server.js new file mode 100644 index 0000000..7aa4de7 --- /dev/null +++ b/cache/server.js @@ -0,0 +1,84 @@ +const express = require("express"); +const redis = require("redis"); + +const { REDIS_URL, REDIS_PORT, REDIS_PASSWORD, REDIS_USER } = process.env; + +const client = redis.createClient({ + url: `redis://${REDIS_USER}:${REDIS_PASSWORD}@${REDIS_URL}:${REDIS_PORT}/`, +}); + +client.on("error", (err) => console.log("Redis Client Error", err)); + +const app = express(); +const port = 4000; + +async function getCachedResult(req, res) { + const { hex } = req.params; + + console.log(`Checking cache for #${hex}...`); + + try { + const cachedResult = await client.get(hex); + let isCached = !!cachedResult; + let result; + + if (isCached) { + isCached = true; + result = JSON.parse(cachedResult); + } else { + result = null; + } + + res.send({ + isCached, + result, + }); + } catch (error) { + console.error(error); + } +} + +async function setCachedResult(req, res) { + const { + body, + params: { hex }, + } = req; + + console.log(`Setting value in cache for #${hex}...`); + + try { + const cachedResult = await client.set(hex, JSON.stringify(body)); + let isCached = !!cachedResult; + + res.send({ + isCached, + result: cachedResult, + }); + } catch (error) { + console.error(error); + } +} + +function quit(signal) { + console.log(`Received termination signal: ${signal}`); + + server.close(() => { + client.quit(() => { + console.log("Redis client quit."); + }); + console.log("HTTP server has been closed."); + process.exit(0); + }); +} + +app.use(express.json()); +app.get("/get/:hex", getCachedResult); +app.post("/set/:hex", setCachedResult); + +const server = app.listen(port, async () => { + await client.connect(); + console.log(`App listening on port ${port}`); +}); + +process.on("SIGINT", quit); +process.on("SIGTERM", quit); diff --git a/img/img-2.jpg b/img/img-2.jpg new file mode 100644 index 0000000..6d88c9a Binary files /dev/null and b/img/img-2.jpg differ diff --git a/img/img.jpeg b/img/img.jpeg new file mode 100644 index 0000000..77c052a Binary files /dev/null and b/img/img.jpeg differ diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..453d1b5 --- /dev/null +++ b/src/api.rs @@ -0,0 +1,394 @@ +use crate::image_utils; + +use postgrest::Postgrest; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::error::Error; +use std::string::String; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Day { + id: Option, + copyright: Option, + date: String, + explanation: Option, + hdurl: Option, + media_type: String, + service_version: Option, + title: String, + url: Option, + exclude_from_results: Option, +} + +impl Day { + pub fn is_picture(&self) -> bool { + self.media_type == "image" + && !self.image_url().contains(".mp4") + && !self.image_url().contains(".mpg") + && !self.image_url().contains(".mov") + && !self.image_url().contains(".wmv") + && !self.image_url().contains("big.gif") + } + + pub fn date(&self) -> String { + self.date.to_string() + } + + pub fn image_url(&self) -> String { + match &self.hdurl { + Some(s) => s.to_string(), + None => "none".to_string(), + } + } + + pub fn url(&self) -> String { + match &self.url { + Some(s) => s.to_string(), + None => "none".to_string(), + } + } + + pub fn db_id(&self) -> String { + match &self.id { + Some(s) => s.to_string(), + None => "none".to_string(), + } + } + + pub fn exclude_from_results(&self) -> bool { + match &self.exclude_from_results { + Some(b) => *b, + None => false, + } + } + + pub fn to_db_string(&self) -> String { + let res = format!( + r#"[{{ + "date": "{}", + "hdurl": "{}", + "media_type": "{}", + "title": "{}", + "url": "{}" + }}]"#, + self.date, + self.image_url(), + self.media_type, + self.title.replace(['\n', '\r'], " ").replace('\"', "'"), + self.url() + ); + println!("{:?}", res); + res + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Color { + id: u32, + hex: String, + created_at: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Cluster { + id: u32, + freq: u32, + color_id: u32, + day_id: u32, +} + +impl Cluster { + pub fn db_id(&self) -> String { + self.id.to_string() + } +} + +pub fn get_client() -> postgrest::Postgrest { + let rest_url = std::env::var("SUPABASE_REST_URL").unwrap(); + + Postgrest::new(rest_url) + .insert_header("apikey", std::env::var("SUPABASE_PUBLIC_API_KEY").unwrap()) +} + +pub async fn insert_color(hex: String) -> Result> { + let client = get_client(); + + let pixel = image_utils::generate_pixel(hex.to_string()); + + let resp = client + .from("colors") + .insert(format!( + r#"[{{ "hex": "{}", "r": "{}", "g": "{}", "b": "{}" }}]"#, + hex, pixel[0], pixel[1], pixel[2] + )) + .execute() + .await?; + + let body = resp.text().await?; + let result: Vec = serde_json::from_str(&body).unwrap(); + + match result.into_iter().next() { + Some(c) => Ok(c), + _ => Err("Error inserting color")?, + } +} + +pub async fn exclude_days(days: String) -> Result<(), Box> { + for day in days.split(',') { + exclude_day(day.to_string()).await?; + } + + Ok(()) +} + +pub async fn exclude_day(date: String) -> Result> { + let client = get_client(); + + let test = r#"[{ "exclude_from_results": "true" }]"#.to_string(); + + println!("update string: {}", test); + + let resp = client + .from("days") + .eq("date", date) + .update(test) + .execute() + .await?; + + let body = resp.text().await?; + let result: Vec = serde_json::from_str(&body).unwrap(); + + match result.into_iter().next() { + Some(d) => Ok(d), + _ => Err("Error excluding day")?, + } +} + +pub async fn _update_color(hex: String, pixel: image::Rgba) -> Result> { + let client = get_client(); + + let test = format!( + r#"[{{ "r": "{}", "g": "{}", "b": "{}" }}]"#, + pixel[0], pixel[1], pixel[2] + ); + + println!("update string: {}", test); + + let resp = client + .from("colors") + .eq("hex", hex) + .update(test) + .execute() + .await?; + + let body = resp.text().await?; + let result: Vec = serde_json::from_str(&body).unwrap(); + + match result.into_iter().next() { + Some(c) => Ok(c), + _ => Err("Error updating color")?, + } +} + +pub async fn has_clusters(day: Day) -> Result> { + let clusters = get_clusters(day).await?; + + Ok(!clusters.is_empty()) +} + +pub async fn get_clusters(day: Day) -> Result, Box> { + let client = get_client(); + + let resp = client + .from("clusters") + .eq("day_id", day.db_id()) + .execute() + .await?; + let body = resp.text().await?; + let results: Vec = serde_json::from_str(&body).unwrap(); + + Ok(results) +} + +pub async fn insert_cluster( + day_id: String, + hex: String, + freq: usize, + percent: f32, +) -> Result> { + let client = get_client(); + + let color = get_or_insert_color(hex).await?; + let color_id = color.id; + + let resp = client + .from("clusters") + .insert(format!( + r#"[{{ "day_id": {}, "color_id": {}, "freq": {}, "percent": {} }}]"#, + day_id, color_id, freq, percent + )) + .execute() + .await?; + + let body = resp.text().await?; + let result: Vec = serde_json::from_str(&body).unwrap(); + + match result.into_iter().next() { + Some(c) => Ok(c), + _ => Err("Error inserting cluster")?, + } +} + +pub async fn _update_cluster( + cluster_id: String, + p: f32, + s: f32, +) -> Result> { + let client = get_client(); + + let test = format!(r#"[{{ "percent": "{}", "significance": "{}" }}]"#, p, s); + + println!("update string: {}", test); + + let resp = client + .from("clusters") + .eq("id", cluster_id) + .update(test) + .execute() + .await?; + + let body = resp.text().await?; + + println!("{}", body); + + let result: Vec = serde_json::from_str(&body).unwrap(); + + match result.into_iter().next() { + Some(c) => Ok(c), + _ => Err("Error updating cluster")?, + } +} + +pub async fn insert_day(day: Day) -> Result> { + let client = get_client(); + + let resp = client + .from("days") + .insert(day.to_db_string()) + .execute() + .await?; + + let body: String = resp.text().await?; + let result: Vec = serde_json::from_str(&body).unwrap(); + + match result.into_iter().next() { + Some(c) => Ok(c), + _ => Err("Error inserting day")?, + } +} + +pub async fn get_or_insert_day(day: Day) -> Result> { + let client = get_client(); + + let resp = client.from("days").eq("date", &day.date).execute().await?; + let body = resp.text().await?; + let result: Vec = serde_json::from_str(&body).unwrap(); + + let res = match result.into_iter().next() { + Some(d) => d, + _ => insert_day(day).await?, + }; + + Ok(res) +} + +pub async fn get_or_insert_color(hex: String) -> Result> { + let client = get_client(); + + let resp = client.from("colors").eq("hex", &hex).execute().await?; + let body = resp.text().await?; + let result: Vec = serde_json::from_str(&body).unwrap(); + + let res = match result.into_iter().next() { + Some(c) => c, + _ => insert_color(hex).await?, + }; + + Ok(res) +} + +pub async fn _get_colors() -> Result, Box> { + let client = get_client(); + + let resp = client.from("colors").execute().await?; + let body = resp.text().await?; + let results: Vec = serde_json::from_str(&body).unwrap(); + + Ok(results) +} + +pub async fn get_days(start_date: &str, end_date: &str) -> Result, Box> { + let api_url = std::env::var("APOD_API_URL").unwrap(); + let api_key = std::env::var("APOD_API_KEY").unwrap(); + let request_url = format!( + "{}?api_key={}&start_date={}&end_date={}", + api_url, api_key, start_date, end_date + ); + + println!("request_url: {}", request_url); + + let resp = reqwest::get(request_url).await?; + let body = resp.text().await?; + let days: Vec = serde_json::from_str(&body).unwrap(); + + Ok(days) +} + +pub async fn get_image(url: &str) -> Result> { + let img_bytes = reqwest::get(url).await?.bytes().await?; + + Ok(img_bytes) +} + +pub async fn load_image(img_bytes: &[u8]) -> image::ImageResult { + let img = image::load_from_memory(img_bytes)?; + + Ok(img) +} + +pub async fn fetch_image(url: &str) -> Result> { + let img_bytes = get_image(url).await?; + let img = load_image(&img_bytes).await?; + + Ok(img) +} + +pub async fn kick_off_batch(year: String) -> Result<(), Box> { + for month in 0..12 { + dispatch_workflow(year.to_string(), (month + 1).to_string()).await?; + } + + Ok(()) +} + +pub async fn dispatch_workflow(year: String, month: String) -> Result> { + let client = reqwest::Client::new(); + + let body = json!({"ref":"main","inputs":{"year":year,"month":month}}); + + let auth_token = format!("Bearer {}", std::env::var("GH_AUTH_TOKEN").unwrap()); + + let res = client + .post("https://api.github.com/repos/brycedorn/apod-color-search/actions/workflows/process.yml/dispatches") + .header("Content-Type", "application/json") + .header("User-Agent", "apod-color-search") + .header("Accept", "application/vnd.github+json") + .header("Authorization", auth_token) + .json(&body) + .send() + .await? + .text() + .await?; + + Ok(res) +} diff --git a/src/image_utils.rs b/src/image_utils.rs new file mode 100644 index 0000000..ffb3776 --- /dev/null +++ b/src/image_utils.rs @@ -0,0 +1,246 @@ +use colorsys::Rgb; +use image::{DynamicImage, GenericImageView, Rgba}; +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::sync::{Arc, Mutex}; +use std::{i64, thread}; + +// Generate hex string from pixel. +pub fn generate_hex(pixel: Rgba) -> String { + Rgb::from((pixel[0] as f32, pixel[1] as f32, pixel[2] as f32)).to_hex_string() +} + +pub fn to_hex_vec(input: Vec>) -> Vec { + Vec::from_iter(input) + .into_iter() + .map(generate_hex) + .collect::>() +} + +// Generate pixel from hex string. +pub fn generate_pixel(h: String) -> Rgba { + let rgb = Rgb::from_hex_str(&h).unwrap(); + + Rgba([rgb.red() as u8, rgb.green() as u8, rgb.blue() as u8, 255]) +} + +pub fn to_pixel_vec(input: BTreeMap) -> Vec<(Rgba, usize)> { + Vec::from_iter(input) + .into_iter() + .map(|(h, f)| (generate_pixel(h), f)) + .collect::, usize)>>() +} + +// Get difference between two pixels for specific channel. +pub fn get_difference(a: &Rgba, b: &Rgba, color: usize) -> i64 { + let color1 = a.0[color] as i64; + let color2 = b.0[color] as i64; + + i64::abs(color1 - color2) +} + +pub fn get_luminance(pixel: Rgba) -> f32 { + let rgb = pixel.0; + + 0.2126 * rgb[0] as f32 + 0.7152 * rgb[1] as f32 + 0.0722 * rgb[2] as f32 +} + +// Determines if two pixels are within a similarity threshold. +pub fn within_threshold(a: &Rgba, b: &Rgba, color: usize, threshold: i64) -> bool { + let color1 = a.0[color] as i64; + let color2 = b.0[color] as i64; + + let mut min = 0; + let mut max = 255; + + if color2 >= threshold { + min = color2 - threshold; + } + + if color2 <= (255 - threshold) { + max = color2 + threshold + } + + color1 >= min && color1 <= max +} + +// Converts pixels to vector of colored hex values. +pub fn get_colors_from(img: &DynamicImage) -> Vec> { + let gray_pixels: HashSet> = + img.grayscale().pixels().into_iter().map(|p| p.2).collect(); + + let all_pixels: Vec> = img.pixels().into_iter().map(|p| p.2).collect(); + + let all_pixels_len = all_pixels.len(); + + let colored_pixels: Vec> = all_pixels + .into_iter() + .filter(|p| !gray_pixels.contains(p)) + .collect(); + + let colored_percent = (colored_pixels.len() as f32) / (all_pixels_len as f32) * 100.0; + + if colored_pixels.is_empty() { + println!("no colored pixels found."); + + return colored_pixels; + } + + println!("filtered out {:.2}% grayscale pixels", colored_percent); + + let luminous_pixels: Vec> = colored_pixels + .into_iter() + .filter(|p| get_luminance(*p) > 50.) + .collect(); + + let luminous_percent = (luminous_pixels.len() as f32) / (all_pixels_len as f32) * 100.0; + + println!("filtered out {:.2}% non-luminous pixels", luminous_percent); + + luminous_pixels +} + +// Get frequency for each color in image. +pub fn get_frequency(input: Vec, worker_count: usize) -> BTreeMap { + let result = Arc::new(Mutex::new(BTreeMap::::new())); + + input + .chunks((input.len() as f64 / worker_count as f64).ceil() as usize) + .enumerate() + .map(|(_, chunk)| { + let chunk = chunk.iter().map(String::from).collect::>(); + + let rresult = result.clone(); + + thread::spawn(move || { + chunk.iter().for_each(|h| { + rresult + .lock() + .unwrap() + .entry(h.to_string()) + .and_modify(|e| *e += 1) + .or_insert(1); + }) + }) + }) + .for_each(|handle| handle.join().unwrap()); + + Arc::try_unwrap(result).unwrap().into_inner().unwrap() +} + +// Combine similar colors. +pub fn assign_clusters(input: Vec<(Rgba, usize)>, threshold: i64) -> HashMap, usize> { + let mut result = HashMap::, usize>::new(); + + for item in input { + let rresult = result.clone(); + + // First filter for any similar R values + let s_r: Vec> = rresult + .into_keys() + .filter(|p| within_threshold(p, &item.0, 0, threshold)) + .collect(); + + if !s_r.is_empty() { + // Then filter for any similar RG values + let s_g: Vec<&Rgba> = s_r + .iter() + .filter(|p| within_threshold(p, &item.0, 1, threshold)) + .collect(); + + if !s_g.is_empty() { + // Then filter for any similar RGB values + let s_b: Vec<&&Rgba> = s_g + .iter() + .filter(|p| within_threshold(p, &item.0, 2, threshold)) + .collect(); + + if !s_b.is_empty() { + // Find closest one + let distances: Vec = + s_b.iter().map(|p| get_difference(p, &item.0, 2)).collect(); + + let index_of_closest: Option = distances + .iter() + .enumerate() + .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap()) + .map(|(index, _)| index); + + let similar_item = s_b[index_of_closest.unwrap()]; + + result.entry(**similar_item).and_modify(|e| *e += item.1); + } else { + result.insert(item.0, item.1); + } + } else { + result.insert(item.0, item.1); + } + } else { + result.insert(item.0, item.1); + } + } + + result +} + +// Reduce and sort hash of results. +pub fn get_top_clusters( + input: HashMap, usize>, + dimensions: (u32, u32), + num_clusters: usize, +) -> Vec<(String, (usize, f32))> { + let mut result = BTreeMap::::new(); + + let min_freq = 10; + + let total_pixels = dimensions.0 as f32 * dimensions.1 as f32; + + for (p, f) in input { + if f > min_freq { + result.insert(generate_hex(p), (f, (f as f32 * 100. / total_pixels))); + } + } + + let mut sorted_result = Vec::from_iter(result); + + sorted_result.sort_by(|(_, f_a), (_, f_b)| f_b.partial_cmp(f_a).unwrap()); + + let size = std::cmp::min(num_clusters, sorted_result.len()); + + sorted_result[0..size].to_vec() +} + +pub fn get_clusters_from_image(img: &image::DynamicImage) -> Vec<(String, (usize, f32))> { + println!( + "image loaded, dimensions: {}x{}", + img.dimensions().0, + img.dimensions().1 + ); + + let colors = get_colors_from(img); + let colors_len = colors.len(); + println!("found {colors_len} pixels..."); + + if colors_len == 0 { + return Vec::from([]); + } + + let colors_vec: Vec = to_hex_vec(colors); + let hex_freq = get_frequency(colors_vec, 5); + let hex_freq_len = hex_freq.len().to_string(); + println!("got frequency for {hex_freq_len} pixels..."); + + let pixel_vec: Vec<(Rgba, usize)> = to_pixel_vec(hex_freq); + let clusters = assign_clusters(pixel_vec, 20); + let clust_len: String = clusters.len().to_string(); + println!("{clust_len} clusters generated"); + + let top_clusters: Vec<(String, (usize, f32))> = + get_top_clusters(clusters, img.dimensions(), 20); + println!("top 10 clusters:"); + + for (h, (f, p)) in top_clusters.clone() { + println!("{h}: {f} ({p}%)"); + } + + top_clusters +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..1b03c99 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,2 @@ +pub mod api; +pub mod image_utils; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e425d0f --- /dev/null +++ b/src/main.rs @@ -0,0 +1,121 @@ +extern crate image; + +mod api; +mod image_utils; + +use chrono::{Datelike, Duration, TimeZone, Utc}; +use howlong::ProcessCPUTimer; +use std::env; +use std::error::Error; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let timer = ProcessCPUTimer::new(); + + let args: Vec = env::args().collect(); + + // TODO: split out of main + if args[1] == "batch" { + api::kick_off_batch(args[2].to_string()).await?; + + return Ok(()); + } else if args[1] == "exclude" { + api::exclude_days(args[2].to_string()).await?; + + return Ok(()); + } + + let first_apod = Utc.ymd(1995, 6, 16); + let today = Utc::today(); + + let numbers: Vec = args.iter().flat_map(|x| x.parse()).collect(); + let day = Utc.ymd(numbers[0] as i32, numbers[1], 1); + + if day < first_apod || day > today { + Err(format!( + "Out of range, date must be between {} and {}.", + first_apod.format("%b %e, %Y"), + today.format("%b %e, %Y") + ))?; + } + + let apods = fetch_month(day).await?; + let apods_len = apods.len(); + + let mut i = 1; + + for apod in apods { + process_apod(apod).await?; + + println!("completed {i} of {apods_len} at {:?}", timer.elapsed().real); + + i += 1; + } + + Ok(()) +} + +async fn fetch_month(first_day: chrono::Date) -> Result, Box> { + let first_day_formatted = first_day.format("%Y-%m-%d").to_string(); + let today = Utc::today(); + + let mut last_day = (first_day + Duration::days(31)).with_day(1).unwrap() - Duration::days(1); + + if last_day > today { + last_day = today; + } + + let last_day_formatted = last_day.format("%Y-%m-%d").to_string(); + + println!( + "retrieving data for range {} - {}", + first_day_formatted, last_day_formatted + ); + + let apods = api::get_days(&first_day_formatted, &last_day_formatted).await?; + + let apods_len = apods.len(); + println!("retrieved data for {} apods.", apods_len); + + Ok(apods) +} + +async fn process_apod(apod: api::Day) -> Result<(), Box> { + let image_url = apod.image_url().to_string(); + println!("loading {}", image_url); + + let day = api::get_or_insert_day(apod).await?; + + let is_picture = day.is_picture(); + let exclude_from_results = day.exclude_from_results(); + let day_id = day.db_id(); + + println!("got day for {}, id: {}", day.date(), day_id); + + if is_picture { + if !exclude_from_results { + let has_clusters = api::has_clusters(day).await?; + + if !has_clusters { + let img = api::fetch_image(&image_url).await?; + let clusters = image_utils::get_clusters_from_image(&img); + + println!("saving {} clusters", clusters.len()); + + for (h, (f, p)) in clusters { + let db_cluster = api::insert_cluster(day_id.to_string(), h, f, p).await?; + + println!("saved cluster {}", db_cluster.db_id()); + } + } else { + println!("already generated clusters, skipping"); + } + } else { + println!("excluded from results, skipping"); + } + } else { + println!("isn't a picture, skipping"); + } + + Ok(()) +} diff --git a/tests/image_utils_test.rs b/tests/image_utils_test.rs new file mode 100644 index 0000000..c5038d2 --- /dev/null +++ b/tests/image_utils_test.rs @@ -0,0 +1,104 @@ +extern crate apod_color_search; + +use apod_color_search::image_utils; +use image::Rgba; + +#[test] +fn test_generate_hex() { + let pixel = Rgba([255, 0, 221, 255]); + let hex = image_utils::generate_hex(pixel); + + assert_eq!(hex, "#ff00dd"); +} + +#[test] +fn test_generate_pixel() { + let hex = "#ff00dd".to_string(); + let pixel = image_utils::generate_pixel(hex); + + assert_eq!(pixel, Rgba([255, 0, 221, 255])); +} + +#[test] +fn test_within_threshold() { + let pixel_a = Rgba([255, 0, 221, 255]); + let pixel_b = Rgba([250, 30, 0, 0]); + + assert!(image_utils::within_threshold(&pixel_a, &pixel_b, 0, 10)); + assert!(!image_utils::within_threshold(&pixel_a, &pixel_b, 1, 10)); + assert!(image_utils::within_threshold(&pixel_a, &pixel_b, 2, 250)); +} + +#[test] +fn test_get_difference() { + let pixel_a = Rgba([255, 0, 221, 255]); + let pixel_b = Rgba([250, 30, 0, 0]); + + assert_eq!(5, image_utils::get_difference(&pixel_a, &pixel_b, 0)); + assert_eq!(30, image_utils::get_difference(&pixel_a, &pixel_b, 1)); +} + +#[test] +fn test_get_luminance() { + let pixel_a = Rgba([255, 0, 221, 255]); + let pixel_b = Rgba([0, 0, 10, 255]); + + assert_eq!(70.1692, image_utils::get_luminance(pixel_a)); + assert_eq!(0.722, image_utils::get_luminance(pixel_b)); +} + +#[test] +fn test_get_colors_from() { + let img = image::open("img/img.jpeg").unwrap(); + let set = image_utils::get_colors_from(&img); + + // Slight differences across OS + assert!([3113, 3111].contains(&set.len())); +} + +#[test] +fn test_get_frequency() { + let vec = Vec::from([ + String::from("#ff00dd"), + String::from("#00ffdd"), + String::from("#ff00dd"), + ]); + + let set = image_utils::get_frequency(vec, 1); + + assert_eq!(set.len(), 2); + assert_eq!(set["#ff00dd"], 2); + assert_eq!(set["#00ffdd"], 1); +} + +#[test] +fn test_assign_clusters() { + let pixel_a = Rgba([255, 0, 221, 255]); + let pixel_b = Rgba([0, 0, 0, 0]); + + let vec = Vec::from([ + (pixel_a, 500), + (Rgba([255, 0, 222, 255]), 50), + (Rgba([255, 10, 221, 255]), 40), + (Rgba([255, 0, 224, 255]), 30), + (pixel_b, 100), + ]); + + let clusters = image_utils::assign_clusters(vec, 10); + assert_eq!(2, clusters.len()); + let keys: Vec> = clusters.into_keys().collect(); + assert!(keys.contains(&pixel_a)); + assert!(keys.contains(&pixel_b)); +} + +#[test] +fn test_clusters_from_image() { + let img = image::open("img/img-2.jpg").unwrap(); + let clusters = image_utils::get_clusters_from_image(&img); + + let (h, (_f, _p)) = &clusters[0]; + + assert_eq!(h, "#436ab7"); + // assert_eq!(*f, 58813); + // assert_eq!(*p, 46.86295); +} diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/web/.vscode/extensions.json b/web/.vscode/extensions.json new file mode 100644 index 0000000..bdef820 --- /dev/null +++ b/web/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["svelte.svelte-vscode"] +} diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..ca83a43 --- /dev/null +++ b/web/index.html @@ -0,0 +1,13 @@ + + + + + + + APOD Color Search + + +
+ + + diff --git a/web/jsconfig.json b/web/jsconfig.json new file mode 100644 index 0000000..ee5e92f --- /dev/null +++ b/web/jsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "moduleResolution": "Node", + "target": "ESNext", + "module": "ESNext", + /** + * svelte-preprocess cannot figure out whether you have + * a value or a type, so tell TypeScript to enforce using + * `import type` instead of `import` for Types. + */ + "importsNotUsedAsValues": "error", + "isolatedModules": true, + "resolveJsonModule": true, + /** + * To have warnings / errors of the Svelte compiler at the + * correct position, enable source maps by default. + */ + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable this if you'd like to use dynamic types. + */ + "checkJs": true + }, + /** + * Use global.d.ts instead of compilerOptions.types + * to avoid limiting type declarations. + */ + "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"] +} diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..0ecae01 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,569 @@ +{ + "name": "apod-color-search", + "version": "0.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "apod-color-search", + "version": "0.0.0", + "dependencies": { + "throttle-debounce": "^5.0.0", + "vanilla-colorful": "^0.7.1" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^1.0.1", + "svelte": "^3.49.0", + "vite": "^3.0.7" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^4.2.1", + "debug": "^4.3.4", + "deepmerge": "^4.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.26.2", + "svelte-hmr": "^0.14.12" + }, + "engines": { + "node": "^14.18.0 || >= 16" + }, + "peerDependencies": { + "diff-match-patch": "^1.0.5", + "svelte": "^3.44.0", + "vite": "^3.0.0" + }, + "peerDependenciesMeta": { + "diff-match-patch": { + "optional": true + } + } + }, + "node_modules/debug": { + "version": "4.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/esbuild": { + "version": "0.14.54", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/linux-loong64": "0.14.54", + "esbuild-android-64": "0.14.54", + "esbuild-android-arm64": "0.14.54", + "esbuild-darwin-64": "0.14.54", + "esbuild-darwin-arm64": "0.14.54", + "esbuild-freebsd-64": "0.14.54", + "esbuild-freebsd-arm64": "0.14.54", + "esbuild-linux-32": "0.14.54", + "esbuild-linux-64": "0.14.54", + "esbuild-linux-arm": "0.14.54", + "esbuild-linux-arm64": "0.14.54", + "esbuild-linux-mips64le": "0.14.54", + "esbuild-linux-ppc64le": "0.14.54", + "esbuild-linux-riscv64": "0.14.54", + "esbuild-linux-s390x": "0.14.54", + "esbuild-netbsd-64": "0.14.54", + "esbuild-openbsd-64": "0.14.54", + "esbuild-sunos-64": "0.14.54", + "esbuild-windows-32": "0.14.54", + "esbuild-windows-64": "0.14.54", + "esbuild-windows-arm64": "0.14.54" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.14.54", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/has": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/is-core-module": { + "version": "2.10.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/magic-string": { + "version": "0.26.2", + "dev": true, + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.4", + "dev": true, + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.4.16", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/resolve": { + "version": "1.22.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "2.77.3", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "3.49.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/svelte-hmr": { + "version": "0.14.12", + "dev": true, + "license": "ISC", + "engines": { + "node": "^12.20 || ^14.13.1 || >= 16" + }, + "peerDependencies": { + "svelte": ">=3.19.0" + } + }, + "node_modules/throttle-debounce": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz", + "integrity": "sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==", + "engines": { + "node": ">=12.22" + } + }, + "node_modules/vanilla-colorful": { + "version": "0.7.1", + "license": "MIT" + }, + "node_modules/vite": { + "version": "3.0.9", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.14.47", + "postcss": "^8.4.16", + "resolve": "^1.22.1", + "rollup": ">=2.75.6 <2.77.0 || ~2.77.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "less": "*", + "sass": "*", + "stylus": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + }, + "dependencies": { + "@rollup/pluginutils": { + "version": "4.2.1", + "dev": true, + "requires": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + } + }, + "@sveltejs/vite-plugin-svelte": { + "version": "1.0.2", + "dev": true, + "requires": { + "@rollup/pluginutils": "^4.2.1", + "debug": "^4.3.4", + "deepmerge": "^4.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.26.2", + "svelte-hmr": "^0.14.12" + } + }, + "debug": { + "version": "4.3.4", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "deepmerge": { + "version": "4.2.2", + "dev": true + }, + "esbuild": { + "version": "0.14.54", + "dev": true, + "requires": { + "@esbuild/linux-loong64": "0.14.54", + "esbuild-android-64": "0.14.54", + "esbuild-android-arm64": "0.14.54", + "esbuild-darwin-64": "0.14.54", + "esbuild-darwin-arm64": "0.14.54", + "esbuild-freebsd-64": "0.14.54", + "esbuild-freebsd-arm64": "0.14.54", + "esbuild-linux-32": "0.14.54", + "esbuild-linux-64": "0.14.54", + "esbuild-linux-arm": "0.14.54", + "esbuild-linux-arm64": "0.14.54", + "esbuild-linux-mips64le": "0.14.54", + "esbuild-linux-ppc64le": "0.14.54", + "esbuild-linux-riscv64": "0.14.54", + "esbuild-linux-s390x": "0.14.54", + "esbuild-netbsd-64": "0.14.54", + "esbuild-openbsd-64": "0.14.54", + "esbuild-sunos-64": "0.14.54", + "esbuild-windows-32": "0.14.54", + "esbuild-windows-64": "0.14.54", + "esbuild-windows-arm64": "0.14.54" + } + }, + "esbuild-darwin-arm64": { + "version": "0.14.54", + "dev": true, + "optional": true + }, + "estree-walker": { + "version": "2.0.2", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "dev": true + }, + "has": { + "version": "1.0.3", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "is-core-module": { + "version": "2.10.0", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "kleur": { + "version": "4.1.5", + "dev": true + }, + "magic-string": { + "version": "0.26.2", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.8" + } + }, + "ms": { + "version": "2.1.2", + "dev": true + }, + "nanoid": { + "version": "3.3.4", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "dev": true + }, + "postcss": { + "version": "8.4.16", + "dev": true, + "requires": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "resolve": { + "version": "1.22.1", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "rollup": { + "version": "2.77.3", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "source-map-js": { + "version": "1.0.2", + "dev": true + }, + "sourcemap-codec": { + "version": "1.4.8", + "dev": true + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true + }, + "svelte": { + "version": "3.49.0", + "dev": true + }, + "svelte-hmr": { + "version": "0.14.12", + "dev": true, + "requires": {} + }, + "throttle-debounce": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz", + "integrity": "sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==" + }, + "vanilla-colorful": { + "version": "0.7.1" + }, + "vite": { + "version": "3.0.9", + "dev": true, + "requires": { + "esbuild": "^0.14.47", + "fsevents": "~2.3.2", + "postcss": "^8.4.16", + "resolve": "^1.22.1", + "rollup": ">=2.75.6 <2.77.0 || ~2.77.0" + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..7e7fb32 --- /dev/null +++ b/web/package.json @@ -0,0 +1,20 @@ +{ + "name": "apod-color-search", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^1.0.1", + "svelte": "^3.49.0", + "vite": "^3.0.7" + }, + "dependencies": { + "throttle-debounce": "^5.0.0", + "vanilla-colorful": "^0.7.1" + } +} diff --git a/web/public/vite.svg b/web/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/web/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/App.svelte b/web/src/App.svelte new file mode 100644 index 0000000..77950ec --- /dev/null +++ b/web/src/App.svelte @@ -0,0 +1,187 @@ + + +
+

+ APOD Color Search +

+

+ 🪐 +

+ + + + {#if showColorPicker} +
+ +
+ {/if} +
+ +{#if results.length > 0} + +{/if} + +{#if inputColor === ""} +

+ Use the color picker at the top right of the page to search for pictures + that match that color! +

+{/if} + +
+ Disclaimer: this site has no affiliation with APOD. All content displayed here is hosted by and links directly to + APOD. +
diff --git a/web/src/app.css b/web/src/app.css new file mode 100644 index 0000000..38c8fbb --- /dev/null +++ b/web/src/app.css @@ -0,0 +1,197 @@ +:root { + --top-bar-height: 60px; + --color-dark: #151515; + --color-light: #f9f9f9; + --color-mid-light: #eaeaea; + --color-dark-navy: #213547; + + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + + color-scheme: light dark; + color: var(--color-light); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +a { + color: var(--color-light); + text-decoration: none; +} + +a:hover { + border-bottom: var(--color-light) solid 2px; +} + +#right { + display: flex; + align-items: center; + margin-right: 20px; +} + +h2 a { + text-decoration: none; + cursor: pointer; +} + +#top-bar { + border-bottom: var(--color) solid 2px; +} + +#color-picker { + position: absolute; + top: calc(var(--top-bar-height) + 15px); + right: 15px; +} + +#color-wheel-button img { + max-width: 37px; +} + +#close-indicator { + font-size: 2em; + color: var(--color); + transform: rotate(45deg); +} + +hex-color-picker { + width: calc(min(100vw, 400px) - 15px); + height: calc(min(100vw, 400px) - 15px); +} + +span { + background: var(--color); + color: white; + border-radius: 4px; + padding: 4px; + transition: color 100ms; +} + +#results { + margin-top: calc(var(--top-bar-height) + 40px); +} + +#results h2 { + margin-top: 40px; + margin: 2em 1em 1em 1em; + line-height: 1.2em; +} + +img { + max-width: 80%; +} + +img:hover { + border-bottom: none; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-height: 100vh; +} + +#app { + width: 100%; + margin: 0 auto; + text-align: center; +} + +footer { + font-size: 0.8em; + margin: 4em 1em 1em 1em; + width: 100%; +} + +footer.bottom { + position: absolute; + bottom: 0; +} + +footer a:hover { + border-width: 1px; +} + +#top-bar { + display: flex; + width: 100%; + justify-content: space-between; + align-items: center; + height: var(--top-bar-height); + top: 0; + position: fixed; + background: var(--color-dark); +} + +#help { + background: var(--color-dark); + padding: 1em 0.6em; + border-radius: 10px; + max-width: 300px; + margin: 0 auto; + line-height: 1.6em; +} + +#color-wheel-button { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + margin-left: 20px; + cursor: pointer; + background: none; + border: var(--color) solid 2px; + border-radius: 50%; +} + +#title, #title-sm { + margin-left: 20px; +} + +#title-sm { + display: none; +} + +@media (prefers-color-scheme: light) { + :root { + color: var(--color-dark-navy); + background-color: var(--color-light); + } + a { + color: var(--color-dark-navy); + } + a:hover { + border-bottom: var(--color-dark-navy) solid 2px; + } + #top-bar, + #help { + background-color: var(--color-mid-light); + } + button { + background-color: var(--color-light); + } +} + +@media (max-width: 600px) { + #title { + display: none; + } + #title-sm { + display: block; + } + footer { + width: unset; + } + a:hover { + border-bottom: none; + } +} diff --git a/web/src/assets/color-wheel.svg b/web/src/assets/color-wheel.svg new file mode 100644 index 0000000..b7d9afd --- /dev/null +++ b/web/src/assets/color-wheel.svg @@ -0,0 +1,1221 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + 2017 + + + Lazur URH + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/src/assets/favicon.ico b/web/src/assets/favicon.ico new file mode 100644 index 0000000..6af6fda Binary files /dev/null and b/web/src/assets/favicon.ico differ diff --git a/web/src/lazy-load.js b/web/src/lazy-load.js new file mode 100644 index 0000000..3a2f224 --- /dev/null +++ b/web/src/lazy-load.js @@ -0,0 +1,31 @@ +export const lazyLoad = (image, options) => { + const { src, callback } = options; + + const onLoad = () => { + if (callback) { + callback(); + } + }; + + const observer = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting) { + if (!image.src) { + image.src = src; + } + + if (image.complete) { + onLoad(); + } else { + image.addEventListener("load", onLoad); + } + } + }); + + observer.observe(image); + + return { + destroy() { + image.removeEventListener("load", onLoad); + }, + }; +}; diff --git a/web/src/main.js b/web/src/main.js new file mode 100644 index 0000000..7ece250 --- /dev/null +++ b/web/src/main.js @@ -0,0 +1,8 @@ +import "./app.css"; +import App from "./App.svelte"; + +const app = new App({ + target: document.getElementById("app"), +}); + +export default app; diff --git a/web/src/vite-env.d.ts b/web/src/vite-env.d.ts new file mode 100644 index 0000000..4078e74 --- /dev/null +++ b/web/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/web/vite.config.js b/web/vite.config.js new file mode 100644 index 0000000..b19daeb --- /dev/null +++ b/web/vite.config.js @@ -0,0 +1,10 @@ +import { defineConfig } from "vite"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; + +export default defineConfig({ + plugins: [svelte()], + server: { + port: 3000, + }, + base: "/apod-color-search/", +});