Compare commits
673 Commits
logging
...
renovate/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e981b32f6b | ||
| 3a9131a022 | |||
| 77433e8505 | |||
| 3845000b3f | |||
| 071fd54825 | |||
| 537f237e83 | |||
| d3bc854e03 | |||
| 15826a00ba | |||
| 4019d63f3b | |||
| 91dcfaccb9 | |||
| 270a259cee | |||
| 162d20ae52 | |||
| 9f1badc1b1 | |||
| ac027a97d6 | |||
| 127db88ded | |||
| 4b07434615 | |||
| 5d6f2baa34 | |||
| 1a1a7328a3 | |||
| b9dec2344e | |||
| 476a34fb69 | |||
| e3ed08e8dd | |||
| 70f6497404 | |||
| 99bab3fb73 | |||
| e6796aff8b | |||
| 784aa2616a | |||
| 7cb55ce054 | |||
| 87eb6de802 | |||
| 840816c930 | |||
| 91d238de7c | |||
| 0ac17d3d0a | |||
| 87c76e3f1d | |||
| e64c4d5d01 | |||
| 22e57c03de | |||
| d80386da40 | |||
| e7c66af3f6 | |||
| 8ece7b84c4 | |||
| 4250b1bd17 | |||
| 7e46d32e30 | |||
| 5a48158f07 | |||
| 161a466ab7 | |||
| 8f5bd44e4d | |||
| 5d8869e042 | |||
| 90b8ee005e | |||
| 1b0525063f | |||
| 41d6bba743 | |||
| 8977a4b195 | |||
| 7e0da028de | |||
| 2250cf2c4b | |||
| b2bd7b6a1f | |||
| a2ad7f5628 | |||
| f85d31991f | |||
| 08dc2153ae | |||
| bc64e69b3e | |||
| a29bca7361 | |||
| d84aa5f173 | |||
| 48ebd398bc | |||
| 1b95103acd | |||
| 6a1d6687eb | |||
| e849864bc2 | |||
| ecc2a67d48 | |||
| bfe0d55f71 | |||
| 634d4513eb | |||
| 0a1276a474 | |||
| 3a34d8995e | |||
| 918e629a06 | |||
| 7dd016a56e | |||
| c10bbcf518 | |||
| 3402a52633 | |||
| 86e9188a5c | |||
| 8918b7906e | |||
| 7e028a461d | |||
| fe5f0c815e | |||
| d02e79e59e | |||
| 5b49216c9d | |||
| 657ab10034 | |||
| ed07c77b13 | |||
| 4fe85d9fae | |||
| b99b5b32ec | |||
| e8058c5e4c | |||
| 7332b7d474 | |||
| 7980f14426 | |||
| 65540fafbd | |||
| 64ede43dec | |||
| bed12cff72 | |||
| 59e7f96643 | |||
| 71e9a5a46e | |||
| fce6dc7658 | |||
| baff59181c | |||
| 0592cca16b | |||
| e4d5f5085c | |||
| 66a2a06f9b | |||
| e984feeb8d | |||
| 490d015f80 | |||
| f1cc2c4ebe | |||
| 2f4421d9e0 | |||
| 92cc094787 | |||
| f30b46c384 | |||
| d9f679603a | |||
| 64bd9d1e14 | |||
| 721826d454 | |||
| 242fe3515c | |||
| ccf40d2161 | |||
| 832b8ba539 | |||
|
|
0477e49eca | ||
| 451b67630a | |||
| 096bbdf085 | |||
| e914e4ab45 | |||
| c1461e1f41 | |||
| 91bf2c1e2a | |||
| da3df383ed | |||
| 9816b978d3 | |||
| 8e22b0f6ea | |||
| 18359f442c | |||
| 42b8b5ea0e | |||
| 996295b1fe | |||
| 1b08c8d3d1 | |||
| 9e145f7068 | |||
| 7051edb212 | |||
| 0581813ee3 | |||
| edf1de223e | |||
| 7595b6df25 | |||
| 04ec0e0785 | |||
| b08dc899c3 | |||
| aafe524681 | |||
| 40b7ceda5f | |||
| 8850f06e3e | |||
| 972dcf4aab | |||
| 8e0eff7120 | |||
| e092eae60a | |||
| 424cf0985d | |||
| ed5d302820 | |||
| 559ef1bc11 | |||
| 9645fba4ef | |||
| e27d5c7084 | |||
| 4ab3946181 | |||
| 3a508b9843 | |||
| d8078570eb | |||
| 47b499c072 | |||
| a69e2d9bbb | |||
| 72a7a7e00d | |||
| 50d3135a81 | |||
| 9e557759ce | |||
| 1806f89e2b | |||
| 9140fa37eb | |||
| 6c062f5fc1 | |||
| 3abd7ddc61 | |||
| 6a7b427ef3 | |||
| 8fbd0eb9d8 | |||
| 127db8b2fe | |||
| 13401a318a | |||
| 72847efb93 | |||
| 7801ea5d05 | |||
| 37474de848 | |||
| 1a4aefda14 | |||
| 40b539db68 | |||
| 453727db1b | |||
| ab2d3c6756 | |||
| 69e694493c | |||
| 27c95375e2 | |||
| ba96e27c43 | |||
| 858f3f5d57 | |||
| 59b206bf2d | |||
| fac9e0e425 | |||
| e65d4125b9 | |||
| 354b833375 | |||
| cffba202b0 | |||
| d3b4a34298 | |||
| 164191f89d | |||
| 02c36d041a | |||
| 404c200474 | |||
| aa0094762b | |||
| 7c4aeb48ab | |||
| ca146d77bc | |||
| cede376349 | |||
| e11dfc7712 | |||
| 1a693ef92f | |||
| 2df734bb32 | |||
| 6619184a45 | |||
| 34a97c069b | |||
| e6a8515432 | |||
| 08b373cba0 | |||
| ec0923f1c0 | |||
| 4bc94ae3b7 | |||
| bf4cf8bef1 | |||
| 1d8a72429d | |||
| 528cbed30f | |||
| 7ee3ace83b | |||
| ba69893e21 | |||
| ad4fa9d95a | |||
| 28a731efbf | |||
| 878c71ef4a | |||
| bd1e3c4455 | |||
| 1be8bc0319 | |||
| f0476b2890 | |||
| 72294e19c5 | |||
| 30b494e356 | |||
| b037cb23a2 | |||
| 98b10aa693 | |||
| 3ca3c06824 | |||
| f6c504223a | |||
| 93e1ef6d99 | |||
| 58449fc753 | |||
| 34982c14fe | |||
| 81aeed86ef | |||
| a8ec7acff5 | |||
| 74d143775b | |||
| dda1db6c5f | |||
| 97217a2826 | |||
| 272300249e | |||
| fe1ad2b1ad | |||
| 444295d5d1 | |||
| a40d4f7cd5 | |||
| 6fd65fdb23 | |||
| 7e874bbc9d | |||
| 6564948af8 | |||
| fafb5c30e0 | |||
| af64a4e28a | |||
| 3c263a1ae7 | |||
| d8b2cef18f | |||
| 6f626d410d | |||
| e6b81ef0dd | |||
| cfb878fa64 | |||
| 44878a295c | |||
| 9b0fa88a72 | |||
| 80582f0022 | |||
| 77d7167fcb | |||
| 39cff99c10 | |||
| 3edf7d43cb | |||
| 4c5384ed2e | |||
| 2c36b7eb30 | |||
| 3d2fb6e28c | |||
| 952fe7220e | |||
| eedfe4cf3f | |||
| cfedd4e9c8 | |||
| cefbb5e41c | |||
| 89ec9d496b | |||
| bd0c7b8ab5 | |||
| 0c3ad3a95e | |||
| c0c9587066 | |||
| 6f6b38c23d | |||
| 3639105240 | |||
| 0b42cf7f12 | |||
| 4f98f2fd23 | |||
| 0addc76126 | |||
| 5d0b766d3d | |||
| 8285f14bfa | |||
| 159de6527c | |||
| fe020faf21 | |||
| 9d2b904260 | |||
| a4146fe055 | |||
| fd53150054 | |||
| 59fe9108ae | |||
| 0e85046751 | |||
| aa1866bbd9 | |||
| bc0211e34f | |||
| 34a0c7f958 | |||
| f6bd547701 | |||
| e0da166406 | |||
| 65bce27a2b | |||
| e9f6f3a656 | |||
| b8fb718540 | |||
| 21a94d88a9 | |||
| 519eba8fda | |||
| 10ef1bfa69 | |||
| da54ad0066 | |||
| c8a2ea9907 | |||
| 262093d196 | |||
| e64d88bfdf | |||
| f7c6f6603b | |||
| 279b004aad | |||
| 33cb6f5f09 | |||
| 4d99cae74c | |||
| 976d5cf69f | |||
| 76069f4fea | |||
| d92e6d8c78 | |||
| 601440d540 | |||
| 7824e8be27 | |||
| b4352ad721 | |||
| 9e160a7a91 | |||
| 2623649c5d | |||
| 908fca6dd0 | |||
| e6a34a0503 | |||
| f0e8f84e12 | |||
| 0459fb9312 | |||
| 6a725e3921 | |||
| fb22576708 | |||
| bced4e052d | |||
| 54eca33dfa | |||
| 93e43d9954 | |||
| fba3845523 | |||
| fe64af856e | |||
| 625717f7ad | |||
| e4573e6808 | |||
| e19b604a78 | |||
| f6c27482e4 | |||
| 00ad5cf7a8 | |||
| ba33552099 | |||
| 99ddb61c37 | |||
| 790f5d6588 | |||
| a51a78bc71 | |||
| 317aa69f18 | |||
| de90b48430 | |||
| 6011615a41 | |||
| 0338e90bf7 | |||
| cf43500c01 | |||
| c65cdd84b7 | |||
| 7849a8ce3f | |||
| 246abd7020 | |||
| 580cdc430f | |||
| 7bde2821d0 | |||
| 45db534681 | |||
| fd0a2c9d50 | |||
| e55067025e | |||
| 187ac6317e | |||
| 9fcc82d7cf | |||
| c11b222ee3 | |||
| 8695a553d6 | |||
| 3fe8f46dd4 | |||
| 5fb1e7ba2e | |||
| e1fed24fc0 | |||
| 8542da34cd | |||
| 7d44f1518f | |||
| fa8f82449b | |||
| f7b772a170 | |||
| d0597b4e1e | |||
| db11c968a3 | |||
| b8647f982d | |||
| ac6bbe6ac6 | |||
| 2b772e3017 | |||
| 57658f10c1 | |||
| 62b6f5c8ca | |||
| 362e5f54d1 | |||
| c5938c3c78 | |||
| 2421b04548 | |||
| ff78dfbd50 | |||
| 36b4f16047 | |||
| 5da57062a7 | |||
| be4c79ca16 | |||
| 613b4ecee3 | |||
| 7f4b95c8c4 | |||
| 0e4f96640c | |||
| dc685ebcdd | |||
| 6e9d511b5c | |||
| 2fc9674548 | |||
| 38383da1b9 | |||
| 7cce61b3bb | |||
| 303ec8fa10 | |||
| e417fedece | |||
| 6dd9cf7083 | |||
| 546f33883a | |||
| d10b974545 | |||
| c6fac11b8d | |||
| d6c066266c | |||
| 5c408b826b | |||
| a0ce4e07fe | |||
| 1b58cb461c | |||
| dbd258d9fa | |||
| 1bdf89b69f | |||
| 498bd13a1f | |||
| 80dcba2fba | |||
| effa41978e | |||
| 7069db8901 | |||
| 81fbc86cad | |||
| 6f12d0ca49 | |||
| 2d986eb5b3 | |||
| 233ad03dd3 | |||
| 40928a6e46 | |||
| c48db517ac | |||
| d836dd01d3 | |||
| da0609d56c | |||
| f15df29801 | |||
| 4b01387e56 | |||
| 0e3189ff11 | |||
| 1925c2a5e3 | |||
| 7952e6015a | |||
| 6e95d48f11 | |||
| d2d791f5b2 | |||
| 0e70a10cc2 | |||
| 592528dec2 | |||
| 354e06a963 | |||
| cc8d9f0c50 | |||
| 8d248135e0 | |||
| 6574c087bc | |||
| d90197b8b4 | |||
| d20ac6470f | |||
| 8f4e3a24f4 | |||
| f89d81681a | |||
| 1b29a08ce8 | |||
| f57878813d | |||
| b82efddeba | |||
| 6ec6fc771a | |||
| de33f02aeb | |||
| 7d4178a901 | |||
| e5b3c8a249 | |||
| 1183126653 | |||
| dbcb316bfd | |||
| b6b9c790e9 | |||
| 1425da3090 | |||
| d1d204691a | |||
| 5a24837163 | |||
| 4c7be51a61 | |||
| 3e6fc0960c | |||
| 3d73000e92 | |||
| 71e7f46927 | |||
| 5d40bd8e06 | |||
| 799b8731c5 | |||
| 74ea05cf17 | |||
| db226d6c95 | |||
| 9b1b041ef5 | |||
| 3223e423b2 | |||
| 99eb5512df | |||
| 63ba10bc5a | |||
| 3c039447f5 | |||
| a6d00714d6 | |||
| 67f5cef718 | |||
| 67d343e446 | |||
| 1f6130a53d | |||
| f0fb791e9d | |||
| 87babed265 | |||
| a0565f79d2 | |||
| 277105e6df | |||
| 87a2de1d0b | |||
| ebfb61d776 | |||
| 4cdf368d2c | |||
| 5aaf0ddf84 | |||
| 39e363dbf3 | |||
| 7b32dfa35a | |||
| cd58a830b5 | |||
| d9f432a8de | |||
| 2a09f67248 | |||
| 65c1f7ad96 | |||
| 2fd515d997 | |||
| 4297505861 | |||
| 5bd1041deb | |||
| e114cff32f | |||
| cdba914758 | |||
| 5f13ad202c | |||
| 8fa7f7f918 | |||
| b2f4151850 | |||
| 45a84cbf85 | |||
| 42d0b40825 | |||
| d9a22f506e | |||
| 307a8352a3 | |||
| 3c372aab92 | |||
| 650c2603e4 | |||
| fb479e7a37 | |||
| ac38dacedd | |||
| f54bd0f743 | |||
| 61c242b5eb | |||
| 51d446f378 | |||
| 9b6e7af3ee | |||
| 42749c5b64 | |||
| 2e47324005 | |||
| f8ff71bcff | |||
| 7e27e19a0d | |||
| c954bd4874 | |||
| dbb8eb4057 | |||
| c03449b9e9 | |||
| 6dd45cf89e | |||
| 3bc8c5912b | |||
| bd74e1dee1 | |||
| ed1d0f3c39 | |||
| 0b79c8679d | |||
| 927352e9ae | |||
| 5b110b9d82 | |||
| 1633be1276 | |||
| 6d5c1e8d6d | |||
| 5e50d52344 | |||
| 26c1bda3df | |||
| da83b73732 | |||
| d9d8d6ab58 | |||
| 5fbedeb4b2 | |||
| 455bf565b2 | |||
| a819a4bbf3 | |||
| f50e0b2a59 | |||
| 9d2c7c4c2f | |||
| 8908e36e89 | |||
| 33aefef362 | |||
| cee27fd9d2 | |||
| 2bacc54687 | |||
| 850401fd7a | |||
| a8166bcad9 | |||
| 149b05ef28 | |||
| e43bcbdd52 | |||
| 509759bca8 | |||
| f1f407c71c | |||
| bab4af08d9 | |||
| a3de70e2da | |||
| 698d2d6072 | |||
| 6ec6c7f9cb | |||
| 72654fd465 | |||
| f4aee549be | |||
| bb6dd1ad59 | |||
| a14d9e5ec6 | |||
| 5ceb19f449 | |||
| 8b5b8bb0b7 | |||
| 6b3fe8c46f | |||
| 98dafe56c4 | |||
| 44d8dbfe0c | |||
| 6d0a91dc93 | |||
| 8014f01766 | |||
| d787fba024 | |||
| 6bd1b29d5e | |||
| 86c479de15 | |||
| daa9a7749e | |||
| d47f2bf757 | |||
| 6f54a61223 | |||
| 1321671840 | |||
| 1af9368a6c | |||
| 5341e940c6 | |||
| 30226af6f6 | |||
| b2f9d6f5f5 | |||
| 6b4daf140a | |||
| a5d270967e | |||
| e281e73293 | |||
| 743c132aef | |||
| b219242787 | |||
| 00d000b8f8 | |||
| c79d5dbc6e | |||
| 372ec1b241 | |||
| 4c2982293b | |||
| 072f0cca93 | |||
| acd0a1782d | |||
| d9dd7ec153 | |||
| ae66e9a684 | |||
| 2d465d841e | |||
| 72d50209a5 | |||
| fae3fcc74c | |||
| 12835d4ed5 | |||
| c187ce7515 | |||
| 4fa32440af | |||
| cf90616ec9 | |||
| 895d52b41a | |||
| d26f4b0531 | |||
| d347a2bbfe | |||
| 8e894b3dfb | |||
| fa3d56c4e5 | |||
| eb2de340a3 | |||
| 0370f051bc | |||
| 3eb2bbe54e | |||
| a8c6850863 | |||
| cd52d295b0 | |||
| e9c0e5bfa6 | |||
| fcf6b324c9 | |||
| 231b70c226 | |||
| 28d0b63960 | |||
| 9eb31ea433 | |||
| 2b7f9551bf | |||
| 6a4e2bc2ab | |||
| 850452db78 | |||
| 7a1f709d90 | |||
| e81f5c8057 | |||
| 789ed77ab6 | |||
| 2dc22b386d | |||
| 3e12cf10fb | |||
| c64f6a861e | |||
| 70b284380f | |||
| e33e7d8dc7 | |||
| a5bb42f175 | |||
| dacd44a8f5 | |||
| 84a897da6e | |||
| d5479be563 | |||
| 849bc4aa5e | |||
| 17dd66e9ac | |||
| 8149c8f732 | |||
| 8652a0ee0f | |||
| 101dc7d570 | |||
| 3bf646483d | |||
| 622e89ea50 | |||
| b4a005da42 | |||
| 1a858d09bb | |||
| a4fe5cc635 | |||
| a82dc1ae78 | |||
| ebe09390d2 | |||
| d4fdf6bdcf | |||
| 72c4a43d2e | |||
| d77a4c6d9e | |||
| e33840f1db | |||
| bd3d8f385b | |||
| 5706c02a5d | |||
| ee019f5674 | |||
| 56405e54f9 | |||
| daa1915203 | |||
| ea97a0b4af | |||
| d2bed84ecb | |||
| b8d01fcf1c | |||
| 47aa638fd8 | |||
| f53ab55d96 | |||
| 20a7cb0ba1 | |||
| 10da9874fc | |||
| bc4eff795e | |||
| 82add07e00 | |||
| e5cb80afc8 | |||
| 891b7ecb28 | |||
| 292922c06c | |||
| 9265b469c4 | |||
| c892755759 | |||
| d2c456305a | |||
| 66b9c8c3db | |||
| fbf49e4cd9 | |||
| a436f79770 | |||
| 6b5a2341bf | |||
| 30d26d82c3 | |||
| a6af5e8c5d | |||
| 6322b0b17b | |||
| e77a05db07 | |||
| f2cc05307b | |||
| 90546755cc | |||
| b34e23077b | |||
| c45df8a131 | |||
| 91606fc9b8 | |||
| 3047dce147 | |||
|
|
2c97803d82 | ||
|
|
97dea47a7a | ||
|
|
c97ab972ef | ||
|
|
29e575cbf1 | ||
| b2848e6a7e | |||
| 1663f5931d | |||
| 5b8394c5a0 | |||
| ba3a1fa028 | |||
| b358c61efb | |||
| 71130e66f3 | |||
| 82d2e439fe | |||
| be839ba2dd | |||
| f884406c06 | |||
| 04066b8da4 | |||
| 34ab8be097 | |||
| 31e16e2784 | |||
| 7cda4accdb | |||
| 7023b135b4 | |||
| 979a95a468 | |||
| c1cd821d8a | |||
| 4b54339b72 | |||
| 3d8dc80bb9 | |||
| 8a53cc4765 | |||
| 3bc539323a | |||
| 80746252c0 | |||
| d5ea7a6bbb | |||
| 17b89748e1 | |||
| 5a45856699 | |||
| 90384e4ebc | |||
| 7f14f64762 | |||
| 2333559477 | |||
| a6d21ed181 | |||
| 1eb46a41e0 | |||
| 3b5d6fbb6c | |||
| 0022f656bd | |||
| 9a02130124 | |||
| 3bb43f08f2 | |||
| 703e3d3785 | |||
| 6496988e51 | |||
| 069d984c39 | |||
| 27edd29bd2 | |||
| 531900744e | |||
| d9cb5b97c7 | |||
| 9d29482857 | |||
| 907bc73a7f | |||
| 6f8a7bc06c | |||
| 25e4cb1870 | |||
| b695d1aaf0 | |||
| d05aa13c39 | |||
| 21c40e9b34 | |||
| 244f606133 | |||
| 7152c987b7 | |||
| 47269ebc70 | |||
| 30dd05ebf3 | |||
| 4f2a69f32d | |||
| 545152c888 | |||
| bd3ceb7b6d | |||
| 7c055bd9d1 | |||
| 9d11798133 | |||
| 53a60de38b | |||
| 74c6ed02a8 |
15
.gitignore
vendored
15
.gitignore
vendored
@@ -1,10 +1,7 @@
|
||||
__pycache__
|
||||
nohup.out
|
||||
shows.db
|
||||
.DS_Store
|
||||
env_variables.py
|
||||
conf/classedOutput.log
|
||||
node_modules
|
||||
*.pyc
|
||||
npm-debug.log
|
||||
webpage/js/env_variables.js
|
||||
|
||||
development.json
|
||||
env
|
||||
shows.db
|
||||
|
||||
*/package-lock.json
|
||||
|
||||
10
.gitmodules
vendored
Normal file
10
.gitmodules
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Docs : https://git-scm.com/book/en/v2/Git-Tools-Submodules
|
||||
|
||||
[submodule "torrent_search"]
|
||||
path = torrent_search
|
||||
url = https://github.com/KevinMidboe/torrent_search.git
|
||||
branch = master
|
||||
|
||||
[submodule "delugeClient"]
|
||||
path = delugeClient
|
||||
url = https://github.com/KevinMidboe/delugeClient.git
|
||||
18
.travis.yml
Normal file
18
.travis.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
language: node_js
|
||||
node_js: '8.7.0'
|
||||
git:
|
||||
submodules: true
|
||||
script:
|
||||
- yarn test
|
||||
- yarn coverage
|
||||
before_install:
|
||||
- cd seasoned_api
|
||||
before_script:
|
||||
- yarn
|
||||
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
||||
- chmod +x ./cc-test-reporter
|
||||
- ./cc-test-reporter before-build
|
||||
after-script:
|
||||
- ./cc-test-resporter after-build --exit-code $TRAVIS_TEST_RESULT
|
||||
cache: false
|
||||
os: linux
|
||||
18
ISSUE_TEMPLATE.md
Normal file
18
ISSUE_TEMPLATE.md
Normal file
@@ -0,0 +1,18 @@
|
||||
## What kind of an issue is this?
|
||||
|
||||
- [ ] Bug report
|
||||
- [ ] Feature request
|
||||
|
||||
## Expected behaviour?
|
||||
|
||||
|
||||
## Current behaviour?
|
||||
*if this is a bug report*
|
||||
|
||||
|
||||
## Steps to reproduce behaviour?
|
||||
*if this is a bug report*
|
||||
|
||||
|
||||
## Screenshot? 📷
|
||||
*A image tells a thousands words*
|
||||
135
README.md
135
README.md
@@ -1,6 +1,137 @@
|
||||
# *Seasoned*: an intelligent organizer for your shows
|
||||
|
||||
*Seasoned* is a intelligent organizer for your tv show episodes. It is made to automate and simplify to process of renaming and moving newly downloaded tv show episodes following Plex file naming and placement.
|
||||
<h1 align="center">
|
||||
🌶 seasonedShows
|
||||
</h1>
|
||||
|
||||
<h4 align="center"> Season your media library with the shows and movies that you and your friends want.</h4>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://travis-ci.org/KevinMidboe/seasonedShows">
|
||||
<img src="https://travis-ci.org/KevinMidboe/seasonedShows.svg?branch=master"
|
||||
alt="Travis CI">
|
||||
</a>
|
||||
<a href="https://coveralls.io/github/KevinMidboe/seasonedShows?branch=coverage">
|
||||
<img src="https://coveralls.io/repos/github/KevinMidboe/seasonedShows/badge.svg?branch=coverage" alt="">
|
||||
</a>
|
||||
<a href="https://snyk.io/test/github/KevinMidboe/seasonedShows?targetFile=seasoned_api/package.json">
|
||||
<img src="https://snyk.io/test/github/KevinMidboe/seasonedShows/badge.svg?targetFile=seasoned_api/package.json" alt="">
|
||||
</a>
|
||||
<a href="https://opensource.org/licenses/MIT">
|
||||
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#demo-documentation">D & D</a> •
|
||||
<a href="#about">About</a> •
|
||||
<a href="#key-features">Key features</a> •
|
||||
<a href="#installation">Installation</a> •
|
||||
<a href="#setup">Setup</a> •
|
||||
<a href="#running">Running</a> •
|
||||
<a href="#daemon">Setup daemon</a> •
|
||||
<a href="#contributing">Contributing</a>
|
||||
</p>
|
||||
|
||||
## <a name="demo-documentation"></a> Demo & Documentation
|
||||
📺 [DEMO](https://kevinmidboe.com/request)
|
||||
📝 Documentation of the api.
|
||||
💖 Checkout my [fancy vue.js page](https://github.com/KevinMidboe/seasonedRequest) for interfacing the api.
|
||||
|
||||
## <a name="about"></a> About
|
||||
This is the backend api for [seasoned request] that allows for uesrs to request movies and shows by fetching movies from themoviedb api and checks them with your plex library to identify if a movie is already present or not. This api allows to search my query, get themoviedb movie lists like popular and now playing, all while checking if the item is already in your plex library. Your friends can create users to see what movies or shows they have requested and searched for.
|
||||
|
||||
The api also uses torrent_search to search for matching torrents and returns results from any site or service available from torrent_search. As a admin of the site you can query torrent_search and return a magnet link that can be added to a autoadd folder of your favorite torrent client.
|
||||
|
||||
## <a name="key-features"></a> Key features
|
||||
### Code
|
||||
- Uses [tmdb api](https://www.themoviedb.org/documentation/api) with over 350k movies and 70k tv shows
|
||||
- Written asynchronously
|
||||
- Uses caching for external requests
|
||||
- Test coverage
|
||||
- CI and dependency integrated
|
||||
- Use either config file or env_variables
|
||||
### Functionality
|
||||
- Queries plex library to check if items exists
|
||||
- Create admin and normal user accounts
|
||||
- [torrent_search](https://github.com/KevinMidboe/torrent_search) to search for torrents
|
||||
- Fetch curated lists from tmdb
|
||||
|
||||
## <a name="installation"></a> Installation
|
||||
Before we can use seasonedShows we need to download node and a package manager. For instructions on how to install [yarn](https://yarnpkg.com/en/) or [npm](https://www.npmjs.com) package managers refer to [wiki: install package manager](https://github.com/KevinMidboe/seasonedShows/wiki/Install-package-manager). This api is written with express using node.js as the JavaScript runtime engine. To download node.js head over the the official [node.js download page](https://nodejs.org/en/download/).
|
||||
|
||||
### Install seasonedShows
|
||||
After you have downloaded a package manager and node.js javascript engine, the following will guide you through how to download, install and run seasonedShows.
|
||||
|
||||
### macOS
|
||||
- Open terminal
|
||||
- Install git. This can be done by running `xcode-select --install` in your favorite terminal.
|
||||
- Install a package manager, refer to this [wiki page] for yarn or [wiki page] for npm
|
||||
- Type: `git clone --recurse-submodules git@github.com:KevinMidboe/seasonedShows.git`
|
||||
- Type: `cd seasonedShows/`
|
||||
- Install required packages
|
||||
* yarn: `yarn install`
|
||||
* npm: `npm install`
|
||||
- Start server:
|
||||
* yarn: `yarn start`
|
||||
* npm: `npm run start`
|
||||
- seasonedShows will now be running at http://localhost:31459
|
||||
- To have seasonedShows run headless on startup, check out this wiki page to [install as a daemon].
|
||||
|
||||
### Linux
|
||||
- Open terminal
|
||||
- Install git
|
||||
* Ubuntu/Debian: `sudo apt-get install git-core`
|
||||
* Fedora: `sudo yum install git`
|
||||
- Type: `git clone --recurse-submodules git@github.com:KevinMidboe/seasonedShows.git`
|
||||
- Type: `cd seasonedShows/`
|
||||
- Install required packages
|
||||
* yarn: `yarn install`
|
||||
* npm: `npm install`
|
||||
- Start server:
|
||||
* yarn: `yarn start`
|
||||
* npm: `npm run start`
|
||||
- seasonedShows will now be running at http://localhost:31459
|
||||
- To have seasonedShows run headless on startup, check out this wiki page to [install as a daemon].
|
||||
|
||||
-- same --
|
||||
(install yarn or npm in a different way)
|
||||
After you have installed the required packages you will have a node_modules directory with all the packages required in packages.json.
|
||||
|
||||
### Requirements
|
||||
- Node 7.6 < [wiki page]
|
||||
- Plex library
|
||||
|
||||
## <a name="setup"></a> Setup and/ configuration
|
||||
There is a config file template, what the values mean and how to change them.
|
||||
Also show how to hide file from git if not want to show up as uncommitted file.
|
||||
Also set variables in environment.
|
||||
|
||||
## <a name="running"></a> Running/using
|
||||
yarn/npm start. (can also say this above)
|
||||
How to create service on linux. This means that
|
||||
|
||||
## <a name="daemon"></a> Setup a daemon
|
||||
The next step is to setup seasonedShows api to run in the background as a daemon. I have written a [wiki page](https://github.com/KevinMidboe/seasonedShows/wiki/Install-as-a-daemon) on how to create a daemon on several unix distors and macOS.
|
||||
*Please don't hesitate to add your own system if you get it running on something that is not yet lists on the formentioned wiki page.*
|
||||
|
||||
## <a name="contributing"></a> Contributing
|
||||
- Fork it!
|
||||
- Create your feature branch: git checkout -b my-new-feature
|
||||
- Commit your changes: git commit -am 'Add some feature'
|
||||
- Push to the branch: git push origin my-new-feature
|
||||
- Submit a pull request
|
||||
|
||||
|
||||
## Api documentation
|
||||
|
||||
|
||||
The goal of this project is to create a full custom stack that can to everything surround downloading, organizing and notifiyng of new media. From the top down we have a website using [tmdb](https://www.themoviedb.com) api to search for from over 350k movies and 70k tv shows. Using [hjone72](https://github.com/hjone72/PlexAuth) great PHP reverse proxy we can have a secure way of allowing users to login with their plex credentials which limits request capabilites to only users that are authenticated to use your plex library.
|
||||
seasonedShows is a intelligent organizer for your tv show episodes. It is made to automate and simplify to process of renaming and moving newly downloaded tv show episodes following Plex file naming and placement.
|
||||
|
||||
So this is a multipart system that lets your plex users request movies, and then from the admin page the owner can.
|
||||
|
||||
## Installation
|
||||
There are two main ways of
|
||||
|
||||
## Architecture
|
||||
The flow of the system will first check for new folders in your tv shows directory, if a new file is found it's contents are analyzed, stored and tweets suggested changes to it's contents to use_admin.
|
||||
|
||||
1
_config.yml
Normal file
1
_config.yml
Normal file
@@ -0,0 +1 @@
|
||||
theme: jekyll-theme-cayman
|
||||
108
app/.gitignore
vendored
Normal file
108
app/.gitignore
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
.static_storage/
|
||||
.media/
|
||||
local_settings.py
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
||||
# - - - - -
|
||||
# My own gitignore files and folders
|
||||
env_variables.py
|
||||
@@ -3,7 +3,7 @@
|
||||
# @Author: KevinMidboe
|
||||
# @Date: 2017-04-05 18:40:11
|
||||
# @Last Modified by: KevinMidboe
|
||||
# @Last Modified time: 2017-06-01 19:02:04
|
||||
# @Last Modified time: 2018-04-03 22:58:20
|
||||
import os.path, hashlib, time, glob, sqlite3, re, json, tweepy
|
||||
import logging
|
||||
from functools import reduce
|
||||
@@ -61,7 +61,7 @@ class strayEpisode(object):
|
||||
return hashlib.md5("b'{}'".format(self.parent).encode()).hexdigest()[:8]
|
||||
|
||||
def findSeriesName(self):
|
||||
find = re.compile("^[a-zA-Z. ]*")
|
||||
find = re.compile("^[a-zA-Z0-9. ]*")
|
||||
m = re.match(find, self.parent)
|
||||
if m:
|
||||
name, hit = process.extractOne(m.group(0), getShowNames().keys())
|
||||
@@ -91,7 +91,12 @@ class strayEpisode(object):
|
||||
|
||||
def analyseSubtitles(self, subFile):
|
||||
# TODO verify that it is a file
|
||||
f = open(os.path.join([env.show_dir, self.parent, subFile]), 'r', encoding='ISO-8859-15')
|
||||
try:
|
||||
subtitlePath = os.path.join([env.input_dir, self.parent, subFile])
|
||||
except TypeError:
|
||||
# TODO don't get a list in subtitlePath
|
||||
return self.removeUploadSign(subFile)
|
||||
f = open(subtitlesPath, 'r', encoding='ISO-8859-15')
|
||||
language = detect(f.read())
|
||||
f.close()
|
||||
|
||||
@@ -126,7 +131,7 @@ class strayEpisode(object):
|
||||
conn = sqlite3.connect(env.db_path)
|
||||
c = conn.cursor()
|
||||
|
||||
path = '/'.join([env.show_dir, self.parent])
|
||||
path = '/'.join([env.input_dir, self.parent])
|
||||
video_files = json.dumps(self.videoFiles)
|
||||
subtitles = json.dumps(self.subtitles)
|
||||
trash = json.dumps(self.trash)
|
||||
@@ -144,14 +149,13 @@ class strayEpisode(object):
|
||||
|
||||
|
||||
|
||||
def getDirContent(dir=env.show_dir):
|
||||
def getDirContent(dir=env.input_dir):
|
||||
# TODO What if item in db is not in this list?
|
||||
try:
|
||||
return [d for d in os.listdir(dir) if d[0] != '.']
|
||||
except FileNotFoundError:
|
||||
# TODO Log to error file
|
||||
logging.info('Error: "' + dir + '" is not a directory.')
|
||||
# TODO Remove this exit(0)
|
||||
|
||||
# Hashes the contents of media folder to easily check for changes.
|
||||
def directoryChecksum():
|
||||
@@ -185,12 +189,12 @@ def XOR(list1, list2):
|
||||
|
||||
def filterChildItems(parent):
|
||||
try:
|
||||
children = getDirContent('/'.join([env.show_dir, parent]))
|
||||
children = getDirContent('/'.join([env.input_dir, parent]))
|
||||
if children:
|
||||
strayEpisode(parent, children)
|
||||
except FileNotFoundError:
|
||||
# TODO Log to error file
|
||||
logging.info('Error: "' + '/'.join([env.show_dir, parent]) + '" is not a valid directory.')
|
||||
logging.info('Error: "' + '/'.join([env.input_dir, parent]) + '" is not a valid directory.')
|
||||
|
||||
def getNewItems():
|
||||
newItems = XOR(getDirContent(), getShowNames())
|
||||
@@ -209,7 +213,7 @@ def main():
|
||||
|
||||
if __name__ == '__main__':
|
||||
if (os.path.exists(env.logfile)):
|
||||
logging.basicConfig(filename=env.logfile, level=logging.INFO)
|
||||
logging.basicConfig(filename=env.logfile, level=logging.DEBUG)
|
||||
else:
|
||||
print('Logfile could not be found at ' + env.logfile + '. Verifiy presence or disable logging in config.')
|
||||
exit(0)
|
||||
@@ -217,3 +221,4 @@ if __name__ == '__main__':
|
||||
while True:
|
||||
main()
|
||||
sleep(30)
|
||||
|
||||
279
app/core.py
Executable file
279
app/core.py
Executable file
@@ -0,0 +1,279 @@
|
||||
#!/usr/bin/env python3.6
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author: KevinMidboe
|
||||
# @Date: 2017-08-25 23:22:27
|
||||
# @Last Modified by: KevinMidboe
|
||||
# @Last Modified time: 2017-10-12 22:44:27
|
||||
|
||||
from guessit import guessit
|
||||
import os, errno
|
||||
import logging
|
||||
import tvdb_api
|
||||
from pprint import pprint
|
||||
|
||||
import env_variables as env
|
||||
|
||||
from video import VIDEO_EXTENSIONS, Episode, Movie, Video
|
||||
from subtitle import SUBTITLE_EXTENSIONS, Subtitle, get_subtitle_path
|
||||
from utils import sanitize
|
||||
|
||||
logging.basicConfig(filename=os.path.dirname(__file__) + '/' + env.logfile, level=logging.INFO)
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
#: Supported archive extensions
|
||||
ARCHIVE_EXTENSIONS = ('.rar',)
|
||||
|
||||
def scan_video(path):
|
||||
"""Scan a video from a `path`.
|
||||
|
||||
:param str path: existing path to the video.
|
||||
:return: the scanned video.
|
||||
:rtype: :class:`~subliminal.video.Video`
|
||||
|
||||
"""
|
||||
# check for non-existing path
|
||||
if not os.path.exists(path):
|
||||
raise ValueError('Path does not exist')
|
||||
|
||||
# check video extension
|
||||
# if not path.endswith(VIDEO_EXTENSIONS):
|
||||
# raise ValueError('%r is not a valid video extension' % os.path.splitext(path)[1])
|
||||
|
||||
dirpath, filename = os.path.split(path)
|
||||
logging.info('Scanning video %r in %r', filename, dirpath)
|
||||
|
||||
# guess
|
||||
parent_path = path.strip(filename)
|
||||
# video = Video.fromguess(filename, parent_path, guessit(path))
|
||||
video = Video(filename)
|
||||
# guessit(path)
|
||||
|
||||
return video
|
||||
|
||||
|
||||
def scan_subtitle(path):
|
||||
if not os.path.exists(path):
|
||||
raise ValueError('Path does not exist')
|
||||
|
||||
dirpath, filename = os.path.split(path)
|
||||
logging.info('Scanning subtitle %r in %r', filename, dirpath)
|
||||
|
||||
# guess
|
||||
parent_path = path.strip(filename)
|
||||
subtitle = Subtitle.fromguess(filename, parent_path, guessit(path))
|
||||
|
||||
|
||||
return subtitle
|
||||
|
||||
|
||||
def scan_files(path, age=None, archives=True):
|
||||
"""Scan `path` for videos and their subtitles.
|
||||
|
||||
See :func:`refine` to find additional information for the video.
|
||||
|
||||
:param str path: existing directory path to scan.
|
||||
:param datetime.timedelta age: maximum age of the video or archive.
|
||||
:param bool archives: scan videos in archives.
|
||||
:return: the scanned videos.
|
||||
:rtype: list of :class:`~subliminal.video.Video`
|
||||
|
||||
"""
|
||||
# check for non-existing path
|
||||
if not os.path.exists(path):
|
||||
raise ValueError('Path does not exist')
|
||||
|
||||
# check for non-directory path
|
||||
if not os.path.isdir(path):
|
||||
raise ValueError('Path is not a directory')
|
||||
|
||||
name_dict = {}
|
||||
|
||||
# walk the path
|
||||
mediafiles = []
|
||||
for dirpath, dirnames, filenames in os.walk(path):
|
||||
logging.debug('Walking directory %r', dirpath)
|
||||
|
||||
# remove badly encoded and hidden dirnames
|
||||
for dirname in list(dirnames):
|
||||
if dirname.startswith('.'):
|
||||
logging.debug('Skipping hidden dirname %r in %r', dirname, dirpath)
|
||||
dirnames.remove(dirname)
|
||||
|
||||
# scan for videos
|
||||
for filename in filenames:
|
||||
# filter on videos and archives
|
||||
if not (filename.endswith(VIDEO_EXTENSIONS) or filename.endswith(SUBTITLE_EXTENSIONS) or archives and filename.endswith(ARCHIVE_EXTENSIONS)):
|
||||
continue
|
||||
|
||||
# skip hidden files
|
||||
if filename.startswith('.'):
|
||||
logging.debug('Skipping hidden filename %r in %r', filename, dirpath)
|
||||
continue
|
||||
|
||||
# reconstruct the file path
|
||||
filepath = os.path.join(dirpath, filename)
|
||||
|
||||
# skip links
|
||||
if os.path.islink(filepath):
|
||||
logging.debug('Skipping link %r in %r', filename, dirpath)
|
||||
continue
|
||||
|
||||
# skip old files
|
||||
if age and datetime.utcnow() - datetime.utcfromtimestamp(os.path.getmtime(filepath)) > age:
|
||||
logging.debug('Skipping old file %r in %r', filename, dirpath)
|
||||
continue
|
||||
|
||||
# scan
|
||||
if filename.endswith(VIDEO_EXTENSIONS): # video
|
||||
try:
|
||||
video = scan_video(filepath)
|
||||
try:
|
||||
name_dict[video.series] += 1
|
||||
except KeyError:
|
||||
name_dict[video.series] = 0
|
||||
mediafiles.append(video)
|
||||
|
||||
except ValueError: # pragma: no cover
|
||||
logging.exception('Error scanning video')
|
||||
continue
|
||||
elif archives and filename.endswith(ARCHIVE_EXTENSIONS): # archive
|
||||
print('archive')
|
||||
pass
|
||||
# try:
|
||||
# video = scan_archive(filepath)
|
||||
# mediafiles.append(video)
|
||||
# except (NotRarFile, RarCannotExec, ValueError): # pragma: no cover
|
||||
# logging.exception('Error scanning archive')
|
||||
# continue
|
||||
elif filename.endswith(SUBTITLE_EXTENSIONS): # subtitle
|
||||
try:
|
||||
subtitle = scan_subtitle(filepath)
|
||||
mediafiles.append(subtitle)
|
||||
except ValueError:
|
||||
logging.exception('Error scanning subtitle')
|
||||
continue
|
||||
else: # pragma: no cover
|
||||
raise ValueError('Unsupported file %r' % filename)
|
||||
|
||||
|
||||
pprint(name_dict)
|
||||
return mediafiles
|
||||
|
||||
|
||||
def organize_files(path):
|
||||
hashList = {}
|
||||
mediafiles = scan_files(path)
|
||||
# print(mediafiles)
|
||||
|
||||
for file in mediafiles:
|
||||
hashList.setdefault(file.__hash__(),[]).append(file)
|
||||
# hashList[file.__hash__()] = file
|
||||
|
||||
return hashList
|
||||
|
||||
|
||||
def save_subtitles(files, single=False, directory=None, encoding=None):
|
||||
t = tvdb_api.Tvdb()
|
||||
|
||||
if not isinstance(files, list):
|
||||
files = [files]
|
||||
|
||||
for file in files:
|
||||
# TODO this should not be done in the loop
|
||||
dirname = "%s S%sE%s" % (file.series, "%02d" % (file.season), "%02d" % (file.episode))
|
||||
|
||||
createParentfolder = not dirname in file.parent_path
|
||||
if createParentfolder:
|
||||
dirname = os.path.join(file.parent_path, dirname)
|
||||
print('Created: %s' % dirname)
|
||||
try:
|
||||
os.makedirs(dirname)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
|
||||
# TODO Clean this !
|
||||
try:
|
||||
tvdb_episode = t[file.series][file.season][file.episode]
|
||||
episode_title = tvdb_episode['episodename']
|
||||
except:
|
||||
episode_title = ''
|
||||
|
||||
old = os.path.join(file.parent_path, file.name)
|
||||
|
||||
if file.name.endswith(SUBTITLE_EXTENSIONS):
|
||||
lang = file.getLanguage()
|
||||
sdh = '.sdh' if file.sdh else ''
|
||||
filename = "%s S%sE%s %s%s.%s.%s" % (file.series, "%02d" % (file.season), "%02d" % (file.episode), episode_title, sdh, lang, file.container)
|
||||
else:
|
||||
filename = "%s S%sE%s %s.%s" % (file.series, "%02d" % (file.season), "%02d" % (file.episode), episode_title, file.container)
|
||||
|
||||
if createParentfolder:
|
||||
newname = os.path.join(dirname, filename)
|
||||
else:
|
||||
newname = os.path.join(file.parent_path, filename)
|
||||
|
||||
|
||||
print('Moved: %s ---> %s' % (old, newname))
|
||||
os.rename(old, newname)
|
||||
|
||||
print()
|
||||
|
||||
|
||||
# for hash in files:
|
||||
# hashIndex = [files[hash]]
|
||||
# for hashItems in hashIndex:
|
||||
# for file in hashItems:
|
||||
# print(file.series)
|
||||
|
||||
# saved_subtitles = []
|
||||
# for subtitle in files:
|
||||
# # check content
|
||||
# if subtitle.name is None:
|
||||
# logging.error('Skipping subtitle %r: no content', subtitle)
|
||||
# continue
|
||||
|
||||
# # check language
|
||||
# if subtitle.language in set(s.language for s in saved_subtitles):
|
||||
# logging.debug('Skipping subtitle %r: language already saved', subtitle)
|
||||
# continue
|
||||
|
||||
# # create subtitle path
|
||||
# subtitle_path = get_subtitle_path(video.name, None if single else subtitle.language)
|
||||
# if directory is not None:
|
||||
# subtitle_path = os.path.join(directory, os.path.split(subtitle_path)[1])
|
||||
|
||||
# # save content as is or in the specified encoding
|
||||
# logging.info('Saving %r to %r', subtitle, subtitle_path)
|
||||
# if encoding is None:
|
||||
# with io.open(subtitle_path, 'wb') as f:
|
||||
# f.write(subtitle.content)
|
||||
# else:
|
||||
# with io.open(subtitle_path, 'w', encoding=encoding) as f:
|
||||
# f.write(subtitle.text)
|
||||
# saved_subtitles.append(subtitle)
|
||||
|
||||
# # check single
|
||||
# if single:
|
||||
# break
|
||||
|
||||
# return saved_subtitles
|
||||
|
||||
def stringTime():
|
||||
return str(datetime.now().strftime("%Y-%m-%d %H:%M:%S:%f"))
|
||||
|
||||
|
||||
def main():
|
||||
# episodePath = '/Volumes/media/tv/Black Mirror/Black Mirror Season 01/'
|
||||
episodePath = '/Volumes/mainframe/shows/Black Mirror/Black Mirror Season 01/'
|
||||
|
||||
t = tvdb_api.Tvdb()
|
||||
|
||||
hashList = organize_files(episodePath)
|
||||
pprint(hashList)
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
99
app/magnet.py
Executable file
99
app/magnet.py
Executable file
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python
|
||||
'''
|
||||
Created on Apr 19, 2012
|
||||
@author: dan, Faless
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE - Version 3
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
http://www.gnu.org/licenses/gpl-3.0.txt
|
||||
|
||||
'''
|
||||
|
||||
import shutil
|
||||
import tempfile
|
||||
import os.path as pt
|
||||
import sys, logging
|
||||
import libtorrent as lt
|
||||
from time import sleep
|
||||
|
||||
import env_variables as env
|
||||
|
||||
logging.basicConfig(filename=pt.dirname(__file__) + '/' + env.logfile)
|
||||
|
||||
def magnet2torrent(magnet, output_name=None):
|
||||
if output_name and \
|
||||
not pt.isdir(output_name) and \
|
||||
not pt.isdir(pt.dirname(pt.abspath(output_name))):
|
||||
logging.info("Invalid output folder: " + pt.dirname(pt.abspath(output_name)))
|
||||
logging.info("")
|
||||
sys.exit(0)
|
||||
|
||||
tempdir = tempfile.mkdtemp()
|
||||
ses = lt.session()
|
||||
params = {
|
||||
'save_path': tempdir,
|
||||
'storage_mode': lt.storage_mode_t(2),
|
||||
'paused': False,
|
||||
'auto_managed': True,
|
||||
'duplicate_is_error': True
|
||||
}
|
||||
handle = lt.add_magnet_uri(ses, magnet, params)
|
||||
|
||||
logging.info("Downloading Metadata (this may take a while)")
|
||||
while (not handle.has_metadata()):
|
||||
try:
|
||||
sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Aborting...")
|
||||
ses.pause()
|
||||
logging.info("Cleanup dir " + tempdir)
|
||||
shutil.rmtree(tempdir)
|
||||
sys.exit(0)
|
||||
ses.pause()
|
||||
logging.info("Done")
|
||||
|
||||
torinfo = handle.get_torrent_info()
|
||||
torfile = lt.create_torrent(torinfo)
|
||||
|
||||
output = pt.abspath(torinfo.name() + ".torrent")
|
||||
|
||||
if output_name:
|
||||
if pt.isdir(output_name):
|
||||
output = pt.abspath(pt.join(
|
||||
output_name, torinfo.name() + ".torrent"))
|
||||
elif pt.isdir(pt.dirname(pt.abspath(output_name))):
|
||||
output = pt.abspath(output_name)
|
||||
|
||||
logging.info("Saving torrent file here : " + output + " ...")
|
||||
torcontent = lt.bencode(torfile.generate())
|
||||
f = open(output, "wb")
|
||||
f.write(lt.bencode(torfile.generate()))
|
||||
f.close()
|
||||
logging.info("Saved! Cleaning up dir: " + tempdir)
|
||||
ses.remove_torrent(handle)
|
||||
shutil.rmtree(tempdir)
|
||||
|
||||
return output
|
||||
|
||||
def main():
|
||||
magnet = sys.argv[1]
|
||||
logging.info('INPUT: {}'.format(magnet))
|
||||
|
||||
magnet2torrent(magnet, env.torrent_dumpsite)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
112
app/moveSeasoned.py
Executable file
112
app/moveSeasoned.py
Executable file
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author: KevinMidboe
|
||||
# @Date: 2017-04-12 23:27:51
|
||||
# @Last Modified by: KevinMidboe
|
||||
# @Last Modified time: 2018-05-13 19:17:17
|
||||
|
||||
import sys, sqlite3, json, os.path
|
||||
import logging
|
||||
import env_variables as env
|
||||
import shutil
|
||||
|
||||
import delugeClient.deluge_cli as delugeCli
|
||||
|
||||
class episode(object):
|
||||
def __init__(self, id):
|
||||
self.id = id
|
||||
self.getVarsFromDB()
|
||||
|
||||
def getVarsFromDB(self):
|
||||
c = sqlite3.connect(env.db_path).cursor()
|
||||
c.execute('SELECT parent, name, season, episode, video_files, subtitles, trash FROM stray_eps WHERE id = ?', (self.id,))
|
||||
returnMsg = c.fetchone()
|
||||
self.parent = returnMsg[0]
|
||||
self.name = returnMsg[1]
|
||||
self.season = returnMsg[2]
|
||||
self.episode = returnMsg[3]
|
||||
self.video_files = json.loads(returnMsg[4])
|
||||
self.subtitles = json.loads(returnMsg[5])
|
||||
self.trash = json.loads(returnMsg[6])
|
||||
c.close()
|
||||
|
||||
self.queries = {
|
||||
'parent_input': [env.input_dir, self.parent],
|
||||
'season': [env.show_dir, self.name, self.name + ' Season ' + "%02d" % self.season],
|
||||
'episode': [env.show_dir, self.name, self.name + ' Season ' + "%02d" % self.season, \
|
||||
self.name + ' S' + "%02d" % self.season + 'E' + "%02d" % self.episode],
|
||||
}
|
||||
|
||||
def typeDir(self, dType, create=False, mergeItem=None):
|
||||
url = '/'.join(self.queries[dType])
|
||||
print(url)
|
||||
if create and not os.path.isdir(url):
|
||||
os.makedirs(url)
|
||||
fix_ownership(url)
|
||||
if mergeItem:
|
||||
return '/'.join([url, str(mergeItem)])
|
||||
return url
|
||||
|
||||
|
||||
def fix_ownership(path):
|
||||
pass
|
||||
# TODO find this from username from config
|
||||
# uid = 1000
|
||||
# gid = 112
|
||||
# os.chown(path, uid, gid)
|
||||
|
||||
def moveStray(strayId):
|
||||
ep = episode(strayId)
|
||||
|
||||
for item in ep.video_files:
|
||||
try:
|
||||
old_dir = ep.typeDir('parent_input', mergeItem=item[0])
|
||||
new_dir = ep.typeDir('episode', mergeItem=item[1], create=True)
|
||||
shutil.move(old_dir, new_dir)
|
||||
except FileNotFoundError:
|
||||
logging.warning(old_dir + ' does not exits, cannot be moved.')
|
||||
|
||||
for item in ep.subtitles:
|
||||
try:
|
||||
old_dir = ep.typeDir('parent_input', mergeItem=item[0])
|
||||
new_dir = ep.typeDir('episode', mergeItem=item[1], create=True)
|
||||
shutil.move(old_dir, new_dir)
|
||||
except FileNotFoundError:
|
||||
logging.warning(old_dir + ' does not exits, cannot be moved.')
|
||||
|
||||
for item in ep.trash:
|
||||
try:
|
||||
os.remove(ep.typeDir('parent_input', mergeItem=item))
|
||||
except FileNotFoundError:
|
||||
logging.warning(ep.typeDir('parent_input', mergeItem=item) + 'does not exist, cannot be removed.')
|
||||
|
||||
fix_ownership(ep.typeDir('episode'))
|
||||
for root, dirs, files in os.walk(ep.typeDir('episode')):
|
||||
for item in files:
|
||||
fix_ownership(os.path.join(ep.typeDir('episode'), item))
|
||||
|
||||
|
||||
# TODO because we might jump over same files, the dir might no longer
|
||||
# be empty and cannot remove dir like this.
|
||||
try:
|
||||
os.rmdir(ep.typeDir('parent_input'))
|
||||
except FileNotFoundError:
|
||||
logging.warning('Cannot remove ' + ep.typeDir('parent_input') + ', file no longer exists.')
|
||||
|
||||
# Remove from deluge client
|
||||
logging.info('Removing {} for deluge'.format(ep.parent))
|
||||
deluge = delugeCli.Deluge()
|
||||
response = deluge.remove(ep.parent)
|
||||
logging.info('Deluge response after delete: {}'.format(response))
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
abspath = os.path.abspath(__file__)
|
||||
dname = os.path.dirname(abspath)
|
||||
if (os.path.exists(os.path.join(dname, env.logfile))):
|
||||
logging.basicConfig(filename=env.logfile, level=logging.INFO)
|
||||
else:
|
||||
print('Logfile could not be found at ' + env.logfile + '. Verifiy presence or disable logging in config.')
|
||||
|
||||
moveStray(sys.argv[-1])
|
||||
318
app/pirateSearch.py
Executable file
318
app/pirateSearch.py
Executable file
@@ -0,0 +1,318 @@
|
||||
#!/usr/bin/env python3.6
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author: KevinMidboe
|
||||
# @Date: 2017-10-12 11:55:03
|
||||
# @Last Modified by: KevinMidboe
|
||||
# @Last Modified time: 2017-10-22 18:54:18
|
||||
|
||||
import sys, logging, re, json
|
||||
from urllib import parse, request
|
||||
from urllib.error import URLError
|
||||
from bs4 import BeautifulSoup
|
||||
from os import path
|
||||
|
||||
import datetime
|
||||
from pprint import pprint
|
||||
|
||||
from core import stringTime
|
||||
import env_variables as env
|
||||
logging.basicConfig(filename=path.dirname(__file__) + '/' + env.logfile, level=logging.INFO)
|
||||
|
||||
RELEASE_TYPES = ('bdremux', 'brremux', 'remux',
|
||||
'bdrip', 'brrip', 'blu-ray', 'bluray', 'bdmv', 'bdr', 'bd5',
|
||||
'web-cap', 'webcap', 'web cap',
|
||||
'webrip', 'web rip', 'web-rip', 'web',
|
||||
'webdl', 'web dl', 'web-dl', 'hdrip',
|
||||
'dsr', 'dsrip', 'satrip', 'dthrip', 'dvbrip', 'hdtv', 'pdtv', 'tvrip', 'hdtvrip',
|
||||
'dvdr', 'dvd-full', 'full-rip', 'iso',
|
||||
'ts', 'hdts', 'hdts', 'telesync', 'pdvd', 'predvdrip',
|
||||
'camrip', 'cam')
|
||||
|
||||
|
||||
def sanitize(string, ignore_characters=None, replace_characters=None):
|
||||
"""Sanitize a string to strip special characters.
|
||||
|
||||
:param str string: the string to sanitize.
|
||||
:param set ignore_characters: characters to ignore.
|
||||
:return: the sanitized string.
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
# only deal with strings
|
||||
if string is None:
|
||||
return
|
||||
|
||||
replace_characters = replace_characters or ''
|
||||
|
||||
ignore_characters = ignore_characters or set()
|
||||
|
||||
characters = ignore_characters
|
||||
if characters:
|
||||
string = re.sub(r'[%s]' % re.escape(''.join(characters)), replace_characters, string)
|
||||
|
||||
return string
|
||||
|
||||
def return_re_match(string, re_statement):
|
||||
if string is None:
|
||||
return
|
||||
|
||||
m = re.search(re_statement, string)
|
||||
if 'Y-day' in m.group():
|
||||
return datetime.timedelta(days=1).strftime('%m-%d %Y')
|
||||
|
||||
if 'Today' in m.group():
|
||||
return datetime.datetime.now().strftime('%m-%d %Y')
|
||||
return sanitize(m.group(), '\xa0', ' ')
|
||||
|
||||
|
||||
# Can maybe be moved away from this class
|
||||
# returns a number that is either the value of multiple_pages
|
||||
# or if it exceeds total_pages, return total_pages.
|
||||
def pagesToCount(multiple, total):
|
||||
if (multiple > total):
|
||||
return total
|
||||
return multiple
|
||||
|
||||
# Should maybe not be able to set values without checking if they are valid?
|
||||
class piratebay(object):
|
||||
def __init__(self, query=None, page=0, sort=None, category=None):
|
||||
# This should be moved to a config file
|
||||
self.url = 'https://thepiratebay.org/search'
|
||||
self.sortTypes = {
|
||||
'size': 5,
|
||||
'seed_count': 99
|
||||
}
|
||||
self.categoryTypes = {
|
||||
'movies': 207,
|
||||
'porn_movies': 505,
|
||||
}
|
||||
# - - -
|
||||
|
||||
# Req params
|
||||
self.query = query
|
||||
self.page = page
|
||||
self.sort = sort
|
||||
self.category = category
|
||||
self.total_pages = 0
|
||||
self.headers = {'User-Agent': 'Mozilla/5.0'}
|
||||
# self.headers = {}
|
||||
|
||||
def build_URL_request(self):
|
||||
url = '/'.join([self.url, parse.quote(self.query), str(self.page), str(self.sort), str(self.category)])
|
||||
return request.Request(url, headers=self.headers)
|
||||
|
||||
def next_page(self):
|
||||
# If page exceeds the max_page, return None
|
||||
# Can either save the last query/url in the object or have it passed
|
||||
# again on call to next_page
|
||||
|
||||
# Throw a error if it is not possible (overflow)
|
||||
self.page += 1
|
||||
raw_page = self.callPirateBaT()
|
||||
return self.parse_raw_page_for_torrents(raw_page)
|
||||
|
||||
def set_total_pages(self, raw_page):
|
||||
# body-id:searchResults-id:content-align:center
|
||||
soup = BeautifulSoup(raw_page, 'html.parser')
|
||||
content_searchResult = soup.body.find(id='SearchResults')
|
||||
page_div = content_searchResult.find_next(attrs={"align": "center"})
|
||||
|
||||
last_page = 0
|
||||
for page in page_div.find_all('a'):
|
||||
last_page += 1
|
||||
|
||||
self.total_pages = last_page
|
||||
|
||||
def callPirateBaT(self):
|
||||
req = self.build_URL_request()
|
||||
|
||||
raw_page = self.fetchURL(req).read()
|
||||
logging.info('Finished searching piratebay for query | %s' % stringTime())
|
||||
|
||||
if raw_page is None:
|
||||
raise ValueError('Search result returned no content. Please check log for error reason.')
|
||||
|
||||
if self.total_pages is 0:
|
||||
self.set_total_pages(raw_page)
|
||||
|
||||
return raw_page
|
||||
|
||||
|
||||
# Sets the search
|
||||
def search(self, query, multiple_pages=1, page=0, sort=None, category=None):
|
||||
# This should not be logged here, but in loop. Something else here maybe?
|
||||
logging.info('Searching piratebay with query: %r, sort: %s and category: %s | %s' %
|
||||
(query, sort, category, stringTime()))
|
||||
|
||||
if sort is not None and sort in self.sortTypes:
|
||||
self.sort = self.sortTypes[sort]
|
||||
else:
|
||||
raise ValueError('Invalid sort category for piratebay search')
|
||||
|
||||
# Verify input? and reset total_pages
|
||||
self.query = query
|
||||
|
||||
self.total_pages = 0
|
||||
|
||||
if str(page).isnumeric() and type(page) == int and page >= 0:
|
||||
self.page = page
|
||||
|
||||
# TODO add category list
|
||||
if category is not None and category in self.categoryTypes:
|
||||
self.category = self.categoryTypes[category]
|
||||
|
||||
# TODO Pull most of this logic out bc it needs to also be done in next_page
|
||||
|
||||
raw_page = self.callPirateBaT()
|
||||
torrents_found = self.parse_raw_page_for_torrents(raw_page)
|
||||
|
||||
# Fetch in parallel
|
||||
n = pagesToCount(multiple_pages, self.total_pages)
|
||||
while n > 1:
|
||||
torrents_found.extend(self.next_page())
|
||||
n -= 1
|
||||
|
||||
return torrents_found
|
||||
|
||||
|
||||
def removeHeader(self, bs4_element):
|
||||
if ('header' in bs4_element['class']):
|
||||
return bs4_element.find_next('tr')
|
||||
|
||||
return bs4_element
|
||||
|
||||
def has_magnet(self, href):
|
||||
return href and re.compile('magnet').search(href)
|
||||
|
||||
def parse_raw_page_for_torrents(self, content):
|
||||
soup = BeautifulSoup(content, 'html.parser')
|
||||
content_searchResult = soup.body.find(id='searchResult')
|
||||
|
||||
if content_searchResult is None:
|
||||
logging.info('No torrents found for the search criteria.')
|
||||
return None
|
||||
|
||||
listElements = content_searchResult.tr
|
||||
|
||||
torrentWrapper = self.removeHeader(listElements)
|
||||
|
||||
torrents_found = []
|
||||
for torrentElement in torrentWrapper.find_all_next('td'):
|
||||
if torrentElement.find_all("div", class_='detName'):
|
||||
|
||||
name = torrentElement.find('a', class_='detLink').get_text()
|
||||
url = torrentElement.find('a', class_='detLink')['href']
|
||||
magnet = torrentElement.find(href=self.has_magnet)
|
||||
|
||||
uploader = torrentElement.find('a', class_='detDesc')
|
||||
|
||||
if uploader is None:
|
||||
uploader = torrentElement.find('i')
|
||||
|
||||
uploader = uploader.get_text()
|
||||
|
||||
info_text = torrentElement.find('font', class_='detDesc').get_text()
|
||||
|
||||
date = return_re_match(info_text, r"(\d+\-\d+(\s\d{4})?)|(Y\-day|Today)")
|
||||
size = return_re_match(info_text, r"(\d+(\.\d+)?\s[a-zA-Z]+)")
|
||||
|
||||
# COULD NOT FIND HREF!
|
||||
if (magnet is None):
|
||||
continue
|
||||
|
||||
seed_and_leech = torrentElement.find_all_next(attrs={"align": "right"})
|
||||
seed = seed_and_leech[0].get_text()
|
||||
leech = seed_and_leech[1].get_text()
|
||||
|
||||
torrent = Torrent(name, magnet['href'], size, uploader, date, seed, leech, url)
|
||||
|
||||
torrents_found.append(torrent)
|
||||
else:
|
||||
# print(torrentElement)
|
||||
continue
|
||||
|
||||
logging.info('Found %s torrents for given search criteria.' % len(torrents_found))
|
||||
return torrents_found
|
||||
|
||||
|
||||
def fetchURL(self, req):
|
||||
try:
|
||||
response = request.urlopen(req)
|
||||
except URLError as e:
|
||||
if hasattr(e, 'reason'):
|
||||
logging.error('We failed to reach a server with request: %s' % req.full_url)
|
||||
logging.error('Reason: %s' % e.reason)
|
||||
elif hasattr(e, 'code'):
|
||||
logging.error('The server couldn\'t fulfill the request.')
|
||||
logging.error('Error code: ', e.code)
|
||||
else:
|
||||
return response
|
||||
|
||||
|
||||
class Torrent(object):
|
||||
def __init__(self, name, magnet=None, size=None, uploader=None, date=None,
|
||||
seed_count=None, leech_count=None, url=None):
|
||||
self.name = name
|
||||
self.magnet = magnet
|
||||
self.size = size
|
||||
self.uploader = uploader
|
||||
self.date = date
|
||||
self.seed_count = seed_count
|
||||
self.leech_count = leech_count
|
||||
self.url = url
|
||||
|
||||
def find_release_type(self):
|
||||
name = self.name.casefold()
|
||||
return [r_type for r_type in RELEASE_TYPES if r_type in name]
|
||||
|
||||
def get_all_attr(self):
|
||||
return ({'name': self.name, 'magnet': self.magnet,'uploader': self.uploader,'size': self.size,'date': self.date,'seed': self.seed_count,'leech': self.leech_count,'url': self.url})
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s [%r]>' % (self.__class__.__name__, self.name)
|
||||
|
||||
|
||||
# This should be done front_end!
|
||||
# I.E. filtering like this should be done in another script
|
||||
# and should be done with the shared standard for types.
|
||||
# PS: Is it the right move to use a shared standard? What
|
||||
# happens if it is no longer public?
|
||||
def chooseCandidate(torrent_list):
|
||||
interesting_torrents = []
|
||||
match_release_type = ['bdremux', 'brremux', 'remux', 'bdrip', 'brrip', 'blu-ray', 'bluray', 'bdmv', 'bdr', 'bd5']
|
||||
|
||||
for torrent in torrent_list:
|
||||
intersecting_release_types = set(torrent.find_release_type()) & set(match_release_type)
|
||||
|
||||
size, _, size_id = torrent.size.partition(' ')
|
||||
if intersecting_release_types and int(torrent.seed_count) > 0 and float(size) > 4 and size_id == 'GiB':
|
||||
# print('{} : {} : {} {}'.format(torrent.name, torrent.size, torrent.seed_count, torrent.magnet))
|
||||
interesting_torrents.append(torrent.get_all_attr())
|
||||
# else:
|
||||
# print('Denied match! %s : %s : %s' % (torrent.name, torrent.size, torrent.seed_count))
|
||||
|
||||
return interesting_torrents
|
||||
|
||||
|
||||
def searchTorrentSite(query, site='piratebay'):
|
||||
pirate = piratebay()
|
||||
torrents_found = pirate.search(query, page=0, multiple_pages=3, sort='size')
|
||||
candidates = {}
|
||||
if (torrents_found):
|
||||
candidates = chooseCandidate(torrents_found)
|
||||
print(json.dumps(candidates))
|
||||
|
||||
# torrents_found = pirate.next_page()
|
||||
# pprint(torrents_found)
|
||||
# candidates = chooseCandidate(torrents_found)
|
||||
|
||||
# Can autocall to next_page in a looped way to get more if nothing is found
|
||||
# and there is more pages to be looked at
|
||||
|
||||
|
||||
def main():
|
||||
query = sys.argv[1]
|
||||
searchTorrentSite(query)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
57
app/seasonMover.py
Executable file
57
app/seasonMover.py
Executable file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author: KevinMidboe
|
||||
# @Date: 2017-07-11 19:16:23
|
||||
# @Last Modified by: KevinMidboe
|
||||
# @Last Modified time: 2017-07-11 19:16:23
|
||||
|
||||
import fire, re, os
|
||||
|
||||
class seasonMover(object):
|
||||
''' Moving multiple files to multiple folders with
|
||||
identifer '''
|
||||
workingDir = os.getcwd()
|
||||
|
||||
def create(self, name, interval):
|
||||
pass
|
||||
|
||||
def move(self, fileSyntax, folderName):
|
||||
episodeRange = self.findInterval(fileSyntax)
|
||||
|
||||
self.motherMover(fileSyntax, folderName, episodeRange)
|
||||
|
||||
def findInterval(self, item):
|
||||
if (re.search(r'\((.*)\)', item) is None):
|
||||
raise ValueError('Need to declare an identifier e.g. (1..3) in: \n\t' + item)
|
||||
|
||||
start = int(re.search('\((\d+)\.\.', item).group(1))
|
||||
end = int(re.search('\.\.(\d+)\)', item).group(1))
|
||||
|
||||
return list(range(start, end+1))
|
||||
|
||||
def removeUploadSign(self, file):
|
||||
match = re.search('-[a-zA-Z\[\]\-]*.[a-z]{3}', file)
|
||||
if match:
|
||||
uploader = match.group(0)[:-4]
|
||||
return re.sub(uploader, '', file)
|
||||
|
||||
return file
|
||||
|
||||
def motherMover(self, fileSyntax, folderName, episodeRange):
|
||||
# Call for sub of fileList
|
||||
# TODO check if range is same as folderContent
|
||||
for episode in episodeRange:
|
||||
leadingZeroNumber = "%02d" % episode
|
||||
fileName = re.sub(r'\((.*)\)', leadingZeroNumber, fileSyntax)
|
||||
|
||||
oldPath = os.path.join(self.workingDir,fileName)
|
||||
newFolder = os.path.join(self.workingDir, folderName + leadingZeroNumber)
|
||||
newPath = os.path.join(newFolder, self.removeUploadSign(fileName))
|
||||
|
||||
os.makedirs(newFolder)
|
||||
os.rename(oldPath, newPath)
|
||||
# print(newFolder)
|
||||
# print(oldPath + ' --> ' + newPath)
|
||||
|
||||
if __name__ == '__main__':
|
||||
fire.Fire(seasonMover)
|
||||
111
app/subtitle.py
Normal file
111
app/subtitle.py
Normal file
@@ -0,0 +1,111 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import codecs
|
||||
import logging
|
||||
import os
|
||||
|
||||
import chardet
|
||||
import hashlib
|
||||
|
||||
from video import Episode, Movie
|
||||
from utils import sanitize
|
||||
|
||||
from langdetect import detect
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
#: Subtitle extensions
|
||||
SUBTITLE_EXTENSIONS = ('.srt', '.sub')
|
||||
|
||||
|
||||
class Subtitle(object):
|
||||
"""Base class for subtitle.
|
||||
|
||||
:param language: language of the subtitle.
|
||||
:type language: :class:`~babelfish.language.Language`
|
||||
:param bool hearing_impaired: whether or not the subtitle is hearing impaired.
|
||||
:param page_link: URL of the web page from which the subtitle can be downloaded.
|
||||
:type page_link: str
|
||||
:param encoding: Text encoding of the subtitle.
|
||||
:type encoding: str
|
||||
|
||||
"""
|
||||
#: Name of the provider that returns that class of subtitle
|
||||
provider_name = ''
|
||||
|
||||
def __init__(self, name, parent_path, series, season, episode, language=None, hash=None, container=None, format=None, sdh=False):
|
||||
#: Language of the subtitle
|
||||
|
||||
self.name = name
|
||||
|
||||
self.parent_path = parent_path
|
||||
|
||||
self.series = series
|
||||
|
||||
self.season = season
|
||||
|
||||
self.episode = episode
|
||||
|
||||
self.language=language
|
||||
|
||||
self.hash = hash
|
||||
|
||||
self.container = container
|
||||
|
||||
self.format = format
|
||||
|
||||
self.sdh = sdh
|
||||
|
||||
@classmethod
|
||||
def fromguess(cls, name, parent_path, guess):
|
||||
if not (guess['type'] == 'movie' or guess['type'] == 'episode'):
|
||||
raise ValueError('The guess must be an episode guess')
|
||||
|
||||
if 'title' not in guess:
|
||||
raise ValueError('Insufficient data to process the guess')
|
||||
|
||||
sdh = 'sdh' in name.lower()
|
||||
|
||||
if guess['type'] is 'episode':
|
||||
return cls(name, parent_path, guess.get('title', 1), guess.get('season'), guess['episode'],
|
||||
container=guess.get('container'), format=guess.get('format'), sdh=sdh)
|
||||
elif guess['type'] is 'movie':
|
||||
return cls(name, parent_path, guess.get('title', 1), container=guess.get('container'),
|
||||
format=guess.get('format'), sdh=sdh)
|
||||
|
||||
|
||||
def getLanguage(self):
|
||||
f = open(os.path.join(self.parent_path, self.name), 'r', encoding='ISO-8859-15')
|
||||
language = detect(f.read())
|
||||
f.close()
|
||||
|
||||
return language
|
||||
|
||||
def __hash__(self):
|
||||
return hashlib.md5("b'{}'".format(str(self.series) + str(self.season) + str(self.episode)).encode()).hexdigest()
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s %s [%sx%s]>' % (self.__class__.__name__, self.series, self.season, str(self.episode))
|
||||
|
||||
|
||||
|
||||
def get_subtitle_path(subtitles_path, language=None, extension='.srt'):
|
||||
"""Get the subtitle path using the `subtitles_path` and `language`.
|
||||
|
||||
:param str subtitles_path: path to the subtitle.
|
||||
:param language: language of the subtitle to put in the path.
|
||||
:type language: :class:`~babelfish.language.Language`
|
||||
:param str extension: extension of the subtitle.
|
||||
:return: path of the subtitle.
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
subtitle_root = os.path.splitext(subtitles_path)[0]
|
||||
|
||||
if language:
|
||||
subtitle_root += '.' + str(language)
|
||||
|
||||
return subtitle_root + extension
|
||||
|
||||
|
||||
|
||||
38
app/utils.py
Normal file
38
app/utils.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
|
||||
def sanitize(string, ignore_characters=None):
|
||||
"""Sanitize a string to strip special characters.
|
||||
|
||||
:param str string: the string to sanitize.
|
||||
:param set ignore_characters: characters to ignore.
|
||||
:return: the sanitized string.
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
# only deal with strings
|
||||
if string is None:
|
||||
return
|
||||
|
||||
ignore_characters = ignore_characters or set()
|
||||
|
||||
# replace some characters with one space
|
||||
# characters = {'-', ':', '(', ')', '.'} - ignore_characters
|
||||
# if characters:
|
||||
# string = re.sub(r'[%s]' % re.escape(''.join(characters)), ' ', string)
|
||||
|
||||
# remove some characters
|
||||
characters = {'\''} - ignore_characters
|
||||
if characters:
|
||||
string = re.sub(r'[%s]' % re.escape(''.join(characters)), '', string)
|
||||
|
||||
# replace multiple spaces with one
|
||||
string = re.sub(r'\s+', ' ', string)
|
||||
|
||||
# strip and lower case
|
||||
return string.strip().lower()
|
||||
|
||||
233
app/video.py
Normal file
233
app/video.py
Normal file
@@ -0,0 +1,233 @@
|
||||
#!/usr/bin/env python3.6
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author: KevinMidboe
|
||||
# @Date: 2017-08-26 08:23:18
|
||||
# @Last Modified by: KevinMidboe
|
||||
# @Last Modified time: 2017-09-29 13:56:21
|
||||
|
||||
from guessit import guessit
|
||||
import os
|
||||
import hashlib, tvdb_api
|
||||
|
||||
#: Video extensions
|
||||
VIDEO_EXTENSIONS = ('.3g2', '.3gp', '.3gp2', '.3gpp', '.60d', '.ajp', '.asf', '.asx', '.avchd', '.avi', '.bik',
|
||||
'.bix', '.box', '.cam', '.dat', '.divx', '.dmf', '.dv', '.dvr-ms', '.evo', '.flc', '.fli',
|
||||
'.flic', '.flv', '.flx', '.gvi', '.gvp', '.h264', '.m1v', '.m2p', '.m2ts', '.m2v', '.m4e',
|
||||
'.m4v', '.mjp', '.mjpeg', '.mjpg', '.mkv', '.moov', '.mov', '.movhd', '.movie', '.movx', '.mp4',
|
||||
'.mpe', '.mpeg', '.mpg', '.mpv', '.mpv2', '.mxf', '.nsv', '.nut', '.ogg', '.ogm' '.ogv', '.omf',
|
||||
'.ps', '.qt', '.ram', '.rm', '.rmvb', '.swf', '.ts', '.vfw', '.vid', '.video', '.viv', '.vivo',
|
||||
'.vob', '.vro', '.wm', '.wmv', '.wmx', '.wrap', '.wvx', '.wx', '.x264', '.xvid')
|
||||
|
||||
class Video(object):
|
||||
"""Base class for videos.
|
||||
Represent a video, existing or not.
|
||||
:param str name: name or path of the video.
|
||||
:param str format: format of the video (HDTV, WEB-DL, BluRay, ...).
|
||||
:param str release_group: release group of the video.
|
||||
:param str resolution: resolution of the video stream (480p, 720p, 1080p or 1080i).
|
||||
:param str video_codec: codec of the video stream.
|
||||
:param str audio_codec: codec of the main audio stream.
|
||||
:param str imdb_id: IMDb id of the video.
|
||||
:param dict hashes: hashes of the video file by provider names.
|
||||
:param int size: size of the video file in bytes.
|
||||
:param set subtitle_languages: existing subtitle languages.
|
||||
"""
|
||||
def __init__(self, name, format=None, release_group=None, resolution=None, video_codec=None, audio_codec=None,
|
||||
imdb_id=None, hashes=None, size=None, subtitle_languages=None):
|
||||
#: Name or path of the video
|
||||
self.name = name
|
||||
|
||||
#: Format of the video (HDTV, WEB-DL, BluRay, ...)
|
||||
self.format = format
|
||||
|
||||
#: Release group of the video
|
||||
self.release_group = release_group
|
||||
|
||||
#: Resolution of the video stream (480p, 720p, 1080p or 1080i)
|
||||
self.resolution = resolution
|
||||
|
||||
#: Codec of the video stream
|
||||
self.video_codec = video_codec
|
||||
|
||||
#: Codec of the main audio stream
|
||||
self.audio_codec = audio_codec
|
||||
|
||||
#: IMDb id of the video
|
||||
self.imdb_id = imdb_id
|
||||
|
||||
#: Hashes of the video file by provider names
|
||||
self.hashes = hashes or {}
|
||||
|
||||
#: Size of the video file in bytes
|
||||
self.size = size
|
||||
|
||||
#: Existing subtitle languages
|
||||
self.subtitle_languages = subtitle_languages or set()
|
||||
|
||||
@property
|
||||
def exists(self):
|
||||
"""Test whether the video exists"""
|
||||
return os.path.exists(self.name)
|
||||
|
||||
@property
|
||||
def age(self):
|
||||
"""Age of the video"""
|
||||
if self.exists:
|
||||
return datetime.utcnow() - datetime.utcfromtimestamp(os.path.getmtime(self.name))
|
||||
|
||||
return timedelta()
|
||||
|
||||
@classmethod
|
||||
def fromguess(cls, name, parent_path, guess):
|
||||
"""Create an :class:`Episode` or a :class:`Movie` with the given `name` based on the `guess`.
|
||||
:param str name: name of the video.
|
||||
:param dict guess: guessed data.
|
||||
:raise: :class:`ValueError` if the `type` of the `guess` is invalid
|
||||
"""
|
||||
if guess['type'] == 'episode':
|
||||
return Episode.fromguess(name, parent_path, guess)
|
||||
|
||||
if guess['type'] == 'movie':
|
||||
return Movie.fromguess(name, guess)
|
||||
|
||||
raise ValueError('The guess must be an episode or a movie guess')
|
||||
|
||||
@classmethod
|
||||
def fromname(cls, name):
|
||||
"""Shortcut for :meth:`fromguess` with a `guess` guessed from the `name`.
|
||||
:param str name: name of the video.
|
||||
"""
|
||||
return cls.fromguess(name, guessit(name))
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s [%r]>' % (self.__class__.__name__, self.name)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.name)
|
||||
|
||||
|
||||
class Episode():
|
||||
"""Episode :class:`Video`.
|
||||
:param str series: series of the episode.
|
||||
:param int season: season number of the episode.
|
||||
:param int episode: episode number of the episode.
|
||||
:param str title: title of the episode.
|
||||
:param int year: year of the series.
|
||||
:param bool original_series: whether the series is the first with this name.
|
||||
:param int tvdb_id: TVDB id of the episode.
|
||||
:param \*\*kwargs: additional parameters for the :class:`Video` constructor.
|
||||
"""
|
||||
def __init__(self, name, parent_path, series, season, episode, year=None, original_series=True, tvdb_id=None,
|
||||
series_tvdb_id=None, series_imdb_id=None, release_group=None, video_codec=None, container=None,
|
||||
format=None, screen_size=None, **kwargs):
|
||||
super(Episode, self).__init__()
|
||||
|
||||
self.name = name
|
||||
|
||||
self.parent_path = parent_path
|
||||
|
||||
#: Series of the episode
|
||||
self.series = series
|
||||
|
||||
#: Season number of the episode
|
||||
self.season = season
|
||||
|
||||
#: Episode number of the episode
|
||||
self.episode = episode
|
||||
|
||||
#: Year of series
|
||||
self.year = year
|
||||
|
||||
#: The series is the first with this name
|
||||
self.original_series = original_series
|
||||
|
||||
#: TVDB id of the episode
|
||||
self.tvdb_id = tvdb_id
|
||||
|
||||
#: TVDB id of the series
|
||||
self.series_tvdb_id = series_tvdb_id
|
||||
|
||||
#: IMDb id of the series
|
||||
self.series_imdb_id = series_imdb_id
|
||||
|
||||
# The release group of the episode
|
||||
self.release_group = release_group
|
||||
|
||||
# The video vodec of the series
|
||||
self.video_codec = video_codec
|
||||
|
||||
# The Video container of the episode
|
||||
self.container = container
|
||||
|
||||
# The Video format of the episode
|
||||
self.format = format
|
||||
|
||||
# The Video screen_size of the episode
|
||||
self.screen_size = screen_size
|
||||
|
||||
@classmethod
|
||||
def fromguess(cls, name, parent_path, guess):
|
||||
if guess['type'] != 'episode':
|
||||
raise ValueError('The guess must be an episode guess')
|
||||
|
||||
if 'title' not in guess or 'episode' not in guess:
|
||||
raise ValueError('Insufficient data to process the guess')
|
||||
|
||||
return cls(name, parent_path, guess['title'], guess.get('season', 1), guess['episode'],
|
||||
year=guess.get('year'), original_series='year' not in guess, release_group=guess.get('release_group'),
|
||||
video_codec=guess.get('video_codec'), audio_codec=guess.get('audio_codec'), container=guess.get('container'),
|
||||
format=guess.get('format'), screen_size=guess.get('screen_size'))
|
||||
|
||||
@classmethod
|
||||
def fromname(cls, name):
|
||||
return cls.fromguess(name, guessit(name, {'type': 'episode'}))
|
||||
|
||||
def __hash__(self):
|
||||
return hashlib.md5("b'{}'".format(str(self.series) + str(self.season) + str(self.episode)).encode()).hexdigest()
|
||||
|
||||
# THE EP NUMBER IS CONVERTED TO STRING AS A QUICK FIX FOR MULTIPLE NUMBERS IN ONE
|
||||
def __repr__(self):
|
||||
if self.year is None:
|
||||
return '<%s [%r, %sx%s]>' % (self.__class__.__name__, self.series, self.season, str(self.episode))
|
||||
|
||||
return '<%s [%r, %d, %sx%s]>' % (self.__class__.__name__, self.series, self.year, self.season, str(self.episode))
|
||||
|
||||
|
||||
|
||||
class Movie():
|
||||
"""Movie :class:`Video`.
|
||||
:param str title: title of the movie.
|
||||
:param int year: year of the movie.
|
||||
:param \*\*kwargs: additional parameters for the :class:`Video` constructor.
|
||||
"""
|
||||
def __init__(self, name, title, year=None, format=None, **kwargs):
|
||||
super(Movie, self).__init__()
|
||||
|
||||
#: Title of the movie
|
||||
self.title = title
|
||||
|
||||
#: Year of the movie
|
||||
self.year = year
|
||||
self.format = format
|
||||
|
||||
@classmethod
|
||||
def fromguess(cls, name, guess):
|
||||
if guess['type'] != 'movie':
|
||||
raise ValueError('The guess must be a movie guess')
|
||||
|
||||
if 'title' not in guess:
|
||||
raise ValueError('Insufficient data to process the guess')
|
||||
|
||||
return cls(name, guess['title'], format=guess.get('format'), release_group=guess.get('release_group'),
|
||||
resolution=guess.get('screen_size'), video_codec=guess.get('video_codec'),
|
||||
audio_codec=guess.get('audio_codec'), year=guess.get('year'))
|
||||
|
||||
@classmethod
|
||||
def fromname(cls, name):
|
||||
return cls.fromguess(name, guessit(name, {'type': 'movie'}))
|
||||
|
||||
def __repr__(self):
|
||||
if self.year is None:
|
||||
return '<%s [%r]>' % (self.__class__.__name__, self.title)
|
||||
|
||||
return '<%s [%r, %d]>' % (self.__class__.__name__, self.title, self.year)
|
||||
58
client/.gitignore
vendored
Normal file
58
client/.gitignore
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
BIN
client/app/DIN-Regular-webfont.woff
Normal file
BIN
client/app/DIN-Regular-webfont.woff
Normal file
Binary file not shown.
25
client/app/Root.jsx
Normal file
25
client/app/Root.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React, { Component } from 'react';
|
||||
import { HashRouter as Router, Route, Switch, IndexRoute } from 'react-router-dom';
|
||||
|
||||
import SearchRequest from './components/SearchRequest.jsx';
|
||||
import AdminComponent from './components/admin/Admin.jsx';
|
||||
|
||||
class Root extends Component {
|
||||
|
||||
// We need to provide a list of routes
|
||||
// for our app, and in this case we are
|
||||
// doing so from a Root component
|
||||
render() {
|
||||
return (
|
||||
<Router>
|
||||
<Switch>
|
||||
<Route exact path='/' component={SearchRequest} />
|
||||
<Route path='/admin/:request' component={AdminComponent} />
|
||||
<Route path='/admin' component={AdminComponent} />
|
||||
</Switch>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Root;
|
||||
41
client/app/app.scss
Normal file
41
client/app/app.scss
Normal file
@@ -0,0 +1,41 @@
|
||||
@font-face {
|
||||
font-family: "din";
|
||||
src: url('/app/DIN-Regular-webfont.woff')
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: 'din', 'Open Sans', sans-serif;
|
||||
display: inline-block;
|
||||
color:red;
|
||||
}
|
||||
|
||||
#requestMovieList {
|
||||
display: flex;
|
||||
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.movie_wrapper {
|
||||
color:red;
|
||||
display: flex;
|
||||
align-content: center;
|
||||
|
||||
width: 30%;
|
||||
background-color: #ffffff;
|
||||
height: 231px;
|
||||
|
||||
margin: 20px;
|
||||
|
||||
-webkit-box-shadow: 0px 0px 5px 1px rgba(0,0,0,0.15);
|
||||
-moz-box-shadow: 0px 0px 5px 1px rgba(0,0,0,0.15);
|
||||
box-shadow: 0px 0px 5px 1px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.movie_content {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.movie_header {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
./app/components/App.jsx
|
||||
|
||||
<FetchData url={"https://apollo.kevinmidboe.com/api/v1/plex/playing"} />
|
||||
*/
|
||||
import React from 'react';
|
||||
import FetchData from './FetchData.js';
|
||||
import ListStrays from './ListStrays.jsx'
|
||||
|
||||
export default class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<h1>Welcome to Seasoned</h1>
|
||||
</div>
|
||||
<ListStrays />
|
||||
|
||||
<FetchData />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
26
client/app/components/Cookie.jsx
Normal file
26
client/app/components/Cookie.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
|
||||
export function getCookie(cname) {
|
||||
var name = cname + "=";
|
||||
var decodedCookie = decodeURIComponent(document.cookie);
|
||||
var ca = decodedCookie.split(';');
|
||||
for(var i = 0; i <ca.length; i++) {
|
||||
var c = ca[i];
|
||||
while (c.charAt(0) == ' ') {
|
||||
c = c.substring(1);
|
||||
}
|
||||
if (c.indexOf(name) == 0) {
|
||||
return c.substring(name.length, c.length);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function setCookie(cname, cvalue, exdays) {
|
||||
var d = new Date();
|
||||
d.setTime(d.getTime() + (exdays*24*60*60*1000));
|
||||
var expires = "expires="+ d.toUTCString();
|
||||
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
|
||||
}
|
||||
@@ -4,7 +4,7 @@ class FetchData extends React.Component {
|
||||
constructor(props){
|
||||
super(props)
|
||||
this.state = {
|
||||
imgUrls: [],
|
||||
playing: [],
|
||||
hei: '1',
|
||||
intervalId: null,
|
||||
url: ''
|
||||
@@ -16,11 +16,9 @@ class FetchData extends React.Component {
|
||||
fetch("https://apollo.kevinmidboe.com/api/v1/plex/playing").then(
|
||||
function(response){
|
||||
response.json().then(function(data){
|
||||
console.log(data.size);
|
||||
that.setState({
|
||||
imgUrls: that.state.imgUrls.concat(data.video)
|
||||
playing: that.state.playing.concat(data.video)
|
||||
})
|
||||
console.log(data.video.title);
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -32,23 +30,30 @@ class FetchData extends React.Component {
|
||||
}
|
||||
|
||||
getPlaying() {
|
||||
console.log('Should not reach')
|
||||
// Need callback to work
|
||||
// Should try to clear out old requests to limit mem use
|
||||
if (this.state.playing.length != 0) {
|
||||
return this.state.playing.map((playingObj) => {
|
||||
if (playingObj.type === 'episode') {
|
||||
console.log('episode')
|
||||
return ([
|
||||
<span>{playingObj.title}</span>,
|
||||
<span>{playingObj.season}</span>,
|
||||
<span>{playingObj.episode}</span>
|
||||
])
|
||||
} else if (playingObj.type === 'movie') {
|
||||
console.log('movie')
|
||||
return ([
|
||||
<span>{playingObj.title}</span>
|
||||
])
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return (<span>Nothing playing</span>)
|
||||
}
|
||||
}
|
||||
|
||||
render(){
|
||||
return(
|
||||
<div className="FetchData">
|
||||
{this.state.imgUrls.map((imgObj) => {
|
||||
return ([
|
||||
<span>{imgObj.title}</span>,
|
||||
<span>{imgObj.season}</span>,
|
||||
<span>{imgObj.episode}</span>,
|
||||
]);
|
||||
})}
|
||||
|
||||
</div>
|
||||
<div className="FetchData">{this.getPlaying()}</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
266
client/app/components/FetchRequested.jsx
Normal file
266
client/app/components/FetchRequested.jsx
Normal file
@@ -0,0 +1,266 @@
|
||||
import React from 'react';
|
||||
|
||||
import requestElement from './styles/requestElementStyle.jsx'
|
||||
|
||||
import { getCookie } from './Cookie.jsx';
|
||||
|
||||
class DropdownList extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
filter: ['all', 'requested', 'downloading', 'downloaded'],
|
||||
sort: ['requested_date', 'name', 'status', 'requested_by', 'ip', 'user_agent'],
|
||||
status: ['requested', 'downloading', 'downloaded'],
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {elementType, elementId, elementStatus, elementCallback, props} = this.props;
|
||||
|
||||
console.log(elementCallback('downloaded'))
|
||||
|
||||
switch (elementType) {
|
||||
case 'status':
|
||||
return (
|
||||
<div>HERE</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div {...props}>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RequestElement extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
dropDownState: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
filterRequestList(requestList, filter) {
|
||||
if (filter === 'all')
|
||||
return requestList
|
||||
|
||||
if (filter === 'movie' || filter === 'show')
|
||||
return requestList.filter(item => item.type === filter)
|
||||
return requestList.filter(item => item.status === filter)
|
||||
}
|
||||
|
||||
sortRequestList(requestList, sort_type, reversed) {
|
||||
requestList.sort(function(a,b) {
|
||||
if(a[sort_type] < b[sort_type]) return -1;
|
||||
if(a[sort_type] > b[sort_type]) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (reversed)
|
||||
requestList.reverse();
|
||||
}
|
||||
|
||||
userAgent(agent) {
|
||||
if (agent) {
|
||||
try {
|
||||
return agent.split(" ")[1].replace(/[\(\;]/g, '');
|
||||
}
|
||||
catch(e) {
|
||||
return agent;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
updateDropDownState(status) {
|
||||
if (status !== this.dropDownState) {
|
||||
this.dropDownState = status;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ItemsStatusDropdown(id, type, status) {
|
||||
return (
|
||||
<div>
|
||||
<select id="lang"
|
||||
defaultValue={status}
|
||||
onChange={event => this.updateDropDownState(event.target.value)}
|
||||
>
|
||||
<option value='requested'>Requested</option>
|
||||
<option value='downloading'>Downloading</option>
|
||||
<option value='downloaded'>Downloaded</option>
|
||||
</select>
|
||||
|
||||
<button onClick={() => { this.updateRequestedItem(id, type)}}>Update Status</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
updateRequestedItem(id, type) {
|
||||
console.log(id, type, this.dropDownState);
|
||||
Promise.resolve()
|
||||
fetch('https://apollo.kevinmidboe.com/api/v1/plex/request/' + id, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
'authorization': getCookie('token')
|
||||
},
|
||||
body: JSON.stringify({
|
||||
type: type,
|
||||
status: this.dropDownState,
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status !== 200) {
|
||||
console.log('error');
|
||||
}
|
||||
|
||||
response.json()
|
||||
.then(data => {
|
||||
if (data.success === true) {
|
||||
console.log('UPDATED :', id, ' with ', this.dropDownState)
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
new Error(error);
|
||||
})
|
||||
}
|
||||
|
||||
createHTMLElement(data, index) {
|
||||
var posterPath = 'https://image.tmdb.org/t/p/w300' + data.image_path;
|
||||
|
||||
return (
|
||||
<div style={requestElement.wrappingDiv} key={index}>
|
||||
<img style={requestElement.requestPoster} src={posterPath}></img>
|
||||
<div style={requestElement.infoDiv}>
|
||||
<span><b>Name</b>: {data.name} </span><br></br>
|
||||
<span><b>Year</b>: {data.year}</span><br></br>
|
||||
<span><b>Type</b>: {data.type}</span><br></br>
|
||||
<span><b>Status</b>: {data.status}</span><br></br>
|
||||
<span><b>Address</b>: {data.ip}</span><br></br>
|
||||
<span><b>Requested Data:</b> {data.requested_date}</span><br></br>
|
||||
<span><b>Requested By:</b> {data.requested_by}</span><br></br>
|
||||
<span><b>Agent</b>: { this.userAgent(data.user_agent) }</span><br></br>
|
||||
</div>
|
||||
|
||||
{ this.ItemsStatusDropdown(data.id, data.type, data.status) }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {requestedElementsList, requestedElementsFilter, requestedElementsSort, props} = this.props;
|
||||
|
||||
var filteredRequestedList = this.filterRequestList(requestedElementsList, requestedElementsFilter)
|
||||
|
||||
this.sortRequestList(filteredRequestedList, requestedElementsSort.value, requestedElementsSort.reversed)
|
||||
|
||||
return (
|
||||
<div {...props} style={requestElement.bodyDiv}>
|
||||
{filteredRequestedList.map((requestItem, index) => this.createHTMLElement(requestItem, index))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class FetchRequested extends React.Component {
|
||||
constructor(props){
|
||||
super(props)
|
||||
this.state = {
|
||||
requested_objects: [],
|
||||
filter: 'all',
|
||||
sort: {
|
||||
value: 'requested_date',
|
||||
reversed: false
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
Promise.resolve()
|
||||
fetch('https://apollo.kevinmidboe.com/api/v1/plex/requests/all', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
'authorization': getCookie('token')
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status !== 200) {
|
||||
console.log('error');
|
||||
}
|
||||
|
||||
response.json()
|
||||
.then(data => {
|
||||
if (data.success === true) {
|
||||
this.setState({
|
||||
requested_objects: data.requestedItems
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(error => {
|
||||
new Error(error);
|
||||
})
|
||||
}
|
||||
|
||||
changeFilter(filter) {
|
||||
this.setState({
|
||||
filter: filter
|
||||
})
|
||||
}
|
||||
|
||||
updateSort(sort=null, reverse=false) {
|
||||
if (sort) {
|
||||
this.setState({
|
||||
sort: { value: sort, reversed: reverse }
|
||||
})
|
||||
}
|
||||
else {
|
||||
this.setState({
|
||||
sort: { value: this.state.sort.value, reversed: reverse }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
render(){
|
||||
return(
|
||||
<div>
|
||||
<select id="lang" onChange={event => this.changeFilter(event.target.value)} value={this.state.value}>
|
||||
<option value="all">All</option>
|
||||
<option value="requested">Requested</option>
|
||||
<option value="downloading">Downloading</option>
|
||||
<option value="downloaded">Downloaded</option>
|
||||
<option value='movie'>Movies</option>
|
||||
<option value='show'>Shows</option>
|
||||
</select>
|
||||
|
||||
<select id="lang" onChange={event => this.updateSort(event.target.value)} value={this.state.value}>
|
||||
<option value='requested_date'>Date</option>
|
||||
<option value='name'>Title</option>
|
||||
<option value='status'>Status</option>
|
||||
<option value='requested_by'>Requested By</option>
|
||||
<option value='ip'>Address</option>
|
||||
<option value='user_agent'>Agent</option>
|
||||
</select>
|
||||
|
||||
<button onClick={() => {this.updateSort(null, !this.state.sort.reversed)}}>Reverse</button>
|
||||
|
||||
<RequestElement
|
||||
requestedElementsList={this.state.requested_objects}
|
||||
requestedElementsFilter={this.state.filter}
|
||||
requestedElementsSort={this.state.sort}
|
||||
/>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default FetchRequested;
|
||||
11
client/app/components/Header.jsx
Normal file
11
client/app/components/Header.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
// The Header creates links that can be used to navigate
|
||||
// between routes.
|
||||
const Header = () => (
|
||||
<header>
|
||||
</header>
|
||||
)
|
||||
|
||||
export default Header
|
||||
@@ -29,7 +29,6 @@ class ListStrays extends React.Component {
|
||||
{this.state.strays.map((strayObj) => {
|
||||
if (strayObj.verified == 0) {
|
||||
var url = "https://kevinmidboe.com/seasoned/verified.html?id=" + strayObj.id;
|
||||
console.log(url);
|
||||
return ([
|
||||
<span key={strayObj.id}>{strayObj.name}</span>,
|
||||
<a href={url}>{strayObj.id}</a>
|
||||
|
||||
10
client/app/components/NotFound.js
Normal file
10
client/app/components/NotFound.js
Normal file
@@ -0,0 +1,10 @@
|
||||
// components/NotFound.js
|
||||
import React from 'react';
|
||||
|
||||
const NotFound = () =>
|
||||
<div>
|
||||
<h3>404 page not found</h3>
|
||||
<p>We are sorry but the page you are looking for does not exist.</p>
|
||||
</div>
|
||||
|
||||
export default NotFound;
|
||||
126
client/app/components/SearchObject.jsx
Normal file
126
client/app/components/SearchObject.jsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import React from 'react';
|
||||
|
||||
import Notifications, {notify} from 'react-notify-toast';
|
||||
|
||||
// StyleComponents
|
||||
import searchObjectCSS from './styles/searchObject.jsx';
|
||||
import buttonsCSS from './styles/buttons.jsx';
|
||||
import InfoButton from './buttons/InfoButton.jsx';
|
||||
|
||||
var MediaQuery = require('react-responsive');
|
||||
|
||||
import { fetchJSON } from './http.jsx';
|
||||
|
||||
import Interactive from 'react-interactive';
|
||||
|
||||
|
||||
class SearchObject {
|
||||
constructor(object) {
|
||||
this.id = object.id;
|
||||
this.title = object.title;
|
||||
this.year = object.year;
|
||||
this.type = object.type;
|
||||
this.rating = object.rating;
|
||||
this.poster = object.poster_path;
|
||||
this.background = object.background_path;
|
||||
this.matchedInPlex = object.matchedInPlex;
|
||||
this.summary = object.summary;
|
||||
}
|
||||
|
||||
requestExisting(movie) {
|
||||
console.log('Exists', movie);
|
||||
}
|
||||
|
||||
requestMovie() {
|
||||
fetchJSON('https://apollo.kevinmidboe.com/api/v1/plex/request/' + this.id + '?type='+this.type, 'POST')
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
notify.show(this.title + ' requested!', 'success', 3000);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error('Request movie fetch went wrong: '+ e);
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
getElement(index) {
|
||||
const element_key = index + this.id;
|
||||
|
||||
if (this.poster == null || this.poster == undefined) {
|
||||
var posterPath = 'https://openclipart.org/image/2400px/svg_to_png/211479/Simple-Image-Not-Found-Icon.png'
|
||||
} else {
|
||||
var posterPath = 'https://image.tmdb.org/t/p/w185' + this.poster;
|
||||
}
|
||||
var backgroundPath = 'https://image.tmdb.org/t/p/w640_and_h360_bestv2/' + this.background;
|
||||
|
||||
var foundInPlex;
|
||||
if (this.matchedInPlex) {
|
||||
foundInPlex = <Interactive
|
||||
as='button'
|
||||
onClick={() => {this.requestExisting(this)}}
|
||||
style={buttonsCSS.submit}
|
||||
focus={buttonsCSS.submit_hover}
|
||||
hover={buttonsCSS.submit_hover}>
|
||||
|
||||
<span>Request Anyway</span>
|
||||
</Interactive>;
|
||||
} else {
|
||||
foundInPlex = <Interactive
|
||||
as='button'
|
||||
onClick={() => {this.requestMovie()}}
|
||||
style={buttonsCSS.submit}
|
||||
focus={buttonsCSS.submit_hover}
|
||||
hover={buttonsCSS.submit_hover}>
|
||||
|
||||
<span>+ Request</span>
|
||||
</Interactive>;
|
||||
}
|
||||
|
||||
// TODO go away from using mediaQuery, and create custom resizer
|
||||
return (
|
||||
<div key={element_key}>
|
||||
<Notifications />
|
||||
|
||||
<div style={searchObjectCSS.container} key={this.id}>
|
||||
<MediaQuery minWidth={600}>
|
||||
<div style={searchObjectCSS.posterContainer}>
|
||||
<img style={searchObjectCSS.posterImage} id='poster' src={posterPath}></img>
|
||||
</div>
|
||||
<span style={searchObjectCSS.title_large}>{this.title}</span>
|
||||
<br></br>
|
||||
<span style={searchObjectCSS.stats_large}>
|
||||
Released: { this.year } | Rating: {this.rating} | Type: {this.type}
|
||||
</span>
|
||||
<br></br>
|
||||
<span style={searchObjectCSS.summary}>{this.summary}</span>
|
||||
<br></br>
|
||||
</MediaQuery>
|
||||
|
||||
<MediaQuery maxWidth={600}>
|
||||
<img src={ backgroundPath } style={searchObjectCSS.backgroundImage}></img>
|
||||
<span style={searchObjectCSS.title_small}>{this.title}</span>
|
||||
<br></br>
|
||||
<span style={searchObjectCSS.stats_small}>Released: {this.year} | Rating: {this.rating}</span>
|
||||
</MediaQuery>
|
||||
|
||||
<div style={searchObjectCSS.buttons}>
|
||||
{foundInPlex}
|
||||
|
||||
<InfoButton id={this.id} type={this.type} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MediaQuery maxWidth={600}>
|
||||
<br />
|
||||
</MediaQuery>
|
||||
|
||||
<div style={searchObjectCSS.dividerRow}>
|
||||
<div style={searchObjectCSS.itemDivider}></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchObject;
|
||||
464
client/app/components/SearchRequest.jsx
Normal file
464
client/app/components/SearchRequest.jsx
Normal file
@@ -0,0 +1,464 @@
|
||||
import React from 'react';
|
||||
|
||||
import URI from 'urijs';
|
||||
import InfiniteScroll from 'react-infinite-scroller';
|
||||
|
||||
// StyleComponents
|
||||
import searchRequestCSS from './styles/searchRequestStyle.jsx';
|
||||
|
||||
import SearchObject from './SearchObject.jsx';
|
||||
import Loading from './images/loading.jsx'
|
||||
|
||||
import { fetchJSON } from './http.jsx';
|
||||
import { getCookie } from './Cookie.jsx';
|
||||
|
||||
var MediaQuery = require('react-responsive');
|
||||
|
||||
// TODO add option for searching multi, movies or tv shows
|
||||
class SearchRequest extends React.Component {
|
||||
constructor(props){
|
||||
super(props)
|
||||
// Constructor with states holding the search query and the element of reponse.
|
||||
this.state = {
|
||||
lastApiCallURI: '',
|
||||
searchQuery: '',
|
||||
responseMovieList: null,
|
||||
movieFilter: false,
|
||||
showFilter: false,
|
||||
discoverType: '',
|
||||
page: 1,
|
||||
resultHeader: '',
|
||||
loadResults: false,
|
||||
scrollHasMore: true,
|
||||
loading: false,
|
||||
}
|
||||
|
||||
this.allowedListTypes = ['discover', 'popular', 'nowplaying', 'upcoming']
|
||||
|
||||
this.baseUrl = 'https://apollo.kevinmidboe.com/api/v1/tmdb/list';
|
||||
// this.baseUrl = 'http://localhost:31459/api/v1/tmdb/list';
|
||||
this.searchUrl = 'https://apollo.kevinmidboe.com/api/v1/plex/request';
|
||||
// this.searchUrl = 'http://localhost:31459/api/v1/plex/request';
|
||||
}
|
||||
|
||||
|
||||
componentWillMount(){
|
||||
var that = this;
|
||||
// this.setState({responseMovieList: null})
|
||||
this.resetPageNumber();
|
||||
this.state.loadResults = true;
|
||||
this.fetchTmdbList(this.allowedListTypes[Math.floor(Math.random()*this.allowedListTypes.length)]);
|
||||
}
|
||||
|
||||
// Handles all errors of the response of a fetch call
|
||||
handleErrors(response) {
|
||||
if (!response.ok)
|
||||
throw Error(response.status);
|
||||
return response;
|
||||
}
|
||||
|
||||
handleQueryError(response) {
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
this.setState({
|
||||
responseMovieList: <h1>Nothing found for search query: { this.findQueryInURI(uri) }</h1>
|
||||
})
|
||||
}
|
||||
console.log('handleQueryError: ', error);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
// Unpacks the query value of a uri
|
||||
findQueryValueInURI(uri) {
|
||||
let uriSearchValues = uri.query(true);
|
||||
let queryValue = uriSearchValues['query']
|
||||
|
||||
return queryValue;
|
||||
}
|
||||
|
||||
// Unpacks the page value of a uri
|
||||
findPageValueInURI(uri) {
|
||||
let uriSearchValues = uri.query(true);
|
||||
let queryValue = uriSearchValues['page']
|
||||
|
||||
return queryValue;
|
||||
}
|
||||
|
||||
resetPageNumber() {
|
||||
this.state.page = 1;
|
||||
}
|
||||
|
||||
setLoading(value) {
|
||||
this.setState({
|
||||
loading: value
|
||||
});
|
||||
}
|
||||
|
||||
// Test this by calling missing endpoint or 404 query and see what code
|
||||
// and filter the error message based on the code.
|
||||
// Calls a uri and returns the response as json
|
||||
callURI(uri, method, data={}) {
|
||||
return fetch(uri, {
|
||||
method: method,
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
'authorization': getCookie('token'),
|
||||
'loggedinuser': getCookie('loggedInUser'),
|
||||
})
|
||||
})
|
||||
.then(response => { return response })
|
||||
.catch((error) => {
|
||||
throw Error(error);
|
||||
});
|
||||
}
|
||||
|
||||
// Saves the input string as a h1 element in responseMovieList state
|
||||
fillResponseMovieListWithError(msg) {
|
||||
this.setState({
|
||||
responseMovieList: <h1>{ msg }</h1>
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Here we first call api for a search with the input uri, handle any errors
|
||||
// and fill the reponseData from api into the state of reponseMovieList as movieObjects
|
||||
callSearchFillMovieList(uri) {
|
||||
Promise.resolve()
|
||||
.then(() => this.callURI(uri, 'GET'))
|
||||
.then(response => {
|
||||
// If we get a error code for the request
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
if (this.findPageValueInURI(new URI(response.url)) > 1) {
|
||||
this.state.scrollHasMore = false;
|
||||
console.log(this.state.scrollHasMore)
|
||||
return null
|
||||
let returnMessage = 'this is the return mesasge than will never be delivered'
|
||||
let theSecondReturnMsg = 'this is the second return messag ethat will NEVE be delivered'
|
||||
}
|
||||
else {
|
||||
|
||||
let errorMsg = 'Nothing found for the search query: ' + this.findQueryValueInURI(uri);
|
||||
this.fillResponseMovieListWithError(errorMsg)
|
||||
}
|
||||
}
|
||||
else {
|
||||
let errorMsg = 'Error fetching query from server ' + this.response.status;
|
||||
this.fillResponseMovieListWithError(errorMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to json and update the state of responseMovieList with the results of the api call
|
||||
// mapped as a SearchObject.
|
||||
response.json()
|
||||
.then(responseData => {
|
||||
if (this.state.page === 1) {
|
||||
this.setState({
|
||||
responseMovieList: responseData.results.map((searchResultItem, index) => this.createMovieObjects(searchResultItem, index)),
|
||||
lastApiCallURI: uri // Save the value of the last sucessfull api call
|
||||
})
|
||||
} else {
|
||||
let responseMovieObjects = responseData.results.map((searchResultItem, index) => this.createMovieObjects(searchResultItem, index));
|
||||
let growingReponseMovieObjectList = this.state.responseMovieList.concat(responseMovieObjects);
|
||||
this.setState({
|
||||
responseMovieList: growingReponseMovieObjectList,
|
||||
lastApiCallURI: uri // Save the value of the last sucessfull api call
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('CallSearchFillMovieList: ', error)
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('Something went wrong when fetching query.', error)
|
||||
})
|
||||
}
|
||||
|
||||
callListFillMovieList(uri) {
|
||||
// Write loading animation
|
||||
|
||||
Promise.resolve()
|
||||
.then(() => this.callURI(uri, 'GET', undefined))
|
||||
.then(response => {
|
||||
// If we get a error code for the request
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
let errorMsg = 'List not found';
|
||||
this.fillResponseMovieListWithError(errorMsg)
|
||||
}
|
||||
else {
|
||||
let errorMsg = 'Error fetching list from server ' + this.response.status;
|
||||
this.fillResponseMovieListWithError(errorMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to json and update the state of responseMovieList with the results of the api call
|
||||
// mapped as a SearchObject.
|
||||
response.json()
|
||||
.then(responseData => {
|
||||
if (this.state.page === 1) {
|
||||
this.setState({
|
||||
responseMovieList: responseData.results.map((searchResultItem, index) => this.createMovieObjects(searchResultItem, index)),
|
||||
lastApiCallURI: uri // Save the value of the last sucessfull api call
|
||||
})
|
||||
} else {
|
||||
let responseMovieObjects = responseData.results.map((searchResultItem, index) => this.createMovieObjects(searchResultItem, index));
|
||||
let growingReponseMovieObjectList = this.state.responseMovieList.concat(responseMovieObjects);
|
||||
this.setState({
|
||||
responseMovieList: growingReponseMovieObjectList,
|
||||
lastApiCallURI: uri // Save the value of the last sucessfull api call
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('Something went wrong when fetching query.', error)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
searchSeasonedRequest() {
|
||||
this.state.resultHeader = 'Search result for: ' + this.state.searchQuery;
|
||||
|
||||
// Build uri with the url for searching requests
|
||||
var uri = new URI(this.searchUrl);
|
||||
// Add input of search query and page count to the uri payload
|
||||
uri = uri.search({ 'query': this.state.searchQuery, 'page': this.state.page });
|
||||
|
||||
if (this.state.showFilter)
|
||||
uri = uri.addSearch('type', 'show');
|
||||
else
|
||||
if (this.state.movieFilter)
|
||||
uri = uri.addSearch('type', 'movie')
|
||||
|
||||
// Send uri to call and fill the response list with movie/show objects
|
||||
this.callSearchFillMovieList(uri);
|
||||
}
|
||||
|
||||
fetchTmdbList(tmdbListType) {
|
||||
console.log(tmdbListType)
|
||||
// Check if it is a whitelisted list, this should be replaced with checking if the return call is 500
|
||||
if (this.allowedListTypes.indexOf(tmdbListType) === -1)
|
||||
throw Error('Invalid discover type: ' + tmdbListType);
|
||||
|
||||
this.state.responseMovieList = []
|
||||
// Captialize the first letter of and save the discoverQueryType to resultHeader state.
|
||||
this.state.resultHeader = tmdbListType.toLowerCase().replace(/\b[a-z]/g, function(letter) {
|
||||
return letter.toUpperCase();
|
||||
});
|
||||
|
||||
// Build uri with the url for searching requests
|
||||
var uri = new URI(this.baseUrl);
|
||||
uri.segment(tmdbListType);
|
||||
// Add input of search query and page count to the uri payload
|
||||
uri = uri.search({ 'page': this.state.page });
|
||||
|
||||
if (this.state.showFilter)
|
||||
uri = uri.addSearch('type', 'show');
|
||||
|
||||
// Send uri to call and fill the response list with movie/show objects
|
||||
this.callListFillMovieList(uri);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Updates the internal state of the query search field.
|
||||
updateQueryState(event){
|
||||
this.setState({
|
||||
searchQuery: event.target.value
|
||||
});
|
||||
}
|
||||
|
||||
// For checking if the enter key was pressed in the search field.
|
||||
_handleQueryKeyPress(e) {
|
||||
if (e.key === 'Enter') {
|
||||
// this.fetchQuery();
|
||||
// Reset page number for a new search
|
||||
this.resetPageNumber();
|
||||
this.searchSeasonedRequest();
|
||||
}
|
||||
}
|
||||
|
||||
// When called passes the variable to SearchObject and calls it's interal function for
|
||||
// generating the wanted HTML
|
||||
createMovieObjects(item, index) {
|
||||
let movie = new SearchObject(item);
|
||||
return movie.getElement(index);
|
||||
}
|
||||
|
||||
toggleFilter(filterType) {
|
||||
if (filterType == 'movies') {
|
||||
this.setState({
|
||||
movieFilter: !this.state.movieFilter
|
||||
})
|
||||
console.log(this.state.movieFilter);
|
||||
}
|
||||
else if (filterType == 'shows') {
|
||||
this.setState({
|
||||
showFilter: !this.state.showFilter
|
||||
})
|
||||
console.log(this.state.showFilter);
|
||||
}
|
||||
}
|
||||
|
||||
pageBackwards() {
|
||||
if (this.state.page > 1) {
|
||||
let pageNumber = this.state.page - 1;
|
||||
let uri = this.state.lastApiCallURI;
|
||||
|
||||
// Augment the page number of the uri with a callback
|
||||
uri.search(function(data) {
|
||||
data.page = pageNumber;
|
||||
});
|
||||
|
||||
// Call the api with the new uri
|
||||
this.callSearchFillMovieList(uri);
|
||||
// Update state of our page number after the call is done
|
||||
this.state.page = pageNumber;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO need to get total page number and save in a state to not overflow
|
||||
pageForwards() {
|
||||
// Wrap this in the check
|
||||
let pageNumber = this.state.page + 1;
|
||||
let uri = this.state.lastApiCallURI;
|
||||
|
||||
// Augment the page number of the uri with a callback
|
||||
uri.search(function(data) {
|
||||
data.page = pageNumber;
|
||||
});
|
||||
|
||||
// Call the api with the new uri
|
||||
this.callSearchFillMovieList(uri);
|
||||
// Update state of our page number after the call is done
|
||||
this.state.page = pageNumber;
|
||||
}
|
||||
|
||||
movieToggle() {
|
||||
if (this.state.movieFilter)
|
||||
return <span style={searchRequestCSS.searchFilterActive}
|
||||
className="search_category hvrUnderlineFromCenter"
|
||||
onClick={() => {this.toggleFilter('movies')}}
|
||||
id="category_active">Movies</span>
|
||||
else
|
||||
return <span style={searchRequestCSS.searchFilterNotActive}
|
||||
className="search_category hvrUnderlineFromCenter"
|
||||
onClick={() => {this.toggleFilter('movies')}}
|
||||
id="category_active">Movies</span>
|
||||
}
|
||||
|
||||
showToggle() {
|
||||
if (this.state.showFilter)
|
||||
return <span style={searchRequestCSS.searchFilterActive}
|
||||
className="search_category hvrUnderlineFromCenter"
|
||||
onClick={() => {this.toggleFilter('shows')}}
|
||||
id="category_active">TV Shows</span>
|
||||
else
|
||||
return <span style={searchRequestCSS.searchFilterNotActive}
|
||||
className="search_category hvrUnderlineFromCenter"
|
||||
onClick={() => {this.toggleFilter('shows')}}
|
||||
id="category_active">TV Shows</span>
|
||||
}
|
||||
|
||||
|
||||
render(){
|
||||
const loader = <div className="loader">Loading ...<br></br></div>;
|
||||
|
||||
|
||||
return(
|
||||
<InfiniteScroll
|
||||
pageStart={0}
|
||||
loadMore={this.pageForwards.bind(this)}
|
||||
hasMore={this.state.scrollHasMore}
|
||||
loader={<Loading />}
|
||||
initialLoad={this.state.loadResults}>
|
||||
|
||||
<MediaQuery minWidth={600}>
|
||||
<div style={searchRequestCSS.body}>
|
||||
<div className='backgroundHeader' style={searchRequestCSS.backgroundLargeHeader}>
|
||||
<div className='pageTitle' style={searchRequestCSS.pageTitle}>
|
||||
<span style={searchRequestCSS.pageTitleLargeSpan}>Request new content for plex</span>
|
||||
</div>
|
||||
|
||||
<div style={searchRequestCSS.searchLargeContainer}>
|
||||
<span style={searchRequestCSS.searchIcon}><i className="fa fa-search"></i></span>
|
||||
|
||||
<input style={searchRequestCSS.searchLargeBar} type="text" id="search" placeholder="Search for new content..."
|
||||
onKeyPress={(event) => this._handleQueryKeyPress(event)}
|
||||
onChange={event => this.updateQueryState(event)}
|
||||
value={this.state.searchQuery}/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id='requestMovieList' ref='requestMovieList' style={searchRequestCSS.requestWrapper}>
|
||||
<div style={{marginLeft: '30px'}}>
|
||||
<div style={searchRequestCSS.resultLargeHeader}>{this.state.resultHeader}</div>
|
||||
<span style={{content: '', display: 'block', width: '2em', borderTop: '2px solid #000,'}}></span>
|
||||
|
||||
</div>
|
||||
|
||||
<br></br>
|
||||
|
||||
{this.state.responseMovieList}
|
||||
</div>
|
||||
</div>
|
||||
</MediaQuery>
|
||||
|
||||
<MediaQuery maxWidth={600}>
|
||||
<div style={searchRequestCSS.body}>
|
||||
<div className='backgroundHeader' style={searchRequestCSS.backgroundSmallHeader}>
|
||||
<div className='pageTitle' style={searchRequestCSS.pageTitle}>
|
||||
<span style={searchRequestCSS.pageTitleSmallSpan}>Request new content</span>
|
||||
</div>
|
||||
|
||||
<div className='box' style={searchRequestCSS.box}>
|
||||
<div style={searchRequestCSS.searchSmallContainer}>
|
||||
<span style={searchRequestCSS.searchIcon}><i className="fa fa-search"></i></span>
|
||||
|
||||
<input style={searchRequestCSS.searchSmallBar} type="text" id="search" placeholder="Search for new content..."
|
||||
onKeyPress={(event) => this._handleQueryKeyPress(event)}
|
||||
onChange={event => this.updateQueryState(event)}
|
||||
value={this.state.searchQuery}/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id='requestMovieList' ref='requestMovieList' style={searchRequestCSS.requestWrapper}>
|
||||
<span style={searchRequestCSS.resultSmallHeader}>{this.state.resultHeader}</span>
|
||||
<br></br><br></br>
|
||||
|
||||
{this.state.responseMovieList}
|
||||
</div>
|
||||
</div>
|
||||
</MediaQuery>
|
||||
</InfiniteScroll>
|
||||
)
|
||||
}
|
||||
|
||||
// <form style={searchRequestCSS.controls}>
|
||||
// <label style={searchRequestCSS.withData}>
|
||||
// <div style={searchRequestCSS.sortOptions}>Discover</div>
|
||||
// </label>
|
||||
// </form>
|
||||
|
||||
// <form style={searchRequestCSS.controls}>
|
||||
// <label style={searchRequestCSS.withData}>
|
||||
// <select style={searchRequestCSS.sortOptions}>
|
||||
// <option value="discover">All</option>
|
||||
// <option value="nowplaying">Movies</option>
|
||||
// <option value="nowplaying">TV Shows</option>
|
||||
// </select>
|
||||
// </label>
|
||||
// </form>
|
||||
}
|
||||
|
||||
export default SearchRequest;
|
||||
92
client/app/components/admin/Admin.jsx
Normal file
92
client/app/components/admin/Admin.jsx
Normal file
@@ -0,0 +1,92 @@
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import LoginForm from './LoginForm/LoginForm.jsx';
|
||||
import { Provider } from 'react-redux';
|
||||
import store from '../redux/store.jsx';
|
||||
|
||||
import { getCookie } from '../Cookie.jsx';
|
||||
import { fetchJSON } from '../http.jsx';
|
||||
|
||||
import Sidebar from './Sidebar.jsx';
|
||||
import AdminRequestInfo from './AdminRequestInfo.jsx';
|
||||
|
||||
import adminCSS from '../styles/adminComponent.jsx'
|
||||
|
||||
|
||||
class AdminComponent extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
requested_objects: '',
|
||||
}
|
||||
|
||||
this.updateHandler = this.updateHandler.bind(this)
|
||||
}
|
||||
|
||||
// Fetches all requested elements and updates the state with response
|
||||
componentWillMount() {
|
||||
this.fetchRequestedItems()
|
||||
}
|
||||
|
||||
fetchRequestedItems() {
|
||||
fetchJSON('https://apollo.kevinmidboe.com/api/v1/plex/requests/all', 'GET')
|
||||
.then(result => {
|
||||
this.setState({
|
||||
requested_objects: result.results.reverse()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
updateHandler() {
|
||||
this.fetchRequestedItems()
|
||||
}
|
||||
|
||||
// Displays loginform if not logged in and passes response from
|
||||
// api call to sidebar and infoPanel through props
|
||||
verifyLoggedIn() {
|
||||
const logged_in = getCookie('logged_in');
|
||||
if (!logged_in) {
|
||||
return <LoginForm />
|
||||
}
|
||||
|
||||
let selectedRequest = undefined;
|
||||
let listItemSelected = undefined;
|
||||
|
||||
const requestParam = this.props.match.params.request;
|
||||
|
||||
if (requestParam && this.state.requested_objects !== '') {
|
||||
selectedRequest = this.state.requested_objects[requestParam]
|
||||
listItemSelected = requestParam;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={adminCSS.selectedObjectPanel}>
|
||||
<AdminRequestInfo
|
||||
selectedRequest={selectedRequest}
|
||||
listItemSelected={listItemSelected}
|
||||
updateHandler = {this.updateHandler}
|
||||
/>
|
||||
</div>
|
||||
<div style={adminCSS.sidebar}>
|
||||
<Sidebar
|
||||
requested_objects={this.state.requested_objects}
|
||||
listItemSelected={listItemSelected}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
{ this.verifyLoggedIn() }
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default AdminComponent;
|
||||
218
client/app/components/admin/AdminRequestInfo.jsx
Normal file
218
client/app/components/admin/AdminRequestInfo.jsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { fetchJSON } from '../http.jsx';
|
||||
|
||||
import PirateSearch from './PirateSearch.jsx'
|
||||
// No in use!
|
||||
import InfoButton from '../buttons/InfoButton.jsx';
|
||||
|
||||
// Stylesheets
|
||||
import requestInfoCSS from '../styles/adminRequestInfo.jsx'
|
||||
import buttonsCSS from '../styles/buttons.jsx';
|
||||
|
||||
|
||||
String.prototype.capitalize = function() {
|
||||
return this.charAt(0).toUpperCase() + this.slice(1);
|
||||
}
|
||||
|
||||
|
||||
class AdminRequestInfo extends Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
statusValue: '',
|
||||
movieInfo: undefined,
|
||||
expandedSummary: false,
|
||||
}
|
||||
|
||||
this.requestInfo = '';
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
this.requestInfo = props.selectedRequest;
|
||||
this.state.statusValue = this.requestInfo.status;
|
||||
this.state.expandedSummary = false;
|
||||
this.fetchIteminfo()
|
||||
}
|
||||
|
||||
userAgent(agent) {
|
||||
if (agent) {
|
||||
try {
|
||||
return agent.split(" ")[1].replace(/[\(\;]/g, '');
|
||||
}
|
||||
catch(e) {
|
||||
return agent;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
generateStatusDropdown() {
|
||||
return (
|
||||
<select onChange={ event => this.updateRequestStatus(event) } value={this.state.statusValue}>
|
||||
<option value='requested'>Requested</option>
|
||||
<option value='downloading'>Downloading</option>
|
||||
<option value='downloaded'>Downloaded</option>
|
||||
</select>
|
||||
)
|
||||
}
|
||||
|
||||
updateRequestStatus(event) {
|
||||
const eventValue = event.target.value;
|
||||
const itemID = this.requestInfo.id;
|
||||
|
||||
const apiData = {
|
||||
type: this.requestInfo.type,
|
||||
status: eventValue,
|
||||
}
|
||||
|
||||
fetchJSON('https://apollo.kevinmidboe.com/api/v1/plex/request/' + itemID, 'PUT', apiData)
|
||||
.then((response) => {
|
||||
console.log('Response, updateRequestStatus: ', response)
|
||||
this.props.updateHandler()
|
||||
})
|
||||
}
|
||||
|
||||
generateStatusIndicator(status) {
|
||||
switch (status) {
|
||||
case 'requested':
|
||||
// Yellow
|
||||
return 'linear-gradient(to right, rgb(63, 195, 243) 0, rgb(63, 195, 243) 10px, #fff 4px, #fff 100%) no-repeat'
|
||||
case 'downloading':
|
||||
// Blue
|
||||
return 'linear-gradient(to right, rgb(255, 225, 77) 0, rgb(255, 225, 77) 10px, #fff 4px, #fff 100%) no-repeat'
|
||||
case 'downloaded':
|
||||
// Green
|
||||
return 'linear-gradient(to right, #39aa56 0, #39aa56 10px, #fff 4px, #fff 100%) no-repeat'
|
||||
default:
|
||||
return 'linear-gradient(to right, grey 0, grey 10px, #fff 4px, #fff 100%) no-repeat'
|
||||
}
|
||||
}
|
||||
|
||||
generateTypeIcon(type) {
|
||||
if (type === 'show')
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="7" width="20" height="15" rx="2" ry="2"></rect><polyline points="17 2 12 7 7 2"></polyline></svg>
|
||||
)
|
||||
else if (type === 'movie')
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="2" width="20" height="20" rx="2.18" ry="2.18"></rect><line x1="7" y1="2" x2="7" y2="22"></line><line x1="17" y1="2" x2="17" y2="22"></line><line x1="2" y1="12" x2="22" y2="12"></line><line x1="2" y1="7" x2="7" y2="7"></line><line x1="2" y1="17" x2="7" y2="17"></line><line x1="17" y1="17" x2="22" y2="17"></line><line x1="17" y1="7" x2="22" y2="7"></line></svg>
|
||||
)
|
||||
else
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12" y2="16"></line></svg>
|
||||
)
|
||||
}
|
||||
|
||||
toggleSummmaryLength() {
|
||||
this.setState({
|
||||
expandedSummary: !this.state.expandedSummary
|
||||
})
|
||||
}
|
||||
|
||||
generateSummary() {
|
||||
// { this.state.movieInfo != undefined ? this.state.movieInfo.summary : 'Loading...' }
|
||||
const info = this.state.movieInfo;
|
||||
if (info !== undefined) {
|
||||
const summary = this.state.movieInfo.summary
|
||||
const summary_short = summary.slice(0, 180);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span><b>Matched: </b> {String(info.matchedInPlex)}</span> <br/>
|
||||
<span><b>Rating: </b> {info.rating}</span> <br/>
|
||||
<span><b>Popularity: </b> {info.popularity}</span> <br/>
|
||||
{
|
||||
(summary.length > 180 && this.state.expandedSummary === false) ?
|
||||
<span><b>Summary: </b> { summary_short }<span onClick = {() => this.toggleSummmaryLength()}>... <span style={{color: 'blue', cursor: 'pointer'}}>Show more</span></span></span>
|
||||
:
|
||||
<span><b>Summary: </b> { summary }<span onClick = {() => this.toggleSummmaryLength()}><span style={{color: 'blue', cursor: 'pointer'}}> Show less</span></span></span>
|
||||
|
||||
}
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return <span>Loading...</span>
|
||||
}
|
||||
}
|
||||
|
||||
requested_by_user(request_user) {
|
||||
if (request_user === 'NULL')
|
||||
return undefined
|
||||
|
||||
return (
|
||||
<span><b>Requested by:</b> {request_user}</span>
|
||||
)
|
||||
}
|
||||
|
||||
fetchIteminfo() {
|
||||
const itemID = this.requestInfo.id;
|
||||
const type = this.requestInfo.type;
|
||||
|
||||
fetchJSON('https://apollo.kevinmidboe.com/api/v1/tmdb/' + itemID +'&type='+type, 'GET')
|
||||
.then((response) => {
|
||||
console.log('Response, getInfo:', response)
|
||||
this.setState({
|
||||
movieInfo: response
|
||||
});
|
||||
console.log(this.state.movieInfo)
|
||||
})
|
||||
}
|
||||
|
||||
displayInfo() {
|
||||
const request = this.props.selectedRequest;
|
||||
|
||||
if (request) {
|
||||
requestInfoCSS.info.background = this.generateStatusIndicator(request.status);
|
||||
|
||||
return (
|
||||
<div style={requestInfoCSS.wrapper}>
|
||||
|
||||
<div style={requestInfoCSS.stick}>
|
||||
<span style={requestInfoCSS.title}> {request.title} {request.year}</span>
|
||||
<span style={{marginLeft: '2em'}}>
|
||||
<span style={requestInfoCSS.type_icon}>{this.generateTypeIcon(request.type)}</span>
|
||||
{/*<span style={style.type_text}>{request.type.capitalize()}</span> <br />*/}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={requestInfoCSS.info}>
|
||||
<div style={requestInfoCSS.info_poster}>
|
||||
<img src={'https://image.tmdb.org/t/p/w185' + request.poster_path} style={requestInfoCSS.image} alt='Movie poster image'></img>
|
||||
</div>
|
||||
|
||||
<div style={requestInfoCSS.info_request}>
|
||||
<h3 style={requestInfoCSS.info_request_header}>Request info</h3>
|
||||
|
||||
<span><b>status:</b>{ request.status }</span><br />
|
||||
<span><b>ip:</b>{ request.ip }</span><br />
|
||||
<span><b>user_agent:</b>{ this.userAgent(request.user_agent) }</span><br />
|
||||
<span><b>request_date:</b>{ request.requested_date}</span><br />
|
||||
{ this.requested_by_user(request.requested_by) }<br />
|
||||
{ this.generateStatusDropdown() }<br />
|
||||
</div>
|
||||
|
||||
<div style={requestInfoCSS.info_movie}>
|
||||
<h3 style={requestInfoCSS.info_movie_header}>Movie info</h3>
|
||||
|
||||
{ this.generateSummary() }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PirateSearch style={requestInfoCSS.search} name={request.title} />
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>{this.displayInfo()}</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AdminRequestInfo;
|
||||
66
client/app/components/admin/LoginForm/LoginForm.jsx
Normal file
66
client/app/components/admin/LoginForm/LoginForm.jsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { login } from '../../redux/reducer.jsx';
|
||||
|
||||
class LoginForm extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
let {email, password} = this.state;
|
||||
let {isLoginPending, isLoginSuccess, loginError} = this.props;
|
||||
return (
|
||||
<form name="loginForm" onSubmit={this.onSubmit}>
|
||||
<div className="form-group-collection">
|
||||
<div className="form-group">
|
||||
<label>Email:</label>
|
||||
<input type="" name="email" onChange={e => this.setState({email: e.target.value})} value={email}/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Password:</label>
|
||||
<input type="password" name="password" onChange={e => this.setState({password: e.target.value})} value={password}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="submit" value="Login" />
|
||||
|
||||
<div className="message">
|
||||
{ isLoginPending && <div>Please wait...</div> }
|
||||
{ isLoginSuccess && <div>Success.</div> }
|
||||
{ loginError && <div>{loginError.message}</div> }
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
onSubmit(e) {
|
||||
e.preventDefault();
|
||||
let { email, password } = this.state;
|
||||
this.props.login(email, password);
|
||||
this.setState({
|
||||
email: '',
|
||||
password: ''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
isLoginPending: state.isLoginPending,
|
||||
isLoginSuccess: state.isLoginSuccess,
|
||||
loginError: state.loginError
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
login: (email, password) => dispatch(login(email, password))
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LoginForm);
|
||||
95
client/app/components/admin/PirateSearch.jsx
Normal file
95
client/app/components/admin/PirateSearch.jsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React, { Component } from 'react';
|
||||
import { fetchJSON } from '../http.jsx';
|
||||
|
||||
// Components
|
||||
import TorrentTable from './TorrentTable.jsx'
|
||||
|
||||
// Stylesheets
|
||||
import btnStylesheet from '../styles/buttons.jsx';
|
||||
|
||||
// Interactive button
|
||||
import Interactive from 'react-interactive';
|
||||
|
||||
import Loading from '../images/loading.jsx'
|
||||
|
||||
class PirateSearch extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
torrentResponse: undefined,
|
||||
name: '',
|
||||
loading: null,
|
||||
showButton: true,
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
if (props.name != this.state.name) {
|
||||
this.setState({
|
||||
torrentResponse: undefined,
|
||||
showButton: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
searchTheBay() {
|
||||
const query = this.props.name;
|
||||
const type = this.props.type;
|
||||
|
||||
this.setState({
|
||||
showButton: false,
|
||||
loading: <Loading />,
|
||||
})
|
||||
|
||||
fetchJSON('https://apollo.kevinmidboe.com/api/v1/pirate/search?query='+query+'&type='+type, 'GET')
|
||||
// fetchJSON('http://localhost:31459/api/v1/pirate/search?query='+query+'&type='+type, 'GET')
|
||||
.then((response) => {
|
||||
console.log('this is the first response: ', response)
|
||||
if (response.success === true) {
|
||||
this.setState({
|
||||
torrentResponse: response.torrents,
|
||||
loading: null,
|
||||
})
|
||||
}
|
||||
else {
|
||||
console.error(response.message)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
this.setState({
|
||||
showButton: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
btnStylesheet.submit.top = '50%'
|
||||
btnStylesheet.submit.position = 'absolute'
|
||||
btnStylesheet.submit.marginLeft = '-75px'
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ this.state.showButton ?
|
||||
<div style={{textAlign:'center'}}>
|
||||
<Interactive
|
||||
as='button'
|
||||
onClick={() => {this.searchTheBay()}}
|
||||
style={btnStylesheet.submit}
|
||||
focus={btnStylesheet.submit_hover}
|
||||
hover={btnStylesheet.submit_hover}>
|
||||
|
||||
<span style={{whiteSpace: 'nowrap'}}>Search for torrents</span>
|
||||
</Interactive>
|
||||
</div>
|
||||
: null }
|
||||
|
||||
{ this.state.loading }
|
||||
|
||||
<TorrentTable response={this.state.torrentResponse} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default PirateSearch
|
||||
247
client/app/components/admin/Sidebar.jsx
Normal file
247
client/app/components/admin/Sidebar.jsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import Interactive from 'react-interactive';
|
||||
|
||||
import sidebarCSS from '../styles/adminSidebar.jsx'
|
||||
|
||||
class SidebarComponent extends Component {
|
||||
|
||||
constructor(props){
|
||||
super(props)
|
||||
// Constructor with states holding the search query and the element of reponse.
|
||||
this.state = {
|
||||
filterValue: '',
|
||||
filterQuery: '',
|
||||
requestItemsToBeDisplayed: [],
|
||||
listItemSelected: '',
|
||||
height: '0',
|
||||
}
|
||||
|
||||
this.updateWindowDimensions = this.updateWindowDimensions.bind(this);
|
||||
}
|
||||
|
||||
// Where we wait for api response to be delivered from parent through props
|
||||
componentWillReceiveProps(props) {
|
||||
this.state.listItemSelected = props.listItemSelected;
|
||||
this.displayRequestedElementsInfo(props.requested_objects);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateWindowDimensions();
|
||||
window.addEventListener('resize', this.updateWindowDimensions);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.updateWindowDimensions);
|
||||
}
|
||||
|
||||
updateWindowDimensions() {
|
||||
this.setState({ height: window.innerHeight });
|
||||
}
|
||||
|
||||
// Inputs a date and returns a text string that matches how long it was since
|
||||
convertDateToDaysSince(date) {
|
||||
var oneDay = 24*60*60*1000;
|
||||
var firstDate = new Date(date);
|
||||
var secondDate = new Date();
|
||||
|
||||
var diffDays = Math.round(Math.abs((firstDate.getTime() - secondDate.getTime()) / oneDay));
|
||||
|
||||
switch (diffDays) {
|
||||
case 0:
|
||||
return 'Today';
|
||||
case 1:
|
||||
return '1 day ago'
|
||||
default:
|
||||
return diffDays + ' days ago'
|
||||
}
|
||||
}
|
||||
|
||||
// Called from our dropdown, receives a filter string and checks it with status field
|
||||
// of our request objects.
|
||||
filterItems(filterValue) {
|
||||
let filteredRequestElements = this.props.requested_objects.map((item, index) => {
|
||||
if (item.status === filterValue || filterValue === 'all')
|
||||
return this.generateListElements(index, item);
|
||||
})
|
||||
|
||||
this.setState({
|
||||
requestItemsToBeDisplayed: filteredRequestElements,
|
||||
filterValue: filterValue,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Updates the internal state of the query filter and updates the list to only
|
||||
// display names matching the query. This is real-time filtering.
|
||||
updateFilterQuery(event) {
|
||||
const query = event.target.value;
|
||||
|
||||
let filteredByQuery = this.props.requested_objects.map((item, index) => {
|
||||
if (item.title.toLowerCase().indexOf(query.toLowerCase()) != -1)
|
||||
return this.generateListElements(index, item);
|
||||
})
|
||||
|
||||
this.setState({
|
||||
requestItemsToBeDisplayed: filteredByQuery,
|
||||
filterQuery: query,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
generateFilterSearch() {
|
||||
return (
|
||||
<div style={sidebarCSS.searchSidebar}>
|
||||
<div style={sidebarCSS.searchInner}>
|
||||
<input
|
||||
type="text"
|
||||
id="search"
|
||||
style={sidebarCSS.searchTextField}
|
||||
placeholder="Search requested items"
|
||||
onChange={event => this.updateFilterQuery(event)}
|
||||
value={this.state.filterQuery}/>
|
||||
<span>
|
||||
<svg id="icon-search" style={sidebarCSS.searchIcon} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15">
|
||||
<g id="search">
|
||||
<circle style={sidebarCSS.searchSVGIcon} cx="6.055" cy="5.805" r="5.305"></circle>
|
||||
<path style={sidebarCSS.searchSVGIcon} d="M9.847 9.727l4.166 4.773"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
generateNav() {
|
||||
let filterValue = this.state.filterValue;
|
||||
|
||||
return (
|
||||
<nav style={sidebarCSS.sidebar_navbar_underline}>
|
||||
<ul style={sidebarCSS.ulFilterSelectors}>
|
||||
<li>
|
||||
<span style={sidebarCSS.aFilterSelectors} onClick = { event => this.filterItems('all') }>All</span>
|
||||
{ (filterValue === 'all' || filterValue === '') && <span style={sidebarCSS.spanFilterSelectors}></span> }
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<span style={sidebarCSS.aFilterSelectors} onClick = { event => this.filterItems('requested') }>Requested</span>
|
||||
{ filterValue === 'requested' && <span style={sidebarCSS.spanFilterSelectors}></span> }
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<span style={sidebarCSS.aFilterSelectors} onClick = { event => this.filterItems('downloading') }>Downloading</span>
|
||||
{ filterValue === 'downloading' && <span style={sidebarCSS.spanFilterSelectors}></span> }
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<span style={sidebarCSS.aFilterSelectors} onClick = { event => this.filterItems('downloaded') }>Downloaded</span>
|
||||
{ filterValue === 'downloaded' && <span style={sidebarCSS.spanFilterSelectors}></span> }
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
generateBody(cards) {
|
||||
let style = sidebarCSS.ulCard;
|
||||
style.maxHeight = this.state.height - 160;
|
||||
|
||||
return (
|
||||
<ul style={style}>
|
||||
{ cards }
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
generateListElements(index, item) {
|
||||
let statusBar;
|
||||
|
||||
switch (item.status) {
|
||||
case 'requested':
|
||||
// Yellow
|
||||
statusBar = { background: 'linear-gradient(to right, rgb(63, 195, 243) 0, rgb(63, 195, 243) 4px, #fff 4px, #fff 100%) no-repeat' }
|
||||
break;
|
||||
case 'downloading':
|
||||
// Blue
|
||||
statusBar = { background: 'linear-gradient(to right, rgb(255, 225, 77) 0, rgb(255, 225, 77) 4px, #fff 4px, #fff 100%) no-repeat' }
|
||||
break;
|
||||
case 'downloaded':
|
||||
// Green
|
||||
statusBar = { background: 'linear-gradient(to right, #39aa56 0, #39aa56 4px, #fff 4px, #fff 100%) no-repeat' }
|
||||
break;
|
||||
default:
|
||||
statusBar = { background: 'linear-gradient(to right, grey 0, grey 4px, #fff 4px, #fff 100%) no-repeat' }
|
||||
}
|
||||
|
||||
statusBar.listStyleType = 'none';
|
||||
|
||||
return (
|
||||
<Link style={sidebarCSS.link} to={{ pathname: '/admin/'+String(index)}} key={index}>
|
||||
<li style={statusBar}>
|
||||
<Interactive
|
||||
as='div'
|
||||
style={ (index != this.state.listItemSelected) ? sidebarCSS.card : sidebarCSS.cardSelected }
|
||||
hover={sidebarCSS.cardSelected}
|
||||
focus={sidebarCSS.cardSelected}
|
||||
active={sidebarCSS.cardSelected}>
|
||||
|
||||
<h2 style={sidebarCSS.titleCard}>
|
||||
<span>{ item.title }</span>
|
||||
</h2>
|
||||
|
||||
<p style={sidebarCSS.pCard}>
|
||||
<span>Requested:
|
||||
<time>
|
||||
{ this.convertDateToDaysSince(item.requested_date) }
|
||||
</time>
|
||||
</span>
|
||||
</p>
|
||||
</Interactive>
|
||||
</li>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
// This is our main loader that gets called when we receive api response through props from parent
|
||||
displayRequestedElementsInfo(requested_objects) {
|
||||
let requestedElement = requested_objects.map((item, index) => {
|
||||
if (['requested', 'downloading', 'downloaded'].indexOf(this.state.filterValue) != -1) {
|
||||
if (item.status === this.state.filterValue){
|
||||
return this.generateListElements(index, item);
|
||||
}
|
||||
}
|
||||
else if (this.state.filterQuery !== '') {
|
||||
if (item.name.toLowerCase().indexOf(this.state.filterQuery.toLowerCase()) != -1)
|
||||
return this.generateListElements(index, item);
|
||||
}
|
||||
else
|
||||
return this.generateListElements(index, item);
|
||||
})
|
||||
|
||||
this.setState({
|
||||
requestItemsToBeDisplayed: this.generateBody(requestedElement)
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
// if (typeof InstallTrigger !== 'undefined')
|
||||
// bodyCSS.width = '-moz-min-content';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 style={sidebarCSS.header}>Requested items</h1>
|
||||
{ this.generateFilterSearch() }
|
||||
{ this.generateNav() }
|
||||
|
||||
<div key='requestedTable' style={sidebarCSS.body}>
|
||||
{ this.state.requestItemsToBeDisplayed }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SidebarComponent;
|
||||
209
client/app/components/admin/TorrentTable.jsx
Normal file
209
client/app/components/admin/TorrentTable.jsx
Normal file
@@ -0,0 +1,209 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { fetchJSON } from '../http.jsx';
|
||||
|
||||
import torrentTableCSS from '../styles/adminTorrentTable.jsx';
|
||||
|
||||
class TorrentTable extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
torrentResponse: [],
|
||||
listElements: undefined,
|
||||
showTable: false,
|
||||
filterQuery: '',
|
||||
sortValue: 'name',
|
||||
sortDesc: true,
|
||||
}
|
||||
|
||||
this.UNITS = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
if (props.response !== undefined && props.response !== this.state.torrentResponse) {
|
||||
console.log('not called', props)
|
||||
this.setState({
|
||||
torrentResponse: props.response,
|
||||
showTable: true,
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
showTable: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// BORROWED FROM GITHUB user sindresorhus
|
||||
// Link to repo: https://github.com/sindresorhus/pretty-bytes
|
||||
convertSizeToHumanSize(num) {
|
||||
if (!Number.isFinite(num)) {
|
||||
return num
|
||||
// throw new TypeError(`Expected a finite number, got ${typeof num}: ${num}`);
|
||||
}
|
||||
const neg = num < 0;
|
||||
|
||||
if (neg) {
|
||||
num = -num;
|
||||
}
|
||||
|
||||
if (num < 1) {
|
||||
return (neg ? '-' : '') + num + ' B';
|
||||
}
|
||||
|
||||
const exponent = Math.min(Math.floor(Math.log10(num) / 3), this.UNITS.length - 1);
|
||||
const numStr = Number((num / Math.pow(1000, exponent)).toPrecision(3));
|
||||
const unit = this.UNITS[exponent];
|
||||
|
||||
return (neg ? '-' : '') + numStr + ' ' + unit;
|
||||
}
|
||||
|
||||
convertHumanSizeToBytes(string) {
|
||||
const [numStr, unit] = string.split(' ');
|
||||
if (this.UNITS.indexOf(unit) === -1) {
|
||||
return string
|
||||
}
|
||||
|
||||
const exponent = this.UNITS.indexOf(unit) * 3
|
||||
|
||||
return numStr * (Math.pow(10, exponent))
|
||||
}
|
||||
|
||||
sendToDownload(magnet) {
|
||||
const apiData = {
|
||||
magnet: magnet,
|
||||
}
|
||||
|
||||
fetchJSON('https://apollo.kevinmidboe.com/api/v1/pirate/add', 'POST', apiData)
|
||||
// fetchJSON('http://localhost:31459/api/v1/pirate/add', 'POST', apiData)
|
||||
.then((response) => {
|
||||
console.log('Response, addMagnet: ', response)
|
||||
// TODO Display the feedback in a notification component (text, status)
|
||||
})
|
||||
}
|
||||
|
||||
// Updates the internal state of the query filter and updates the list to only
|
||||
// display names matching the query. This is real-time filtering.
|
||||
updateFilterQuery(event) {
|
||||
const query = event.target.value;
|
||||
|
||||
let filteredByQuery = this.props.response.map((item, index) => {
|
||||
if (item.name.toLowerCase().indexOf(query.toLowerCase()) != -1)
|
||||
return item
|
||||
})
|
||||
|
||||
this.setState({
|
||||
torrentResponse: filteredByQuery,
|
||||
filterQuery: query,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
sortTable(col) {
|
||||
let direction = this.state.sortDesc;
|
||||
if (col === this.state.sortValue)
|
||||
direction = !direction;
|
||||
else
|
||||
direction = true
|
||||
|
||||
let sortedItems = this.state.torrentResponse.sort((a,b) => {
|
||||
// This is so we also can sort string that only contain numbers
|
||||
let valueA = isNaN(a[col]) ? a[col] : parseInt(a[col])
|
||||
let valueB = isNaN(b[col]) ? b[col] : parseInt(b[col])
|
||||
|
||||
valueA = (col == 'size') ? this.convertHumanSizeToBytes(valueA) : valueA
|
||||
valueB = (col == 'size') ? this.convertHumanSizeToBytes(valueB) : valueB
|
||||
|
||||
if (direction)
|
||||
return valueA<valueB? 1:valueA>valueB?-1:0;
|
||||
else
|
||||
return valueA>valueB? 1:valueA<valueB?-1:0;
|
||||
})
|
||||
|
||||
this.setState({
|
||||
torrentResponse: sortedItems,
|
||||
sortDesc: direction,
|
||||
sortValue: col,
|
||||
})
|
||||
}
|
||||
|
||||
generateFilterSearch() {
|
||||
return (
|
||||
<div style={torrentTableCSS.searchSidebar}>
|
||||
<div style={torrentTableCSS.searchInner}>
|
||||
<input
|
||||
type="text"
|
||||
id="search"
|
||||
style={torrentTableCSS.searchTextField}
|
||||
placeholder="Filter torrents by query"
|
||||
onChange={event => this.updateFilterQuery(event)}
|
||||
value={this.state.filterQuery}/>
|
||||
<span>
|
||||
<svg id="icon-search" style={torrentTableCSS.searchIcon} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15">
|
||||
<g id="search">
|
||||
<circle style={torrentTableCSS.searchSVGIcon} cx="6.055" cy="5.805" r="5.305"></circle>
|
||||
<path style={torrentTableCSS.searchSVGIcon} d="M9.847 9.727l4.166 4.773"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
generateListElements() {
|
||||
let listElements = this.state.torrentResponse.map((item, index) => {
|
||||
if (item !== undefined) {
|
||||
let title = item.name
|
||||
let size = this.convertSizeToHumanSize(item.size)
|
||||
|
||||
return (
|
||||
<tr key={index} style={torrentTableCSS.bodyCol}>
|
||||
<td>{ item.name }</td>
|
||||
<td>{ item.uploader }</td>
|
||||
<td>{ size }</td>
|
||||
<td>{ item.seed }</td>
|
||||
<td><button onClick = { event => this.sendToDownload(item.magnet) }>Send to download</button></td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
})
|
||||
return listElements
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style= { this.state.showTable ? null : {display: 'none'}}>
|
||||
{ this.generateFilterSearch() }
|
||||
<table style={torrentTableCSS.table} cellSpacing="0" cellPadding="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={torrentTableCSS.col} onClick = {event => this.sortTable('name') }>
|
||||
Title
|
||||
<svg style={ ( this.state.sortDesc && this.state.sortValue == 'name' ) ? null : {transform: 'rotate(180deg)'} } height="15" viewBox="0 3.5 10 13" version="1.1" width="25" aria-hidden="true"><path fillRule="evenodd" d="M10 10l-1.5 1.5L5 7.75 1.5 11.5 0 10l5-5z"></path></svg>
|
||||
</th>
|
||||
<th style={torrentTableCSS.col} onClick = {event => this.sortTable('uploader') }>
|
||||
Uploader
|
||||
<svg style={ ( this.state.sortDesc && this.state.sortValue == 'uploader' ) ? null : {transform: 'rotate(180deg)'} } height="15" viewBox="0 3.5 10 13" version="1.1" width="25" aria-hidden="true"><path fillRule="evenodd" d="M10 10l-1.5 1.5L5 7.75 1.5 11.5 0 10l5-5z"></path></svg>
|
||||
</th>
|
||||
<th style={torrentTableCSS.col} onClick = {event => this.sortTable('size') }>
|
||||
Size
|
||||
<svg style={ ( this.state.sortDesc && this.state.sortValue == 'size' ) ? null : {transform: 'rotate(180deg)'} } height="15" viewBox="0 3.5 10 13" version="1.1" width="25" aria-hidden="true"><path fillRule="evenodd" d="M10 10l-1.5 1.5L5 7.75 1.5 11.5 0 10l5-5z"></path></svg>
|
||||
</th>
|
||||
<th style={torrentTableCSS.col} onClick = {event => this.sortTable('seed') }>
|
||||
Seeds
|
||||
<svg style={ ( this.state.sortDesc && this.state.sortValue == 'seed' ) ? null : {transform: 'rotate(180deg)'} } height="15" viewBox="0 3.5 10 13" version="1.1" width="25" aria-hidden="true"><path fillRule="evenodd" d="M10 10l-1.5 1.5L5 7.75 1.5 11.5 0 10l5-5z"></path></svg>
|
||||
</th>
|
||||
<th style={torrentTableCSS.col}>Magnet</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.generateListElements()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default TorrentTable;
|
||||
52
client/app/components/buttons/InfoButton.jsx
Normal file
52
client/app/components/buttons/InfoButton.jsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React, { Component } from 'react';
|
||||
import Interactive from 'react-interactive';
|
||||
|
||||
import buttonsCSS from '../styles/buttons.jsx';
|
||||
|
||||
|
||||
class InfoButton extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
if (props) {
|
||||
this.state = {
|
||||
id: props.id,
|
||||
type: props.type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
this.setState({
|
||||
id: props.id,
|
||||
type: props.type,
|
||||
})
|
||||
}
|
||||
|
||||
getTMDBLink() {
|
||||
const id = this.state.id;
|
||||
const type = this.state.type;
|
||||
|
||||
if (type === 'movie')
|
||||
return 'https://www.themoviedb.org/movie/' + id
|
||||
else if (type === 'show')
|
||||
return 'https://www.themoviedb.org/tv/' + id
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<a href={this.getTMDBLink()}>
|
||||
<Interactive
|
||||
as='button'
|
||||
hover={buttonsCSS.info_hover}
|
||||
focus={buttonsCSS.info_hover}
|
||||
style={buttonsCSS.info}>
|
||||
|
||||
<span>More info</span>
|
||||
</Interactive>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default InfoButton;
|
||||
22
client/app/components/buttons/request_button.jsx
Normal file
22
client/app/components/buttons/request_button.jsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
|
||||
class RequestButton extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {textColor: 'white'};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Text
|
||||
style={{color: this.state.textColor}}
|
||||
onEnter={() => this.setState({textColor: 'red'})}
|
||||
onExit={() => this.setState({textColor: 'white'})}>
|
||||
This text will turn red when you look at it.
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RequestButton;
|
||||
53
client/app/components/http.jsx
Normal file
53
client/app/components/http.jsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getCookie } from './Cookie.jsx';
|
||||
|
||||
// class http {
|
||||
// dispatch(obj) {
|
||||
// console.log(obj);
|
||||
// }
|
||||
|
||||
function checkStatus(response) {
|
||||
const hasError = (response.status < 200 || response.status >= 300)
|
||||
if (hasError) {
|
||||
throw response.text();
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
function parseJSON(response) { return response.json(); }
|
||||
|
||||
|
||||
|
||||
// *
|
||||
// * Retrieve search results from tmdb with added seasoned information.
|
||||
// * @param {String} uri query you want to search for
|
||||
// * @param {Number} page representing pagination of results
|
||||
// * @returns {Promise} succeeds if results were found
|
||||
|
||||
// fetchSearch(uri) {
|
||||
// fetch(uri, {
|
||||
// method: 'GET',
|
||||
// headers: {
|
||||
// 'authorization': getCookie('token')
|
||||
// },
|
||||
// })
|
||||
// .then(response => {
|
||||
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// export default http;
|
||||
|
||||
export function fetchJSON(url, method, data) {
|
||||
return fetch(url, {
|
||||
method: method,
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
'authorization': getCookie('token'),
|
||||
'loggedinuser': getCookie('loggedInUser'),
|
||||
}),
|
||||
body: JSON.stringify(data)
|
||||
}).then(checkStatus).then(parseJSON);
|
||||
}
|
||||
34
client/app/components/images/loading.jsx
Normal file
34
client/app/components/images/loading.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
|
||||
function Loading() {
|
||||
return (
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<svg version="1.1"
|
||||
style={{height: '75px'}}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 80 80">
|
||||
<path
|
||||
fill="#e9a131"
|
||||
d="M40,72C22.4,72,8,57.6,8,40C8,22.4,
|
||||
22.4,8,40,8c17.6,0,32,14.4,32,32c0,1.1-0.9,2-2,2
|
||||
s-2-0.9-2-2c0-15.4-12.6-28-28-28S12,24.6,12,40s12.6,
|
||||
28,28,28c1.1,0,2,0.9,2,2S41.1,72,40,72z">
|
||||
|
||||
<animateTransform
|
||||
attributeType="xml"
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
from="0 40 40"
|
||||
to="360 40 40"
|
||||
dur="1.0s"
|
||||
repeatCount="indefinite"/>
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default Loading;
|
||||
109
client/app/components/redux/reducer.jsx
Normal file
109
client/app/components/redux/reducer.jsx
Normal file
@@ -0,0 +1,109 @@
|
||||
|
||||
import { setCookie } from '../Cookie.jsx';
|
||||
|
||||
const SET_LOGIN_PENDING = 'SET_LOGIN_PENDING';
|
||||
const SET_LOGIN_SUCCESS = 'SET_LOGIN_SUCCESS';
|
||||
const SET_LOGIN_ERROR = 'SET_LOGIN_ERROR';
|
||||
|
||||
export function login(email, password) {
|
||||
return dispatch => {
|
||||
dispatch(setLoginPending(true));
|
||||
dispatch(setLoginSuccess(false));
|
||||
dispatch(setLoginError(null));
|
||||
|
||||
callLoginApi(email, password, error => {
|
||||
dispatch(setLoginPending(false));
|
||||
if (!error) {
|
||||
dispatch(setLoginSuccess(true));
|
||||
} else {
|
||||
dispatch(setLoginError(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setLoginPending(isLoginPending) {
|
||||
return {
|
||||
type: SET_LOGIN_PENDING,
|
||||
isLoginPending
|
||||
};
|
||||
}
|
||||
|
||||
function setLoginSuccess(isLoginSuccess) {
|
||||
return {
|
||||
type: SET_LOGIN_SUCCESS,
|
||||
isLoginSuccess
|
||||
};
|
||||
}
|
||||
|
||||
function setLoginError(loginError) {
|
||||
return {
|
||||
type: SET_LOGIN_ERROR,
|
||||
loginError
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function callLoginApi(username, password, callback) {
|
||||
|
||||
Promise.resolve()
|
||||
fetch('https://apollo.kevinmidboe.com/api/v1/user/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
password: password,
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
switch (response.status) {
|
||||
case 200:
|
||||
response.json()
|
||||
.then((data) => {
|
||||
if (data.success === true) {
|
||||
let token = data.token;
|
||||
setCookie('token', token, 10);
|
||||
setCookie('logged_in', true, 10);
|
||||
setCookie('loggedInUser', username, 10);
|
||||
|
||||
window.location.reload();
|
||||
}
|
||||
return callback(null);
|
||||
})
|
||||
|
||||
case 401:
|
||||
return callback(new Error(response.statusText));
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
return callback(new Error('Invalid username and password'));
|
||||
});
|
||||
}
|
||||
|
||||
export default function reducer(state = {
|
||||
isLoginSuccess: false,
|
||||
isLoginPending: false,
|
||||
loginError: null
|
||||
}, action) {
|
||||
switch (action.type) {
|
||||
case SET_LOGIN_PENDING:
|
||||
return Object.assign({}, state, {
|
||||
isLoginPending: action.isLoginPending
|
||||
});
|
||||
|
||||
case SET_LOGIN_SUCCESS:
|
||||
return Object.assign({}, state, {
|
||||
isLoginSuccess: action.isLoginSuccess
|
||||
});
|
||||
|
||||
case SET_LOGIN_ERROR:
|
||||
return Object.assign({}, state, {
|
||||
loginError: action.loginError
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
7
client/app/components/redux/store.jsx
Normal file
7
client/app/components/redux/store.jsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createStore, applyMiddleware } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import logger from 'redux-logger';
|
||||
import reducer from './reducer.jsx';
|
||||
|
||||
const store = createStore(reducer, {}, applyMiddleware(thunk, logger));
|
||||
export default store;
|
||||
16
client/app/components/styles/adminComponent.jsx
Normal file
16
client/app/components/styles/adminComponent.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
export default {
|
||||
sidebar: {
|
||||
float: 'left',
|
||||
width: '18%',
|
||||
minWidth: '250px',
|
||||
fontFamily: '"Open Sans", sans-serif',
|
||||
fontSize: '14px',
|
||||
borderRight: '2px solid #f2f2f2',
|
||||
},
|
||||
selectedObjectPanel: {
|
||||
width: '80%',
|
||||
float: 'right',
|
||||
fontFamily: '"Open Sans", sans-serif',
|
||||
marginTop: '1em',
|
||||
}
|
||||
}
|
||||
58
client/app/components/styles/adminRequestInfo.jsx
Normal file
58
client/app/components/styles/adminRequestInfo.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
export default {
|
||||
wrapper: {
|
||||
width: '100%',
|
||||
},
|
||||
stick: {
|
||||
marginBottom: '1em',
|
||||
},
|
||||
|
||||
title: {
|
||||
fontSize: '2em',
|
||||
},
|
||||
image: {
|
||||
width: '105px',
|
||||
borderRadius: '4px',
|
||||
},
|
||||
|
||||
info: {
|
||||
paddingTop: '1em',
|
||||
paddingBottom: '0.5em',
|
||||
marginRight: '2em',
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid #d0d0d0',
|
||||
borderRadius: '2px',
|
||||
display: 'flex',
|
||||
},
|
||||
|
||||
type_icon: {
|
||||
marginLeft: '-0.2em',
|
||||
marginRight: '0.7em',
|
||||
},
|
||||
type_text: {
|
||||
verticalAlign: 'super',
|
||||
},
|
||||
|
||||
|
||||
info_poster: {
|
||||
marginLeft: '2em',
|
||||
flex: '0 1 10%'
|
||||
},
|
||||
|
||||
info_request: {
|
||||
flex: '0 1 auto'
|
||||
},
|
||||
info_request_header: {
|
||||
margin: '0',
|
||||
marginBottom: '0.5em',
|
||||
},
|
||||
|
||||
info_movie: {
|
||||
maxWidth: '70%',
|
||||
marginLeft: '1em',
|
||||
flex: '0 1 auto',
|
||||
},
|
||||
info_movie_header: {
|
||||
margin: '0',
|
||||
marginBottom: '0.5em',
|
||||
}
|
||||
}
|
||||
153
client/app/components/styles/adminSidebar.jsx
Normal file
153
client/app/components/styles/adminSidebar.jsx
Normal file
@@ -0,0 +1,153 @@
|
||||
export default {
|
||||
header: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
body: {
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
|
||||
parentElement: {
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
border: '1px solid grey',
|
||||
borderRadius: '2px',
|
||||
padding: '4px',
|
||||
margin: '4px',
|
||||
marginLeft: '4px',
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
|
||||
parentElement_hover: {
|
||||
backgroundColor: '#f8f8f8',
|
||||
pointer: 'hand',
|
||||
},
|
||||
|
||||
parentElement_active: {
|
||||
textDecoration: 'none',
|
||||
},
|
||||
|
||||
parentElement_selected: {
|
||||
display: 'inline-block',
|
||||
width: '100%',
|
||||
border: '1px solid grey',
|
||||
borderRadius: '2px',
|
||||
padding: '4px',
|
||||
margin: '4px 0px 4px 4px',
|
||||
marginLeft: '10px',
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
|
||||
title: {
|
||||
maxWidth: '65%',
|
||||
display: 'inline-flex',
|
||||
},
|
||||
|
||||
link: {
|
||||
color: 'black',
|
||||
textDecoration: 'none',
|
||||
},
|
||||
|
||||
rightContainer: {
|
||||
float: 'right',
|
||||
},
|
||||
|
||||
|
||||
|
||||
searchSidebar: {
|
||||
height: '4em',
|
||||
},
|
||||
searchInner: {
|
||||
top: '0',
|
||||
right: '0',
|
||||
left: '0',
|
||||
bottom: '0',
|
||||
margin: 'auto',
|
||||
width: '90%',
|
||||
minWidth: '280px',
|
||||
height: '30px',
|
||||
border: '1px solid #d0d0d0',
|
||||
borderRadius: '4px',
|
||||
overflow: 'hidden'
|
||||
},
|
||||
searchTextField: {
|
||||
display: 'inline-block',
|
||||
width: '90%',
|
||||
padding: '.3em',
|
||||
verticalAlign: 'middle',
|
||||
border: 'none',
|
||||
background: '#fff',
|
||||
fontSize: '14px',
|
||||
marginTop: '-7px',
|
||||
},
|
||||
searchIcon: {
|
||||
width: '15px',
|
||||
height: '16px',
|
||||
marginRight: '4px',
|
||||
marginTop: '7px',
|
||||
},
|
||||
searchSVGIcon: {
|
||||
fill: 'none',
|
||||
stroke: '#9d9d9d',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
strokeMiterlimit: '10',
|
||||
},
|
||||
|
||||
|
||||
ulFilterSelectors: {
|
||||
borderBottom: '2px solid #f1f1f1',
|
||||
display: 'flex',
|
||||
padding: '0',
|
||||
margin: '0',
|
||||
listStyle: 'none',
|
||||
justifyContent: 'space-evenly',
|
||||
},
|
||||
aFilterSelectors: {
|
||||
color: '#3eaaaf',
|
||||
fontSize: '16px',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
spanFilterSelectors: {
|
||||
content: '""',
|
||||
bottom: '-2px',
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: '2px',
|
||||
backgroundColor: '#3eaaaa',
|
||||
},
|
||||
|
||||
|
||||
ulCard: {
|
||||
margin: '1em 0 0 0',
|
||||
padding: '0',
|
||||
listStyle: 'none',
|
||||
borderBottom: '.46rem solid #f1f1f',
|
||||
backgroundColor: '#f1f1f1',
|
||||
overflow: 'scroll',
|
||||
},
|
||||
|
||||
|
||||
card: {
|
||||
padding: '.1em .5em .8em 1.5em',
|
||||
marginBottom: '.26rem',
|
||||
height: 'auto',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
cardSelected: {
|
||||
padding: '.1em .5em .8em 1.5em',
|
||||
marginBottom: '.26rem',
|
||||
height: 'auto',
|
||||
cursor: 'pointer',
|
||||
|
||||
backgroundColor: '#f9f9f9',
|
||||
},
|
||||
titleCard: {
|
||||
fontSize: '15px',
|
||||
fontWeight: '400',
|
||||
whiteSpace: 'no-wrap',
|
||||
textDecoration: 'none',
|
||||
},
|
||||
pCard: {
|
||||
margin: '0',
|
||||
},
|
||||
}
|
||||
59
client/app/components/styles/adminTorrentTable.jsx
Normal file
59
client/app/components/styles/adminTorrentTable.jsx
Normal file
@@ -0,0 +1,59 @@
|
||||
export default {
|
||||
table: {
|
||||
width: '80%',
|
||||
marginRight: 'auto',
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
tableHeader: {
|
||||
},
|
||||
col: {
|
||||
cursor: 'pointer',
|
||||
borderBottom: '1px solid #e0e0e0',
|
||||
paddingBottom: '0.5em',
|
||||
textAlign: 'left',
|
||||
},
|
||||
bodyCol: {
|
||||
marginTop: '0.5em',
|
||||
},
|
||||
|
||||
searchSidebar: {
|
||||
height: '4em',
|
||||
marginTop: '1em',
|
||||
},
|
||||
searchInner: {
|
||||
top: '0',
|
||||
right: '0',
|
||||
left: '0',
|
||||
bottom: '0',
|
||||
margin: 'auto',
|
||||
width: '50%',
|
||||
minWidth: '280px',
|
||||
height: '30px',
|
||||
border: '1px solid #d0d0d0',
|
||||
borderRadius: '4px',
|
||||
overflow: 'hidden'
|
||||
},
|
||||
searchTextField: {
|
||||
display: 'inline-block',
|
||||
width: '95%',
|
||||
padding: '.3em',
|
||||
verticalAlign: 'middle',
|
||||
border: 'none',
|
||||
background: '#fff',
|
||||
fontSize: '14px',
|
||||
marginTop: '-7px',
|
||||
},
|
||||
searchIcon: {
|
||||
width: '15px',
|
||||
height: '16px',
|
||||
marginRight: '4px',
|
||||
marginTop: '7px',
|
||||
},
|
||||
searchSVGIcon: {
|
||||
fill: 'none',
|
||||
stroke: '#9d9d9d',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
strokeMiterlimit: '10',
|
||||
},
|
||||
}
|
||||
80
client/app/components/styles/buttons.jsx
Normal file
80
client/app/components/styles/buttons.jsx
Normal file
@@ -0,0 +1,80 @@
|
||||
|
||||
export default {
|
||||
|
||||
submit: {
|
||||
color: '#e9a131',
|
||||
marginRight: '10px',
|
||||
backgroundColor: 'white',
|
||||
border: '#e9a131 2px solid',
|
||||
borderColor: '#e9a131',
|
||||
borderRadius: '4px',
|
||||
textAlign: 'center',
|
||||
padding: '10px',
|
||||
minWidth: '100px',
|
||||
float: 'left',
|
||||
fontSize: '13px',
|
||||
fontWeight: '800',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
|
||||
submit_hover: {
|
||||
backgroundColor: '#e9a131',
|
||||
color: 'white',
|
||||
},
|
||||
|
||||
info: {
|
||||
color: '#00d17c',
|
||||
marginRight: '10px',
|
||||
backgroundColor: 'white',
|
||||
border: '#00d17c 2px solid',
|
||||
borderRadius: '4px',
|
||||
textAlign: 'center',
|
||||
padding: '10px',
|
||||
minWidth: '100px',
|
||||
float: 'left',
|
||||
fontSize: '13px',
|
||||
fontWeight: '800',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
|
||||
info_hover: {
|
||||
backgroundColor: '#00d17c',
|
||||
color: 'white',
|
||||
},
|
||||
|
||||
edit: {
|
||||
color: '#4a95da',
|
||||
marginRight: '10px',
|
||||
backgroundColor: 'white',
|
||||
border: '#4a95da 2px solid',
|
||||
borderRadius: '4px',
|
||||
textAlign: 'center',
|
||||
padding: '10px',
|
||||
minWidth: '100px',
|
||||
float: 'left',
|
||||
fontSize: '13px',
|
||||
fontWeight: '800',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
|
||||
edit_small: {
|
||||
color: '#4a95da',
|
||||
marginRight: '10px',
|
||||
backgroundColor: 'white',
|
||||
border: '#4a95da 2px solid',
|
||||
borderRadius: '4px',
|
||||
textAlign: 'center',
|
||||
padding: '4px',
|
||||
minWidth: '50px',
|
||||
float: 'left',
|
||||
fontSize: '13px',
|
||||
fontWeight: '800',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
|
||||
edit_hover: {
|
||||
backgroundColor: '#4a95da',
|
||||
color: 'white',
|
||||
},
|
||||
|
||||
}
|
||||
24
client/app/components/styles/requestElementStyle.jsx
Normal file
24
client/app/components/styles/requestElementStyle.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
export default {
|
||||
bodyDiv: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
flexFlow: 'row wrap',
|
||||
justifyContent: 'space-around',
|
||||
},
|
||||
|
||||
wrappingDiv: {
|
||||
|
||||
},
|
||||
|
||||
requestPoster: {
|
||||
height: '150px',
|
||||
},
|
||||
|
||||
infoDiv: {
|
||||
marginTop: 0,
|
||||
marginLeft: '10px',
|
||||
float: 'right',
|
||||
},
|
||||
}
|
||||
62
client/app/components/styles/searchObject.jsx
Normal file
62
client/app/components/styles/searchObject.jsx
Normal file
@@ -0,0 +1,62 @@
|
||||
|
||||
export default {
|
||||
container: {
|
||||
maxWidth: '95%',
|
||||
margin: '0 auto',
|
||||
minHeight: '230px'
|
||||
},
|
||||
|
||||
title_large: {
|
||||
color: 'black',
|
||||
fontSize: '2em',
|
||||
},
|
||||
|
||||
title_small: {
|
||||
color: 'black',
|
||||
fontSize: '22px',
|
||||
},
|
||||
|
||||
stats_large: {
|
||||
fontSize: '0.8em'
|
||||
},
|
||||
|
||||
stats_small: {
|
||||
marginTop: '5px',
|
||||
fontSize: '0.8em'
|
||||
},
|
||||
|
||||
posterContainer: {
|
||||
float: 'left',
|
||||
zIndex: '3',
|
||||
position: 'relative',
|
||||
marginRight: '30px'
|
||||
},
|
||||
|
||||
posterImage: {
|
||||
border: '2px none',
|
||||
borderRadius: '2px',
|
||||
width: '150px'
|
||||
},
|
||||
|
||||
backgroundImage: {
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
buttons: {
|
||||
paddingTop: '20px',
|
||||
},
|
||||
|
||||
summary: {
|
||||
fontSize: '15px',
|
||||
},
|
||||
|
||||
dividerRow: {
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
itemDivider: {
|
||||
width: '90%',
|
||||
borderBottom: '1px solid grey',
|
||||
margin: '2rem auto'
|
||||
}
|
||||
}
|
||||
177
client/app/components/styles/searchRequestStyle.jsx
Normal file
177
client/app/components/styles/searchRequestStyle.jsx
Normal file
@@ -0,0 +1,177 @@
|
||||
|
||||
export default {
|
||||
body: {
|
||||
fontFamily: "'Open Sans', sans-serif",
|
||||
backgroundColor: '#f7f7f7',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
minHeight: '100%',
|
||||
},
|
||||
|
||||
backgroundLargeHeader: {
|
||||
width: '100%',
|
||||
minHeight: '180px',
|
||||
backgroundColor: 'rgb(1, 28, 35)',
|
||||
// backgroundImage: 'radial-gradient(circle, #004c67 0, #005771 120%)',
|
||||
zIndex: 1,
|
||||
marginBottom: '70px'
|
||||
},
|
||||
|
||||
backgroundSmallHeader: {
|
||||
width: '100%',
|
||||
minHeight: '120px',
|
||||
backgroundColor: '#011c23',
|
||||
zIndex: 1,
|
||||
marginBottom: '40px'
|
||||
},
|
||||
|
||||
requestWrapper: {
|
||||
maxWidth: '1200px',
|
||||
margin: 'auto',
|
||||
paddingTop: '10px',
|
||||
backgroundColor: 'white',
|
||||
position: 'relative',
|
||||
zIndex: '10',
|
||||
boxShadow: '0 1px 2px grey',
|
||||
},
|
||||
|
||||
pageTitle: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
},
|
||||
|
||||
pageTitleLargeSpan: {
|
||||
color: 'white',
|
||||
fontSize: '3em',
|
||||
marginTop: '4vh',
|
||||
marginBottom: '6vh'
|
||||
},
|
||||
|
||||
pageTitleSmallSpan: {
|
||||
color: 'white',
|
||||
fontSize: '2em',
|
||||
marginTop: '3vh',
|
||||
marginBottom: '3vh'
|
||||
},
|
||||
|
||||
searchLargeContainer: {
|
||||
height: '52px',
|
||||
width: '77%',
|
||||
paddingLeft: '23%',
|
||||
backgroundColor: 'white',
|
||||
boxShadow: 'grey 0px 1px 2px',
|
||||
},
|
||||
|
||||
searchSmallContainer: {
|
||||
},
|
||||
|
||||
searchIcon: {
|
||||
position: 'absolute',
|
||||
fontSize: '1.6em',
|
||||
marginTop: '7px',
|
||||
color: '#4f5b66',
|
||||
display: 'block',
|
||||
},
|
||||
|
||||
searchLargeBar: {
|
||||
width: '50%',
|
||||
height: '50px',
|
||||
background: '#ffffff',
|
||||
border: 'none',
|
||||
fontSize: '12pt',
|
||||
float: 'left',
|
||||
color: '#63717f',
|
||||
paddingLeft: '40px',
|
||||
},
|
||||
|
||||
searchSmallBar: {
|
||||
width: '100%',
|
||||
height: '50px',
|
||||
background: '#ffffff',
|
||||
border: 'none',
|
||||
fontSize: '11pt',
|
||||
float: 'left',
|
||||
color: '#63717f',
|
||||
paddingLeft: '65px',
|
||||
marginLeft: '-25px',
|
||||
borderRadius: '5px',
|
||||
},
|
||||
|
||||
|
||||
// Dropdown for selecting tmdb lists
|
||||
controls: {
|
||||
textAlign: 'left',
|
||||
paddingTop: '8px',
|
||||
width: '33.3333%',
|
||||
marginLeft: '0',
|
||||
marginRight: '0',
|
||||
},
|
||||
|
||||
withData: {
|
||||
boxSizing: 'border-box',
|
||||
marginBottom: '0',
|
||||
display: 'block',
|
||||
padding: '0',
|
||||
verticalAlign: 'baseline',
|
||||
font: 'inherit',
|
||||
textAlign: 'left',
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
|
||||
sortOptions: {
|
||||
border: '1px solid #000',
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden',
|
||||
lineHeight: 'normal',
|
||||
textAlign: 'left',
|
||||
padding: '4px 12px',
|
||||
paddingRight: '2rem',
|
||||
backgroundImage: 'url("data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxOCAxOCI+CiAgPHRpdGxlPmFycm93LWRvd24tbWljcm88L3RpdGxlPgogIDxwb2x5bGluZSBwb2ludHM9IjE0IDQuNjcgOSAxMy4zMyA0IDQuNjciIHN0eWxlPSJmaWxsOiBub25lO3N0cm9rZTogIzAwMDtzdHJva2UtbWl0ZXJsaW1pdDogMTA7c3Ryb2tlLXdpZHRoOiAycHgiLz4KPC9zdmc+Cg==")',
|
||||
backgroundSize: '18px 18px',
|
||||
backgroundPosition: 'right 8px center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
width: 'auto',
|
||||
display: 'inline-block',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
fontSize: '15px',
|
||||
WebkitAppearance: 'none',
|
||||
MozAppearance: 'none',
|
||||
appearance: 'none',
|
||||
},
|
||||
|
||||
|
||||
searchFilterActive: {
|
||||
color: '#00d17c',
|
||||
fontSize: '1em',
|
||||
marginLeft: '10px',
|
||||
cursor: 'pointer'
|
||||
},
|
||||
|
||||
searchFilterNotActive: {
|
||||
color: 'white',
|
||||
fontSize: '1em',
|
||||
marginLeft: '10px',
|
||||
cursor: 'pointer'
|
||||
},
|
||||
|
||||
filter: {
|
||||
color: 'white',
|
||||
paddingLeft: '40px',
|
||||
width: '60%',
|
||||
},
|
||||
|
||||
resultLargeHeader: {
|
||||
color: 'black',
|
||||
fontSize: '1.6em',
|
||||
width: '20%',
|
||||
},
|
||||
|
||||
resultSmallHeader: {
|
||||
paddingLeft: '12px',
|
||||
color: 'black',
|
||||
fontSize: '1.4em',
|
||||
},
|
||||
}
|
||||
@@ -2,9 +2,12 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300" rel="stylesheet">
|
||||
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0 maximum-scale=1.0, user-scalable=0">
|
||||
<title>seasoned Shows</title>
|
||||
</head>
|
||||
<body>
|
||||
<body style='margin: 0'>
|
||||
<div id="root">
|
||||
|
||||
</div>
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
* @Author: KevinMidboe
|
||||
* @Date: 2017-06-01 21:08:55
|
||||
* @Last Modified by: KevinMidboe
|
||||
* @Last Modified time: 2017-06-01 21:34:32
|
||||
* @Last Modified time: 2017-10-20 19:24:52
|
||||
|
||||
./client/index.js
|
||||
which is the webpack entry file
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './components/App.jsx';
|
||||
import { render } from 'react-dom';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
import Root from './Root.jsx';
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
render((
|
||||
<HashRouter>
|
||||
<Root />
|
||||
</HashRouter>
|
||||
), document.getElementById('root'));
|
||||
@@ -6,20 +6,38 @@
|
||||
"author": "Kevin Midboe",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"start": "webpack-dev-server"
|
||||
"start": "webpack-dev-server --open --config webpack.dev.js",
|
||||
"build": "NODE_ENV=production webpack --config webpack.prod.js",
|
||||
"build_dev": "webpack --config webpack.dev.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"clean-webpack-plugin": "^0.1.17",
|
||||
"css-loader": "^0.28.4",
|
||||
"html-webpack-plugin": "^2.28.0",
|
||||
"path": "^0.12.7",
|
||||
"react": "^15.5.4",
|
||||
"react": "^15.6.1",
|
||||
"react-burger-menu": "^2.1.6",
|
||||
"react-dom": "^15.5.4",
|
||||
"webpack": "^2.6.1",
|
||||
"webpack-dev-server": "^2.4.5"
|
||||
"react-infinite-scroller": "^1.0.15",
|
||||
"react-interactive": "^0.8.1",
|
||||
"react-notify-toast": "^0.3.2",
|
||||
"react-redux": "^5.0.6",
|
||||
"react-responsive": "^1.3.4",
|
||||
"react-router": "^4.2.0",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"redux": "^3.7.2",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"urijs": "^1.18.12",
|
||||
"webfontloader": "^1.6.28",
|
||||
"webpack": "^3.5.5",
|
||||
"webpack-dev-server": "^2.4.5",
|
||||
"webpack-merge": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.24.1",
|
||||
"babel-loader": "^7.0.0",
|
||||
"babel-preset-env": "^1.5.1",
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-preset-env": "^1.6.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-react": "^6.24.1"
|
||||
}
|
||||
|
||||
33
client/webpack.common.js
Normal file
33
client/webpack.common.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* @Author: KevinMidboe
|
||||
* @Date: 2017-06-01 19:09:16
|
||||
* @Last Modified by: KevinMidboe
|
||||
* @Last Modified time: 2017-10-24 21:55:41
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
app: './app/index.js',
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin(['dist']),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './app/index.html',
|
||||
})
|
||||
],
|
||||
module: {
|
||||
loaders: [
|
||||
{ test: /\.(js|jsx)$/, loader: 'babel-loader', exclude: /node_modules/ },
|
||||
]
|
||||
},
|
||||
|
||||
output: {
|
||||
filename: '[name].bundle.js',
|
||||
path: path.resolve(__dirname, 'dist')
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* @Author: KevinMidboe
|
||||
* @Date: 2017-06-01 19:09:16
|
||||
* @Last Modified by: KevinMidboe
|
||||
* @Last Modified time: 2017-06-01 22:11:51
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const HtmlWebpackPluginConfig = new HtmlWebpackPlugin({
|
||||
template: './app/index.html',
|
||||
filename: 'index.html',
|
||||
inject: 'body'
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
entry: './app/index.js',
|
||||
output: {
|
||||
path: path.resolve('dist'),
|
||||
filename: 'index_bundle.js'
|
||||
},
|
||||
devServer: {
|
||||
headers: { "Access-Control-Allow-Origin": "*" }
|
||||
},
|
||||
module: {
|
||||
loaders: [
|
||||
{ test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ },
|
||||
{ test: /\.jsx$/, loader: 'babel-loader', exclude: /node_modules/ }
|
||||
]
|
||||
},
|
||||
plugins: [HtmlWebpackPluginConfig]
|
||||
}
|
||||
17
client/webpack.dev.js
Normal file
17
client/webpack.dev.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* @Author: KevinMidboe
|
||||
* @Date: 2017-06-01 19:09:16
|
||||
* @Last Modified by: KevinMidboe
|
||||
* @Last Modified time: 2017-10-24 22:12:52
|
||||
*/
|
||||
|
||||
const merge = require('webpack-merge');
|
||||
const common = require('./webpack.common.js');
|
||||
|
||||
module.exports = merge(common, {
|
||||
devtool: 'inline-source-map',
|
||||
devServer: {
|
||||
contentBase: './dist',
|
||||
headers: {'Access-Control-Allow-Origin': '*'}
|
||||
}
|
||||
});;
|
||||
28
client/webpack.prod.js
Normal file
28
client/webpack.prod.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* @Author: KevinMidboe
|
||||
* @Date: 2017-06-01 19:09:16
|
||||
* @Last Modified by: KevinMidboe
|
||||
* @Last Modified time: 2017-10-24 22:26:29
|
||||
*/
|
||||
|
||||
const merge = require('webpack-merge');
|
||||
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const common = require('./webpack.common.js');
|
||||
var webpack = require('webpack')
|
||||
|
||||
module.exports = merge(common, {
|
||||
plugins: [
|
||||
new UglifyJSPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './app/index.html',
|
||||
title: 'Caching'
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
|
||||
}),
|
||||
],
|
||||
output: {
|
||||
filename: '[name].[chunkhash].js',
|
||||
}
|
||||
});
|
||||
3323
client/yarn.lock
3323
client/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"database": {
|
||||
"host": "shows.db"
|
||||
},
|
||||
"webserver": {
|
||||
"port": 31459
|
||||
},
|
||||
"tmdb": {
|
||||
"apiKey": "9fa154f5355c37a1b9b57ac06e7d6712"
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author: KevinMidboe
|
||||
# @Date: 2017-04-12 23:27:51
|
||||
# @Last Modified by: KevinMidboe
|
||||
# @Last Modified time: 2017-04-13 16:22:23
|
||||
|
||||
import sys, sqlite3, json, os
|
||||
import env_variables as env
|
||||
|
||||
class episode(object):
|
||||
def __init__(self, id):
|
||||
self.id = id
|
||||
self.getVarsFromDB()
|
||||
|
||||
def getVarsFromDB(self):
|
||||
c = sqlite3.connect(env.db_path).cursor()
|
||||
c.execute('SELECT parent, name, season, episode, video_files, subtitles, trash FROM stray_eps WHERE id = ?', (self.id,))
|
||||
returnMsg = c.fetchone()
|
||||
self.parent = returnMsg[0]
|
||||
self.name = returnMsg[1]
|
||||
self.season = returnMsg[2]
|
||||
self.episode = returnMsg[3]
|
||||
self.video_files = json.loads(returnMsg[4])
|
||||
self.subtitles = json.loads(returnMsg[5])
|
||||
self.trash = json.loads(returnMsg[6])
|
||||
c.close()
|
||||
|
||||
self.queries = {
|
||||
'parent': [env.show_dir, self.parent],
|
||||
'season': [env.show_dir, self.name, self.name + ' Season ' + "%02d" % self.season],
|
||||
'episode': [env.show_dir, self.name, self.name + ' Season ' + "%02d" % self.season, \
|
||||
self.name + ' S' + "%02d" % self.season + 'E' + "%02d" % self.episode],
|
||||
}
|
||||
|
||||
def typeDir(self, dType, create=False, mergeItem=None):
|
||||
url = '/'.join(self.queries[dType])
|
||||
if create and not os.path.isdir(url):
|
||||
os.makedirs(url)
|
||||
if mergeItem:
|
||||
return '/'.join([url, str(mergeItem)])
|
||||
return url
|
||||
|
||||
|
||||
def moveStray(strayId):
|
||||
ep = episode(strayId)
|
||||
|
||||
for item in ep.video_files:
|
||||
os.rename(ep.typeDir('parent', mergeItem=item[0]), ep.typeDir('episode', mergeItem=item[1], create=True))
|
||||
|
||||
for item in ep.subtitles:
|
||||
os.rename(ep.typeDir('parent', mergeItem=item[0]), ep.typeDir('episode', mergeItem=item[1], create=True))
|
||||
|
||||
for item in ep.trash:
|
||||
os.remove(ep.typeDir('parent', mergeItem=item))
|
||||
|
||||
os.rmdir(ep.typeDir('parent'))
|
||||
|
||||
if __name__ == '__main__':
|
||||
moveStray(sys.argv[-1])
|
||||
19
package.json
19
package.json
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "node-api",
|
||||
"main": "src/webserver/server.js",
|
||||
"scripts": {
|
||||
"start": "cross-env SEASONED_CONFIG=conf/development.json NODE_PATH=. node src/webserver/server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "~4.0.0",
|
||||
"mongoose": "~3.6.13",
|
||||
"body-parser": "~1.0.1",
|
||||
"cross-env": "^3.1.3",
|
||||
"sqlite": "^2.5.0",
|
||||
"request": "^2.81.0",
|
||||
"python-shell": "^0.4.0",
|
||||
"moviedb": "^0.2.7",
|
||||
"node-cache": "^4.1.1",
|
||||
"request-promise": "^4.2"
|
||||
}
|
||||
}
|
||||
5
renovate.json
Normal file
5
renovate.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base"
|
||||
]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
So to get people to sign up for a account could be to have them sign up for shows that they can be alerted when are added. They can choose by SMS, twitter, email or maybe newsletter.
|
||||
14
seasoned_api/.eslintrc.json
Normal file
14
seasoned_api/.eslintrc.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": [
|
||||
"airbnb-base"
|
||||
],
|
||||
"rules": {
|
||||
"indent": ["error", 3],
|
||||
"prefer-destructuring": 0,
|
||||
"camelcase": 0,
|
||||
"import/no-unresolved": 0,
|
||||
"import/no-extraneous-dependencies": 0,
|
||||
"object-shorthand": 0,
|
||||
"comma-dangle": 0
|
||||
}
|
||||
}
|
||||
66
seasoned_api/.gitignore
vendored
Normal file
66
seasoned_api/.gitignore
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
|
||||
# - - - - -
|
||||
# My own gitignore files and folders
|
||||
shows.db
|
||||
conf/development.json
|
||||
|
||||
# conf/development-prod.json
|
||||
20
seasoned_api/conf/development.json.example
Normal file
20
seasoned_api/conf/development.json.example
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"database": {
|
||||
"host": "../shows.db"
|
||||
},
|
||||
"webserver": {
|
||||
"port": 31459
|
||||
},
|
||||
"tmdb": {
|
||||
"apiKey": ""
|
||||
},
|
||||
"plex": {
|
||||
"ip": ""
|
||||
},
|
||||
"raven": {
|
||||
"DSN": ""
|
||||
},
|
||||
"authentication": {
|
||||
"secret": "secret"
|
||||
}
|
||||
}
|
||||
20
seasoned_api/conf/test.json
Normal file
20
seasoned_api/conf/test.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"database": {
|
||||
"host": ":memory:"
|
||||
},
|
||||
"webserver": {
|
||||
"port": 31400
|
||||
},
|
||||
"tmdb": {
|
||||
"apiKey": "bogus-api-key"
|
||||
},
|
||||
"plex": {
|
||||
"ip": "0.0.0.0"
|
||||
},
|
||||
"raven": {
|
||||
"DSN": ""
|
||||
},
|
||||
"authentication": {
|
||||
"secret": "secret"
|
||||
}
|
||||
}
|
||||
45
seasoned_api/package.json
Normal file
45
seasoned_api/package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "seasoned-api",
|
||||
"description": "Packages needed to build and commands to run seasoned api node server.",
|
||||
"license": {
|
||||
"type": "MIT",
|
||||
"url": "https://www.opensource.org/licenses/mit-license.php"
|
||||
},
|
||||
"main": "webserver/server.js",
|
||||
"scripts": {
|
||||
"start": "cross-env SEASONED_CONFIG=conf/development.json PROD=true NODE_PATH=. node src/webserver/server.js",
|
||||
"test": "cross-env SEASONED_CONFIG=conf/test.json NODE_PATH=. mocha --recursive test/unit test/system",
|
||||
"coverage": "cross-env SEASONED_CONFIG=conf/test.json NODE_PATH=. nyc mocha --recursive test && nyc report --reporter=text-lcov | coveralls",
|
||||
"lint": "./node_modules/.bin/eslint src/",
|
||||
"update": "cross-env SEASONED_CONFIG=conf/development.json NODE_PATH=. node src/plex/updateRequestsInPlex.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.18.0",
|
||||
"bcrypt-nodejs": "^0.0.3",
|
||||
"body-parser": "~1.18.2",
|
||||
"cross-env": "~5.1.4",
|
||||
"express": "~4.16.0",
|
||||
"jsonwebtoken": "^8.0.1",
|
||||
"km-moviedb": "^0.2.13",
|
||||
"mongoose": "~5.0.11",
|
||||
"km-moviedb": "^0.2.12",
|
||||
"node-cache": "^4.1.1",
|
||||
"python-shell": "^0.5.0",
|
||||
"request": "^2.85.0",
|
||||
"request-promise": "^4.2",
|
||||
"sqlite3": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"coveralls": "^3.0.0",
|
||||
"eslint": "^4.9.0",
|
||||
"eslint-config-airbnb-base": "^12.1.0",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"istanbul": "^0.4.5",
|
||||
"mocha": "^5.0.4",
|
||||
"mocha-lcov-reporter": "^1.3.0",
|
||||
"nyc": "^11.6.0",
|
||||
"raven": "^2.4.2",
|
||||
"supertest": "^3.0.0",
|
||||
"supertest-as-promised": "^4.0.1"
|
||||
}
|
||||
}
|
||||
43
seasoned_api/src/config/configuration.js
Normal file
43
seasoned_api/src/config/configuration.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const path = require('path');
|
||||
const Field = require('./field.js');
|
||||
|
||||
let instance = null;
|
||||
|
||||
class Config {
|
||||
constructor() {
|
||||
this.location = Config.determineLocation();
|
||||
this.fields = require(`${this.location}`);
|
||||
}
|
||||
|
||||
static getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new Config();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
static determineLocation() {
|
||||
return path.join(__dirname, '..', '..', process.env.SEASONED_CONFIG);
|
||||
}
|
||||
|
||||
get(section, option) {
|
||||
if (this.fields[section] === undefined || this.fields[section][option] === undefined) {
|
||||
throw new Error(`Filed "${section} => ${option}" does not exist.`);
|
||||
}
|
||||
|
||||
const field = new Field(this.fields[section][option]);
|
||||
|
||||
if (field.value === '') {
|
||||
const envField = process.env[['SEASONED', section.toUpperCase(), option.toUpperCase()].join('_')];
|
||||
if (envField !== undefined && envField.length !== 0) { return envField; }
|
||||
}
|
||||
|
||||
if (field.value === undefined) {
|
||||
throw new Error(`${section} => ${option} is empty.`);
|
||||
}
|
||||
|
||||
return field.value;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Config;
|
||||
15
seasoned_api/src/config/environmentVariables.js
Normal file
15
seasoned_api/src/config/environmentVariables.js
Normal file
@@ -0,0 +1,15 @@
|
||||
class EnvironmentVariables {
|
||||
constructor(variables) {
|
||||
this.variables = variables || process.env;
|
||||
}
|
||||
|
||||
get(variable) {
|
||||
return this.variables[variable];
|
||||
}
|
||||
|
||||
has(variable) {
|
||||
return this.get(variable) !== undefined;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EnvironmentVariables;
|
||||
49
seasoned_api/src/config/field.js
Normal file
49
seasoned_api/src/config/field.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const Filters = require('./filters.js');
|
||||
const EnvironmentVariables = require('./environmentVariables.js');
|
||||
|
||||
class Field {
|
||||
constructor(rawValue, environmentVariables) {
|
||||
this.rawValue = rawValue;
|
||||
this.filters = new Filters(rawValue);
|
||||
this.valueWithoutFilters = this.filters.removeFiltersFromValue();
|
||||
this.environmentVariables = new EnvironmentVariables(environmentVariables);
|
||||
}
|
||||
|
||||
get value() {
|
||||
if (this.filters.isEmpty()) {
|
||||
return this.valueWithoutFilters;
|
||||
}
|
||||
|
||||
if (this.filters.has('base64') && !this.filters.has('env')) {
|
||||
return Field.base64Decode(this.valueWithoutFilters);
|
||||
}
|
||||
|
||||
if (this.environmentVariables.has(this.valueWithoutFilters) &&
|
||||
this.environmentVariables.get(this.valueWithoutFilters) === '') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!this.filters.has('base64') && this.filters.has('env')) {
|
||||
if (this.environmentVariables.has(this.valueWithoutFilters)) {
|
||||
return this.environmentVariables.get(this.valueWithoutFilters);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (this.filters.has('env') && this.filters.has('base64')) {
|
||||
if (this.environmentVariables.has(this.valueWithoutFilters)) {
|
||||
const encodedEnvironmentVariable = this.environmentVariables.get(this.valueWithoutFilters);
|
||||
return Field.base64Decode(encodedEnvironmentVariable);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.valueWithoutFilters;
|
||||
}
|
||||
|
||||
static base64Decode(string) {
|
||||
return new Buffer(string, 'base64').toString('utf-8');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Field;
|
||||
34
seasoned_api/src/config/filters.js
Normal file
34
seasoned_api/src/config/filters.js
Normal file
@@ -0,0 +1,34 @@
|
||||
class Filters {
|
||||
constructor(value) {
|
||||
this.value = value;
|
||||
this.delimiter = '|';
|
||||
}
|
||||
|
||||
get filters() {
|
||||
return this.value.split(this.delimiter).slice(0, -1);
|
||||
}
|
||||
|
||||
isEmpty() {
|
||||
return !this.hasValidType() || this.value.length === 0;
|
||||
}
|
||||
|
||||
has(filter) {
|
||||
return this.filters.includes(filter);
|
||||
}
|
||||
|
||||
hasValidType() {
|
||||
return (typeof this.value === 'string');
|
||||
}
|
||||
|
||||
removeFiltersFromValue() {
|
||||
if (this.hasValidType() === false) {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
let filtersCombined = this.filters.join(this.delimiter);
|
||||
filtersCombined += this.filters.length >= 1 ? this.delimiter : '';
|
||||
return this.value.replace(filtersCombined, '');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Filters;
|
||||
@@ -1,7 +1,7 @@
|
||||
const configuration = require('src/config/configuration').getInstance();
|
||||
const SqliteDatabase = require('src/database/sqliteDatabase');
|
||||
const database = new SqliteDatabase(configuration.get('database', 'host'));
|
||||
|
||||
const database = new SqliteDatabase(configuration.get('database', 'host'));
|
||||
/**
|
||||
* This module establishes a connection to the database
|
||||
* specified in the confgiuration file. It tries to setup
|
||||
@@ -9,7 +9,6 @@ const database = new SqliteDatabase(configuration.get('database', 'host'));
|
||||
* If the tables already exists, it simply proceeds.
|
||||
*/
|
||||
Promise.resolve()
|
||||
.then(() => database.connect())
|
||||
// .then(() => database.setUp());
|
||||
.then(() => database.setUp());
|
||||
|
||||
module.exports = database;
|
||||
86
seasoned_api/src/database/schemas/setup.sql
Normal file
86
seasoned_api/src/database/schemas/setup.sql
Normal file
@@ -0,0 +1,86 @@
|
||||
CREATE TABLE IF NOT EXISTS user (
|
||||
user_name varchar(127) UNIQUE,
|
||||
password varchar(127),
|
||||
email varchar(127) UNIQUE,
|
||||
admin boolean DEFAULT 0,
|
||||
primary key (user_name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cache (
|
||||
key varchar(255),
|
||||
value blob,
|
||||
time_to_live INTEGER DEFAULT 60,
|
||||
created_at DATE DEFAULT (datetime('now','localtime')),
|
||||
primary key(key)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS search_history (
|
||||
id integer,
|
||||
user_name varchar(127),
|
||||
search_query varchar(255),
|
||||
primary key (id),
|
||||
foreign key(user_name) REFERENCES user(user_name) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS requests(
|
||||
id TEXT,
|
||||
title TEXT,
|
||||
year NUMBER,
|
||||
poster_path TEXT DEFAULT NULL,
|
||||
background_path TEXT DEFAULT NULL,
|
||||
requested_by TEXT,
|
||||
ip TEXT,
|
||||
date DATE DEFAULT CURRENT_TIMESTAMP,
|
||||
status CHAR(25) DEFAULT 'requested' NOT NULL,
|
||||
user_agent CHAR(255) DEFAULT NULL,
|
||||
type CHAR(50) DEFAULT 'movie'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS request(
|
||||
id int not null,
|
||||
title text not null,
|
||||
year int not null,
|
||||
type char(10) not null,
|
||||
date timestamp default (strftime('%s', 'now'))
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS stray_eps(
|
||||
id TEXT UNIQUE,
|
||||
parent TEXT,
|
||||
path TEXT,
|
||||
name TEXT,
|
||||
season NUMBER,
|
||||
episode NUMBER,
|
||||
video_files TEXT,
|
||||
subtitles TEXT,
|
||||
trash TEXT,
|
||||
verified BOOLEAN DEFAULT 0,
|
||||
primary key(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS shows(
|
||||
show_names TEXT,
|
||||
date_added DATE,
|
||||
date_modified DATE DEFUALT CURRENT_DATE NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS requested_torrent (
|
||||
magnet TEXT UNIQUE,
|
||||
torrent_name TEXT,
|
||||
tmdb_id TEXT
|
||||
date_added DATE DEFAULT (datetime('now','localtime'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS deluge_torrent (
|
||||
key TEXT UNIQUE,
|
||||
name TEXT,
|
||||
progress TEXT,
|
||||
eta NUMBER,
|
||||
save_path TEXT,
|
||||
state TEXT,
|
||||
paused BOOLEAN,
|
||||
finished BOOLEAN,
|
||||
files TEXT,
|
||||
is_folder BOOLEAN
|
||||
)
|
||||
4
seasoned_api/src/database/schemas/teardown.sql
Normal file
4
seasoned_api/src/database/schemas/teardown.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
DROP TABLE IF EXISTS user;
|
||||
DROP TABLE IF EXISTS search_history;
|
||||
DROP TABLE IF EXISTS requests;
|
||||
DROP TABLE IF EXISTS request;
|
||||
119
seasoned_api/src/database/sqliteDatabase.js
Normal file
119
seasoned_api/src/database/sqliteDatabase.js
Normal file
@@ -0,0 +1,119 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
|
||||
class SqliteDatabase {
|
||||
constructor(host) {
|
||||
this.host = host;
|
||||
this.connection = new sqlite3.Database(this.host);
|
||||
this.schemaDirectory = path.join(__dirname, 'schemas');
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the database.
|
||||
* @returns {Promise} succeeds if connection was established
|
||||
*/
|
||||
// connect() {
|
||||
// let database = ;
|
||||
// this.connection = database;
|
||||
// return database;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Run a SQL query against the database.
|
||||
* @param {String} sql SQL query
|
||||
* @param {Array} parameters in the SQL query
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async run(sql, parameters) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.connection.run(sql, parameters, (error, result) => {
|
||||
if (error)
|
||||
reject(error);
|
||||
resolve(result)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a SQL query against the database and retrieve all the rows.
|
||||
* @param {String} sql SQL query
|
||||
* @param {Array} parameters in the SQL query
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async all(sql, parameters) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.connection.all(sql, parameters, (err, rows) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve(rows);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a SQL query against the database and retrieve one row.
|
||||
* @param {String} sql SQL query
|
||||
* @param {Array} parameters in the SQL query
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async get(sql, parameters) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.connection.get(sql, parameters, (err, rows) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
resolve(rows);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a SQL query against the database and retrieve the status.
|
||||
* @param {String} sql SQL query
|
||||
* @param {Array} parameters in the SQL query
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async execute(sql) {
|
||||
return new Promise(resolve => {
|
||||
this.connection.exec(sql, (err, database) => {
|
||||
if (err) {
|
||||
console.log('ERROR: ', err);
|
||||
reject(err);
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the database by running setup.sql file in schemas/.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
setUp() {
|
||||
const setupSchema = this.readSqlFile('setup.sql');
|
||||
return Promise.resolve(this.execute(setupSchema));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tears down the database by running tearDown.sql file in schemas/.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
tearDown() {
|
||||
const tearDownSchema = this.readSqlFile('teardown.sql');
|
||||
return Promise.resolve(this.execute(tearDownSchema));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file contents of a SQL file in schemas/.
|
||||
* @returns {String}
|
||||
*/
|
||||
readSqlFile(filename) {
|
||||
const schemaPath = path.join(this.schemaDirectory, filename);
|
||||
const schema = fs.readFileSync(schemaPath).toString('utf-8');
|
||||
return schema;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SqliteDatabase;
|
||||
9
seasoned_api/src/git/gitRepository.js
Normal file
9
seasoned_api/src/git/gitRepository.js
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
class GitRepository {
|
||||
static dumpHook(body) {
|
||||
/* eslint-disable no-console */
|
||||
console.log(body);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GitRepository;
|
||||
19
seasoned_api/src/media_classes/media.js
Normal file
19
seasoned_api/src/media_classes/media.js
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
class Media {
|
||||
constructor(title, year, type) {
|
||||
this.title = title;
|
||||
this.year = year;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `N: ${this.title} | Y: ${this.year} | T: ${this.type}`;
|
||||
}
|
||||
|
||||
print() {
|
||||
/* eslint-disable no-console */
|
||||
console.log(this.toString());
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Media;
|
||||
15
seasoned_api/src/media_classes/mediaInfo.js
Normal file
15
seasoned_api/src/media_classes/mediaInfo.js
Normal file
@@ -0,0 +1,15 @@
|
||||
class MediaInfo {
|
||||
constructor() {
|
||||
this.duration = undefined;
|
||||
this.height = undefined;
|
||||
this.width = undefined;
|
||||
this.bitrate = undefined;
|
||||
this.resolution = undefined;
|
||||
this.framerate = undefined;
|
||||
this.protocol = undefined;
|
||||
this.container = undefined;
|
||||
this.audioCodec = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MediaInfo;
|
||||
12
seasoned_api/src/media_classes/player.js
Normal file
12
seasoned_api/src/media_classes/player.js
Normal file
@@ -0,0 +1,12 @@
|
||||
class Player {
|
||||
constructor(device, address) {
|
||||
this.device = device;
|
||||
this.ip = address;
|
||||
this.platform = undefined;
|
||||
this.product = undefined;
|
||||
this.title = undefined;
|
||||
this.state = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Player;
|
||||
22
seasoned_api/src/media_classes/plex.js
Normal file
22
seasoned_api/src/media_classes/plex.js
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
const Media = require('src/media_classes/media');
|
||||
|
||||
class Plex extends Media {
|
||||
constructor(title, year, type, summary, poster_path, background_path, added, seasons, episodes) {
|
||||
super(title, year, type);
|
||||
|
||||
this.summary = summary;
|
||||
this.poster_path = poster_path;
|
||||
this.background_path = background_path;
|
||||
this.added = added;
|
||||
|
||||
this.seasons = seasons;
|
||||
this.episodes = episodes;
|
||||
}
|
||||
|
||||
print() {
|
||||
super.print();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Plex;
|
||||
33
seasoned_api/src/media_classes/tmdb.js
Normal file
33
seasoned_api/src/media_classes/tmdb.js
Normal file
@@ -0,0 +1,33 @@
|
||||
|
||||
const Media = require('src/media_classes/media');
|
||||
|
||||
class TMDB extends Media {
|
||||
// constructor(...args) {
|
||||
constructor(title, year, type, id, summary, poster_path, background_path, popularity, score, release_status, tagline, seasons, episodes) {
|
||||
super(title, year, type);
|
||||
|
||||
this.id = id;
|
||||
this.summary = summary;
|
||||
this.poster_path = poster_path;
|
||||
this.background_path = background_path;
|
||||
this.popularity = popularity;
|
||||
this.score = score;
|
||||
|
||||
this.release_status = release_status;
|
||||
this.tagline = tagline;
|
||||
|
||||
this.seasons = seasons;
|
||||
this.episodes = episodes;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `${super.toString()} | ID: ${this.id}`;
|
||||
}
|
||||
|
||||
print() {
|
||||
/* eslint-disable no-console */
|
||||
console.log(this.toString());
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TMDB;
|
||||
8
seasoned_api/src/media_classes/user.js
Normal file
8
seasoned_api/src/media_classes/user.js
Normal file
@@ -0,0 +1,8 @@
|
||||
class User {
|
||||
constructor(id, title) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = User;
|
||||
87
seasoned_api/src/pirate/pirateRepository.js
Normal file
87
seasoned_api/src/pirate/pirateRepository.js
Normal file
@@ -0,0 +1,87 @@
|
||||
const assert = require('assert');
|
||||
const http = require('http');
|
||||
const { URL } = require('url');
|
||||
const PythonShell = require('python-shell');
|
||||
|
||||
const establishedDatabase = require('src/database/database');
|
||||
|
||||
function getMagnetFromURL(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = new URL(url);
|
||||
if (options.protocol.includes('magnet'))
|
||||
resolve(url)
|
||||
|
||||
http.get(options, (res) => {
|
||||
if (res.statusCode == 301) {
|
||||
resolve(res.headers.location)
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function find(searchterm, callback) {
|
||||
const options = {
|
||||
pythonPath: '../torrent_search/env/bin/python3.6',
|
||||
scriptPath: '../torrent_search',
|
||||
args: [searchterm, '-s', 'jackett', '-f', '--print']
|
||||
}
|
||||
|
||||
PythonShell.run('torrentSearch/search.py', options, callback);
|
||||
// PythonShell does not support return
|
||||
}
|
||||
|
||||
|
||||
async function callPythonAddMagnet(url, callback) {
|
||||
getMagnetFromURL(url)
|
||||
.then((magnet) => {
|
||||
const options = {
|
||||
pythonPath: '../delugeClient/env/bin/python3.6',
|
||||
scriptPath: '../delugeClient',
|
||||
args: ['add', magnet]
|
||||
};
|
||||
|
||||
PythonShell.run('deluge_cli.py', options, callback);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
throw new Error(err);
|
||||
})
|
||||
}
|
||||
|
||||
async function SearchPiratebay(query) {
|
||||
return await new Promise((resolve, reject) => find(query, (err, results) => {
|
||||
if (err) {
|
||||
/* eslint-disable no-console */
|
||||
console.log('THERE WAS A FUCKING ERROR!\n', err);
|
||||
reject(Error('There was a error when searching for torrents'));
|
||||
}
|
||||
if (results) {
|
||||
/* eslint-disable no-console */
|
||||
console.log('result', results);
|
||||
resolve(JSON.parse(results, null, '\t'));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
async function AddMagnet(magnet, name, tmdb_id) {
|
||||
return await new Promise((resolve, reject) => callPythonAddMagnet(magnet, (err, results) => {
|
||||
if (err) {
|
||||
/* eslint-disable no-console */
|
||||
console.log(err);
|
||||
reject(Error('Enable to add torrent', err))
|
||||
}
|
||||
/* eslint-disable no-console */
|
||||
console.log('result/error:', err, results);
|
||||
|
||||
database = establishedDatabase;
|
||||
insert_query = "INSERT INTO requested_torrent(magnet,torrent_name,tmdb_id) \
|
||||
VALUES (?,?,?)";
|
||||
|
||||
let response = database.run(insert_query, [magnet, name, tmdb_id]);
|
||||
console.log('Response from requsted_torrent insert: ' + response);
|
||||
|
||||
resolve({ success: true });
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = { SearchPiratebay, AddMagnet };
|
||||
20
seasoned_api/src/plex/convertPlexToEpisode.js
Normal file
20
seasoned_api/src/plex/convertPlexToEpisode.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const Episode = require('src/plex/types/episode');
|
||||
|
||||
function convertPlexToEpisode(plexEpisode) {
|
||||
const episode = new Episode(plexEpisode.title, plexEpisode.grandparentTitle, plexEpisode.year);
|
||||
episode.season = plexEpisode.parentIndex;
|
||||
episode.episode = plexEpisode.index;
|
||||
episode.summary = plexEpisode.summary;
|
||||
episode.rating = plexEpisode.rating;
|
||||
|
||||
if (plexEpisode.viewCount !== undefined) {
|
||||
episode.views = plexEpisode.viewCount;
|
||||
}
|
||||
|
||||
if (plexEpisode.originallyAvailableAt !== undefined) {
|
||||
episode.airdate = new Date(plexEpisode.originallyAvailableAt)
|
||||
}
|
||||
|
||||
return episode;
|
||||
}
|
||||
module.exports = convertPlexToEpisode;
|
||||
15
seasoned_api/src/plex/convertPlexToMovie.js
Normal file
15
seasoned_api/src/plex/convertPlexToMovie.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const Movie = require('src/plex/types/movie');
|
||||
|
||||
function convertPlexToMovie(plexMovie) {
|
||||
const movie = new Movie(plexMovie.title, plexMovie.year);
|
||||
movie.rating = plexMovie.rating;
|
||||
movie.tagline = plexMovie.tagline;
|
||||
|
||||
if (plexMovie.summary !== undefined) {
|
||||
movie.summary = plexMovie.summary;
|
||||
}
|
||||
|
||||
return movie;
|
||||
}
|
||||
|
||||
module.exports = convertPlexToMovie;
|
||||
24
seasoned_api/src/plex/convertPlexToSeasoned.js
Normal file
24
seasoned_api/src/plex/convertPlexToSeasoned.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const Plex = require('src/media_classes/plex');
|
||||
|
||||
function translateAdded(date_string) {
|
||||
return new Date(date_string * 1000);
|
||||
}
|
||||
|
||||
function convertPlexToSeasoned(plex) {
|
||||
const title = plex.title;
|
||||
const year = plex.year;
|
||||
const type = plex.type;
|
||||
const summary = plex.summary;
|
||||
const poster_path = plex.thumb;
|
||||
const background_path = plex.art;
|
||||
const added = translateAdded(plex.addedAt);
|
||||
// const genre = plex.genre;
|
||||
const seasons = plex.childCount;
|
||||
const episodes = plex.leafCount;
|
||||
|
||||
const seasoned = new Plex(title, year, type, summary, poster_path, background_path, added, seasons, episodes);
|
||||
// seasoned.print();
|
||||
return seasoned;
|
||||
}
|
||||
|
||||
module.exports = convertPlexToSeasoned;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user