From 803c813786f3b91efddae9a90817ea7f03055d78 Mon Sep 17 00:00:00 2001 From: geoffrey45 Date: Tue, 14 Dec 2021 09:02:02 +0300 Subject: [PATCH] add server code --- server/.gitignore | 1 + server/Pipfile | 23 + server/Pipfile.lock | 662 ++++++++++++++++++ server/app/__init__.py | 17 + .../app/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 618 bytes server/app/api.py | 335 +++++++++ server/app/configs.py | 6 + server/app/helpers.py | 262 +++++++ server/app/models.py | 82 +++ server/app/watchdoge.py | 58 ++ server/roadmap.md | 63 ++ server/start.sh | 10 + server/test.py | 29 + server/thumbnail_extractor.py | 132 ++++ 14 files changed, 1680 insertions(+) create mode 100644 server/.gitignore create mode 100644 server/Pipfile create mode 100644 server/Pipfile.lock create mode 100644 server/app/__init__.py create mode 100644 server/app/__pycache__/__init__.cpython-38.pyc create mode 100644 server/app/api.py create mode 100644 server/app/configs.py create mode 100644 server/app/helpers.py create mode 100644 server/app/models.py create mode 100644 server/app/watchdoge.py create mode 100644 server/roadmap.md create mode 100755 server/start.sh create mode 100644 server/test.py create mode 100644 server/thumbnail_extractor.py diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..600d2d3 --- /dev/null +++ b/server/.gitignore @@ -0,0 +1 @@ +.vscode \ No newline at end of file diff --git a/server/Pipfile b/server/Pipfile new file mode 100644 index 0000000..2f27e44 --- /dev/null +++ b/server/Pipfile @@ -0,0 +1,23 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +flask = "*" +tk = "*" +flask-cors = "*" +mutagen = "*" +pymongo = "*" +click = "*" +requests = "*" +watchdog = "*" +progress = "*" +pillow = "*" + +[dev-packages] +autopep8 = "*" +ipykernel = "*" + +[requires] +python_version = "3.8" diff --git a/server/Pipfile.lock b/server/Pipfile.lock new file mode 100644 index 0000000..cccae98 --- /dev/null +++ b/server/Pipfile.lock @@ -0,0 +1,662 @@ +{ + "_meta": { + "hash": { + "sha256": "99f472c203bd38494cccddd96820194910b1693da8ea6ed6d4b141895ff3002e" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.8" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", + "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" + ], + "version": "==2021.10.8" + }, + "charset-normalizer": { + "hashes": [ + "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721", + "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c" + ], + "markers": "python_version >= '3'", + "version": "==2.0.9" + }, + "click": { + "hashes": [ + "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", + "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" + ], + "index": "pypi", + "version": "==8.0.3" + }, + "flask": { + "hashes": [ + "sha256:7b2fb8e934ddd50731893bdcdb00fc8c0315916f9fcd50d22c7cc1a95ab634e2", + "sha256:cb90f62f1d8e4dc4621f52106613488b5ba826b2e1e10a33eac92f723093ab6a" + ], + "index": "pypi", + "version": "==2.0.2" + }, + "flask-cors": { + "hashes": [ + "sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438", + "sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de" + ], + "index": "pypi", + "version": "==3.0.10" + }, + "idna": { + "hashes": [ + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + ], + "markers": "python_version >= '3'", + "version": "==3.3" + }, + "itsdangerous": { + "hashes": [ + "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c", + "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0" + ], + "version": "==2.0.1" + }, + "jinja2": { + "hashes": [ + "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8", + "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7" + ], + "version": "==3.0.3" + }, + "markupsafe": { + "hashes": [ + "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", + "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", + "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", + "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194", + "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", + "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", + "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", + "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", + "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", + "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", + "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", + "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a", + "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", + "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", + "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", + "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", + "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", + "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", + "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", + "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047", + "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", + "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", + "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b", + "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", + "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", + "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", + "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", + "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1", + "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", + "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", + "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", + "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee", + "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f", + "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", + "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", + "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", + "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", + "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", + "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", + "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86", + "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6", + "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", + "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", + "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", + "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", + "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e", + "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", + "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", + "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f", + "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", + "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", + "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", + "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", + "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", + "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", + "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", + "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a", + "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207", + "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", + "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", + "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd", + "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", + "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", + "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9", + "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", + "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", + "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", + "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", + "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" + ], + "version": "==2.0.1" + }, + "mutagen": { + "hashes": [ + "sha256:6397602efb3c2d7baebd2166ed85731ae1c1d475abca22090b7141ff5034b3e1", + "sha256:9c9f243fcec7f410f138cb12c21c84c64fde4195481a30c9bfb05b5f003adfed" + ], + "index": "pypi", + "version": "==1.45.1" + }, + "pillow": { + "hashes": [ + "sha256:066f3999cb3b070a95c3652712cffa1a748cd02d60ad7b4e485c3748a04d9d76", + "sha256:0a0956fdc5defc34462bb1c765ee88d933239f9a94bc37d132004775241a7585", + "sha256:0b052a619a8bfcf26bd8b3f48f45283f9e977890263e4571f2393ed8898d331b", + "sha256:1394a6ad5abc838c5cd8a92c5a07535648cdf6d09e8e2d6df916dfa9ea86ead8", + "sha256:1bc723b434fbc4ab50bb68e11e93ce5fb69866ad621e3c2c9bdb0cd70e345f55", + "sha256:244cf3b97802c34c41905d22810846802a3329ddcb93ccc432870243211c79fc", + "sha256:25a49dc2e2f74e65efaa32b153527fc5ac98508d502fa46e74fa4fd678ed6645", + "sha256:2e4440b8f00f504ee4b53fe30f4e381aae30b0568193be305256b1462216feff", + "sha256:3862b7256046fcd950618ed22d1d60b842e3a40a48236a5498746f21189afbbc", + "sha256:3eb1ce5f65908556c2d8685a8f0a6e989d887ec4057326f6c22b24e8a172c66b", + "sha256:3f97cfb1e5a392d75dd8b9fd274d205404729923840ca94ca45a0af57e13dbe6", + "sha256:493cb4e415f44cd601fcec11c99836f707bb714ab03f5ed46ac25713baf0ff20", + "sha256:4acc0985ddf39d1bc969a9220b51d94ed51695d455c228d8ac29fcdb25810e6e", + "sha256:5503c86916d27c2e101b7f71c2ae2cddba01a2cf55b8395b0255fd33fa4d1f1a", + "sha256:5b7bb9de00197fb4261825c15551adf7605cf14a80badf1761d61e59da347779", + "sha256:5e9ac5f66616b87d4da618a20ab0a38324dbe88d8a39b55be8964eb520021e02", + "sha256:620582db2a85b2df5f8a82ddeb52116560d7e5e6b055095f04ad828d1b0baa39", + "sha256:62cc1afda735a8d109007164714e73771b499768b9bb5afcbbee9d0ff374b43f", + "sha256:70ad9e5c6cb9b8487280a02c0ad8a51581dcbbe8484ce058477692a27c151c0a", + "sha256:72b9e656e340447f827885b8d7a15fc8c4e68d410dc2297ef6787eec0f0ea409", + "sha256:72cbcfd54df6caf85cc35264c77ede902452d6df41166010262374155947460c", + "sha256:792e5c12376594bfcb986ebf3855aa4b7c225754e9a9521298e460e92fb4a488", + "sha256:7b7017b61bbcdd7f6363aeceb881e23c46583739cb69a3ab39cb384f6ec82e5b", + "sha256:81f8d5c81e483a9442d72d182e1fb6dcb9723f289a57e8030811bac9ea3fef8d", + "sha256:82aafa8d5eb68c8463b6e9baeb4f19043bb31fefc03eb7b216b51e6a9981ae09", + "sha256:84c471a734240653a0ec91dec0996696eea227eafe72a33bd06c92697728046b", + "sha256:8c803ac3c28bbc53763e6825746f05cc407b20e4a69d0122e526a582e3b5e153", + "sha256:93ce9e955cc95959df98505e4608ad98281fff037350d8c2671c9aa86bcf10a9", + "sha256:9a3e5ddc44c14042f0844b8cf7d2cd455f6cc80fd7f5eefbe657292cf601d9ad", + "sha256:a4901622493f88b1a29bd30ec1a2f683782e57c3c16a2dbc7f2595ba01f639df", + "sha256:a5a4532a12314149d8b4e4ad8ff09dde7427731fcfa5917ff16d0291f13609df", + "sha256:b8831cb7332eda5dc89b21a7bce7ef6ad305548820595033a4b03cf3091235ed", + "sha256:b8e2f83c56e141920c39464b852de3719dfbfb6e3c99a2d8da0edf4fb33176ed", + "sha256:c70e94281588ef053ae8998039610dbd71bc509e4acbc77ab59d7d2937b10698", + "sha256:c8a17b5d948f4ceeceb66384727dde11b240736fddeda54ca740b9b8b1556b29", + "sha256:d82cdb63100ef5eedb8391732375e6d05993b765f72cb34311fab92103314649", + "sha256:d89363f02658e253dbd171f7c3716a5d340a24ee82d38aab9183f7fdf0cdca49", + "sha256:d99ec152570e4196772e7a8e4ba5320d2d27bf22fdf11743dd882936ed64305b", + "sha256:ddc4d832a0f0b4c52fff973a0d44b6c99839a9d016fe4e6a1cb8f3eea96479c2", + "sha256:e3dacecfbeec9a33e932f00c6cd7996e62f53ad46fbe677577394aaa90ee419a", + "sha256:eb9fc393f3c61f9054e1ed26e6fe912c7321af2f41ff49d3f83d05bacf22cc78" + ], + "index": "pypi", + "version": "==8.4.0" + }, + "progress": { + "hashes": [ + "sha256:c9c86e98b5c03fa1fe11e3b67c1feda4788b8d0fe7336c2ff7d5644ccfba34cd" + ], + "index": "pypi", + "version": "==1.6" + }, + "pymongo": { + "hashes": [ + "sha256:0238e53b452ab699b5e2e3f8af2557844c80ab0d0c7a0e066226882838e72756", + "sha256:0271bbba36bb130202e011171c1883c4c193036ad0b1e02ecfbea6837790b7de", + "sha256:069d49b193f94bb1d748cfd8faf697060a2299f40d86bf5b6d41dd3cedf0fd48", + "sha256:06af6e6374ee2bb70f724e09ddf9402907a3d6714828b908737948cd83e5685c", + "sha256:0c77cd3dbe0dd9e7cdf8c93dc24e5a4fcb56e115ffb259d4f399e4aaf3f3c62d", + "sha256:0e9a2628bcd896368ede456bcfe189d9ca65b18fb0dd91974cb734baf2e24af9", + "sha256:12d336bdbe60982de55651be397b5e49d7eadd2aa144f11da353002cd52502ed", + "sha256:132cc67b909835d7c230888387b4cc9596d4559a3ce90d947e03bc0b0ffe420b", + "sha256:13d0624c13a91da71fa0d960205d93b3d98344481be865ee7cc238c972d41d73", + "sha256:1498f388181ae5592c7b60549faaefaffc62d6e3754097576611cb642d21d37b", + "sha256:1617fd52da7b208fe5ea176d251dd7cf1b5309e5a4272754b9599edfdf7e64e5", + "sha256:177ed1b14aa4f84f00ebef1b0f785680fbaa610361942b23eb54f562fe4c6b34", + "sha256:186b2ff4518c1c169fcef5047deb0e6c13a2354d143859587e745fd9f2cf68e9", + "sha256:1ba8eb426d56556fffec53d600a8f2572589c19d50b30f61daa8f4d72ab92fbe", + "sha256:1c153274699424e8f89f2097d5113f8cbe7898a8d62afaad0270a0f0bd0af53b", + "sha256:1fd71b4d7070b01c7f66edc44c1ec2f8bcace2761c3a6ecc10449a40e474d2fa", + "sha256:28afb00423e521f4b04fb8f75da7c0215e46631e821e27abf5a7176f9b671f47", + "sha256:349c8e522e0b785f442fc9d7fc01c59f7f13f1abe9395310d0d817cff03ec034", + "sha256:35a5843546bcbe0422f30b4b2bd5e0b630b04cc4006492c70e8168a921d94b9e", + "sha256:38b21eddd021a943b1978b0a3d42e974956a338e3dbb88d56aeb8b8799abd6e8", + "sha256:3a4eb0a4db8a2d960bdd5354f05e2e57530e83d333cb644fb2b7120a7a954a69", + "sha256:40269fe6bb79fe00c8ba7c2f2d542a82711eb234c3dedb90b7e489386120e9d1", + "sha256:426584e99af31ad2398e617c3eb0f1ebcda37f0ffb2d3e56087cdaf23a2f1689", + "sha256:47a58f15fc70198cf95982f9699e17fec12287b90f30e90c5e2b7c1c1bc07914", + "sha256:512059a902ea2cbcd0afac370af580e67ccd4c7e41ecaff0f0fbd03653b25ca2", + "sha256:51664dac8d9b138259876f324adca5ab31d991acf88d1d0ffcc94f423ff2e31b", + "sha256:59a4a5fe5379e4fa93380fd0b55bccbdbeb8d04fcfbbad8b42bd31610d5ed3ad", + "sha256:5cbfa85a12cfe3dca21951cd432051c505ac461bd9f4a635207d982dd9df2373", + "sha256:5fea4207fec8909e155a7948c987eac61949dbbe97fd0c388e587d06ba9bc78d", + "sha256:6183476860511cb553a7e4c40936221b6985af7852029c84df898370ec8a028c", + "sha256:62459b91a513a7b441cfd70ea7fd15c50b858877ca823915d32bab08fe173edb", + "sha256:633ca2001f80900142068bab907feca99554b557ac105c74a9ed157ed38ca5d6", + "sha256:65f8a93816dcb2202710839907759aca9eece94d9f13215686f224fcc8966f9e", + "sha256:686c40344f7f82c4deaa4e17aa46ad97df51263be1434aeedd2d6b6f38c7f44a", + "sha256:6cd7a4321e718cb98a7c7c475b0757e77fdaf1cdb013d7d2e781ba45219e1144", + "sha256:6f0605b1146bc24c720aac0e806492144aea9d5a4dc956589e0544301862756a", + "sha256:716499113650aacfe1b94d37e0a863f1e84b8d47737c74a2f44f8dfccad46952", + "sha256:71810eade75ae1c466adc158d1fa8141040f75427b76240316d97f3c89edd72f", + "sha256:72a0c06b76b254bdec18af9add3b8d35796dda51e64a5e0e48d40bff7b41ab13", + "sha256:7450b25a803b0f57dae4c3fbd0df742f7f3344c3c9cabb86e4180083c3ebd893", + "sha256:75e449ab068af63b7729195343315bc63d242166d88467314be182cc54ce235d", + "sha256:7629abba158610cb5db6c22041b287f9398555d72bf9468d44d2efc03d837b81", + "sha256:774b9f48bdc385af6654def31e7a7617e01b99cc8aaca1ab3ef6ea0492205e57", + "sha256:7a091050bb8d54a5200193b4998e0cf763d083f93d97c7780963c09996f85a38", + "sha256:7bdb66340e246b5dcddfcfe79a63ac2ec3808dc394853476f49fc785425040f4", + "sha256:812650a2e8a08b812d6a3c937f482bd2c9355e90574964fa283b4d8ef4ae665e", + "sha256:84eec41ed982f21ceb58689e16a630a70301eb14499c929388a5bf6464518d9d", + "sha256:86d0e28dd5867153d9d9963a4eb17764854a925758fc2db0a814260f82fd4319", + "sha256:87dce7c85387ca033cf76cce773ace7675550dcffc456db32a34403439e53e45", + "sha256:8869feff59f08cd63979df26aa12343a85bdc7fbd1b79fda8ae39f31a310fa62", + "sha256:8baf23d6a0a08b697854e5bcdf82afb91da732cf575fd47ee93945c3654132d8", + "sha256:8da525765dbcc1b7abf1bba623f9f701d8759a8fb19594cd71a13b7b0c2c56bd", + "sha256:9043bfb816ed50d831acc8d06469dcc41597b4f50c30e62227a93f9f9e37d6c7", + "sha256:91c049104b51321e4e18d41edc6850d9f0890ac609b3cb3b8db86dc51666de17", + "sha256:93c25fbb5dbc436edbb74101f4da49a42bd3af534513fdf8e75fc72ef035d5e0", + "sha256:953129b6b952a9d22042ac23050053444624f630e1928f5f590788660905fa9c", + "sha256:9ff0dbec451a2c6226bbd6f2bbbde438bc263e002f3f97d151c8708732ba5197", + "sha256:a47f4b24b1360da172cae07ce90e9bd425b6db0052d76142c7fef47173a27283", + "sha256:a57e271a0647002b5683dd0c7c2fd7f5fb939357c44396d85298e51a3561b9e3", + "sha256:b0606d14892ae2a2b1450e37c8924381e9b64683386a9853e4467f02fd5b44b6", + "sha256:b73ff8582964f52ab1bf1a9fdddc1454143172a0b8a9d7d6e3972dd1134f7982", + "sha256:bf6047dea1bc8ae19fc14e01b5cb70b3810f91b100d9a535751dd3eadcd3016c", + "sha256:c0efc5ab7d9b9e64726496bf650dbc7f1754124a48d076e5292cc5306e61a530", + "sha256:c86a0614eda95db036fae01a89f3917d7abdc657c806bac2a32eec74724d9330", + "sha256:c878286b1464f462616a47f315d14f02f03512c6b81cb568e996c3f1f79bff8a", + "sha256:cd4cde3dfdd347d638171eca53ee6e787d4b1247c6e182f8616039b1df6278d5", + "sha256:ceb9a4986f56595e73fffeef3ec037280eda938ed5fe6e4e0961656669d89b32", + "sha256:d419e2dbc4943ad6df7ee05e707d7b2c2b512b92407bb6ff643bccbdea399c3a", + "sha256:d66462f740dcea496bd779775688a0f805860f0b01998bb59ca22566b098ee26", + "sha256:d7514231a03e95072b32d9b335b96253799802ab94647ce83585d5010749380a", + "sha256:d9f61b08b60909d936c1f3a4e12c163ca71fd1a4665fc6e078afc6f54f886977", + "sha256:da576e59f5f8a642ee26d027479325a45be45defe075b6fa7c84506dabc76883", + "sha256:ddaf391ba74eef47eb5afbc40d0b6ddcdbdb417ec8edc8ae95352d25485076db", + "sha256:e2b6a323ca545bcb4286d14c0bd75d9a1f5bce2fa1d7fa3621e5f71fd9b8d196", + "sha256:e3f6faea65a73ed54111f209b4a411fe012c68f04e8bde96dd7af89b13cac92b", + "sha256:e4e36810c541bd1976cd05452e797860b775886cf32c3e8136b9fe48c2c8ba95", + "sha256:e5441f4c8142a250695e249e432637c14f79d856a2b60e0974da082e006c53e2", + "sha256:e7aedefc87cb46544a3865a19c1d5ca7ddf5ec5ed7dfe162d9538d7543aef499", + "sha256:ee2c1fd5bd57fd0092dfa31c1f9f166cf2850f191311603ce343cadcc8608d60", + "sha256:f2b6e12f98cce588525f3db802c88f9795d294549ebfe7c2c9bb81333f533ecd", + "sha256:f333c0d71dd892683e608f8d1731785a0aa67b1ec012b0d9fc863e8d7224f64e", + "sha256:f3e20eb096deea92350f7198a4287d45883a62fe4459d027ce789e72ceba12ee", + "sha256:f785375ca2b4e2192786f1e0d2a94c66900d12e780ebae1eccbbab85eb9a7054" + ], + "index": "pypi", + "version": "==4.0.1" + }, + "requests": { + "hashes": [ + "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24", + "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7" + ], + "index": "pypi", + "version": "==2.26.0" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "version": "==1.16.0" + }, + "tk": { + "hashes": [ + "sha256:60bc8923d5d35f67f5c6bd93d4f0c49d2048114ec077768f959aef36d4ed97f8", + "sha256:703a69ff0d5ba2bd2f7440582ad10160e4a6561595d33457dc6caa79b9bf4930" + ], + "index": "pypi", + "version": "==0.1.0" + }, + "urllib3": { + "hashes": [ + "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece", + "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844" + ], + "version": "==1.26.7" + }, + "watchdog": { + "hashes": [ + "sha256:25fb5240b195d17de949588628fdf93032ebf163524ef08933db0ea1f99bd685", + "sha256:3386b367e950a11b0568062b70cc026c6f645428a698d33d39e013aaeda4cc04", + "sha256:3becdb380d8916c873ad512f1701f8a92ce79ec6978ffde92919fd18d41da7fb", + "sha256:4ae38bf8ba6f39d5b83f78661273216e7db5b00f08be7592062cb1fc8b8ba542", + "sha256:8047da932432aa32c515ec1447ea79ce578d0559362ca3605f8e9568f844e3c6", + "sha256:8f1c00aa35f504197561060ca4c21d3cc079ba29cf6dd2fe61024c70160c990b", + "sha256:922a69fa533cb0c793b483becaaa0845f655151e7256ec73630a1b2e9ebcb660", + "sha256:9693f35162dc6208d10b10ddf0458cc09ad70c30ba689d9206e02cd836ce28a3", + "sha256:a0f1c7edf116a12f7245be06120b1852275f9506a7d90227648b250755a03923", + "sha256:a36e75df6c767cbf46f61a91c70b3ba71811dfa0aca4a324d9407a06a8b7a2e7", + "sha256:aba5c812f8ee8a3ff3be51887ca2d55fb8e268439ed44110d3846e4229eb0e8b", + "sha256:ad6f1796e37db2223d2a3f302f586f74c72c630b48a9872c1e7ae8e92e0ab669", + "sha256:ae67501c95606072aafa865b6ed47343ac6484472a2f95490ba151f6347acfc2", + "sha256:b2fcf9402fde2672545b139694284dc3b665fd1be660d73eca6805197ef776a3", + "sha256:b52b88021b9541a60531142b0a451baca08d28b74a723d0c99b13c8c8d48d604", + "sha256:b7d336912853d7b77f9b2c24eeed6a5065d0a0cc0d3b6a5a45ad6d1d05fb8cd8", + "sha256:bd9ba4f332cf57b2c1f698be0728c020399ef3040577cde2939f2e045b39c1e5", + "sha256:be9be735f827820a06340dff2ddea1fb7234561fa5e6300a62fe7f54d40546a0", + "sha256:cca7741c0fcc765568350cb139e92b7f9f3c9a08c4f32591d18ab0a6ac9e71b6", + "sha256:d0d19fb2441947b58fbf91336638c2b9f4cc98e05e1045404d7a4cb7cddc7a65", + "sha256:e02794ac791662a5eafc6ffeaf9bcc149035a0e48eb0a9d40a8feb4622605a3d", + "sha256:e0f30db709c939cabf64a6dc5babb276e6d823fd84464ab916f9b9ba5623ca15", + "sha256:e92c2d33858c8f560671b448205a268096e17870dcf60a9bb3ac7bfbafb7f5f9" + ], + "index": "pypi", + "version": "==2.1.6" + }, + "werkzeug": { + "hashes": [ + "sha256:63d3dc1cf60e7b7e35e97fa9861f7397283b75d765afcaefd993d6046899de8f", + "sha256:aa2bb6fc8dee8d6c504c0ac1e7f5f7dc5810a9903e793b6f715a9f015bdadb9a" + ], + "version": "==2.0.2" + } + }, + "develop": { + "autopep8": { + "hashes": [ + "sha256:44f0932855039d2c15c4510d6df665e4730f2b8582704fa48f9c55bd3e17d979", + "sha256:ed77137193bbac52d029a52c59bec1b0629b5a186c495f1eb21b126ac466083f" + ], + "index": "pypi", + "version": "==1.6.0" + }, + "backcall": { + "hashes": [ + "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e", + "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255" + ], + "version": "==0.2.0" + }, + "debugpy": { + "hashes": [ + "sha256:01e98c594b3e66d529e40edf314f849cd1a21f7a013298df58cd8e263bf8e184", + "sha256:16db27b4b91991442f91d73604d32080b30de655aca9ba821b1972ea8171021b", + "sha256:17a25ce9d7714f92fc97ef00cc06269d7c2b163094990ada30156ed31d9a5030", + "sha256:194f95dd3e84568b5489aab5689a3a2c044e8fdc06f1890b8b4f70b6b89f2778", + "sha256:1ec3a086e14bba6c472632025b8fe5bdfbaef2afa1ebd5c6615ce6ed8d89bc67", + "sha256:23df67fc56d59e386c342428a7953c2c06cc226d8525b11319153e96afb65b0c", + "sha256:26fbe53cca45a608679094791ce587b6e2798acd1d4777a8b303b07622e85182", + "sha256:2b073ad5e8d8c488fbb6a116986858bab0c9c4558f28deb8832c7a5a27405bd6", + "sha256:318f81f37341e4e054b4267d39896b73cddb3612ca13b39d7eea45af65165e1d", + "sha256:3a457ad9c0059a21a6c7d563c1f18e924f5cf90278c722bd50ede6f56b77c7fe", + "sha256:4404a62fb5332ea5c8c9132290eef50b3a0ba38cecacad5529e969a783bcbdd7", + "sha256:5d76a4fd028d8009c3faf1185b4b78ceb2273dd2499447664b03939e0368bb90", + "sha256:70b422c63a833630c33e3f9cdbd9b6971f8c5afd452697e464339a21bbe862ba", + "sha256:82f5f9ce93af6861a0713f804e62ab390bb12a17f113153e47fea8bbb1dfbe36", + "sha256:a2aa64f6d2ca7ded8a7e8a4e7cae3bc71866b09876b7b05cecad231779cb9156", + "sha256:b2df2c373e85871086bd55271c929670cd4e1dba63e94a08d442db830646203b", + "sha256:b5b3157372e0e0a1297a8b6b5280bcf1d35a40f436c7973771c972726d1e32d5", + "sha256:d2b09e91fbd1efa4f4fda121d49af89501beda50c18ed7499712c71a4bf3452e", + "sha256:d876db8c312eeb02d85611e0f696abe66a2c1515e6405943609e725d5ff36f2a", + "sha256:f3a3dca9104aa14fd4210edcce6d9ce2b65bd9618c0b222135a40b9d6e2a9eeb", + "sha256:f73988422b17f071ad3c4383551ace1ba5ed810cbab5f9c362783d22d40a08dc" + ], + "version": "==1.5.1" + }, + "decorator": { + "hashes": [ + "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374", + "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7" + ], + "version": "==5.1.0" + }, + "entrypoints": { + "hashes": [ + "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", + "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" + ], + "version": "==0.3" + }, + "ipykernel": { + "hashes": [ + "sha256:3a227788216b43982d9ac28195949467627b0d16e6b8af9741d95dcaa8c41a89", + "sha256:82ded8919fa7f5483be2b6219c3b13380d93faab1fc49cc2cfcd10e9e24cc158" + ], + "index": "pypi", + "version": "==6.6.0" + }, + "ipython": { + "hashes": [ + "sha256:cb6aef731bf708a7727ab6cde8df87f0281b1427d41e65d62d4b68934fa54e97", + "sha256:fc60ef843e0863dd4e24ab2bb5698f071031332801ecf8d1aeb4fb622056545c" + ], + "version": "==7.30.1" + }, + "jedi": { + "hashes": [ + "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d", + "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab" + ], + "version": "==0.18.1" + }, + "jupyter-client": { + "hashes": [ + "sha256:64d93752d8cbfba0c1030c3335c3f0d9797cd1efac012652a14aac1653db11a3", + "sha256:a5f995a73cffb314ed262713ae6dfce53c6b8216cea9f332071b8ff44a6e1654" + ], + "version": "==7.1.0" + }, + "jupyter-core": { + "hashes": [ + "sha256:1c091f3bbefd6f2a8782f2c1db662ca8478ac240e962ae2c66f0b87c818154ea", + "sha256:dce8a7499da5a53ae3afd5a9f4b02e5df1d57250cf48f3ad79da23b4778cd6fa" + ], + "version": "==4.9.1" + }, + "matplotlib-inline": { + "hashes": [ + "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee", + "sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c" + ], + "version": "==0.1.3" + }, + "nest-asyncio": { + "hashes": [ + "sha256:3fdd0d6061a2bb16f21fe8a9c6a7945be83521d81a0d15cff52e9edee50101d6", + "sha256:f969f6013a16fadb4adcf09d11a68a4f617c6049d7af7ac2c676110169a63abd" + ], + "version": "==1.5.4" + }, + "parso": { + "hashes": [ + "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0", + "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75" + ], + "version": "==0.8.3" + }, + "pexpect": { + "hashes": [ + "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937", + "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c" + ], + "markers": "sys_platform != 'win32'", + "version": "==4.8.0" + }, + "pickleshare": { + "hashes": [ + "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", + "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" + ], + "version": "==0.7.5" + }, + "prompt-toolkit": { + "hashes": [ + "sha256:1bb05628c7d87b645974a1bad3f17612be0c29fa39af9f7688030163f680bad6", + "sha256:e56f2ff799bacecd3e88165b1e2f5ebf9bcd59e80e06d395fa0cc4b8bd7bb506" + ], + "version": "==3.0.24" + }, + "ptyprocess": { + "hashes": [ + "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", + "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220" + ], + "version": "==0.7.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20", + "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f" + ], + "version": "==2.8.0" + }, + "pygments": { + "hashes": [ + "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380", + "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6" + ], + "version": "==2.10.0" + }, + "python-dateutil": { + "hashes": [ + "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", + "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + ], + "version": "==2.8.2" + }, + "pyzmq": { + "hashes": [ + "sha256:08c4e315a76ef26eb833511ebf3fa87d182152adf43dedee8d79f998a2162a0b", + "sha256:0ca6cd58f62a2751728016d40082008d3b3412a7f28ddfb4a2f0d3c130f69e74", + "sha256:1621e7a2af72cced1f6ec8ca8ca91d0f76ac236ab2e8828ac8fe909512d566cb", + "sha256:18cd854b423fce44951c3a4d3e686bac8f1243d954f579e120a1714096637cc0", + "sha256:2841997a0d85b998cbafecb4183caf51fd19c4357075dfd33eb7efea57e4c149", + "sha256:2b97502c16a5ec611cd52410bdfaab264997c627a46b0f98d3f666227fd1ea2d", + "sha256:3a4c9886d61d386b2b493377d980f502186cd71d501fffdba52bd2a0880cef4f", + "sha256:3c1895c95be92600233e476fe283f042e71cf8f0b938aabf21b7aafa62a8dac9", + "sha256:42abddebe2c6a35180ca549fadc7228d23c1e1f76167c5ebc8a936b5804ea2df", + "sha256:468bd59a588e276961a918a3060948ae68f6ff5a7fa10bb2f9160c18fe341067", + "sha256:480b9931bfb08bf8b094edd4836271d4d6b44150da051547d8c7113bf947a8b0", + "sha256:53f4fd13976789ffafedd4d46f954c7bb01146121812b72b4ddca286034df966", + "sha256:62bcade20813796c426409a3e7423862d50ff0639f5a2a95be4b85b09a618666", + "sha256:67db33bea0a29d03e6eeec55a8190e033318cee3cbc732ba8fd939617cbf762d", + "sha256:6b217b8f9dfb6628f74b94bdaf9f7408708cb02167d644edca33f38746ca12dd", + "sha256:7661fc1d5cb73481cf710a1418a4e1e301ed7d5d924f91c67ba84b2a1b89defd", + "sha256:76c532fd68b93998aab92356be280deec5de8f8fe59cd28763d2cc8a58747b7f", + "sha256:79244b9e97948eaf38695f4b8e6fc63b14b78cc37f403c6642ba555517ac1268", + "sha256:7c58f598d9fcc52772b89a92d72bf8829c12d09746a6d2c724c5b30076c1f11d", + "sha256:7dc09198e4073e6015d9a8ea093fc348d4e59de49382476940c3dd9ae156fba8", + "sha256:80e043a89c6cadefd3a0712f8a1322038e819ebe9dbac7eca3bce1721bcb63bf", + "sha256:851977788b9caa8ed011f5f643d3ee8653af02c5fc723fa350db5125abf2be7b", + "sha256:8eddc033e716f8c91c6a2112f0a8ebc5e00532b4a6ae1eb0ccc48e027f9c671c", + "sha256:902319cfe23366595d3fa769b5b751e6ee6750a0a64c5d9f757d624b2ac3519e", + "sha256:954e73c9cd4d6ae319f1c936ad159072b6d356a92dcbbabfd6e6204b9a79d356", + "sha256:ab888624ed68930442a3f3b0b921ad7439c51ba122dbc8c386e6487a658e4a4e", + "sha256:acebba1a23fb9d72b42471c3771b6f2f18dcd46df77482612054bd45c07dfa36", + "sha256:b4ebed0977f92320f6686c96e9e8dd29eed199eb8d066936bac991afc37cbb70", + "sha256:badb868fff14cfd0e200eaa845887b1011146a7d26d579aaa7f966c203736b92", + "sha256:be4e0f229cf3a71f9ecd633566bd6f80d9fa6afaaff5489492be63fe459ef98c", + "sha256:c0f84360dcca3481e8674393bdf931f9f10470988f87311b19d23cda869bb6b7", + "sha256:c1e41b32d6f7f9c26bc731a8b529ff592f31fc8b6ef2be9fa74abd05c8a342d7", + "sha256:c88fa7410e9fc471e0858638f403739ee869924dd8e4ae26748496466e27ac59", + "sha256:cf98fd7a6c8aaa08dbc699ffae33fd71175696d78028281bc7b832b26f00ca57", + "sha256:d072f7dfbdb184f0786d63bda26e8a0882041b1e393fbe98940395f7fab4c5e2", + "sha256:d1b5d457acbadcf8b27561deeaa386b0217f47626b29672fa7bd31deb6e91e1b", + "sha256:d3dcb5548ead4f1123851a5ced467791f6986d68c656bc63bfff1bf9e36671e2", + "sha256:d6157793719de168b199194f6b6173f0ccd3bf3499e6870fac17086072e39115", + "sha256:d728b08448e5ac3e4d886b165385a262883c34b84a7fe1166277fe675e1c197a", + "sha256:de8df0684398bd74ad160afdc2a118ca28384ac6f5e234eb0508858d8d2d9364", + "sha256:e6a02cf7271ee94674a44f4e62aa061d2d049001c844657740e156596298b70b", + "sha256:ea12133df25e3a6918718fbb9a510c6ee5d3fdd5a346320421aac3882f4feeea", + "sha256:ea5a79e808baef98c48c884effce05c31a0698c1057de8fc1c688891043c1ce1", + "sha256:f43b4a2e6218371dd4f41e547bd919ceeb6ebf4abf31a7a0669cd11cd91ea973", + "sha256:f762442bab706fd874064ca218b33a1d8e40d4938e96c24dafd9b12e28017f45", + "sha256:f89468059ebc519a7acde1ee50b779019535db8dcf9b8c162ef669257fef7a93", + "sha256:f907c7359ce8bf7f7e63c82f75ad0223384105f5126f313400b7e8004d9b33c3" + ], + "version": "==22.3.0" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "version": "==1.16.0" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "version": "==0.10.2" + }, + "tornado": { + "hashes": [ + "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb", + "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c", + "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288", + "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95", + "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558", + "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe", + "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791", + "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d", + "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326", + "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b", + "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4", + "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c", + "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910", + "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5", + "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c", + "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0", + "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675", + "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd", + "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f", + "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c", + "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea", + "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6", + "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05", + "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd", + "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575", + "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a", + "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37", + "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795", + "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f", + "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32", + "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c", + "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01", + "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4", + "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2", + "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921", + "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085", + "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df", + "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102", + "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5", + "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68", + "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5" + ], + "version": "==6.1" + }, + "traitlets": { + "hashes": [ + "sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7", + "sha256:2d313cc50a42cd6c277e7d7dc8d4d7fedd06a2c215f78766ae7b1a66277e0033" + ], + "version": "==5.1.1" + }, + "wcwidth": { + "hashes": [ + "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784", + "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83" + ], + "version": "==0.2.5" + } + } +} diff --git a/server/app/__init__.py b/server/app/__init__.py new file mode 100644 index 0000000..0fb609a --- /dev/null +++ b/server/app/__init__.py @@ -0,0 +1,17 @@ +from flask import Flask +from flask_cors import CORS + + +def create_app(): + app = Flask(__name__) + CORS(app) + + from . import api + app.register_blueprint(api.bp, url_prefix='/') + + return app + + +if __name__ == '__main__': + app = create_app() + app.run(debug=True) diff --git a/server/app/__pycache__/__init__.cpython-38.pyc b/server/app/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..edfc3453a803b9f7b3b98c9c9172162d086ead76 GIT binary patch literal 618 zcmYjPy>8nu5ay30(}IJ#$x~=(w6{eN*L9ZwDG)nDwGfg>yR=0~r6k8KoXJ`94qfvC zeHgBt@(LM(9#yo+0eAR#Ki?gXygoV#5y;Q+uhd|Ke!Ig

Q|{ZO*_5ByfQ=PB33s0vW6aA`pXnm(AuV8a@ICim>%R6=|~p;7vbY&2#7d zCj@{8It6Xc!L+Ew2YicKvPTDC+M_jHdjbn0=sf{uPxytegWJEc!5*(!-xXqe1YOys zr9y6h3`KU@d@EEdBVxn(YeKo*qMUYMlRJZ z8X7m7Wt^9(mYf^68XM$Qxl}7VTAE68oz}G~R~DIp3mzM}QcWuj&x%ggMwP84Sq=T6 zMcwB-AoChhpWek^D=p*vMitv#oL54|x@%Ovd$aBS!z|4=vJ^2C$DFHDwVa>U-))#1 znYNOH-v_eh7!1M;GZJ8e$#dY3@R*nr@VfbRs!Glm?jeNCy47XG5`XEQq`zVk_L%3D rY5Gg{g~t68Gj') +def send_audio(file_id): + song_obj = all_songs_instance.get_song_by_id(file_id) + loaded_song = convert_one_to_json(song_obj) + + filepath = loaded_song['filepath'].split('/')[-1] + print(loaded_song['folder'] + filepath) + + return send_from_directory(home_dir + loaded_song['folder'], filepath) + + +@bp.route("/folder/artists") +def get_folder_artists(): + dir = request.args.get('dir') + + songs = all_songs_instance.find_songs_by_folder(dir) + songs_array = convert_to_json(songs) + without_duplicates = remove_duplicates(songs_array) + + artists = [] + + for song in without_duplicates: + this_artists = song['artists'].split(', ') + + for artist in this_artists: + + if artist not in artists: + artists.append(artist) + + final_artists = [] + + for artist in artists[:15]: + artist_obj = artist_instance.find_artists_by_name(artist) + + if artist_obj != []: + final_artists.append(convert_to_json(artist_obj)) + + return {'artists': final_artists} + + +@bp.route("/populate/images") +def populate_images(): + all_songs = all_songs_instance.get_all_songs() + songs_array = convert_to_json(all_songs) + remove_duplicates(songs_array) + + artists = [] + + for song in songs_array: + this_artists = song['artists'].split(', ') + + for artist in this_artists: + if artist not in artists: + artists.append(artist) + + bar = Bar('Processing images', max=len(artists)) + for artist in artists: + file_path = app_dir + '/images/artists/' + artist + '.jpg' + + if not os.path.exists(file_path): + url = 'https://api.deezer.com/search/artist?q={}'.format(artist) + response = requests.get(url) + data = response.json() + + try: + image_path = data['data'][0]['picture_xl'] + except: + image_path = None + + if image_path is not None: + try: + save_image(image_path, file_path) + artist_obj = { + 'name': artist + } + + artist_instance.insert_artist(artist_obj) + except: + pass + else: + pass + + bar.next() + + bar.finish() + + artists_in_db = artist_instance.get_all_artists() + artists_in_db_array = convert_to_json(artists_in_db) + + return {'sample': artists_in_db_array[:25]} + + +@bp.route("/artist") +def getArtistData(): + artist = urllib.parse.unquote(request.args.get('q')) + artist_obj = artist_instance.find_artists_by_name(artist) + artist_obj_json = convert_to_json(artist_obj) + + def getArtistSongs(): + songs = all_songs_instance.find_songs_by_artist(artist) + songs_array = convert_to_json(songs) + + return songs_array + + artist_songs = getArtistSongs() + songs = remove_duplicates(artist_songs) + + def getArtistAlbums(): + artist_albums = [] + albums_with_count = [] + + albums = all_songs_instance.find_songs_by_album_artist(artist) + albums_array = convert_to_json(albums) + + for song in songs: + song['artists'] = song['artists'].split(', ') + + for song in albums_array: + if song['album'] not in artist_albums: + artist_albums.append(song['album']) + + for album in artist_albums: + count = 0 + length = 0 + + for song in artist_songs: + if song['album'] == album: + count = count + 1 + length = length + song['length'] + + album_ = { + "title": album, + "count": count, + "length": length + } + + albums_with_count.append(album_) + + return albums_with_count + + return {'artist': artist_obj_json, 'songs': songs, 'albums': getArtistAlbums()} + + +@bp.route("/") +def getFolderTree(): + start = time.time() + + req_dir = request.args.get('f') + + if req_dir is not None: + requested_dir = home_dir + req_dir + else: + requested_dir = home_dir + + dir_content = os.scandir(requested_dir) + + folders = [] + files = [] + + for entry in dir_content: + if entry.is_dir() and not entry.name.startswith('.'): + files_in_dir = run_fast_scandir(entry.path, [".flac", ".mp3"])[1] + + if len(files_in_dir) != 0: + dir = { + "name": entry.name, + "count": len(files_in_dir), + "path": entry.path.replace(home_dir, "") + } + + folders.append(dir) + + if entry.is_file(): + if isValidFile(entry.name) == True: + songs_array = all_songs_instance.find_songs_by_folder(req_dir) + songs = convert_to_json(songs_array) + for song in songs: + song['artists'] = song['artists'].split(', ') + + files = songs + + for file in files: + del file['filepath'] + + dir_content.close() + end = time.time() + print(end - start) + return {"requested": req_dir, "files": files[:25], "folders": folders} + + +@bp.route('/image//') +def send_image(img_type, image_id): + if img_type == "thumbnail": + song_obj = all_songs_instance.get_song_by_id(image_id) + loaded_song = convert_one_to_json(song_obj) + + img_dir = app_dir + "/images/thumbnails" + image = loaded_song['image'] + + if img_type == "artist": + artist_obj = artist_instance.get_artist_by_id(image_id) + artist = convert_one_to_json(artist_obj) + + img_dir = app_dir + "/images/artists" + + image = artist['name'] + ".jpg" + print(img_dir + image) + + return send_from_directory(img_dir, image) diff --git a/server/app/configs.py b/server/app/configs.py new file mode 100644 index 0000000..8e8aeda --- /dev/null +++ b/server/app/configs.py @@ -0,0 +1,6 @@ +default_configs = { + "dirs": [ + "/home/cwilvx/Music/", + "/home/cwilvx/FreezerMusic" + ] +} diff --git a/server/app/helpers.py b/server/app/helpers.py new file mode 100644 index 0000000..4f0abcd --- /dev/null +++ b/server/app/helpers.py @@ -0,0 +1,262 @@ +from genericpath import exists +import os +import json +import requests +import urllib + +from mutagen.mp3 import MP3 +from mutagen.id3 import ID3 +from mutagen.flac import FLAC + +from bson import json_util + +from io import BytesIO +from PIL import Image + +from app.models import AllSongs +from app.configs import default_configs + +all_songs_instance = AllSongs() +music_dir = os.environ.get("music_dir") +music_dirs = os.environ.get("music_dirs") + +home_dir = os.path.expanduser('~') +app_dir = home_dir + '/.shit' + + +PORT = os.environ.get("PORT") + + +def run_fast_scandir(dir, ext): + subfolders = [] + files = [] + + for f in os.scandir(dir): + if f.is_dir() and not f.name.startswith('.'): + subfolders.append(f.path) + if f.is_file(): + if os.path.splitext(f.name)[1].lower() in ext: + files.append(f.path) + + for dir in list(subfolders): + sf, f = run_fast_scandir(dir, ext) + subfolders.extend(sf) + files.extend(f) + + return subfolders, files + + +def extract_thumb(path): + img_path = app_dir + "/images/thumbnails/" + path.split('/')[-1] + '.jpg' + + if os.path.exists(img_path): + return path.split('/')[-1] + '.jpg' + + if path.endswith('.flac'): + audio = FLAC(path) + try: + album_art = audio.pictures[0].data + except IndexError: + album_art = None + elif path.endswith('.mp3'): + audio = ID3(path) + try: + album_art = audio.getall('APIC')[0].data + except IndexError: + album_art = None + + if album_art is not None: + + img = Image.open(BytesIO(album_art)) + + try: + img.save(img_path, 'JPEG') + except OSError: + try: + img.convert('RGB'.save(img_path, 'JPEG')) + except: + img_path = None + return path.split('/')[-1] + '.jpg' + + +def getTags(full_path): + if full_path.endswith('.flac'): + audio = FLAC(full_path) + elif full_path.endswith('.mp3'): + audio = MP3(full_path) + + try: + artists = audio['artist'][0] + except KeyError: + try: + artists = audio['TPE1'][0] + except: + artists = 'Unknown' + except IndexError: + artists = 'Unknown' + + try: + album_artist = audio['albumartist'][0] + except KeyError: + try: + album_artist = audio['TPE2'][0] + except: + album_artist = 'Unknown' + except IndexError: + album_artist = 'Unknown' + + try: + title = audio['title'][0] + except KeyError: + try: + title = audio['TIT2'][0] + except: + title = 'Unknown' + except IndexError: + title = 'Unknown' + + try: + album = audio['album'][0] + except KeyError: + try: + album = audio['TALB'][0] + except: + album = "Unknown" + except IndexError: + album = "Unknown" + + try: + genre = audio['genre'][0] + except KeyError: + try: + genre = audio['TCON'][0] + except: + genre = "Unknown" + except IndexError: + genre = "Unknown" + + img_path = extract_thumb(full_path) + + tags = { + "filepath": full_path, + "folder": os.path.dirname(full_path).replace(home_dir, ""), + "title": title, + "artists": artists, + "album_artist": album_artist, + "album": album, + "genre": genre, + "length": round(audio.info.length), + "bitrate": audio.info.bitrate, + "image": img_path + } + + all_songs_instance.insert_song(tags) + return tags + + +def convert_one_to_json(song): + json_song = json.dumps(song, default=json_util.default) + loaded_song = json.loads(json_song) + + return loaded_song + + +def convert_to_json(array): + songs = [] + + for song in array: + json_song = json.dumps(song, default=json_util.default) + loaded_song = json.loads(json_song) + + songs.append(loaded_song) + + return songs + + +def get_folders(): + folders = [] + + for dir in default_configs['dirs']: + entry = os.scandir(dir) + folders.append(entry) + + +def remove_duplicates(array): + return array + + +def save_image(url, path): + response = requests.get(url) + img = Image.open(BytesIO(response.content)) + img.save(path, 'JPEG') + + +def isValidFile(filename): + if filename.endswith('.flac') or filename.endswith('.mp3'): + return True + else: + return False + + +def isValidAudioFrom(folder): + folder_content = os.scandir(folder) + files = [] + + for entry in folder_content: + if isValidFile(entry.name) == True: + file = { + "path": entry.path, + "name": entry.name + } + + files.append(file) + + return files + + +def getFolderContents(filepath, folder): + + folder_name = urllib.parse.unquote(folder) + + path = filepath + name = filepath.split('/')[-1] + tags = {} + + if name.endswith('.flac'): + image_path = folder_name + '/.thumbnails/' + \ + name.replace('.flac', '.jpg') + audio = FLAC(path) + + if name.endswith('.mp3'): + image_path = folder_name + '/.thumbnails/' + \ + name.replace('.mp3', '.jpg') + audio = MP3(path) + + abslt_path = urllib.parse.quote(path.replace(music_dir, '')) + + if os.path.exists(image_path): + img_url = 'http://localhost:{}/{}'.format( + PORT, + urllib.parse.quote(image_path.replace(music_dir, '')) + ) + + try: + audio_url = 'http://localhost:{}/{}'.format( + PORT, abslt_path + ) + tags = getTags(audio_url, audio, img_url, folder_name) + except: + pass + + return tags + + +def create_config_dir(): + home_dir = os.path.expanduser('~') + config_folder = home_dir + "/.shit" + + dirs = ["", "/images", "/images/artists", "/images/thumbnails"] + + for dir in dirs: + if not os.path.exists(config_folder + dir): + os.makedirs(config_folder + dir) diff --git a/server/app/models.py b/server/app/models.py new file mode 100644 index 0000000..93f2219 --- /dev/null +++ b/server/app/models.py @@ -0,0 +1,82 @@ +import pymongo +from bson import ObjectId + + +class Mongo: + def __init__(self, database): + mongo_uri = pymongo.MongoClient() + self.db = mongo_uri[database] + + +class Folders(Mongo): + def __init__(self): + super(Folders, self).__init__('LOCAL_FOLDERS') + self.collection = self.db['LOCAL_FOLDERS'] + + def insert_folder(self, folder): + self.collection.insert_one(folder) + + def find_folder(self, folder_id): + return self.collection.find_one({'_id': ObjectId(folder_id)}) + + +class Artists(Mongo): + def __init__(self): + super(Artists, self).__init__('ALL_ARTISTS') + self.collection = self.db['THEM_ARTISTS'] + + def insert_artist(self, artist_obj): + self.collection.update(artist_obj, artist_obj, upsert=True) + + def get_all_artists(self): + return self.collection.find() + + def get_artist_by_id(self, artist_id): + return self.collection.find_one({'_id': ObjectId(artist_id)}) + + def find_artists_by_name(self, query): + return self.collection.find({'name': {'$regex': query, '$options': 'i'}}) + + +class AllSongs(Mongo): + def __init__(self): + super(AllSongs, self).__init__('ALL_SONGS') + self.collection = self.db['ALL_SONGS'] + + # def drop_db(self): + # self.collection.drop() + + def get_song_by_id(self, file_id): + return self.collection.find_one({'_id': ObjectId(file_id)}) + + def insert_song(self, song_obj): + self.collection.update({'filepath': song_obj['filepath']}, song_obj, upsert=True) + + def find_song_by_title(self, query): + self.collection.create_index([('title', pymongo.TEXT)]) + return self.collection.find({'title': {'$regex': query, '$options': 'i'}}) + + def find_songs_by_album(self, query): + return self.collection.find({'album': {'$regex': query, '$options': 'i'}}) + + def get_all_songs(self): + return self.collection.find() + + def find_songs_by_folder(self, query): + return self.collection.find({'folder': query}) + + def find_songs_by_artist(self, query): + return self.collection.find({'artists': {'$regex': query, '$options': 'i'}}) + + def find_songs_by_album_artist(self, query): + return self.collection.find({'album_artist': {'$regex': query, '$options': 'i'}}) + + def find_song_by_path(self, path): + return self.collection.find_one({'filepath': path}) + + def remove_song_by_filepath(self, filepath): + try: + self.collection.remove({'filepath': filepath}) + return True + except: + return False \ No newline at end of file diff --git a/server/app/watchdoge.py b/server/app/watchdoge.py new file mode 100644 index 0000000..11ec5c7 --- /dev/null +++ b/server/app/watchdoge.py @@ -0,0 +1,58 @@ +import time +import os + +from watchdog.observers import Observer +from watchdog.events import PatternMatchingEventHandler + + +class OnMyWatch: + watchDirectory = "/home/cwilvx/Music" + + def __init__(self): + self.observer = Observer() + + def run(self): + event_handler = Handler() + self.observer.schedule( + event_handler, self.watchDirectory, recursive=True) + self.observer.start() + try: + while True: + time.sleep(5) + except: + self.observer.stop() + print("Observer Stopped") + + self.observer.join() + + +def create_thumb_dir(filepath): + f_name = filepath.split('/')[-1] + parent_dir = filepath.replace(f_name, '') + + thumb_dir = parent_dir + ".thumbnails" + + if not os.path.exists(thumb_dir): + os.makedirs(thumb_dir) + + +class Handler(PatternMatchingEventHandler): + def __init__(self): + PatternMatchingEventHandler.__init__( + self, patterns=['*.flac', '*.mp3'], ignore_directories=True, case_sensitive=False) + + def on_created(self, event): + print(event.src_path) + create_thumb_dir(event.src_path) + + def on_deleted(self, event): + print(event.src_path) + + def on_moved(self, event): + print(event.src_path) + print(event.dest_path) + + +if __name__ == '__main__': + watch = OnMyWatch() + watch.run() diff --git a/server/roadmap.md b/server/roadmap.md new file mode 100644 index 0000000..82626e1 --- /dev/null +++ b/server/roadmap.md @@ -0,0 +1,63 @@ +# Fixes ! + +- [ ] Use click event to play song instead of url ⚠ +- [ ] Show play/pause button correctly according to state ⚠ +- [ ] Click on artist image to go to artist page ⚠ +- [ ] Play next song if current song can't be loaded ⚠ +- [ ] List item song icon for long song titles ⚠ + +- [ ] Broken CSS +- [ ] Prevent scanning unchanged folders +- [ ] Handle '/' and '&' characters in song artists +- [ ] Nginx not serving all files in a folder +- [ ] Removing song duplicates from queries +- [ ] Different songs having same link +- [ ] ConnectionError +- [ ] Move thumbnails to .config +- [ ] Write a multithreaded file server +- [ ] Add support for WAV files +- [ ] Support multiple folders +- [ ] Compress thumbnails + +# Features + +## Needed features +- [ ] Seeking current song +- [ ] Adding songs to queue +- [ ] Implement search on frontend + +- [ ] Watching for changes in folders and updating them instantly ⚠ +- [ ] Display folders and files in a tree view. ⚠ 🔵 + +- [ ] Add favicon +- [ ] Add keyboard shortcuts +- [ ] Right click on song to do stuff +- [ ] Adjust volume +- [ ] Add listening statistics for all songs +- [ ] Extract color from artist image [for use with artist card gradient] +- [ ] Adding songs to favorites +- [ ] Adding songs to playlist +- [ ] Playing song radio + +## Future features +- [ ] Toggle shuffle +- [ ] Toggle repeat +- [ ] Display artist albums +- [ ] Suggest similar artists +- [ ] Getting artist info +- [ ] Getting album info +- [ ] Create a Python script to build, bundle and serve the app +- [ ] Getting extra song info (probably from genius) +- [ ] Getting lyrics +- [ ] Notifications +- [ ] Sorting songs +- [ ] Suggest undiscorvered artists, albums and songs +- [ ] Remember last played song +- [ ] Add next and previous song transition and progress bar reset animations +- [ ] Hover animations for list items +- [ ] Highlight currently playing song in playlist +- [ ] Add functionality to 'Listen now' button +- [ ] Add a 'Scan' button to the sidebar +- [ ] Paginated requests for songs +- [ ] Package app as installable PWA + +## Finished ✅ \ No newline at end of file diff --git a/server/start.sh b/server/start.sh new file mode 100755 index 0000000..1c38d40 --- /dev/null +++ b/server/start.sh @@ -0,0 +1,10 @@ +export PORT=8000 +export music_dir="/home/cwilvx/Music/" + +export FLASK_APP=app +export FLASK_DEBUG=1 +export FLASK_RUN_PORT=8008 + +# export music_dirs="['/home/cwilvx/Music/', '/home/cwilvx/FreezerMusic']" + +flask run \ No newline at end of file diff --git a/server/test.py b/server/test.py new file mode 100644 index 0000000..94cad36 --- /dev/null +++ b/server/test.py @@ -0,0 +1,29 @@ +import os + +def fast_scandir(dirname): + subfolders= [f.path for f in os.scandir(dirname) if f.is_dir()] + + for dirname in list(subfolders): + subfolders.extend(fast_scandir(dirname)) + + return subfolders + + +list = fast_scandir('/home/cwilvx/Music') + +def remove_rejects(folders): + rejects = [] + + for item in folders: + if item.find(".thumbnails") != -1: + rejects.append(item) + + if len(os.listdir(item)) == 0 and item not in rejects: + rejects.append(item) + + for item in rejects: + folders.remove(item) + + print(len(folders)) + +remove_rejects(list) \ No newline at end of file diff --git a/server/thumbnail_extractor.py b/server/thumbnail_extractor.py new file mode 100644 index 0000000..e380686 --- /dev/null +++ b/server/thumbnail_extractor.py @@ -0,0 +1,132 @@ +import os, sys, logging, time + +from io import BytesIO +from pathlib import Path + +import PIL + +from watchdog import observers + +from watchdog.observers import Observer +from watchdog.events import LoggingEventHandler, FileSystemEventHandler + +from mutagen.mp3 import MP3, MutagenError +from mutagen.id3 import ID3 +from mutagen.flac import FLAC +from PIL import Image + +music_dir = "/home/cwilvx/Music/" +folders = os.listdir(music_dir) + + +def updateThumbnails(): + start_time = time.time() + print("Updating thumbnails ...") + + for folder in folders: + print(folder) + try: + dir = music_dir + folder + thumbnail_folder = dir + "/"+ ".thumbnails" + + if not os.path.exists(thumbnail_folder): + os.makedirs(thumbnail_folder) + + def thumbnail_extractor(type, song): + if type == "mp3": + tags = ID3(song) + image_path = "{}/.thumbnails/{}".format(dir, song.name.replace(".mp3", ".jpg")) + album_art = tags.getall('APIC')[0].data + elif type == "flac": + tags = FLAC(song) + image_path = "{}/.thumbnails/{}".format(dir, song.name.replace(".flac", ".jpg")) + album_art = tags.pictures[0].data + else: + print("Unsupported file type") + return + + image = Image.open(BytesIO(album_art)) + + if not os.path.exists(image_path): + try: + image.save(image_path, 'JPEG') + except OSError: + image.convert('RGB').save(image_path, 'JPEG') + + for song in Path(dir).rglob('*.mp3'): + try: + thumbnail_extractor("mp3", song) + except (MutagenError, IndexError): + pass + for song in Path(dir).rglob('*.flac'): + try: + thumbnail_extractor("flac", song) + except (MutagenError, IndexError): + pass + except NotADirectoryError: + pass + + print("done") + + print("Done in: %s seconds" % round((time.time() - start_time), 1)) + + +class watchMusicDirs(FileSystemEventHandler): + def __init__(self, logger=None): + super().__init__() + + self.logger = logger or logging.root + + # def on_moved(self, event): + # super().on_moved(event) + + # what = 'directory' if event.is_directory else 'file' + # self.logger.info("Moved %s: from %s to %s", what, event.src_path, + # event.dest_path) + + # def on_created(self, event): + # super().on_created(event) + + # what = 'directory' if event.is_directory else 'file' + # self.logger.info("Created %s: %s", what, event.src_path) + + # def on_deleted(self, event): + # super().on_deleted(event) + + # what = 'directory' if event.is_directory else 'file' + # self.logger.info("Deleted %s: %s", what, event.src_path) + + def on_modified(self, event): + super().on_modified(event) + + what = 'directory' if event.is_directory else 'file' + # self.logger.info("Modified %s: %s", what, event.src_path) + print("Modified %s: %s" % (what, event.src_path)) + updateThumbnails() + +paths = [music_dir, '/home/cwilvx/watched'] + +if __name__ == "__main__": + observer = Observer() + event_handler = watchMusicDirs() + + observers = [] + + for path in paths: + observer.schedule(event_handler, path, recursive=True) + observers.append(observer) + + observer.start() + + try: + while True: + time.sleep(1) + + except KeyboardInterrupt: + for observer in observers: + observer.unschedule_all() + + observer.stop() + + for observer in observers: + observer.join() \ No newline at end of file