mirror of
https://github.com/AnimeThemes/animethemes-server.git
synced 2026-07-11 01:24:46 +02:00
Compare commits
684 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0404625001 | |||
| 72b24f2eb6 | |||
| 00c60c58c4 | |||
| 5be6f661a4 | |||
| 4e1d4236fc | |||
| 75f391cc65 | |||
| 80d59f7e45 | |||
| b1d7e9ed30 | |||
| a1341d0157 | |||
| cac463383e | |||
| a433a7484b | |||
| 8cae74c677 | |||
| eeb5fd006a | |||
| b9b95acb0b | |||
| 582ca2b7dd | |||
| 884f6921a0 | |||
| 3ae5898927 | |||
| 37e21989bb | |||
| 756d57bfeb | |||
| 042decc60c | |||
| 2f1a55a433 | |||
| cd11c5fa3b | |||
| 93da5aafe4 | |||
| 07933577c1 | |||
| 00560e9ab4 | |||
| 6c4378b29a | |||
| a5483871d4 | |||
| 549f5b6461 | |||
| 544155052c | |||
| e522eaf1f9 | |||
| b6d42e21b4 | |||
| 4f61a86638 | |||
| fcaa13891b | |||
| 8d350a89f4 | |||
| 3ec851fc23 | |||
| 7d949e8947 | |||
| c42771a411 | |||
| fa8d223b4b | |||
| 0f6b009e37 | |||
| f370caf6cf | |||
| 23df360b60 | |||
| efcc70c332 | |||
| 0e1235cf5a | |||
| 5b9a8e3976 | |||
| d2055c3726 | |||
| 37076c44cd | |||
| aff2250f48 | |||
| fc3f146bb4 | |||
| 722aaf9c14 | |||
| d8f4db64a7 | |||
| b73d98fc2d | |||
| 6a0ddcad96 | |||
| a230b803b7 | |||
| 5839dfc4c2 | |||
| cd2039d6e1 | |||
| b05a6022ac | |||
| ea21ce28ee | |||
| 2ae33b0fed | |||
| 5d22f66eba | |||
| 8323620f83 | |||
| 6b1f130843 | |||
| e84df9059e | |||
| 8386535251 | |||
| 93a48173e5 | |||
| 8995240844 | |||
| 26f9885968 | |||
| be08cf0d11 | |||
| 24ec43bdab | |||
| a80f8d4d35 | |||
| fb05c1770e | |||
| e4e28b74b5 | |||
| 74cfefb20e | |||
| c1bf8be3a1 | |||
| b66c5cb3da | |||
| e4179cf312 | |||
| 73ea075f54 | |||
| 92c54ccdf5 | |||
| d2ced8db70 | |||
| fcf64b6406 | |||
| dff1634480 | |||
| ef56c3e130 | |||
| 5061fc126d | |||
| 1f5e48da37 | |||
| a2d159a90d | |||
| 5e7b03b167 | |||
| 747d1d8fc3 | |||
| e99e692694 | |||
| c7e83e0ae9 | |||
| 3815716a9f | |||
| b685c656df | |||
| 3356167e3b | |||
| 7d676d3010 | |||
| f8786cb6bf | |||
| da54035060 | |||
| 5817636003 | |||
| 2257d4b6ed | |||
| cdfcc901ab | |||
| af2946155e | |||
| 742dc20106 | |||
| 03103389b2 | |||
| 44b9354495 | |||
| 6c86a0b9de | |||
| b09ceebe4b | |||
| 58cc05a07e | |||
| 7c5075b188 | |||
| 60c5351b04 | |||
| c6f43a395e | |||
| fa5468d1a5 | |||
| 7eac12d788 | |||
| a71ca35efb | |||
| 05b6bb0a03 | |||
| a4975c8813 | |||
| 45ea839c4e | |||
| 1f815a6082 | |||
| fdb6b69687 | |||
| 01411c6f77 | |||
| 1f8b9daa1f | |||
| 9fb9c5e11b | |||
| d8dc07010a | |||
| dfd958ef69 | |||
| 33650905d9 | |||
| 871b9fe86f | |||
| 117970261b | |||
| 1ee1067594 | |||
| 4120a73228 | |||
| 4f7ab1ca5a | |||
| bcc9dec704 | |||
| 6d9d549d52 | |||
| b7d10d81e8 | |||
| 10ae7b865f | |||
| d74ba4c8e5 | |||
| 84cdc19fb0 | |||
| 22918e3adb | |||
| c2d6b81158 | |||
| 743b319340 | |||
| 67d759a129 | |||
| d88376a289 | |||
| ece37dcc73 | |||
| 1aa38b391d | |||
| 6ac84415c8 | |||
| bd3419715c | |||
| 3f1bd8a327 | |||
| 110950e0f2 | |||
| 1c71ed0571 | |||
| f90c3d3732 | |||
| 60406d89fa | |||
| 90b7a2820e | |||
| 48208ce42d | |||
| 3d1d2dcfbc | |||
| d73ff841bd | |||
| 869677e49f | |||
| c91de1acf9 | |||
| 03dd572300 | |||
| 8077b37b9d | |||
| 245aadf381 | |||
| 144994676e | |||
| aad47fdc90 | |||
| fefff9b8ab | |||
| 7b786cf911 | |||
| 696a9882a9 | |||
| 8258bc05af | |||
| 1745f1f32f | |||
| ac5df24981 | |||
| 037a460add | |||
| 3306e8502e | |||
| 01eb8c175d | |||
| ee7549f9c6 | |||
| f95cac61a2 | |||
| 3698939f88 | |||
| 75696b7a15 | |||
| 2eb9d2e75d | |||
| a4c2d3a0de | |||
| 44c412fc32 | |||
| 4a6739b8cd | |||
| 0f290b1677 | |||
| f0437bf764 | |||
| d225bd60fc | |||
| 1f8827996c | |||
| 59e770bef0 | |||
| f75765f27e | |||
| 7a9dc62022 | |||
| af88a1098e | |||
| 7b69fbadd8 | |||
| 07cdddcbcb | |||
| 8dd25de1a4 | |||
| c0ffa7083d | |||
| 56737563b2 | |||
| e5c3273755 | |||
| 848c6efa69 | |||
| 5fd00ec028 | |||
| 5120342f37 | |||
| 12d53fd99a | |||
| e0011d8e98 | |||
| 270ca8ed7b | |||
| 3de70d1199 | |||
| 3f7cbdf24a | |||
| efef58eb24 | |||
| da93812c12 | |||
| 4a7b960d34 | |||
| 6721443646 | |||
| 520578e97a | |||
| 1aa4d020a8 | |||
| e9246d0e0d | |||
| 506c7e115f | |||
| f074b82d80 | |||
| ba7fb60383 | |||
| 269cf22996 | |||
| ad899b7271 | |||
| ac9b4be61b | |||
| 0ed5ad68d8 | |||
| b262fdf08c | |||
| 414836a58f | |||
| 63cb73fe8d | |||
| ff6aa20807 | |||
| ec29509163 | |||
| ed8a6b9212 | |||
| bc33705162 | |||
| 4241c6ed5e | |||
| 164a9f9573 | |||
| 4dc8bb9238 | |||
| 128a2bf9cf | |||
| 8c62a8e892 | |||
| 0a05a6c73b | |||
| 213727c2a2 | |||
| a97e1d5abd | |||
| 08dfb5b099 | |||
| a4ed8f257e | |||
| db4495436a | |||
| a4d3191294 | |||
| 69c748dd9c | |||
| a4ef61b134 | |||
| ab572e951c | |||
| b220154cd3 | |||
| 3173c2818a | |||
| ad7c23b788 | |||
| 7c22f3acc1 | |||
| 9963515a31 | |||
| bdceea0a7c | |||
| ed8322d62a | |||
| 7e2712386d | |||
| 7cdf7c90dc | |||
| a15a9d141c | |||
| 01827680b6 | |||
| 40c334ccb3 | |||
| 7382ed4a37 | |||
| 5614c0d4c6 | |||
| 4eb60cd6f3 | |||
| 13074d716a | |||
| 9b5b69fb01 | |||
| da39dc0b74 | |||
| 1348e16648 | |||
| 4b3ecbe793 | |||
| eb265c839b | |||
| 9b08e233e3 | |||
| dc522d1a5f | |||
| 444884921c | |||
| c540c7bb7f | |||
| 8bfa2bb44c | |||
| 7513cf90fe | |||
| b4542a50c8 | |||
| 5ad3ab5c0f | |||
| 5ac1e9f3fe | |||
| 7fffee1ebb | |||
| 87adc2ff32 | |||
| 6a4b3c37c7 | |||
| 5501526a1d | |||
| 1b5f42e601 | |||
| 6fe8385054 | |||
| e1029cb4ab | |||
| 0484fd73cb | |||
| 3a79dc85c8 | |||
| 8f2787553c | |||
| 7c146e465f | |||
| 6ae0cf12b1 | |||
| 0a9f393c8c | |||
| 3860f3ef73 | |||
| 51bcd875d4 | |||
| f6d75ea29e | |||
| a90befebfe | |||
| e58518f51a | |||
| a2ed59cc01 | |||
| 5c5a9fda71 | |||
| 1b72050cda | |||
| 2400f663e1 | |||
| 9070068308 | |||
| aed66607be | |||
| 239664851b | |||
| 799d258857 | |||
| a0788eeeea | |||
| 8065d567f3 | |||
| c8266936e5 | |||
| a5c3914478 | |||
| 173407348d | |||
| 25cf1a3a08 | |||
| ae26b4135c | |||
| 904b8a8b65 | |||
| 2cba594edf | |||
| 1a07ac1d00 | |||
| 5c5e96f2e6 | |||
| 076eea744a | |||
| 1c23cc3298 | |||
| 7fd7141a38 | |||
| 37662dd4c4 | |||
| 7bb4c221ce | |||
| f8b9aca0fc | |||
| 0fb8c9b372 | |||
| 413498e153 | |||
| 7afa555e2a | |||
| b24dfebbf3 | |||
| f84a4b33a5 | |||
| 1dd9a8a606 | |||
| 9ad19f260a | |||
| 6bbfa1e86a | |||
| 7b75be439c | |||
| b0dee5998f | |||
| 286796bc0f | |||
| 1eaa7fff9b | |||
| 77869066f2 | |||
| e525cd7367 | |||
| ecf7f2d7da | |||
| 59f8ce12dc | |||
| 6fc78d3709 | |||
| f9c932fd30 | |||
| 3417eb63a1 | |||
| e7b2e540c2 | |||
| a32fa7f6d3 | |||
| c4385cf6d9 | |||
| 682e02895c | |||
| 63395481f7 | |||
| abbadf9fcd | |||
| c7ba086947 | |||
| 69cc755077 | |||
| 6f8bcd9ba0 | |||
| 002358af1b | |||
| 38f0e2fa76 | |||
| 453cc1e72a | |||
| c0db39c697 | |||
| b426620534 | |||
| 7d5156e937 | |||
| 5962a7c3c2 | |||
| efbdcc1e6a | |||
| d468811adc | |||
| 6c3837aca2 | |||
| 88aeaecafa | |||
| bc9df20162 | |||
| 9e46ba9a2c | |||
| 5fb12ae228 | |||
| 936aafcc4f | |||
| 7a2fb3fbfd | |||
| b181a0b254 | |||
| 7178470f50 | |||
| d49085432c | |||
| ad79ed1117 | |||
| fdceea037c | |||
| d4b39d0c8e | |||
| fc7ec8d3d1 | |||
| 7bfa1ec0dd | |||
| 89ce3e05be | |||
| aad7f54c92 | |||
| c406d73270 | |||
| d45545e7e1 | |||
| d609fbd68a | |||
| e10bcfc0df | |||
| 33f62dae1a | |||
| 912a9c737b | |||
| af1d86f8d8 | |||
| d34309467a | |||
| a38c023caa | |||
| 310e300bbd | |||
| 2a856c3eee | |||
| 7be14cb615 | |||
| c67af74c5f | |||
| 3ff694fba2 | |||
| ed6662c11f | |||
| e374b7d1d2 | |||
| 55e37e1f3c | |||
| 7c23f5fb4b | |||
| 6c4d9a11c0 | |||
| 3ce9bb09b7 | |||
| 0e6a401154 | |||
| fe9a24167c | |||
| 08459d85ad | |||
| 2fd17ac2f7 | |||
| 4acd5e39df | |||
| ef0bfb8298 | |||
| 61a8d53fde | |||
| 997391f440 | |||
| 326f8283a6 | |||
| 84a0281169 | |||
| 3acac99104 | |||
| eeff29b0fc | |||
| 2e0e411dd5 | |||
| 3ce94d9c40 | |||
| 4013c91a63 | |||
| a27a46305a | |||
| 8d4f27f8ce | |||
| e4c924fdcb | |||
| 6fd693189c | |||
| 745865394f | |||
| db0fbe9d32 | |||
| 8a6a2c1420 | |||
| f782d17eb2 | |||
| cc2e3e16cf | |||
| d8a673bb74 | |||
| cab98d822c | |||
| 70d325374b | |||
| 13f6b23490 | |||
| c04b155767 | |||
| 6b1b6124ad | |||
| 8890171698 | |||
| 14c378bd80 | |||
| fdd767f49c | |||
| 5ccbd0455f | |||
| ae8747da56 | |||
| 89fe315c08 | |||
| c02840a580 | |||
| e1b25f46d4 | |||
| 7c0e2b1087 | |||
| 115e5b32f6 | |||
| 85878dc7c1 | |||
| 36d7c3ea34 | |||
| 50e0bab1fd | |||
| 9238fd3d57 | |||
| 294ab4fb75 | |||
| acd95fe2ff | |||
| f68e87aa25 | |||
| 9a22e2a5b2 | |||
| 483bec0231 | |||
| ea5683d080 | |||
| 847189aad4 | |||
| 41e4a90347 | |||
| 0416985efc | |||
| d53f49654d | |||
| 6d18ac287c | |||
| 67f4cd83ce | |||
| 376e3a96a3 | |||
| 63a86e708a | |||
| 6a76dbf85d | |||
| 68999dc4f6 | |||
| c27059b7d2 | |||
| 354c6b1bc7 | |||
| 09f91ca426 | |||
| 9f0a6a3ae2 | |||
| d4dea635b0 | |||
| 80e48e543c | |||
| f0d1e0687e | |||
| f2e4dce092 | |||
| 80ed802c97 | |||
| cbd7fe75d3 | |||
| a7f314d357 | |||
| 7f00114415 | |||
| 97b49dbb5e | |||
| f33db28908 | |||
| 229b89c46f | |||
| 52dd8c2e94 | |||
| 74f8395ec4 | |||
| 92dc3a5138 | |||
| 1744b5457a | |||
| 811e8672e3 | |||
| 2da92a9cf0 | |||
| b265ad0bbc | |||
| e472a1d3bd | |||
| 99153cbf5f | |||
| 5100f13dd8 | |||
| 928de7c96c | |||
| 991491f746 | |||
| c2cdb15287 | |||
| 1f2b81e05c | |||
| 55a471369a | |||
| e4d068f428 | |||
| 856b4b53d7 | |||
| de0d09a68e | |||
| 4f6c7aeed8 | |||
| c7cbed03bf | |||
| ee7e2c1412 | |||
| f50dd32daa | |||
| 559db075b1 | |||
| 4c8eded119 | |||
| 17ec9613bd | |||
| 229d064b69 | |||
| a46ed2bcf9 | |||
| 9b18c6ac3d | |||
| be69acd47a | |||
| 46fcecd5ab | |||
| 68bd228931 | |||
| a609bf0be7 | |||
| 6fefbf372e | |||
| 67828f430b | |||
| 342a409bed | |||
| 01952f3ff4 | |||
| 03778f8339 | |||
| 46dca69a82 | |||
| 5a9fa858b3 | |||
| c7a9f6af4d | |||
| 0e7def3024 | |||
| 3c33fd5d66 | |||
| 1d1509b35b | |||
| 55301d3c91 | |||
| 6a2b0f3a51 | |||
| f8ed4da5a2 | |||
| 1e8e816abc | |||
| 6aa43bbdf9 | |||
| 683f9ff41b | |||
| 8279465803 | |||
| 708e6e41a0 | |||
| 6fe645d783 | |||
| 3aa49ba7ed | |||
| b39c321683 | |||
| 8c789924ae | |||
| 9b81a5476d | |||
| 5bfee4aafb | |||
| 9456a47de3 | |||
| 26c2fb5d38 | |||
| a77f3c5fa4 | |||
| 5302b2b820 | |||
| 1d205a8041 | |||
| 89d4dd3d31 | |||
| e3baa82f75 | |||
| 016593b3ba | |||
| 5f5d015b79 | |||
| d376477705 | |||
| ce794f40ff | |||
| 4c33114f38 | |||
| 6015dcbbbf | |||
| fd2cebe3cf | |||
| d857d922d6 | |||
| 54943c9b2c | |||
| 5a0e3404ea | |||
| 9334d05b13 | |||
| dd011b2886 | |||
| 02fef3aed2 | |||
| 7ba0416c20 | |||
| e2d0fe3cd6 | |||
| d168afd4e7 | |||
| 088ea287c5 | |||
| 2e930b17ff | |||
| 1e586c94a0 | |||
| 295a997354 | |||
| ae9952ad68 | |||
| 22d57c1fb4 | |||
| a3cf74bb2d | |||
| c356db1ed7 | |||
| cadaa73f18 | |||
| 638549b329 | |||
| 9c3413789f | |||
| 37f6ab1bc1 | |||
| 864c70198b | |||
| 3ab6a476be | |||
| 8faf07d56f | |||
| b6aca7b877 | |||
| 54af771c4f | |||
| 69a7f80efb | |||
| 315de28398 | |||
| 4dfd46be9a | |||
| 5cf9301710 | |||
| fbff1356a9 | |||
| a7b74acaa0 | |||
| 3d633be59d | |||
| 62cba9bd34 | |||
| 78f02c0064 | |||
| 17f2df3e48 | |||
| a721faa645 | |||
| 3dcd4e27fc | |||
| de084e4eda | |||
| ca7a2b8960 | |||
| da02b741e1 | |||
| 411eb51fbc | |||
| 8ac62b267b | |||
| 2328acff83 | |||
| 5a29b6b8ca | |||
| ef7f2f8048 | |||
| 1e7648b4b4 | |||
| a0d560cb32 | |||
| bff3c8601d | |||
| f9c6990171 | |||
| 688381c7b0 | |||
| f87046724b | |||
| ecf9b83997 | |||
| 3a9d78f122 | |||
| 47074aee36 | |||
| 779a6a602c | |||
| 808d0cd56d | |||
| f326a1623d | |||
| d289dcf9df | |||
| 1bd30c8bd5 | |||
| 454a1c02c3 | |||
| c414809f40 | |||
| bbed2f9bba | |||
| 71c96a93be | |||
| 27778857b1 | |||
| 09aec12920 | |||
| 90bbe971ed | |||
| 0fc9fd832c | |||
| 128b90dae2 | |||
| e0f6dc7d97 | |||
| 909e19d709 | |||
| a63addd6fb | |||
| a97a1663d4 | |||
| 3d239a624a | |||
| a8c462fd7f | |||
| 12871c3252 | |||
| 9d51617cd6 | |||
| c2e3aecf05 | |||
| d0d0fafe3e | |||
| 7bb45de3d0 | |||
| 642815b3ff | |||
| e0ac3ddee2 | |||
| afb432f72f | |||
| 65392944c3 | |||
| c98058a61d | |||
| 4017d69a1f | |||
| cc3ff63177 | |||
| c4220e55d0 | |||
| eb7f07fe34 | |||
| 327f998cb9 | |||
| e914fd7e3a | |||
| 76c0734dca | |||
| d6d59ca4f8 | |||
| 52bd044cf2 | |||
| 963a291d42 | |||
| 517ba0ebf8 | |||
| 1f7505ac60 | |||
| 6d66a3b6d1 | |||
| 4c4977ca8c | |||
| 4cf68a96ac | |||
| 8b6b196c99 | |||
| 99cf085666 | |||
| 1d93000c6f | |||
| 57cc3d508d | |||
| 2792961a06 | |||
| 95134a384c | |||
| 16c8d1781b | |||
| 23afebc70e | |||
| 7d5cd09768 | |||
| b1e2a65b85 | |||
| 99ec12c690 | |||
| 979773d476 | |||
| ff2de264c8 | |||
| 3151921c58 | |||
| 118dd45085 | |||
| 5517d4061e | |||
| 952d3775f9 | |||
| 43c97f1a7c | |||
| d8354c536a | |||
| 80899b07df | |||
| d71c675b23 | |||
| bb667d0b45 | |||
| e812b94b87 | |||
| 9a0989a3f7 | |||
| 00e03a9f38 | |||
| 146833338d | |||
| 298c4b31d8 | |||
| e973f68537 | |||
| 3a77f95e49 | |||
| c18006badb | |||
| 2d17f7b9f7 | |||
| 5f915c86ea | |||
| d1a4e99303 | |||
| 191a5e890c | |||
| 07eb52b68f | |||
| 39f665a524 | |||
| 4258e0727d | |||
| 06ee91f968 | |||
| 95f8229b50 | |||
| 3c128ff036 | |||
| 9975feb7ec | |||
| ad6eca731b | |||
| 211c164908 | |||
| ee1f93f011 | |||
| 8818bd0fd2 | |||
| 1611c73001 | |||
| b05e61e4ac | |||
| 57d5d9b2ef | |||
| 9c569bc435 | |||
| ea8a644aa3 | |||
| ad07aa3703 | |||
| 101feb031f | |||
| f5942d0de5 | |||
| 4b01e3ac17 | |||
| ccdf3b423a | |||
| 6a6d28c9c7 | |||
| ebfa4f96af | |||
| 899d01fada | |||
| 3ed878ab46 |
+2
-2
@@ -3,9 +3,9 @@ root = true
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
|
||||
+168
-56
@@ -6,22 +6,38 @@ API_URL=
|
||||
APP_NAME=AnimeThemes
|
||||
APP_ENV=local
|
||||
APP_DEBUG=true
|
||||
DEBUGBAR_ENABLED=true
|
||||
DEBUGBAR_EDITOR=
|
||||
AUDITING_ENABLED=true
|
||||
APP_URL=http://localhost
|
||||
ASSET_URL=null
|
||||
APP_KEY=
|
||||
|
||||
# audit
|
||||
AUDITING_ENABLED=true
|
||||
# audio
|
||||
AUDIO_DISK_DEFAULT=audios_local
|
||||
AUDIO_DISKS=audios_local
|
||||
AUDIO_DISK_ROOT=
|
||||
AUDIO_PATH=/audio
|
||||
AUDIO_URL=
|
||||
AUDIO_STREAMING_METHOD=response
|
||||
AUDIO_NGINX_REDIRECT=
|
||||
|
||||
# broadcasting
|
||||
BROADCAST_DRIVER=log
|
||||
PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_HOST=
|
||||
PUSHER_APP_CLUSTER=mt1
|
||||
PUSHER_PORT=443
|
||||
PUSHER_SCHEME=https
|
||||
ABLY_KEY=
|
||||
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||
|
||||
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||
VITE_PUSHER_HOST="${PUSHER_HOST}"
|
||||
VITE_PUSHER_PORT="${PUSHER_PORT}"
|
||||
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
|
||||
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||
|
||||
# cache
|
||||
CACHE_DRIVER=file
|
||||
@@ -35,18 +51,26 @@ DYNAMODB_ENDPOINT=
|
||||
CACHE_PREFIX=animethemes
|
||||
|
||||
# database
|
||||
DB_CONNECTION=mysql
|
||||
DB_CONNECTION=mysql_prod
|
||||
DATABASE_URL=
|
||||
DB_HOST_READ=127.0.0.1
|
||||
DB_HOST_WRITE=127.0.0.1
|
||||
DB_STICKY=false
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=laravel
|
||||
DB_DATABASE=animethemes
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
DB_SOCKET=
|
||||
MYSQL_ATTR_SSL_CA=null
|
||||
|
||||
DATABASE_BETA_URL=
|
||||
DB_BETA_HOST=
|
||||
DB_BETA_PORT=3306
|
||||
DB_BETA_DATABASE=animethemes-beta
|
||||
DB_BETA_USERNAME=root
|
||||
DB_BETA_PASSWORD=
|
||||
DB_BETA_SOCKET=
|
||||
|
||||
REDIS_CLIENT=predis
|
||||
REDIS_CLUSTER=redis
|
||||
REDIS_URL=null
|
||||
@@ -57,29 +81,24 @@ REDIS_SCHEME=tcp
|
||||
REDIS_DB=0
|
||||
REDIS_CACHE_DB=1
|
||||
|
||||
# elastic client
|
||||
ELASTIC_HOST=localhost
|
||||
ELASTIC_PORT=9200
|
||||
ELASTIC_SCHEME=http
|
||||
ELASTIC_USER=
|
||||
ELASTIC_PASS=
|
||||
# dump
|
||||
DUMP_DISK=dumps_local
|
||||
DUMP_DISK_ROOT=
|
||||
DUMP_URL=http://localhost
|
||||
DUMP_PATH=/dump
|
||||
|
||||
# elastic driver
|
||||
ELASTIC_SCOUT_DRIVER_REFRESH_DOCUMENTS=false
|
||||
# external profile
|
||||
USER_MAX_PROFILES=5
|
||||
|
||||
# elastic migrations
|
||||
ELASTIC_MIGRATIONS_TABLE=elastic_migrations
|
||||
# ffmpeg
|
||||
FFMPEG_BINARIES=
|
||||
FFPROBE_BINARIES=
|
||||
|
||||
# enlightn
|
||||
ENLIGHTN_SKIP_ENVIRONMENT_SPECIFIC=false
|
||||
ENLIGHTN_USERNAME=
|
||||
ENLIGHTN_API_TOKEN=
|
||||
ENLIGHTN_GITHUB_REPO=
|
||||
|
||||
# feature flags
|
||||
ALLOW_VIDEO_STREAMS=false
|
||||
ALLOW_DISCORD_NOTIFICATIONS=false
|
||||
ALLOW_VIEW_RECORDING=false
|
||||
# filament
|
||||
FILAMENT_URL=
|
||||
FILAMENT_PATH=admin
|
||||
FILAMENT_SUBMISSION_URL=
|
||||
FILAMENT_SUBMISSION_PATH=submission
|
||||
|
||||
# filesystems
|
||||
FILESYSTEM_DISK=local
|
||||
@@ -94,24 +113,84 @@ IMAGE_DISABLE_ASSERTS=
|
||||
IMAGE_VISIBILITY=
|
||||
IMAGE_URL=
|
||||
|
||||
VIDEO_ACCESS_KEY_ID=
|
||||
VIDEO_SECRET_ACCESS_KEY=
|
||||
VIDEO_DEFAULT_REGION=
|
||||
VIDEO_ENDPOINT=
|
||||
VIDEO_BUCKET=
|
||||
VIDEO_STREAM_READS=
|
||||
VIDEO_DISABLE_ASSERTS=
|
||||
VIDEO_VISIBILITY=
|
||||
VIDEO_NYC_ACCESS_KEY_ID=
|
||||
VIDEO_NYC_SECRET_ACCESS_KEY=
|
||||
VIDEO_NYC_DEFAULT_REGION=
|
||||
VIDEO_NYC_ENDPOINT=
|
||||
VIDEO_NYC_BUCKET=
|
||||
VIDEO_NYC_STREAM_READS=
|
||||
VIDEO_NYC_DISABLE_ASSERTS=
|
||||
VIDEO_NYC_VISIBILITY=
|
||||
|
||||
VIDEO_FRA_ACCESS_KEY_ID=
|
||||
VIDEO_FRA_SECRET_ACCESS_KEY=
|
||||
VIDEO_FRA_DEFAULT_REGION=
|
||||
VIDEO_FRA_ENDPOINT=
|
||||
VIDEO_FRA_BUCKET=
|
||||
VIDEO_FRA_STREAM_READS=
|
||||
VIDEO_FRA_DISABLE_ASSERTS=
|
||||
VIDEO_FRA_VISIBILITY=
|
||||
|
||||
AUDIO_NYC_ACCESS_KEY_ID=
|
||||
AUDIO_NYC_SECRET_ACCESS_KEY=
|
||||
AUDIO_NYC_DEFAULT_REGION=
|
||||
AUDIO_NYC_ENDPOINT=
|
||||
AUDIO_NYC_BUCKET=
|
||||
AUDIO_NYC_STREAM_READS=
|
||||
AUDIO_NYC_DISABLE_ASSERTS=
|
||||
AUDIO_NYC_VISIBILITY=
|
||||
|
||||
AUDIO_FRA_ACCESS_KEY_ID=
|
||||
AUDIO_FRA_SECRET_ACCESS_KEY=
|
||||
AUDIO_FRA_DEFAULT_REGION=
|
||||
AUDIO_FRA_ENDPOINT=
|
||||
AUDIO_FRA_BUCKET=
|
||||
AUDIO_FRA_STREAM_READS=
|
||||
AUDIO_FRA_DISABLE_ASSERTS=
|
||||
AUDIO_FRA_VISIBILITY=
|
||||
|
||||
DUMP_ACCESS_KEY_ID=
|
||||
DUMP_SECRET_ACCESS_KEY=
|
||||
DUMP_DEFAULT_REGION=
|
||||
DUMP_ENDPOINT=
|
||||
DUMP_BUCKET=
|
||||
DUMP_STREAM_READS=
|
||||
DUMP_DISABLE_ASSERTS=
|
||||
DUMP_VISIBILITY=
|
||||
|
||||
SCRIPT_ACCESS_KEY_ID=
|
||||
SCRIPT_SECRET_ACCESS_KEY=
|
||||
SCRIPT_DEFAULT_REGION=
|
||||
SCRIPT_ENDPOINT=
|
||||
SCRIPT_BUCKET=
|
||||
SCRIPT_STREAM_READS=
|
||||
SCRIPT_DISABLE_ASSERTS=
|
||||
SCRIPT_VISIBILITY=
|
||||
|
||||
# fortify
|
||||
FORTIFY_HOME=http://localhost
|
||||
FORTIFY_PATH=
|
||||
FORTIFY_URL=http://localhost
|
||||
|
||||
# graphql
|
||||
GRAPHQL_URL=
|
||||
GRAPHQL_PATH=/graphql
|
||||
GRAPHIQL_ENABLED=true
|
||||
|
||||
# hashids
|
||||
HASHIDS_SALT_MAIN=
|
||||
HASHIDS_SALT_PLAYLISTS=
|
||||
|
||||
# hashing
|
||||
BCRYPT_ROUNDS=10
|
||||
|
||||
# horizon
|
||||
HORIZON_DOMAIN=
|
||||
HORIZON_URL=
|
||||
HORIZON_PATH=horizon
|
||||
|
||||
# jetstream
|
||||
TEAM_CREATOR_ID=
|
||||
# image
|
||||
IMAGE_DISK=images_local
|
||||
IMAGE_DISK_ROOT=
|
||||
|
||||
# logging
|
||||
LOG_CHANNEL=daily
|
||||
@@ -127,29 +206,43 @@ MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS=null
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
MAIL_SENDMAIL_PATH=/usr/sbin/sendmail
|
||||
MAIL_SENDMAIL_PATH="/usr/sbin/sendmail -bs -i"
|
||||
MAIL_LOG_CHANNEL=
|
||||
|
||||
# nova
|
||||
NOVA_DOMAIN_NAME=null
|
||||
NOVA_GUARD=null
|
||||
NOVA_LICENSE_KEY=
|
||||
NOVA_PASSWORDS=null
|
||||
NOVA_PATH=/admin
|
||||
# pennant
|
||||
PENNANT_STORE=database
|
||||
|
||||
# playlist
|
||||
PLAYLIST_MAX_TRACKS=1000
|
||||
USER_MAX_PLAYLISTS=1000
|
||||
|
||||
# queue
|
||||
QUEUE_CONNECTION=sync
|
||||
REDIS_QUEUE=default
|
||||
QUEUE_FAILED_DRIVER=database-uuids
|
||||
|
||||
# sanctum
|
||||
SANCTUM_STATEFUL_DOMAINS=
|
||||
|
||||
# scout
|
||||
SCOUT_DRIVER=null
|
||||
SCOUT_PREFIX=
|
||||
SCOUT_QUEUE=false
|
||||
SCOUT_IDENTIFY=false
|
||||
|
||||
ALGOLIA_APP_ID=
|
||||
ALGOLIA_SECRET=
|
||||
|
||||
ELASTIC_CONNECTION=default
|
||||
ELASTIC_HOST=http://localhost:9200
|
||||
ELASTIC_SCOUT_DRIVER_REFRESH_DOCUMENTS=false
|
||||
ELASTIC_MIGRATIONS_TABLE=elastic_migrations
|
||||
|
||||
TYPESENSE_HOST=localhost
|
||||
TYPESENSE_PORT=8108
|
||||
TYPESENSE_PROTOCOL=http
|
||||
TYPESENSE_API_KEY=
|
||||
|
||||
# secure headers
|
||||
ENABLE_CLEAR_SITE_DATA=false
|
||||
ENABLE_HSTS=false
|
||||
@@ -163,12 +256,20 @@ MAILGUN_SECRET=
|
||||
MAILGUN_ENDPOINT=api.mailgun.net
|
||||
POSTMARK_TOKEN=
|
||||
DISCORD_BOT_API_TOKEN=
|
||||
DISCORD_BOT_API_URL=
|
||||
DISCORD_BOT_API_KEY=
|
||||
DB_UPDATES_DISCORD_CHANNEL=
|
||||
ADMIN_DISCORD_CHANNEL=
|
||||
OPENAI_BEARER_TOKEN=null
|
||||
MAL_CLIENT_ID=null
|
||||
DO_BEARER_TOKEN=null
|
||||
MAL_CLIENT_SECRET=null
|
||||
MAL_REDIRECT_URI=null
|
||||
ANILIST_CLIENT_ID=null
|
||||
ANILIST_CLIENT_SECRET=null
|
||||
ANILIST_REDIRECT_URI=null
|
||||
|
||||
# session
|
||||
SESSION_COOKIE=animethemes_session
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_CONNECTION=null
|
||||
@@ -176,21 +277,32 @@ SESSION_STORE=null
|
||||
SESSION_DOMAIN=null
|
||||
SESSION_SECURE_COOKIE=
|
||||
|
||||
# teams
|
||||
HORIZON_TEAM_ID=
|
||||
NOVA_TEAM_ID=
|
||||
TELESCOPE_TEAM_ID=
|
||||
# submission
|
||||
USER_MAX_SUBMISSIONS=50
|
||||
|
||||
# telescope
|
||||
TELESCOPE_DOMAIN=null
|
||||
TELESCOPE_PATH=telescope
|
||||
TELESCOPE_DRIVER=database
|
||||
TELESCOPE_ENABLED=true
|
||||
# validation
|
||||
MODERATION_SERVICE=none
|
||||
|
||||
# video
|
||||
VIDEO_DISK_DEFAULT=videos_local
|
||||
VIDEO_DISKS=videos_local
|
||||
VIDEO_DISK_ROOT=
|
||||
VIDEO_PATH=/video
|
||||
VIDEO_URL=
|
||||
VIDEO_RATE_LIMITER=90
|
||||
VIDEO_STREAMING_METHOD=response
|
||||
VIDEO_NGINX_REDIRECT=
|
||||
|
||||
SCRIPT_DISK=scripts_local
|
||||
SCRIPT_DISK_ROOT=
|
||||
SCRIPT_URL=
|
||||
SCRIPT_PATH=/videoscript
|
||||
|
||||
# web
|
||||
WEB_URL=http://localhost
|
||||
WEB_PATH=
|
||||
|
||||
# wiki
|
||||
WIKI_FEATURED_ENTRY=
|
||||
WIKI_FEATURED_VIDEO=
|
||||
WIKI_LOGIN=http://localhost/login
|
||||
WIKI_RESET_PASSWORD=http://localhost/reset-password
|
||||
WIKI_EXTERNAL_PROFILE=http://localhost/external
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
# api
|
||||
API_PATH=/api
|
||||
API_URL=
|
||||
|
||||
# app
|
||||
APP_NAME=AnimeThemes
|
||||
APP_ENV=local
|
||||
APP_DEBUG=true
|
||||
DEBUGBAR_ENABLED=true
|
||||
DEBUGBAR_EDITOR=
|
||||
AUDITING_ENABLED=true
|
||||
APP_URL=http://localhost
|
||||
ASSET_URL=null
|
||||
APP_KEY=
|
||||
|
||||
# audio
|
||||
AUDIO_DISK_DEFAULT=audios_local
|
||||
AUDIO_DISKS=audios_local
|
||||
AUDIO_DISK_ROOT=
|
||||
AUDIO_PATH=/audio
|
||||
AUDIO_URL=
|
||||
AUDIO_STREAMING_METHOD=response
|
||||
AUDIO_NGINX_REDIRECT=
|
||||
|
||||
# broadcasting
|
||||
BROADCAST_DRIVER=log
|
||||
PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_HOST=
|
||||
PUSHER_APP_CLUSTER=mt1
|
||||
PUSHER_PORT=443
|
||||
PUSHER_SCHEME=https
|
||||
ABLY_KEY=
|
||||
|
||||
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||
VITE_PUSHER_HOST="${PUSHER_HOST}"
|
||||
VITE_PUSHER_PORT="${PUSHER_PORT}"
|
||||
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
|
||||
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||
|
||||
# cache
|
||||
CACHE_DRIVER=file
|
||||
MEMCACHED_PERSISTENT_ID=
|
||||
MEMCACHED_USERNAME=
|
||||
MEMCACHED_PASSWORD=
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
MEMCACHED_PORT=11211
|
||||
DYNAMODB_CACHE_TABLE=cache
|
||||
DYNAMODB_ENDPOINT=
|
||||
CACHE_PREFIX=animethemes
|
||||
|
||||
# database
|
||||
DB_CONNECTION=mysql_prod
|
||||
DATABASE_URL=
|
||||
DB_HOST=mysql
|
||||
DB_HOST_READ=mysql
|
||||
DB_HOST_WRITE=mysql
|
||||
DB_STICKY=false
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=animethemes
|
||||
DB_USERNAME=sail
|
||||
DB_PASSWORD=password
|
||||
DB_SOCKET=
|
||||
MYSQL_ATTR_SSL_CA=null
|
||||
|
||||
DATABASE_BETA_URL=
|
||||
DB_BETA_HOST=
|
||||
DB_BETA_PORT=3306
|
||||
DB_BETA_DATABASE=animethemes-beta
|
||||
DB_BETA_USERNAME=root
|
||||
DB_BETA_PASSWORD=
|
||||
DB_BETA_SOCKET=
|
||||
|
||||
REDIS_CLIENT=predis
|
||||
REDIS_CLUSTER=redis
|
||||
REDIS_URL=null
|
||||
REDIS_HOST=redis
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
REDIS_SCHEME=tcp
|
||||
REDIS_DB=0
|
||||
REDIS_CACHE_DB=1
|
||||
|
||||
# dump
|
||||
DUMP_DISK=dumps
|
||||
DUMP_DISK_ROOT=
|
||||
DUMP_URL=http://localhost
|
||||
DUMP_PATH=/dump
|
||||
|
||||
# ffmpeg
|
||||
FFMPEG_BINARIES=
|
||||
FFPROBE_BINARIES=
|
||||
|
||||
# filament
|
||||
FILAMENT_URL=
|
||||
FILAMENT_PATH=admin
|
||||
FILAMENT_SUBMISSION_URL=
|
||||
FILAMENT_SUBMISSION_PATH=submission
|
||||
|
||||
# filesystems
|
||||
FILESYSTEM_DISK=local
|
||||
|
||||
IMAGE_ACCESS_KEY_ID=
|
||||
IMAGE_SECRET_ACCESS_KEY=
|
||||
IMAGE_DEFAULT_REGION=
|
||||
IMAGE_ENDPOINT=
|
||||
IMAGE_BUCKET=
|
||||
IMAGE_STREAM_READS=
|
||||
IMAGE_DISABLE_ASSERTS=
|
||||
IMAGE_VISIBILITY=
|
||||
IMAGE_URL=
|
||||
|
||||
VIDEO_NYC_ACCESS_KEY_ID=
|
||||
VIDEO_NYC_SECRET_ACCESS_KEY=
|
||||
VIDEO_NYC_DEFAULT_REGION=
|
||||
VIDEO_NYC_ENDPOINT=
|
||||
VIDEO_NYC_BUCKET=
|
||||
VIDEO_NYC_STREAM_READS=
|
||||
VIDEO_NYC_DISABLE_ASSERTS=
|
||||
VIDEO_NYC_VISIBILITY=
|
||||
|
||||
VIDEO_FRA_ACCESS_KEY_ID=
|
||||
VIDEO_FRA_SECRET_ACCESS_KEY=
|
||||
VIDEO_FRA_DEFAULT_REGION=
|
||||
VIDEO_FRA_ENDPOINT=
|
||||
VIDEO_FRA_BUCKET=
|
||||
VIDEO_FRA_STREAM_READS=
|
||||
VIDEO_FRA_DISABLE_ASSERTS=
|
||||
VIDEO_FRA_VISIBILITY=
|
||||
|
||||
AUDIO_NYC_ACCESS_KEY_ID=
|
||||
AUDIO_NYC_SECRET_ACCESS_KEY=
|
||||
AUDIO_NYC_DEFAULT_REGION=
|
||||
AUDIO_NYC_ENDPOINT=
|
||||
AUDIO_NYC_BUCKET=
|
||||
AUDIO_NYC_STREAM_READS=
|
||||
AUDIO_NYC_DISABLE_ASSERTS=
|
||||
AUDIO_NYC_VISIBILITY=
|
||||
|
||||
AUDIO_FRA_ACCESS_KEY_ID=
|
||||
AUDIO_FRA_SECRET_ACCESS_KEY=
|
||||
AUDIO_FRA_DEFAULT_REGION=
|
||||
AUDIO_FRA_ENDPOINT=
|
||||
AUDIO_FRA_BUCKET=
|
||||
AUDIO_FRA_STREAM_READS=
|
||||
AUDIO_FRA_DISABLE_ASSERTS=
|
||||
AUDIO_FRA_VISIBILITY=
|
||||
|
||||
DUMP_ACCESS_KEY_ID=
|
||||
DUMP_SECRET_ACCESS_KEY=
|
||||
DUMP_DEFAULT_REGION=
|
||||
DUMP_ENDPOINT=
|
||||
DUMP_BUCKET=
|
||||
DUMP_STREAM_READS=
|
||||
DUMP_DISABLE_ASSERTS=
|
||||
DUMP_VISIBILITY=
|
||||
|
||||
SCRIPT_ACCESS_KEY_ID=
|
||||
SCRIPT_SECRET_ACCESS_KEY=
|
||||
SCRIPT_DEFAULT_REGION=
|
||||
SCRIPT_ENDPOINT=
|
||||
SCRIPT_BUCKET=
|
||||
SCRIPT_STREAM_READS=
|
||||
SCRIPT_DISABLE_ASSERTS=
|
||||
SCRIPT_VISIBILITY=
|
||||
|
||||
# fortify
|
||||
FORTIFY_HOME=http://localhost:3000
|
||||
FORTIFY_PATH=
|
||||
FORTIFY_URL=http://localhost
|
||||
|
||||
# graphql
|
||||
GRAPHQL_URL=
|
||||
GRAPHQL_PATH=/graphql
|
||||
GRAPHIQL_ENABLED=true
|
||||
|
||||
# hashids
|
||||
HASHIDS_SALT_MAIN=
|
||||
HASHIDS_SALT_PLAYLISTS=
|
||||
|
||||
# hashing
|
||||
BCRYPT_ROUNDS=10
|
||||
|
||||
# horizon
|
||||
HORIZON_URL=
|
||||
HORIZON_PATH=horizon
|
||||
|
||||
# image
|
||||
IMAGE_DISK=images_local
|
||||
IMAGE_DISK_ROOT=
|
||||
|
||||
# logging
|
||||
LOG_CHANNEL=daily
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# mail
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=mailpit
|
||||
MAIL_PORT=1025
|
||||
MAIL_ENCRYPTION=tls
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS=admin@animethemes.moe
|
||||
MAIL_FROM_NAME=AnimeThemes
|
||||
MAIL_SENDMAIL_PATH="/usr/sbin/sendmail -bs -i"
|
||||
MAIL_LOG_CHANNEL=
|
||||
|
||||
# pennant
|
||||
PENNANT_STORE=database
|
||||
|
||||
# playlist
|
||||
PLAYLIST_MAX_TRACKS=1000
|
||||
USER_MAX_PLAYLISTS=1000
|
||||
|
||||
# queue
|
||||
QUEUE_CONNECTION=sync
|
||||
REDIS_QUEUE=default
|
||||
QUEUE_FAILED_DRIVER=database-uuids
|
||||
|
||||
# sanctum
|
||||
SANCTUM_STATEFUL_DOMAINS=*
|
||||
|
||||
# scout
|
||||
SCOUT_DRIVER=elastic
|
||||
SCOUT_PREFIX=
|
||||
SCOUT_QUEUE=false
|
||||
SCOUT_IDENTIFY=false
|
||||
|
||||
ALGOLIA_APP_ID=
|
||||
ALGOLIA_SECRET=
|
||||
|
||||
ELASTIC_CONNECTION=default
|
||||
ELASTIC_HOST=http://elasticsearch:9200
|
||||
ELASTIC_SCOUT_DRIVER_REFRESH_DOCUMENTS=false
|
||||
ELASTIC_MIGRATIONS_TABLE=elastic_migrations
|
||||
|
||||
TYPESENSE_HOST=localhost
|
||||
TYPESENSE_PORT=8108
|
||||
TYPESENSE_PROTOCOL=http
|
||||
TYPESENSE_API_KEY=
|
||||
|
||||
# secure headers
|
||||
ENABLE_CLEAR_SITE_DATA=false
|
||||
ENABLE_HSTS=false
|
||||
ENABLE_EXPECT_CT=false
|
||||
ENABLE_PERMISSIONS_POLICY=false
|
||||
ENABLE_CSP=false
|
||||
|
||||
# services
|
||||
MAILGUN_DOMAIN=
|
||||
MAILGUN_SECRET=
|
||||
MAILGUN_ENDPOINT=api.mailgun.net
|
||||
POSTMARK_TOKEN=
|
||||
DISCORD_BOT_API_TOKEN=
|
||||
DISCORD_BOT_API_URL=
|
||||
DISCORD_BOT_API_KEY=
|
||||
DB_UPDATES_DISCORD_CHANNEL=
|
||||
ADMIN_DISCORD_CHANNEL=
|
||||
OPENAI_BEARER_TOKEN=null
|
||||
MAL_CLIENT_ID=null
|
||||
MAL_CLIENT_SECRET=null
|
||||
MAL_REDIRECT_URI=null
|
||||
ANILIST_CLIENT_ID=null
|
||||
ANILIST_CLIENT_SECRET=null
|
||||
ANILIST_REDIRECT_URI=null
|
||||
|
||||
# session
|
||||
SESSION_COOKIE=animethemes_session
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_CONNECTION=null
|
||||
SESSION_STORE=null
|
||||
SESSION_DOMAIN=null
|
||||
SESSION_SECURE_COOKIE=
|
||||
|
||||
# submission
|
||||
USER_MAX_SUBMISSIONS=50
|
||||
|
||||
# validation
|
||||
MODERATION_SERVICE=none
|
||||
|
||||
# video
|
||||
VIDEO_DISK_DEFAULT=videos_local
|
||||
VIDEO_DISKS=videos_local
|
||||
VIDEO_DISK_ROOT=
|
||||
VIDEO_PATH=/video
|
||||
VIDEO_URL=
|
||||
VIDEO_RATE_LIMITER=90
|
||||
VIDEO_STREAMING_METHOD=response
|
||||
VIDEO_NGINX_REDIRECT=
|
||||
|
||||
SCRIPT_DISK=scripts_local
|
||||
SCRIPT_DISK_ROOT=
|
||||
SCRIPT_URL=
|
||||
SCRIPT_PATH=/videoscript
|
||||
|
||||
# web
|
||||
WEB_URL=http://localhost
|
||||
WEB_PATH=
|
||||
|
||||
# wiki
|
||||
WIKI_LOGIN=http://localhost/login
|
||||
WIKI_RESET_PASSWORD=http://localhost/reset-password
|
||||
WIKI_EXTERNAL_PROFILE=http://localhost/external
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
* text=auto
|
||||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
@@ -8,3 +8,4 @@
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
|
||||
+49
-53
@@ -1,66 +1,65 @@
|
||||
- [Assignment](#assignment)
|
||||
- [Development Environment](#development-environment)
|
||||
- [Feature Branch Workflow](#feature-branch-workflow)
|
||||
- [Cleanup Before Pull Request](#cleanup-before-pull-request)
|
||||
- [Pull Request](#pull-request)
|
||||
|
||||
## Assignment
|
||||
|
||||
Unless otherwise specified, any issue that does not have an assignee can be claimed by any volunteer.
|
||||
|
||||
Please leave a comment in the issue indicating interest so that you can be designated as the assignee by a member. This is a requirement in Github to allow asssignment of contributors outside of the organization by members.
|
||||
Leave a comment in the issue indicating interest so that the assignee can be designated by a member. This is a requirement in Github to allow assignment of contributors outside of the organization by members.
|
||||
|
||||
If further discussion is needed before work can be started, please make use of the programming channels on the discord.
|
||||
If further discussion is needed before work can be started, please make use of the `#api` channel on the discord.
|
||||
|
||||
## Development Environment
|
||||
|
||||
Ensure that you have a functional local instance of AnimeThemes forked from this repository by following the [Installation Guide](https://github.com/AnimeThemes/animethemes-server/wiki/Installation).
|
||||
Follow the [Installation Guide](/AnimeThemes/animethemes-server/wiki/Installation) to ensure a functional local instance of AnimeThemes forked from this repository.
|
||||
|
||||
Enable your repository in [StyleCI](https://styleci.io/) so that code styling guidelines are followed. At the time of writing, analysis is run on push and merge.
|
||||
Enable the forked repository in [StyleCI](https://styleci.io/) so that code styling guidelines are followed. At the time of writing, analysis is run on push and merge.
|
||||
|
||||
Set this repository as upstream to your fork.
|
||||
```sh
|
||||
# Set this repository as upstream to the forked repository.
|
||||
git remote add upstream git@github.com:AnimeThemes/animethemes-server.git
|
||||
|
||||
`git remote add upstream https://github.com/AnimeThemes/animethemes-server.git`
|
||||
# Pull upstream changes so that the local main branch is even with the upstream main branch.
|
||||
git pull upstream main
|
||||
|
||||
Checkout release branch.
|
||||
|
||||
`git checkout release-branch`
|
||||
|
||||
Pull upstream changes so that your local branch is even with upstream.
|
||||
|
||||
`git pull upstream release-branch`
|
||||
|
||||
Push upstream changes to your remote if needed.
|
||||
|
||||
`git push`
|
||||
# Push upstream changes to the forked remote main branch if needed.
|
||||
git push origin main
|
||||
```
|
||||
|
||||
## Feature Branch Workflow
|
||||
|
||||
Once your release branch is even with upstream, create feature branch from release branch.
|
||||
```sh
|
||||
# Once the release branch is even with upstream, create a new feature branch from the release branch. The new feature branch name should be descriptive.
|
||||
git checkout -b new-feature-branch
|
||||
|
||||
`git branch new-feature-branch`
|
||||
# Make changes in the new feature branch. Once completed, run tests.
|
||||
php artisan config:clear && php artisan test --parallel
|
||||
|
||||
Switch to feature branch.
|
||||
# Execute static code analysis.
|
||||
composer test:types
|
||||
|
||||
`git checkout new-feature-branch`
|
||||
# Execute pint.
|
||||
composer lint
|
||||
```
|
||||
|
||||
Make changes in feature branch. Once completed, run tests.
|
||||
Evaluate code changes against [conventions & standards](/AnimeThemes/animethemes-server/wiki/Coding-Conventions-&-Standards).
|
||||
|
||||
`php artisan config:clear && php artisan test --parallel`
|
||||
|
||||
Execute static code analysis.
|
||||
|
||||
`./vendor/bin/phpstan analyse --memory-limit=-1`
|
||||
|
||||
Stage changes. Commit changes. Please include ticket ID's and use [Semantic Commit Messages](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716).
|
||||
Stage changes. Commit changes. Please include issue ID's and use [Semantic Commit Messages](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716).
|
||||
|
||||
## Cleanup Before Pull Request
|
||||
|
||||
Fetch upstream master and merge with your repo's master branch
|
||||
```
|
||||
git fetch upstream release-branch
|
||||
git checkout release-branch
|
||||
git merge upstream/release-branch
|
||||
```
|
||||
```sh
|
||||
# Fetch the upstream release branch and merge with the fork repo's release branch.
|
||||
git fetch upstream main
|
||||
git checkout main
|
||||
git merge upstream/main
|
||||
|
||||
If there were any new commits, rebase your development branch
|
||||
```
|
||||
# If there were any new commits, rebase the new feature branch.
|
||||
git checkout new-feature-branch
|
||||
git rebase release-branch
|
||||
git rebase main
|
||||
```
|
||||
|
||||
## Pull Request
|
||||
@@ -69,27 +68,24 @@ Push changes to remote.
|
||||
|
||||
`git push --set-upstream origin new-feature-branch`
|
||||
|
||||
Confirm StyleCI passing status on push.
|
||||
|
||||
Create Pull Request from your new feature branch to upstream release branch.
|
||||
Create Pull Request from the new feature branch to the upstream release branch.
|
||||
|
||||
## Pruning
|
||||
|
||||
Once the Pull Request is merged, delete the feature branch locally.
|
||||
```sh
|
||||
# Once the Pull Request is merged, check out the release branch and delete the feature branch locally.
|
||||
git checkout main
|
||||
git branch -D new-feature-branch
|
||||
|
||||
`git branch --delete new-feature-branch`
|
||||
# Delete the remote feature branch.
|
||||
git push origin -D new-feature-branch
|
||||
|
||||
Delete the remote feature branch.
|
||||
# Pull upstream changes so that the fork release branch is even with upstream.
|
||||
git pull upstream main
|
||||
|
||||
`git push origin --delete new-feature-branch`
|
||||
|
||||
Pull upstream changes so that your local branch is even with upstream.
|
||||
|
||||
`git pull upstream release-branch`
|
||||
|
||||
Push upstream changes to your remote if needed.
|
||||
|
||||
`git push`
|
||||
# Push upstream changes to the fork's remote if needed.
|
||||
git push origin main
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: AnimeThemes
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: animethemes
|
||||
ko_fi: # Replace with a Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,4 @@
|
||||
contact_links:
|
||||
- name: API question
|
||||
url: https://discord.gg/m9zbVyQ
|
||||
about: 'For API questions, go to the #api channel on the Discord server'
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement, php
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -5,32 +5,21 @@ on: push
|
||||
jobs:
|
||||
static-analysis:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.1'
|
||||
php-version: '8.5'
|
||||
tools: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, phpunit
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Copy .env
|
||||
run: php -r "file_exists('.env') || copy('.env.example', '.env');"
|
||||
- name: Install Dependencies
|
||||
env:
|
||||
NOVA_USERNAME: ${{ secrets.NOVA_USERNAME }}
|
||||
NOVA_PASSWORD: ${{ secrets.NOVA_PASSWORD }}
|
||||
ENLIGHTN_USERNAME: ${{ secrets.ENLIGHTN_USERNAME }}
|
||||
ENLIGHTN_PASSWORD: ${{ secrets.ENLIGHTN_PASSWORD }}
|
||||
run: |
|
||||
composer config "http-basic.nova.laravel.com" "$NOVA_USERNAME" "$NOVA_PASSWORD"
|
||||
composer config "http-basic.satis.laravel-enlightn.com" "$ENLIGHTN_USERNAME" "$ENLIGHTN_PASSWORD"
|
||||
composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
|
||||
- name: Generate key
|
||||
run: php artisan key:generate
|
||||
- name: Install NPM dependencies
|
||||
run: npm install
|
||||
- name: Compile assets
|
||||
run: npm run production
|
||||
- name: Directory Permissions
|
||||
run: chmod -R 777 storage bootstrap/cache
|
||||
- name: Create Database
|
||||
|
||||
+10
-14
@@ -5,38 +5,34 @@ on: push
|
||||
jobs:
|
||||
test:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.1'
|
||||
php-version: '8.5'
|
||||
tools: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, phpunit
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Copy .env
|
||||
run: php -r "file_exists('.env') || copy('.env.example', '.env');"
|
||||
- name: Install Dependencies
|
||||
env:
|
||||
NOVA_USERNAME: ${{ secrets.NOVA_USERNAME }}
|
||||
NOVA_PASSWORD: ${{ secrets.NOVA_PASSWORD }}
|
||||
ENLIGHTN_USERNAME: ${{ secrets.ENLIGHTN_USERNAME }}
|
||||
ENLIGHTN_PASSWORD: ${{ secrets.ENLIGHTN_PASSWORD }}
|
||||
run: |
|
||||
composer config "http-basic.nova.laravel.com" "$NOVA_USERNAME" "$NOVA_PASSWORD"
|
||||
composer config "http-basic.satis.laravel-enlightn.com" "$ENLIGHTN_USERNAME" "$ENLIGHTN_PASSWORD"
|
||||
composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
|
||||
- name: Generate key
|
||||
run: php artisan key:generate
|
||||
- name: Install NPM dependencies
|
||||
run: npm install
|
||||
- name: Compile assets
|
||||
run: npm run production
|
||||
- name: Directory Permissions
|
||||
run: chmod -R 777 storage bootstrap/cache
|
||||
- name: Create Database
|
||||
run: |
|
||||
mkdir -p database
|
||||
touch database/database.sqlite
|
||||
- name: Install static FFmpeg
|
||||
run: |
|
||||
curl -L -o ffmpeg.tar.xz https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz
|
||||
tar -xf ffmpeg.tar.xz
|
||||
sudo mv ffmpeg-*-amd64-static/ffmpeg /usr/local/bin/
|
||||
sudo mv ffmpeg-*-amd64-static/ffprobe /usr/local/bin/
|
||||
ffmpeg -encoders | grep av1 || true
|
||||
- name: Execute tests (Unit and Feature tests) via PHPUnit
|
||||
env:
|
||||
DB_CONNECTION: sqlite
|
||||
|
||||
+11
@@ -1,19 +1,30 @@
|
||||
/.phpunit.cache
|
||||
/node_modules
|
||||
/public/hot
|
||||
/public/storage
|
||||
/public/build
|
||||
/public/audios
|
||||
/public/images
|
||||
/public/videos
|
||||
/public/dumps
|
||||
/public/scripts
|
||||
/storage/*.key
|
||||
/vendor
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpunit.result.cache
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
/.fleet
|
||||
.vscode
|
||||
.idea
|
||||
auth.json
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
*.sql
|
||||
public/css/app.css
|
||||
public/js/app.js
|
||||
test.php
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
version: 8.1
|
||||
version: 8.5
|
||||
preset: laravel
|
||||
finder:
|
||||
not-name:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/AnimeThemes/animethemes-server/actions/workflows/test.yml"><img src="https://github.com/AnimeThemes/animethemes-server/workflows/Tests/badge.svg?branch=wiki" alt="tests"></a>
|
||||
<a href="https://github.com/AnimeThemes/animethemes-server/actions/workflows/static-analysis.yml"><img src="https://github.com/AnimeThemes/animethemes-server/workflows/Static%20Analysis/badge.svg?branch=wiki" alt="static-analysis"></a>
|
||||
<a href="https://github.styleci.io/repos/111264405?branch=wiki"><img src="https://github.styleci.io/repos/111264405/shield?branch=wiki" alt="StyleCI"></a>
|
||||
<a href="https://github.com/AnimeThemes/animethemes-server/actions/workflows/test.yml"><img src="https://github.com/AnimeThemes/animethemes-server/actions/workflows/test.yml/badge.svg?branch=main" alt="tests"></a>
|
||||
<a href="https://github.com/AnimeThemes/animethemes-server/actions/workflows/static-analysis.yml"><img src="https://github.com/AnimeThemes/animethemes-server/actions/workflows/static-analysis.yml/badge.svg?branch=main" alt="static-analysis"></a>
|
||||
<a href="https://github.styleci.io/repos/111264405?branch=main"><img src="https://github.styleci.io/repos/111264405/shield?branch=main" alt="StyleCI"></a>
|
||||
<a href="https://discordapp.com/invite/m9zbVyQ"><img src="https://img.shields.io/discord/354388306580078594.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2"></a>
|
||||
<a href="https://github.com/AnimeThemes/animethemes-server/blob/wiki/LICENSE"><img src="https://img.shields.io/github/license/AnimeThemes/animethemes-server"></a>
|
||||
<a href="https://github.com/AnimeThemes/animethemes-server/blob/main/LICENSE"><img src="https://img.shields.io/github/license/AnimeThemes/animethemes-server"></a>
|
||||
<a href="https://reddit.com/r/AnimeThemes"><img src="https://img.shields.io/reddit/subreddit-subscribers/AnimeThemes?style=social"></a>
|
||||
<a href="https://twitter.com/AnimeThemesMoe"><img src="https://img.shields.io/twitter/follow/AnimeThemesMoe?style=social"></a>
|
||||
<a href="https://x.com/AnimeThemesMoe"><img src="https://img.shields.io/twitter/follow/AnimeThemesMoe?style=social"></a>
|
||||
</p>
|
||||
|
||||
[**AnimeThemes**](https://animethemes.moe/) is a simple and consistent repository of anime opening and ending themes. We provide direct links to high quality WebMs of your favorite OPs and EDs for your listening and discussion needs.
|
||||
@@ -14,14 +14,168 @@ This is the repository for the server application that is responsible for AnimeT
|
||||
|
||||
This project is powered by [**Laravel**](https://laravel.com/), a PHP framework for web artisans.
|
||||
|
||||
## Installation
|
||||
# Installation
|
||||
|
||||
Please review the [**Installation Guide**](https://github.com/AnimeThemes/animethemes-server/wiki/Installation) in the wiki for detailed instructions.
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Pre Setup](#pre-setup)
|
||||
- [PHP](#php)
|
||||
- [Setup](#setup)
|
||||
- [Web Server](#web-server)
|
||||
- [Running](#running)
|
||||
- [Extra Configuration](#extra-configuration)
|
||||
- [Feature Flags](#feature-flags)
|
||||
- [Users](#users)
|
||||
- [Search](#search)
|
||||
- [Local Storage](#local-storage)
|
||||
- [Contributing](#contributing)
|
||||
- [Resources](#resources)
|
||||
|
||||
## Contributing
|
||||
## Prerequisites
|
||||
|
||||
* [Laravel Herd](https://herd.laravel.com/) or a webserver such as [Apache](https://httpd.apache.org/download.cgi) or
|
||||
[Nginx](https://www.nginx.com/resources/wiki/start/topics/tutorials/install/)
|
||||
* PHP 8.5
|
||||
* MySQL 8+
|
||||
* [composer](https://getcomposer.org/download/) for vendor dependencies
|
||||
|
||||
A LAMP stack, such as [XAMPP](https://www.apachefriends.org/download.html), can
|
||||
also be used to set up Apache, MySQL, and PHP.
|
||||
|
||||
Alternatively, you may use [Laravel Herd](https://herd.laravel.com), which
|
||||
provides a simple local development environment with PHP and a web server.
|
||||
|
||||
## Pre Setup
|
||||
|
||||
### PHP
|
||||
|
||||
We should ensure that we have the following extensions enabled for php in `php.ini`.
|
||||
|
||||
`fileinfo` - Needed to detect MIME type of files during seeding.
|
||||
|
||||
`gd` - Needed to fake image files for testing.
|
||||
|
||||
`pdo_mysql` - Needed to use MySQL.
|
||||
|
||||
In order to accept video uploads, we should ensure that php will accept requests of adequate sizes.
|
||||
|
||||
Set `post_max_size` to `200M`.
|
||||
|
||||
Set `upload_max_filesize` to `200M`.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone git@github.com:AnimeThemes/animethemes-server.git
|
||||
cd animethemes-server
|
||||
|
||||
# Create the database
|
||||
mysql -h localhost -u root -p -e "CREATE DATABASE IF NOT EXISTS ``animethemes`` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
||||
|
||||
# Install dependencies, migrate the database, run seeders and import dumps
|
||||
composer setup
|
||||
```
|
||||
|
||||
### Web Server
|
||||
|
||||
#### Laravel Herd
|
||||
|
||||
```sh
|
||||
# Run the following in the project directory:
|
||||
herd link animethemes.test
|
||||
```
|
||||
|
||||
### Running
|
||||
|
||||
After installation, restart the web server to apply the configuration.
|
||||
|
||||
If all went well, AnimeThemes should be live at `http://animethemes.test` (or whatever set the server name is set to).
|
||||
|
||||
## Extra Configuration
|
||||
|
||||
### Feature Flags
|
||||
|
||||
Features that require external services are disabled by default. Here we will review the configuration options for enabling additional features.
|
||||
|
||||
For example, if we want to enable video streams, we need to set the `App\Features\AllowVideoStreams` value on DB to `true`. We recommend setting up a local archive for the `videos_local` disk.
|
||||
|
||||
### Users
|
||||
|
||||
```sh
|
||||
# Open the terminal of tinker
|
||||
php artisan tinker
|
||||
|
||||
# Create the user
|
||||
$user = User::factory()->create(['name' => 'User Name', 'email' => 'example@example.com', 'password' => 'password', 'email_verified_at' => now()]);
|
||||
|
||||
# It is useful to create a user with the Admin role with permissions to all actions
|
||||
# Assign the Admin role to the user
|
||||
$user->assignRole('Admin');
|
||||
```
|
||||
|
||||
### Search
|
||||
|
||||
If we want to enable scout, we need to configure a search engine (either Elasticsearch or Typesense).
|
||||
|
||||
Change the `SCOUT_DRIVER` variable in `.env` to either "elastic" or "typesense". Add additional configuration like host and port.
|
||||
|
||||
Migrate and import models into our indices using:
|
||||
|
||||
```sh
|
||||
# Elasticsearch: run the elastic migrations
|
||||
php artisan elastic:migrate
|
||||
|
||||
# Elasticsearch and Typesense: Import Models with a seeder
|
||||
php artisan db:seed --class="Database\Seeders\Scout\ImportModelsSeeder"
|
||||
```
|
||||
|
||||
### Local Storage
|
||||
|
||||
We are not required to set up s3 buckets in order to interact with media. We have the option to configure local filesystems that we can stream audio/video from and download scripts/dumps from.
|
||||
|
||||
Configure local filesystem disks in `.env`
|
||||
|
||||
```sh
|
||||
AUDIO_DISK_DEFAULT=audios_local
|
||||
AUDIO_DISKS=audios_local
|
||||
...
|
||||
DUMP_DISK=dumps_local
|
||||
...
|
||||
IMAGE_DISK=images_local
|
||||
...
|
||||
VIDEO_DISK_DEFAULT=videos_local
|
||||
VIDEO_DISKS=videos_local
|
||||
...
|
||||
SCRIPT_DISK=scripts_local
|
||||
|
||||
```
|
||||
|
||||
By default, app storage directories will be used to store media. External directories can be specified as the root if media is stored elsewhere.
|
||||
|
||||
Remark: It is recommended to include a `.gitignore` at the root directory of the filesystem so that media files are not indexed by git.
|
||||
|
||||
```sh
|
||||
AUDIO_DISK_ROOT="E:\\animethemes-audios\\"
|
||||
...
|
||||
DUMP_DISK_ROOT="E:\\animethemes-db-dumps\\"
|
||||
...
|
||||
IMAGE_DISK_ROOT="E:\\animethemes-images\\"
|
||||
...
|
||||
SCRIPT_DISK_ROOT="E:\\animethemes-scripts\\"
|
||||
...
|
||||
VIDEO_DISK_ROOT="E:\\animethemes-videos\\"
|
||||
```
|
||||
|
||||
Create symbolic links to target storage directories.
|
||||
|
||||
```php
|
||||
php artisan storage:link
|
||||
```
|
||||
|
||||
# Contributing
|
||||
|
||||
Please review the [**Contributing Guide**](https://github.com/AnimeThemes/animethemes-server/wiki/Contributing) in the wiki for detailed instructions.
|
||||
|
||||
## Resources
|
||||
# Resources
|
||||
|
||||
Please make use of the #development channel in the [**Discord Server**](https://discordapp.com/invite/m9zbVyQ) for questions pertaining to the AnimeThemes database or API.
|
||||
Please make use of the #api channel in the [**Discord Server**](https://discordapp.com/invite/m9zbVyQ) for questions pertaining to the AnimeThemes database or API.
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions;
|
||||
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ActionResult
|
||||
{
|
||||
public function __construct(protected readonly ActionStatus $status, protected readonly ?string $message = null) {}
|
||||
|
||||
public function getStatus(): ActionStatus
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function getMessage(): ?string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
public function hasFailed(): bool
|
||||
{
|
||||
return $this->status === ActionStatus::FAILED;
|
||||
}
|
||||
|
||||
public function toLog(): void
|
||||
{
|
||||
$this->hasFailed()
|
||||
? Log::error($this->getMessage())
|
||||
: Log::info($this->getMessage());
|
||||
}
|
||||
|
||||
public function toConsole(Command $command): void
|
||||
{
|
||||
$this->hasFailed()
|
||||
? $command->error($this->getMessage())
|
||||
: $command->info($this->getMessage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Discord;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use App\Models\Discord\DiscordThread;
|
||||
use App\Models\Wiki\Anime;
|
||||
use Exception;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class DiscordThreadAction
|
||||
{
|
||||
/**
|
||||
* Get the HTTP client for Discord API.
|
||||
*/
|
||||
public static function getHttp(): PendingRequest
|
||||
{
|
||||
return Http::withHeaders(['x-api-key' => Config::get('services.discord.api_key')])
|
||||
->baseUrl(Config::get('services.discord.api_url'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $fields
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function handle(Anime $anime, array $fields): ActionResult
|
||||
{
|
||||
try {
|
||||
$response = static::getHttp()
|
||||
->acceptJson()
|
||||
->post('/thread', ['name' => Arr::get($fields, 'name'), 'slug' => $anime->slug])
|
||||
->throw()
|
||||
->json();
|
||||
|
||||
if (Arr::has($response, 'id')) {
|
||||
DiscordThread::query()->create([
|
||||
DiscordThread::ATTRIBUTE_NAME => Arr::get($response, 'name'),
|
||||
DiscordThread::ATTRIBUTE_ID => intval(Arr::get($response, 'id')),
|
||||
DiscordThread::ATTRIBUTE_ANIME => $anime->getKey(),
|
||||
]);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return new ActionResult(ActionStatus::PASSED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the thread by ID.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function get(string $id): array
|
||||
{
|
||||
return static::getHttp()
|
||||
->acceptJson()
|
||||
->get('/thread', ['id' => $id])
|
||||
->throw()
|
||||
->json();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Discord;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use App\Enums\Actions\Models\Wiki\Video\DiscordNotificationType;
|
||||
use App\Models\Wiki\Video;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DiscordVideoNotificationAction
|
||||
{
|
||||
/**
|
||||
* @param Collection<int, Video> $videos
|
||||
* @param array<string, mixed> $fields
|
||||
*/
|
||||
public function handle(Collection $videos, array $fields): ActionResult
|
||||
{
|
||||
$type = Arr::get($fields, DiscordNotificationType::getFieldKey());
|
||||
|
||||
$newVideos = [];
|
||||
|
||||
foreach ($videos as $video) {
|
||||
$video
|
||||
->load([
|
||||
'animethemeentries.animetheme.anime.discordthread',
|
||||
]);
|
||||
|
||||
$anime = $video->animethemeentries->first()->animetheme->anime;
|
||||
|
||||
if ($anime->discordthread === null) {
|
||||
if (Str::length($anime->name) >= 100) {
|
||||
$anime->name = Str::limit($anime->name, 96, '...');
|
||||
}
|
||||
|
||||
$threadAction = new DiscordThreadAction();
|
||||
|
||||
$threadAction->handle($anime, ['name' => $anime->getName()]);
|
||||
$anime->load('discordthread');
|
||||
}
|
||||
|
||||
$newVideos[] = [
|
||||
'threadId' => $anime->discordthread->getKey(),
|
||||
'videoId' => $video->getKey(),
|
||||
];
|
||||
}
|
||||
|
||||
DiscordThreadAction::getHttp()
|
||||
->post('/notification', [
|
||||
'type' => $type->value,
|
||||
'videos' => $newVideos,
|
||||
])
|
||||
->throw();
|
||||
|
||||
return new ActionResult(ActionStatus::PASSED);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\Auth\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Rules\ModerationRule;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
@@ -13,34 +13,33 @@ use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Fortify\Contracts\CreatesNewUsers;
|
||||
use Laravel\Jetstream\Jetstream;
|
||||
|
||||
/**
|
||||
* Class CreateNewUser.
|
||||
*/
|
||||
class CreateNewUser implements CreatesNewUsers
|
||||
{
|
||||
/**
|
||||
* Validate and create a newly registered user.
|
||||
*
|
||||
* @param array $input
|
||||
* @return Model
|
||||
* @param array<string, string> $input
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function create(array $input): Model
|
||||
public function create(array $input): User
|
||||
{
|
||||
Validator::make($input, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'string', 'email', 'max:255', Rule::unique(User::TABLE)],
|
||||
'password' => Password::required(),
|
||||
'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['required'] : '',
|
||||
User::ATTRIBUTE_NAME => ['required', 'string', 'max:35', 'alpha_dash', Rule::unique(User::class), new ModerationRule()],
|
||||
User::ATTRIBUTE_EMAIL => ['required', 'string', 'email', 'max:255', 'indisposable', Rule::unique(User::class)],
|
||||
User::ATTRIBUTE_PASSWORD => Password::required(),
|
||||
'terms' => ['required'],
|
||||
])->validate();
|
||||
|
||||
return User::query()->create([
|
||||
User::ATTRIBUTE_NAME => Arr::get($input, 'name'),
|
||||
User::ATTRIBUTE_EMAIL => Arr::get($input, 'email'),
|
||||
User::ATTRIBUTE_PASSWORD => Hash::make(Arr::get($input, 'password')),
|
||||
$user = new User([
|
||||
User::ATTRIBUTE_NAME => Arr::get($input, User::ATTRIBUTE_NAME),
|
||||
User::ATTRIBUTE_EMAIL => Arr::get($input, User::ATTRIBUTE_EMAIL),
|
||||
User::ATTRIBUTE_PASSWORD => Hash::make(Arr::get($input, User::ATTRIBUTE_PASSWORD)),
|
||||
]);
|
||||
|
||||
$user->save();
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,28 +12,23 @@ use Illuminate\Validation\Rules\Password;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Fortify\Contracts\ResetsUserPasswords;
|
||||
|
||||
/**
|
||||
* Class ResetUserPassword.
|
||||
*/
|
||||
class ResetUserPassword implements ResetsUserPasswords
|
||||
{
|
||||
/**
|
||||
* Validate and reset the user's forgotten password.
|
||||
*
|
||||
* @param mixed $user
|
||||
* @param array $input
|
||||
* @return void
|
||||
* @param array<string, string> $input
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function reset($user, array $input): void
|
||||
public function reset(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'password' => Password::required(),
|
||||
User::ATTRIBUTE_PASSWORD => Password::required(),
|
||||
])->validate();
|
||||
|
||||
$user->forceFill([
|
||||
User::ATTRIBUTE_PASSWORD => Hash::make(Arr::get($input, 'password')),
|
||||
User::ATTRIBUTE_PASSWORD => Hash::make(Arr::get($input, User::ATTRIBUTE_PASSWORD)),
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,26 +13,21 @@ use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\Validation\Validator as IlluminateValidator;
|
||||
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
|
||||
|
||||
/**
|
||||
* Class UpdateUserPassword.
|
||||
*/
|
||||
class UpdateUserPassword implements UpdatesUserPasswords
|
||||
{
|
||||
/**
|
||||
* Validate and update the user's password.
|
||||
*
|
||||
* @param mixed $user
|
||||
* @param array $input
|
||||
* @return void
|
||||
* @param array<string, string> $input
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function update($user, array $input): void
|
||||
public function update(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'current_password' => ['required', 'string'],
|
||||
'password' => Password::required(),
|
||||
])->after(function (IlluminateValidator $validator) use ($user, $input) {
|
||||
User::ATTRIBUTE_PASSWORD => Password::required(),
|
||||
])->after(function (IlluminateValidator $validator) use ($user, $input): void {
|
||||
if (! isset($input['current_password']) || ! Hash::check($input['current_password'], $user->password)) {
|
||||
$validator->errors()
|
||||
->add('current_password', __('The provided password does not match your current password.'));
|
||||
@@ -40,7 +35,7 @@ class UpdateUserPassword implements UpdatesUserPasswords
|
||||
})->validateWithBag('updatePassword');
|
||||
|
||||
$user->forceFill([
|
||||
User::ATTRIBUTE_PASSWORD => Hash::make(Arr::get($input, 'password')),
|
||||
User::ATTRIBUTE_PASSWORD => Hash::make(Arr::get($input, User::ATTRIBUTE_PASSWORD)),
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,64 +5,38 @@ declare(strict_types=1);
|
||||
namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\Auth\User;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use App\Rules\ModerationRule;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
|
||||
|
||||
/**
|
||||
* Class UpdateUserProfileInformation.
|
||||
*/
|
||||
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
||||
{
|
||||
/**
|
||||
* Validate and update the given user's profile information.
|
||||
*
|
||||
* @param mixed $user
|
||||
* @param array $input
|
||||
* @return void
|
||||
* @param array<string, string> $input
|
||||
*
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function update(mixed $user, array $input): void
|
||||
public function update(User $user, array $input): void
|
||||
{
|
||||
Validator::make($input, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'email', 'max:255', Rule::unique(User::TABLE)->ignore($user->id)],
|
||||
'photo' => ['nullable', 'mimes:jpg,jpeg,png', 'max:1024'],
|
||||
])->validateWithBag('updateProfileInformation');
|
||||
$validated = Validator::make($input, [
|
||||
User::ATTRIBUTE_NAME => ['required_without:'.User::ATTRIBUTE_EMAIL, 'string', 'max:35', 'alpha_dash', Rule::unique(User::class)->ignore($user->id), new ModerationRule()],
|
||||
User::ATTRIBUTE_EMAIL => ['required_without:'.User::ATTRIBUTE_NAME, 'string', 'email', 'max:255', 'indisposable', Rule::unique(User::class)->ignore($user->id)],
|
||||
])->validate();
|
||||
|
||||
if (isset($input['photo'])) {
|
||||
$user->updateProfilePhoto($input['photo']);
|
||||
}
|
||||
$email = Arr::get($validated, User::ATTRIBUTE_EMAIL);
|
||||
if (Arr::has($validated, User::ATTRIBUTE_EMAIL) && $email !== $user->email) {
|
||||
$validated += [User::ATTRIBUTE_EMAIL_VERIFIED_AT => null];
|
||||
|
||||
if ($input['email'] !== $user->email && $user instanceof MustVerifyEmail) {
|
||||
$this->updateVerifiedUser($user, $input);
|
||||
$user->update($validated);
|
||||
|
||||
$user->sendEmailVerificationNotification();
|
||||
} else {
|
||||
$user->forceFill([
|
||||
User::ATTRIBUTE_NAME => Arr::get($input, 'name'),
|
||||
User::ATTRIBUTE_EMAIL => Arr::get($input, 'email'),
|
||||
])->save();
|
||||
$user->update($validated);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the given verified user's profile information.
|
||||
*
|
||||
* @param mixed $user
|
||||
* @param array $input
|
||||
* @return void
|
||||
*/
|
||||
protected function updateVerifiedUser(mixed $user, array $input): void
|
||||
{
|
||||
$user->forceFill([
|
||||
User::ATTRIBUTE_NAME => Arr::get($input, 'name'),
|
||||
User::ATTRIBUTE_EMAIL => Arr::get($input, 'email'),
|
||||
User::ATTRIBUTE_EMAIL_VERIFIED_AT => null,
|
||||
])->save();
|
||||
|
||||
$user->sendEmailVerificationNotification();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\GraphQL;
|
||||
|
||||
use App\Concerns\Actions\GraphQL\ConstrainsEagerLoads;
|
||||
use App\Concerns\Actions\GraphQL\FieldSelection;
|
||||
use App\Concerns\Actions\GraphQL\PaginatesModels;
|
||||
use App\Concerns\Actions\GraphQL\SortsModels;
|
||||
use App\Contracts\GraphQL\EnumSort;
|
||||
use App\GraphQL\Argument\SortArgument;
|
||||
use App\GraphQL\Criteria\Sort\RelationSortCriteria;
|
||||
use App\GraphQL\Schema\Types\BaseType;
|
||||
use App\Rules\GraphQL\Argument\FirstArgumentRule;
|
||||
use App\Scout\Criteria;
|
||||
use App\Scout\Search;
|
||||
use GraphQL\Type\Definition\ResolveInfo;
|
||||
use Illuminate\Contracts\Pagination\Paginator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use UnitEnum;
|
||||
|
||||
class IndexAction
|
||||
{
|
||||
use ConstrainsEagerLoads;
|
||||
use FieldSelection;
|
||||
use PaginatesModels;
|
||||
use SortsModels;
|
||||
|
||||
public function index(Builder $builder, array $args, BaseType $type, ResolveInfo $resolveInfo): Paginator
|
||||
{
|
||||
$this->withAggregates($builder, $args, $this->getSelection($resolveInfo), $type);
|
||||
|
||||
$this->filter($builder, $args, $type);
|
||||
|
||||
$this->sort($builder, $args);
|
||||
|
||||
$this->constrainEagerLoads($builder, $resolveInfo, $type);
|
||||
|
||||
return $this->paginate($builder, $args);
|
||||
}
|
||||
|
||||
public function search(Builder $builder, array $args, BaseType $type, ResolveInfo $resolveInfo): Paginator
|
||||
{
|
||||
$criteria = new Criteria(Arr::get($args, 'search'));
|
||||
|
||||
$searchBuilder = Search::getSearch($builder->getModel(), $criteria);
|
||||
|
||||
$eloquentCallback = function (Builder $builder) use ($args, $type, $resolveInfo): void {
|
||||
$this->withAggregates($builder, $args, $this->getSelection($resolveInfo), $type);
|
||||
|
||||
$this->filter($builder, $args, $type);
|
||||
|
||||
$this->sort($builder, $args);
|
||||
|
||||
$this->constrainEagerLoads($builder, $resolveInfo, $type);
|
||||
};
|
||||
|
||||
// Note: First for searching must not be too high.
|
||||
$first = min(100, Arr::get($args, 'first'));
|
||||
$page = Arr::get($args, 'page', 1);
|
||||
|
||||
Validator::make(['first' => $first], [
|
||||
'first' => ['required', 'integer', 'min:1', new FirstArgumentRule()],
|
||||
])->validate();
|
||||
|
||||
/** @var array<int, UnitEnum&EnumSort> $sorts */
|
||||
$sorts = Arr::get($args, SortArgument::ARGUMENT, []);
|
||||
|
||||
$sortsRaw = [];
|
||||
foreach ($sorts as $sort) {
|
||||
$criterion = $sort->getSortCriteria();
|
||||
|
||||
$column = $criterion->getColumn();
|
||||
$direction = $criterion->getDirection();
|
||||
$isString = $criterion->isStringField();
|
||||
|
||||
if ($criterion instanceof RelationSortCriteria) {
|
||||
$sortsRaw[$column] = [
|
||||
'direction' => $direction->value,
|
||||
'isString' => $isString,
|
||||
'relation' => $criterion->getRelation(),
|
||||
];
|
||||
} else {
|
||||
$sortsRaw[$column] = [
|
||||
'direction' => $direction,
|
||||
'isString' => $isString,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $searchBuilder->search($eloquentCallback, $first, $page, $sortsRaw);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\GraphQL;
|
||||
|
||||
use App\Concerns\Actions\GraphQL\ConstrainsEagerLoads;
|
||||
use App\Concerns\Actions\GraphQL\FiltersModels;
|
||||
use App\GraphQL\Schema\Types\BaseType;
|
||||
use GraphQL\Type\Definition\ResolveInfo;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ShowAction
|
||||
{
|
||||
use ConstrainsEagerLoads;
|
||||
use FiltersModels;
|
||||
|
||||
public function show(Builder $builder, array $args, BaseType $type, ResolveInfo $resolveInfo): Model
|
||||
{
|
||||
$this->withAggregates($builder, $args, $this->getSelection($resolveInfo), $type);
|
||||
|
||||
$this->filter($builder, $args, $type);
|
||||
|
||||
$this->constrainEagerLoads($builder, $resolveInfo, $type);
|
||||
|
||||
return $builder->firstOrFail();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Http\Admin\Dump;
|
||||
|
||||
use App\Actions\Http\DownloadAction;
|
||||
use App\Constants\Config\DumpConstants;
|
||||
use App\Models\Admin\Dump;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
|
||||
/**
|
||||
* @extends DownloadAction<Dump>
|
||||
*/
|
||||
class DumpDownloadAction extends DownloadAction
|
||||
{
|
||||
/**
|
||||
* Get the path of the resource in storage.
|
||||
*/
|
||||
protected function path(): string
|
||||
{
|
||||
return $this->model->path;
|
||||
}
|
||||
|
||||
public function disk(): string
|
||||
{
|
||||
return Config::get(DumpConstants::DISK_QUALIFIED);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Http\Api;
|
||||
|
||||
use App\Contracts\Models\Nameable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @template TModel of \Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
class DestroyAction
|
||||
{
|
||||
/**
|
||||
* @param TModel $model
|
||||
* @return TModel
|
||||
*/
|
||||
public function destroy(Model $model): Model
|
||||
{
|
||||
$model->delete();
|
||||
|
||||
// Scout will load relations to refresh related search indices.
|
||||
$model->unsetRelations();
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Force delete model that doesn't apply soft deletes.
|
||||
*
|
||||
* @param TModel&Nameable $model
|
||||
*/
|
||||
public function forceDelete(Model&Nameable $model): string
|
||||
{
|
||||
$message = Str::of(Str::headline(class_basename($model)))
|
||||
->append(' \'')
|
||||
->append($model->getName())
|
||||
->append('\' was deleted.')
|
||||
->__toString();
|
||||
|
||||
$model->delete();
|
||||
|
||||
return $message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Http\Api;
|
||||
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ForceDeleteAction
|
||||
{
|
||||
public function forceDelete(BaseModel $model): string
|
||||
{
|
||||
$message = Str::of(Str::headline(class_basename($model)))
|
||||
->append(' \'')
|
||||
->append($model->getName())
|
||||
->append('\' was deleted.')
|
||||
->__toString();
|
||||
|
||||
$model->forceDelete();
|
||||
|
||||
return $message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Http\Api;
|
||||
|
||||
use App\Concerns\Actions\Http\Api\ConstrainsEagerLoads;
|
||||
use App\Enums\Http\Api\Paging\PaginationStrategy;
|
||||
use App\Http\Api\Criteria\Paging\Criteria;
|
||||
use App\Http\Api\Filter\HasFilter;
|
||||
use App\Http\Api\Query\Query;
|
||||
use App\Http\Api\Schema\EloquentSchema;
|
||||
use App\Http\Api\Schema\Schema;
|
||||
use App\Http\Api\Scope\ScopeParser;
|
||||
use App\Scout\Search;
|
||||
use Illuminate\Contracts\Pagination\Paginator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use RuntimeException;
|
||||
|
||||
class IndexAction
|
||||
{
|
||||
use ConstrainsEagerLoads;
|
||||
|
||||
/**
|
||||
* Show paginated listing of models.
|
||||
*/
|
||||
public function index(Builder $builder, Query $query, Schema $schema): Collection|Paginator
|
||||
{
|
||||
$builder->with($this->constrainEagerLoads($query, $schema));
|
||||
|
||||
$this->select($builder, $query, $schema);
|
||||
|
||||
$this->withAggregates($builder, $query, $schema);
|
||||
|
||||
$scope = ScopeParser::parse($schema->type());
|
||||
$this->filter($builder, $query, $schema, $scope);
|
||||
|
||||
// special case: only apply HasFilter to top-level models
|
||||
if (filled($schema->allowedIncludes())) {
|
||||
$hasFilter = new HasFilter($schema->allowedIncludes());
|
||||
foreach ($query->getFilterCriteria() as $criteria) {
|
||||
if ($criteria->shouldFilter($hasFilter, $scope)) {
|
||||
$criteria->filter($builder, $hasFilter, $query, $schema);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->sort($builder, $query, $schema, $scope);
|
||||
|
||||
// paginate
|
||||
$paginationCriteria = $query->getPagingCriteria(PaginationStrategy::OFFSET);
|
||||
|
||||
return $paginationCriteria instanceof Criteria
|
||||
? $paginationCriteria->paginate($builder)
|
||||
: $builder->get();
|
||||
}
|
||||
|
||||
public function search(
|
||||
Query $query,
|
||||
Schema $schema,
|
||||
PaginationStrategy $paginationStrategy = PaginationStrategy::OFFSET
|
||||
): Collection|Paginator {
|
||||
if ($query->hasSearchCriteria() && $schema instanceof EloquentSchema) {
|
||||
$search = Search::getSearch($schema->model(), $query->getSearchCriteria());
|
||||
|
||||
if ($search->shouldSearch()) {
|
||||
return $search->searchViaJSONAPI($query, $schema, $paginationStrategy);
|
||||
}
|
||||
}
|
||||
|
||||
// Let developer know why search can't be performed
|
||||
$driver = Config::get('scout.driver');
|
||||
$term = $query->getSearchCriteria()?->getTerm();
|
||||
throw new RuntimeException("Can't search for term '$term' with driver '$driver' and type '{$schema->type()}'");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Http\Api\List\Playlist\Track;
|
||||
|
||||
use App\Actions\Http\Api\DestroyAction;
|
||||
use App\Actions\Models\List\Playlist\RemoveTrackAction;
|
||||
use App\Concerns\Actions\List\LocksPlaylist;
|
||||
use App\Models\List\Playlist;
|
||||
use App\Models\List\Playlist\PlaylistTrack;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DestroyTrackAction
|
||||
{
|
||||
use LocksPlaylist;
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function destroy(Playlist $playlist, PlaylistTrack $track): string
|
||||
{
|
||||
return $this->withPlaylistLock($playlist, function () use ($playlist, $track) {
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$removeAction = new RemoveTrackAction();
|
||||
|
||||
$removeAction->remove($playlist, $track);
|
||||
|
||||
$message = Str::of(Str::headline(class_basename($track)))
|
||||
->append(' \'')
|
||||
->append($track->getName())
|
||||
->append('\' was deleted.')
|
||||
->__toString();
|
||||
|
||||
$destroyAction = new DestroyAction();
|
||||
|
||||
$destroyAction->forceDelete($track);
|
||||
|
||||
DB::commit();
|
||||
|
||||
return $message;
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
DB::rollBack();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Http\Api\List\Playlist\Track;
|
||||
|
||||
use App\Actions\Http\Api\StoreAction;
|
||||
use App\Actions\Models\List\Playlist\InsertTrackAction;
|
||||
use App\Actions\Models\List\Playlist\InsertTrackAfterAction;
|
||||
use App\Actions\Models\List\Playlist\InsertTrackBeforeAction;
|
||||
use App\Concerns\Actions\List\LocksPlaylist;
|
||||
use App\Contracts\Models\HasHashids;
|
||||
use App\Models\List\Playlist;
|
||||
use App\Models\List\Playlist\PlaylistTrack;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class StoreTrackAction
|
||||
{
|
||||
use LocksPlaylist;
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function store(Playlist $playlist, Builder $builder, array $parameters): PlaylistTrack
|
||||
{
|
||||
return $this->withPlaylistLock($playlist, function () use ($playlist, $builder, $parameters) {
|
||||
$trackParameters = $parameters;
|
||||
|
||||
$previousHashid = Arr::pull($trackParameters, PlaylistTrack::RELATION_PREVIOUS);
|
||||
$nextHashid = Arr::pull($trackParameters, PlaylistTrack::RELATION_NEXT);
|
||||
|
||||
$trackParameters += [PlaylistTrack::ATTRIBUTE_PLAYLIST => $playlist->getKey()];
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
/** @var StoreAction<PlaylistTrack> $storeAction */
|
||||
$storeAction = new StoreAction();
|
||||
|
||||
$track = $storeAction->store($builder, $trackParameters);
|
||||
|
||||
if (filled($nextHashid) && blank($previousHashid)) {
|
||||
/** @var PlaylistTrack $next */
|
||||
$next = PlaylistTrack::query()
|
||||
->with(PlaylistTrack::RELATION_PREVIOUS)
|
||||
->where(PlaylistTrack::ATTRIBUTE_PLAYLIST, $playlist->getKey())
|
||||
->where(HasHashids::ATTRIBUTE_HASHID, $nextHashid)
|
||||
->firstOrFail();
|
||||
|
||||
$insertAction = new InsertTrackBeforeAction();
|
||||
|
||||
$insertAction->insertBefore($playlist, $track, $next);
|
||||
}
|
||||
|
||||
if (filled($previousHashid) && blank($nextHashid)) {
|
||||
/** @var PlaylistTrack $previous */
|
||||
$previous = PlaylistTrack::query()
|
||||
->with(PlaylistTrack::RELATION_NEXT)
|
||||
->where(PlaylistTrack::ATTRIBUTE_PLAYLIST, $playlist->getKey())
|
||||
->where(HasHashids::ATTRIBUTE_HASHID, $previousHashid)
|
||||
->firstOrFail();
|
||||
|
||||
$insertAction = new InsertTrackAfterAction();
|
||||
|
||||
$insertAction->insertAfter($playlist, $track, $previous);
|
||||
}
|
||||
|
||||
if (blank($nextHashid) && blank($previousHashid)) {
|
||||
$insertAction = new InsertTrackAction();
|
||||
|
||||
$insertAction->insert($playlist, $track);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
return $storeAction->cleanup($track);
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
DB::rollBack();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Http\Api\List\Playlist\Track;
|
||||
|
||||
use App\Actions\Http\Api\UpdateAction;
|
||||
use App\Actions\Models\List\Playlist\InsertTrackAfterAction;
|
||||
use App\Actions\Models\List\Playlist\InsertTrackBeforeAction;
|
||||
use App\Actions\Models\List\Playlist\RemoveTrackAction;
|
||||
use App\Concerns\Actions\List\LocksPlaylist;
|
||||
use App\Contracts\Models\HasHashids;
|
||||
use App\Models\List\Playlist;
|
||||
use App\Models\List\Playlist\PlaylistTrack;
|
||||
use Exception;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class UpdateTrackAction
|
||||
{
|
||||
use LocksPlaylist;
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function update(Playlist $playlist, PlaylistTrack $track, array $parameters): PlaylistTrack
|
||||
{
|
||||
return $this->withPlaylistLock($playlist, function () use ($playlist, $track, $parameters) {
|
||||
$trackParameters = $parameters;
|
||||
|
||||
$previousHashid = Arr::pull($trackParameters, PlaylistTrack::RELATION_PREVIOUS);
|
||||
$nextHashid = Arr::pull($trackParameters, PlaylistTrack::RELATION_NEXT);
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
/** @var UpdateAction<PlaylistTrack> $updateAction */
|
||||
$updateAction = new UpdateAction();
|
||||
|
||||
$updateAction->update($track, $trackParameters);
|
||||
|
||||
if (filled($nextHashid) || filled($previousHashid)) {
|
||||
$removeAction = new RemoveTrackAction();
|
||||
|
||||
$removeAction->remove($playlist, $track);
|
||||
}
|
||||
|
||||
if (filled($nextHashid) && blank($previousHashid)) {
|
||||
$next = PlaylistTrack::query()
|
||||
->with(PlaylistTrack::RELATION_PREVIOUS)
|
||||
->where(PlaylistTrack::ATTRIBUTE_PLAYLIST, $playlist->getKey())
|
||||
->where(HasHashids::ATTRIBUTE_HASHID, $nextHashid)
|
||||
->firstOrFail();
|
||||
|
||||
$insertAction = new InsertTrackBeforeAction();
|
||||
|
||||
$insertAction->insertBefore($playlist, $track, $next);
|
||||
}
|
||||
|
||||
if (filled($previousHashid) && blank($nextHashid)) {
|
||||
$previous = PlaylistTrack::query()
|
||||
->with(PlaylistTrack::RELATION_NEXT)
|
||||
->where(PlaylistTrack::ATTRIBUTE_PLAYLIST, $playlist->getKey())
|
||||
->where(HasHashids::ATTRIBUTE_HASHID, $previousHashid)
|
||||
->firstOrFail();
|
||||
|
||||
$insertAction = new InsertTrackAfterAction();
|
||||
|
||||
$insertAction->insertAfter($playlist, $track, $previous);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
return $updateAction->cleanup($track);
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
DB::rollBack();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Http\Api;
|
||||
|
||||
use App\Contracts\Models\SoftDeletable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class RestoreAction
|
||||
{
|
||||
public function restore(Model&SoftDeletable $model): Model&SoftDeletable
|
||||
{
|
||||
$model->restore();
|
||||
|
||||
return $this->cleanup($model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform model cleanup for presentation.
|
||||
*/
|
||||
public function cleanup(Model&SoftDeletable $model): Model&SoftDeletable
|
||||
{
|
||||
// Scout will load relations to refresh related search indices.
|
||||
$model->unsetRelations();
|
||||
|
||||
return $model;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Http\Api;
|
||||
|
||||
use App\Concerns\Actions\Http\Api\ConstrainsEagerLoads;
|
||||
use App\Http\Api\Query\Query;
|
||||
use App\Http\Api\Schema\Schema;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ShowAction
|
||||
{
|
||||
use ConstrainsEagerLoads;
|
||||
|
||||
public function show(Model $model, Query $query, Schema $schema): Model
|
||||
{
|
||||
// eager load relations with constraints
|
||||
$model->load($this->constrainEagerLoads($query, $schema));
|
||||
|
||||
// Load aggregate relation values
|
||||
$this->loadAggregates($model, $query, $schema);
|
||||
|
||||
return $model;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Http\Api;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @template TModel of \Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
class StoreAction
|
||||
{
|
||||
/**
|
||||
* @param Builder<TModel> $builder
|
||||
* @return TModel
|
||||
*/
|
||||
public function store(Builder $builder, array $parameters): Model
|
||||
{
|
||||
$model = $builder->create($parameters);
|
||||
|
||||
return $this->cleanup($model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform model cleanup for presentation.
|
||||
*
|
||||
* @param TModel $model
|
||||
* @return TModel
|
||||
*/
|
||||
public function cleanup(Model $model): Model
|
||||
{
|
||||
// Scout will load relations to refresh related search indices.
|
||||
$model->unsetRelations();
|
||||
|
||||
// Columns with default values may be unset if not provided in the query string.
|
||||
$model->refresh();
|
||||
|
||||
return $model;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Http\Api;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @template TModel of \Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
class UpdateAction
|
||||
{
|
||||
/**
|
||||
* @param TModel $model
|
||||
* @return TModel
|
||||
*/
|
||||
public function update(Model $model, array $parameters): Model
|
||||
{
|
||||
$model->update($parameters);
|
||||
|
||||
return $this->cleanup($model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform model cleanup for presentation.
|
||||
*
|
||||
* @param TModel $model
|
||||
* @return TModel
|
||||
*/
|
||||
public function cleanup(Model $model): Model
|
||||
{
|
||||
// Scout will load relations to refresh related search indices.
|
||||
$model->unsetRelations();
|
||||
|
||||
return $model;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Http\Api\Wiki\Image;
|
||||
|
||||
use App\Actions\Http\Api\StoreAction;
|
||||
use App\Http\Api\Field\Wiki\Image\ImageFileField;
|
||||
use App\Models\Wiki\Image;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Filesystem\FilesystemAdapter;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* @extends StoreAction<Image>
|
||||
*/
|
||||
class StoreImageAction extends StoreAction
|
||||
{
|
||||
/**
|
||||
* @param Builder<Image> $builder
|
||||
*/
|
||||
public function store(Builder $builder, array $parameters): Image
|
||||
{
|
||||
$imageParameters = Arr::except($parameters, ImageFileField::ATTRIBUTE_FILE);
|
||||
|
||||
$file = Arr::get($parameters, ImageFileField::ATTRIBUTE_FILE);
|
||||
if ($file instanceof UploadedFile) {
|
||||
/** @var FilesystemAdapter $fs */
|
||||
$fs = Storage::disk(Config::get('image.disk'));
|
||||
|
||||
$fsFile = $fs->putFile('', $file);
|
||||
|
||||
Arr::set($imageParameters, Image::ATTRIBUTE_PATH, $fsFile);
|
||||
}
|
||||
|
||||
return parent::store($builder, $imageParameters);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Http;
|
||||
|
||||
use App\Contracts\Storage\InteractsWithDisk;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Filesystem\FilesystemAdapter;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
/**
|
||||
* @template TModel of \App\Models\BaseModel
|
||||
*/
|
||||
abstract class DownloadAction implements InteractsWithDisk
|
||||
{
|
||||
/**
|
||||
* @param TModel $model
|
||||
*/
|
||||
public function __construct(protected readonly BaseModel $model) {}
|
||||
|
||||
public function download(): StreamedResponse
|
||||
{
|
||||
/** @var FilesystemAdapter $fs */
|
||||
$fs = Storage::disk($this->disk());
|
||||
|
||||
return $fs->download($this->path());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path of the resource in storage.
|
||||
*/
|
||||
abstract protected function path(): string;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Http;
|
||||
|
||||
use Illuminate\Filesystem\FilesystemAdapter;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
abstract class NginxStreamAction extends StreamAction
|
||||
{
|
||||
public function stream(string $disposition = 'inline'): Response
|
||||
{
|
||||
/** @var FilesystemAdapter $fs */
|
||||
$fs = Storage::disk($this->disk());
|
||||
|
||||
$temporaryURL = $fs->temporaryUrl($this->streamable->path(), now()->addMinutes(5));
|
||||
|
||||
// Get the url information
|
||||
$url_scheme = parse_url($temporaryURL, PHP_URL_SCHEME);
|
||||
$url_host = parse_url($temporaryURL, PHP_URL_HOST);
|
||||
$url_path_query = parse_url($temporaryURL, PHP_URL_PATH).'?'.parse_url($temporaryURL, PHP_URL_QUERY);
|
||||
|
||||
$link = "{$this->nginxRedirect()}$url_scheme/$url_host$url_path_query";
|
||||
|
||||
$response = new Response();
|
||||
|
||||
$disposition = $response->headers->makeDisposition($disposition, $this->streamable->basename());
|
||||
|
||||
return $response->withHeaders([
|
||||
'Accept-Ranges' => 'bytes',
|
||||
'Content-Type' => $this->streamable->mimetype(),
|
||||
'Content-Length' => $this->streamable->size(),
|
||||
'Content-Disposition' => $disposition,
|
||||
'X-Accel-Redirect' => $link,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the location of the nginx internal redirect.
|
||||
*/
|
||||
abstract protected function nginxRedirect(): string;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Http;
|
||||
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
abstract class ResponseStreamAction extends StreamAction
|
||||
{
|
||||
public function stream(string $disposition = 'inline'): StreamedResponse
|
||||
{
|
||||
$response = new StreamedResponse();
|
||||
|
||||
$disposition = $response->headers->makeDisposition($disposition, $this->streamable->basename());
|
||||
|
||||
$response->headers->replace([
|
||||
'Accept-Ranges' => 'bytes',
|
||||
'Content-Type' => $this->streamable->mimetype(),
|
||||
'Content-Length' => $this->streamable->size(),
|
||||
'Content-Disposition' => $disposition,
|
||||
]);
|
||||
|
||||
$fs = Storage::disk($this->disk());
|
||||
|
||||
$response->setCallback(function () use ($fs): void {
|
||||
$stream = $fs->readStream($this->streamable->path());
|
||||
fpassthru($stream);
|
||||
fclose($stream);
|
||||
});
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Http;
|
||||
|
||||
use App\Contracts\Models\Streamable;
|
||||
use App\Contracts\Storage\InteractsWithDisk;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
abstract class StreamAction implements InteractsWithDisk
|
||||
{
|
||||
public function __construct(protected readonly Streamable $streamable) {}
|
||||
|
||||
abstract public function stream(string $disposition = 'inline'): Response;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Http\Wiki\Audio;
|
||||
|
||||
use App\Actions\Http\NginxStreamAction;
|
||||
use App\Constants\Config\AudioConstants;
|
||||
use App\Models\Wiki\Audio;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
|
||||
class AudioNginxStreamAction extends NginxStreamAction
|
||||
{
|
||||
public function __construct(Audio $audio)
|
||||
{
|
||||
parent::__construct($audio);
|
||||
}
|
||||
|
||||
public function disk(): string
|
||||
{
|
||||
return Config::get(AudioConstants::DEFAULT_DISK_QUALIFIED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the location of the nginx internal redirect.
|
||||
*/
|
||||
protected function nginxRedirect(): string
|
||||
{
|
||||
return Config::get(AudioConstants::NGINX_REDIRECT_QUALIFIED);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Http\Wiki\Audio;
|
||||
|
||||
use App\Actions\Http\ResponseStreamAction;
|
||||
use App\Constants\Config\AudioConstants;
|
||||
use App\Models\Wiki\Audio;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
|
||||
class AudioResponseStreamAction extends ResponseStreamAction
|
||||
{
|
||||
public function __construct(Audio $audio)
|
||||
{
|
||||
parent::__construct($audio);
|
||||
}
|
||||
|
||||
public function disk(): string
|
||||
{
|
||||
return Config::get(AudioConstants::DEFAULT_DISK_QUALIFIED);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Http\Wiki\Video\Script;
|
||||
|
||||
use App\Actions\Http\DownloadAction;
|
||||
use App\Constants\Config\VideoConstants;
|
||||
use App\Models\Wiki\Video\VideoScript;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
|
||||
/**
|
||||
* @extends DownloadAction<VideoScript>
|
||||
*/
|
||||
class ScriptDownloadAction extends DownloadAction
|
||||
{
|
||||
/**
|
||||
* Get the path of the resource in storage.
|
||||
*/
|
||||
protected function path(): string
|
||||
{
|
||||
return $this->model->path;
|
||||
}
|
||||
|
||||
public function disk(): string
|
||||
{
|
||||
return Config::get(VideoConstants::SCRIPT_DISK_QUALIFIED);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Http\Wiki\Video;
|
||||
|
||||
use App\Actions\Http\NginxStreamAction;
|
||||
use App\Constants\Config\VideoConstants;
|
||||
use App\Models\Wiki\Video;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
|
||||
class VideoNginxStreamAction extends NginxStreamAction
|
||||
{
|
||||
public function __construct(Video $video)
|
||||
{
|
||||
parent::__construct($video);
|
||||
}
|
||||
|
||||
public function disk(): string
|
||||
{
|
||||
return Config::get(VideoConstants::DEFAULT_DISK_QUALIFIED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the location of the nginx internal redirect.
|
||||
*/
|
||||
protected function nginxRedirect(): string
|
||||
{
|
||||
return Config::get(VideoConstants::NGINX_REDIRECT_QUALIFIED);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Http\Wiki\Video;
|
||||
|
||||
use App\Actions\Http\ResponseStreamAction;
|
||||
use App\Constants\Config\VideoConstants;
|
||||
use App\Models\Wiki\Video;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
|
||||
class VideoResponseStreamAction extends ResponseStreamAction
|
||||
{
|
||||
public function __construct(Video $video)
|
||||
{
|
||||
parent::__construct($video);
|
||||
}
|
||||
|
||||
public function disk(): string
|
||||
{
|
||||
return Config::get(VideoConstants::DEFAULT_DISK_QUALIFIED);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Jetstream;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Laravel\Jetstream\Contracts\DeletesUsers;
|
||||
|
||||
/**
|
||||
* Class DeleteUser.
|
||||
*/
|
||||
class DeleteUser implements DeletesUsers
|
||||
{
|
||||
/**
|
||||
* Delete the given user.
|
||||
*
|
||||
* @param mixed $user
|
||||
* @return void
|
||||
*/
|
||||
public function delete($user): void
|
||||
{
|
||||
DB::transaction(function () use ($user) {
|
||||
$user->tokens->each->delete();
|
||||
$user->delete();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Aggregate;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class SyncLikeAggregatesAction
|
||||
{
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function handle(): ActionResult
|
||||
{
|
||||
try {
|
||||
DB::statement('
|
||||
INSERT INTO like_aggregates (likeable_id, likeable_type, value)
|
||||
SELECT likeable_id, likeable_type, COUNT(*) as value
|
||||
FROM likes
|
||||
GROUP BY likeable_type, likeable_id
|
||||
ON DUPLICATE KEY UPDATE value = VALUES(value);
|
||||
');
|
||||
} catch (Exception $e) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED,
|
||||
$e->getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
return new ActionResult(
|
||||
ActionStatus::PASSED,
|
||||
'Like aggregates synced successfully',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models;
|
||||
|
||||
use App\Contracts\Models\HasHashids;
|
||||
use App\Models\BaseModel;
|
||||
use Vinkla\Hashids\Facades\Hashids;
|
||||
|
||||
class AssignHashidsAction
|
||||
{
|
||||
public function assign(HasHashids&BaseModel $model, ?string $connection = null): void
|
||||
{
|
||||
$hashids = Hashids::connection($connection);
|
||||
|
||||
$model->setAttribute(HasHashids::ATTRIBUTE_HASHID, $hashids->encode($model->hashids()));
|
||||
|
||||
$model->save();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @template TModel of \App\Models\BaseModel
|
||||
*/
|
||||
abstract class BackfillAction
|
||||
{
|
||||
/**
|
||||
* @param TModel $model
|
||||
*/
|
||||
public function __construct(protected BaseModel $model) {}
|
||||
|
||||
abstract public function handle(): ActionResult;
|
||||
|
||||
/**
|
||||
* Get the model the action is handling.
|
||||
*
|
||||
* @return TModel
|
||||
*/
|
||||
abstract protected function getModel(): BaseModel;
|
||||
|
||||
/**
|
||||
* Get the relation to resources.
|
||||
*/
|
||||
abstract protected function relation(): Relation;
|
||||
|
||||
/**
|
||||
* Get the human-friendly label for the underlying model.
|
||||
*/
|
||||
protected function label(): string
|
||||
{
|
||||
return Str::headline(class_basename($this->getModel()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Actions\Models\Wiki\ExternalApiAction;
|
||||
use App\Actions\Storage\Wiki\Image\UploadImageAction;
|
||||
use App\Concerns\Models\CanCreateExternalResource;
|
||||
use App\Contracts\Models\HasImages;
|
||||
use App\Contracts\Models\HasResources;
|
||||
use App\Enums\Models\Wiki\ImageFacet;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\BaseModel;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Models\Wiki\Image;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Uri;
|
||||
|
||||
abstract class BackfillWikiAction
|
||||
{
|
||||
use CanCreateExternalResource;
|
||||
|
||||
final public const RESOURCES = 'resources';
|
||||
final public const IMAGES = 'images';
|
||||
|
||||
public function __construct(protected BaseModel $model, protected array $toBackfill) {}
|
||||
|
||||
abstract public function handle(): ActionResult;
|
||||
|
||||
/**
|
||||
* Get the external API actions available for the backfill action.
|
||||
*
|
||||
* @return ExternalApiAction[]
|
||||
*/
|
||||
abstract protected function getExternalApiActions(): array;
|
||||
|
||||
/**
|
||||
* Create the resources given the response.
|
||||
*/
|
||||
protected function forResources(ExternalApiAction $api): void
|
||||
{
|
||||
$toBackfill = $this->toBackfill[self::RESOURCES];
|
||||
|
||||
foreach ($api->getResources() as $site => $url) {
|
||||
$site = ResourceSite::from($site);
|
||||
|
||||
if (! in_array($site, $toBackfill)) {
|
||||
Log::info("Resource {$site->localize()} should not be backfilled for {$this->label()} {$this->getModel()->getName()}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->getModel()->resources()->getQuery()->where(ExternalResource::ATTRIBUTE_SITE, $site->value)->exists()) {
|
||||
Log::info("Resource {$site->localize()} already exists for {$this->label()} {$this->getModel()->getName()}");
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->createResource(Uri::of($url), $site, $this->getModel());
|
||||
|
||||
$this->backfilled($site, self::RESOURCES);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the images given the response.
|
||||
*/
|
||||
protected function forImages(ExternalApiAction $api): void
|
||||
{
|
||||
$toBackfill = $this->toBackfill[self::IMAGES];
|
||||
|
||||
foreach ($api->getImages() as $facet => $url) {
|
||||
$facet = ImageFacet::from($facet);
|
||||
|
||||
if (! in_array($facet, $toBackfill)) {
|
||||
Log::info("Skipping {$facet->localize()} for {$this->label()} {$this->getModel()->getName()}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->getModel()->images()->getQuery()->where(Image::ATTRIBUTE_FACET, $facet->value)->exists()) {
|
||||
Log::info("Image {$facet->localize()} already exists for {$this->label()} {$this->getModel()->getName()}");
|
||||
continue;
|
||||
}
|
||||
|
||||
UploadImageAction::createImageFromUrl($url, $facet, $this->getModel());
|
||||
|
||||
$this->backfilled($facet, self::IMAGES);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove element already backfilled.
|
||||
*/
|
||||
protected function backfilled(mixed $enum, string $scope): void
|
||||
{
|
||||
$index = array_search($enum, $this->toBackfill[$scope]);
|
||||
|
||||
if ($index !== false) {
|
||||
unset($this->toBackfill[$scope][$index]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model for the action.
|
||||
*
|
||||
* @return BaseModel&HasResources&HasImages
|
||||
*/
|
||||
abstract protected function getModel(): BaseModel;
|
||||
|
||||
/**
|
||||
* Get the human-friendly label for the underlying model.
|
||||
*/
|
||||
protected function label(): string
|
||||
{
|
||||
return Str::headline(class_basename($this->getModel()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Document;
|
||||
|
||||
use App\Models\Document\Page;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class UpdatePageRelations
|
||||
{
|
||||
public function handle(Page $page): void
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$page->previous?->next()?->associate($page)?->saveQuietly();
|
||||
$page->next?->previous()?->associate($page)?->saveQuietly();
|
||||
|
||||
$oldPreviousId = $page->getOriginal(Page::ATTRIBUTE_PREVIOUS);
|
||||
if ($oldPreviousId && $oldPreviousId !== $page->previous?->getKey()) {
|
||||
Page::query()->find($oldPreviousId)->next()->disassociate()->saveQuietly();
|
||||
}
|
||||
|
||||
$oldNextId = $page->getOriginal(Page::ATTRIBUTE_NEXT);
|
||||
if ($oldNextId && $oldNextId !== $page->next?->getKey()) {
|
||||
Page::query()->find($oldNextId)->previous()->disassociate()->saveQuietly();
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
DB::rollBack();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\List\External\Entry;
|
||||
|
||||
use App\Models\List\External\ExternalToken;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
/**
|
||||
* This action will create the entries through the authentication method.
|
||||
*/
|
||||
abstract class BaseExternalEntryClaimedAction
|
||||
{
|
||||
/**
|
||||
* The JSON response of the external API.
|
||||
*
|
||||
* @var array<string, mixed>|null
|
||||
*/
|
||||
protected ?array $data = null;
|
||||
|
||||
/**
|
||||
* The id of the external user.
|
||||
*/
|
||||
protected ?int $userId = null;
|
||||
|
||||
public function __construct(protected ExternalToken $token) {}
|
||||
|
||||
public function getUserId(): ?int
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
protected function getToken(): string
|
||||
{
|
||||
return Crypt::decrypt($this->token->access_token);
|
||||
}
|
||||
|
||||
abstract public function getUsername(): ?string;
|
||||
|
||||
/**
|
||||
* Get the entries of the response.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
abstract public function getEntries(): array;
|
||||
|
||||
/**
|
||||
* @throws RequestException
|
||||
*/
|
||||
abstract protected function makeRequest(): void;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\List\External\Entry;
|
||||
|
||||
use App\Models\List\ExternalProfile;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
/**
|
||||
* This action will create the entries through the username method.
|
||||
*/
|
||||
abstract class BaseExternalEntryUnclaimedAction
|
||||
{
|
||||
/**
|
||||
* The JSON response of the external API.
|
||||
*
|
||||
* @var array<string, mixed>|null
|
||||
*/
|
||||
protected ?array $data = null;
|
||||
|
||||
public function __construct(protected ExternalProfile|array $profile) {}
|
||||
|
||||
public function getUsername(): string
|
||||
{
|
||||
if ($this->profile instanceof ExternalProfile) {
|
||||
return $this->profile->name;
|
||||
}
|
||||
|
||||
return Arr::get($this->profile, ExternalProfile::ATTRIBUTE_NAME);
|
||||
}
|
||||
|
||||
abstract public function getUserId(): ?int;
|
||||
|
||||
/**
|
||||
* Get the entries of the response.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
abstract public function getEntries(): array;
|
||||
|
||||
/**
|
||||
* @throws RequestException
|
||||
*/
|
||||
abstract protected function makeRequest(): void;
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\List\External\Entry\Claimed;
|
||||
|
||||
use App\Actions\Models\List\External\Entry\BaseExternalEntryClaimedAction;
|
||||
use App\Enums\Models\List\ExternalEntryStatus;
|
||||
use App\Models\List\External\ExternalEntry;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class AnilistExternalEntryClaimedAction extends BaseExternalEntryClaimedAction
|
||||
{
|
||||
/**
|
||||
* Get the entries of the response.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function getEntries(): array
|
||||
{
|
||||
$entries = [];
|
||||
|
||||
if ($this->data === null) {
|
||||
$this->makeRequest();
|
||||
}
|
||||
|
||||
if ($data = $this->data) {
|
||||
$lists = Arr::where(Arr::get($data, 'MediaListCollection.lists'), fn ($value): bool => $value['isCustomList'] === false);
|
||||
|
||||
foreach ($lists as $list) {
|
||||
foreach (Arr::get($list, 'entries') as $entry) {
|
||||
$entryId = intval(Arr::get($entry, 'media.id'));
|
||||
$entries[] = [
|
||||
ExternalResource::ATTRIBUTE_EXTERNAL_ID => $entryId,
|
||||
ExternalEntry::ATTRIBUTE_SCORE => Arr::get($entry, 'score'),
|
||||
ExternalEntry::ATTRIBUTE_STATUS => ExternalEntryStatus::getAnilistMapping(Arr::get($entry, 'status'))->value,
|
||||
ExternalEntry::ATTRIBUTE_IS_FAVORITE => Arr::get($entry, 'media.isFavourite'),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
public function getUsername(): ?string
|
||||
{
|
||||
if ($this->data === null) {
|
||||
$this->makeRequest();
|
||||
}
|
||||
|
||||
return Arr::get($this->data, 'Viewer.name');
|
||||
}
|
||||
|
||||
public function getUserId(): ?int
|
||||
{
|
||||
if ($this->userId !== null) {
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
[, $payload] = explode('.', $this->getToken());
|
||||
|
||||
$decodedArray = json_decode(base64_decode($payload), true);
|
||||
|
||||
$this->userId = intval(Arr::get($decodedArray, 'sub'));
|
||||
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function makeRequest(): void
|
||||
{
|
||||
try {
|
||||
$query = '
|
||||
query($userId: Int) {
|
||||
Viewer {
|
||||
name
|
||||
}
|
||||
MediaListCollection(userId: $userId, type: ANIME) {
|
||||
lists {
|
||||
name
|
||||
status
|
||||
isCustomList
|
||||
entries {
|
||||
status
|
||||
score
|
||||
media {
|
||||
id
|
||||
isFavourite
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$variables = [
|
||||
'userId' => $this->getUserId(),
|
||||
];
|
||||
|
||||
$this->data = Http::withToken($this->getToken())
|
||||
->post('https://graphql.anilist.co', [
|
||||
'query' => $query,
|
||||
'variables' => $variables,
|
||||
])
|
||||
->throw()
|
||||
->json('data');
|
||||
} catch (RequestException $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\List\External\Entry\Claimed;
|
||||
|
||||
use App\Actions\Models\List\External\Entry\BaseExternalEntryClaimedAction;
|
||||
use App\Enums\Models\List\ExternalEntryStatus;
|
||||
use App\Enums\Models\List\ExternalProfileSite;
|
||||
use App\Models\List\External\ExternalEntry;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
|
||||
class MalExternalEntryClaimedAction extends BaseExternalEntryClaimedAction
|
||||
{
|
||||
/**
|
||||
* The JSON response of the user endpoint in external API.
|
||||
*
|
||||
* @var array<string, mixed>|null
|
||||
*/
|
||||
protected ?array $userData = null;
|
||||
|
||||
/**
|
||||
* Get the entries of the response.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function getEntries(): array
|
||||
{
|
||||
$entries = [];
|
||||
|
||||
if ($this->data === null) {
|
||||
$this->makeRequest();
|
||||
}
|
||||
|
||||
if ($data = $this->data) {
|
||||
foreach ($data as $info) {
|
||||
$animeInfo = Arr::get($info, 'node');
|
||||
$listStatus = Arr::get($info, 'list_status');
|
||||
|
||||
$status = Arr::get($listStatus, 'is_rewatching')
|
||||
? 'rewatching'
|
||||
: Arr::get($listStatus, 'status');
|
||||
|
||||
$entries[] = [
|
||||
ExternalResource::ATTRIBUTE_EXTERNAL_ID => Arr::get($animeInfo, 'id'),
|
||||
ExternalEntry::ATTRIBUTE_SCORE => Arr::get($listStatus, 'score'),
|
||||
ExternalEntry::ATTRIBUTE_STATUS => ExternalEntryStatus::getMalMapping($status)->value,
|
||||
ExternalEntry::ATTRIBUTE_IS_FAVORITE => false,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
public function getUsername(): ?string
|
||||
{
|
||||
if ($this->userData === null) {
|
||||
$this->makeUserRequest();
|
||||
}
|
||||
|
||||
return Arr::get($this->userData, 'name');
|
||||
}
|
||||
|
||||
public function getUserId(): ?int
|
||||
{
|
||||
if ($this->data === null) {
|
||||
$this->makeUserRequest();
|
||||
}
|
||||
|
||||
return Arr::get($this->userData, 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function makeUserRequest(): void
|
||||
{
|
||||
try {
|
||||
$this->userData = Http::withToken($this->getToken())
|
||||
->get('https://api.myanimelist.net/v2/users/@me')
|
||||
->throw()
|
||||
->json();
|
||||
} catch (RequestException $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function makeRequest(): void
|
||||
{
|
||||
$next = null;
|
||||
|
||||
try {
|
||||
do {
|
||||
$response = Http::withToken($this->getToken())
|
||||
->get($next ?? 'https://api.myanimelist.net/v2/users/@me/animelist?fields=list_status&limit=1000')
|
||||
->throw()
|
||||
->json();
|
||||
|
||||
$this->data = array_merge($this->data ?? [], Arr::get($response, 'data'));
|
||||
|
||||
$next = Arr::get($response, 'paging.next');
|
||||
|
||||
// TODO: test
|
||||
RateLimiter::hit(ExternalProfileSite::MAL->name);
|
||||
} while (filled($next));
|
||||
} catch (RequestException $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\List\External\Entry\Unclaimed;
|
||||
|
||||
use App\Actions\Models\List\External\Entry\BaseExternalEntryUnclaimedAction;
|
||||
use App\Enums\Models\List\ExternalEntryStatus;
|
||||
use App\Models\List\External\ExternalEntry;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class AnilistExternalEntryUnclaimedAction extends BaseExternalEntryUnclaimedAction
|
||||
{
|
||||
/**
|
||||
* Get the entries of the response.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function getEntries(): array
|
||||
{
|
||||
$entries = [];
|
||||
|
||||
if ($this->data === null) {
|
||||
$this->makeRequest();
|
||||
}
|
||||
|
||||
$favorites = Arr::map(Arr::get($this->data, 'User.favourites.anime.nodes'), fn ($value) => $value['id']);
|
||||
$lists = Arr::where(Arr::get($this->data, 'MediaListCollection.lists'), fn ($value): bool => $value['isCustomList'] === false);
|
||||
|
||||
foreach ($lists as $list) {
|
||||
foreach (Arr::get($list, 'entries') as $entry) {
|
||||
$entryId = intval(Arr::get($entry, 'media.id'));
|
||||
$entries[] = [
|
||||
ExternalResource::ATTRIBUTE_EXTERNAL_ID => $entryId,
|
||||
ExternalEntry::ATTRIBUTE_SCORE => Arr::get($entry, 'score'),
|
||||
ExternalEntry::ATTRIBUTE_STATUS => ExternalEntryStatus::getAnilistMapping(Arr::get($entry, 'status'))->value,
|
||||
ExternalEntry::ATTRIBUTE_IS_FAVORITE => in_array($entryId, $favorites),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
|
||||
public function getUserId(): ?int
|
||||
{
|
||||
if ($this->data === null) {
|
||||
$this->makeRequest();
|
||||
}
|
||||
|
||||
return Arr::get($this->data, 'User.id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RequestException
|
||||
*/
|
||||
protected function makeRequest(): void
|
||||
{
|
||||
try {
|
||||
$query = '
|
||||
query($userName: String) {
|
||||
User(name: $userName) {
|
||||
id
|
||||
favourites {
|
||||
anime {
|
||||
nodes {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MediaListCollection(userName: $userName, type: ANIME) {
|
||||
lists {
|
||||
name
|
||||
status
|
||||
isCustomList
|
||||
entries {
|
||||
status
|
||||
score
|
||||
media {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$variables = [
|
||||
'userName' => $this->getUsername(),
|
||||
];
|
||||
|
||||
$this->data = Http::post('https://graphql.anilist.co', [
|
||||
'query' => $query,
|
||||
'variables' => $variables,
|
||||
])
|
||||
->throw()
|
||||
->json('data');
|
||||
} catch (RequestException $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\List\External;
|
||||
|
||||
use App\Actions\Http\Api\StoreAction;
|
||||
use App\Actions\Models\List\External\Entry\BaseExternalEntryClaimedAction;
|
||||
use App\Actions\Models\List\External\Entry\Claimed\AnilistExternalEntryClaimedAction;
|
||||
use App\Actions\Models\List\External\Entry\Claimed\MalExternalEntryClaimedAction;
|
||||
use App\Enums\Models\List\ExternalProfileSite;
|
||||
use App\Enums\Models\List\ExternalProfileVisibility;
|
||||
use App\Models\List\External\ExternalToken;
|
||||
use App\Models\List\ExternalProfile;
|
||||
use Exception;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use RuntimeException;
|
||||
|
||||
class StoreExternalProfileClaimedAction
|
||||
{
|
||||
protected Collection $resources;
|
||||
|
||||
/**
|
||||
* Get the first record or store external profile given determined external token.
|
||||
*
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function firstOrCreate(ExternalToken $token, array $parameters): ExternalProfile
|
||||
{
|
||||
try {
|
||||
$site = ExternalProfileSite::fromLocalizedName(Arr::get($parameters, ExternalProfile::ATTRIBUTE_SITE));
|
||||
|
||||
$action = static::getActionClass($site, $token);
|
||||
|
||||
$userId = $action->getUserId();
|
||||
|
||||
return $this->firstForUserIdOrCreate($userId, $site, $action, $parameters);
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first record or create the profile for a userId and site.
|
||||
*/
|
||||
protected function firstForUserIdOrCreate(int $userId, ExternalProfileSite $site, BaseExternalEntryClaimedAction $action, array $parameters): ExternalProfile
|
||||
{
|
||||
$claimedProfile = ExternalProfile::query()
|
||||
->where(ExternalProfile::ATTRIBUTE_EXTERNAL_USER_ID, $userId)
|
||||
->where(ExternalProfile::ATTRIBUTE_SITE, $site->value)
|
||||
->whereHas(ExternalProfile::RELATION_USER)
|
||||
->first();
|
||||
|
||||
if ($claimedProfile instanceof ExternalProfile) {
|
||||
return $claimedProfile;
|
||||
}
|
||||
|
||||
$unclaimedProfile = ExternalProfile::query()
|
||||
->where(ExternalProfile::ATTRIBUTE_EXTERNAL_USER_ID, $userId)
|
||||
->where(ExternalProfile::ATTRIBUTE_SITE, $site->value)
|
||||
->whereDoesntHave(ExternalProfile::RELATION_USER)
|
||||
->first();
|
||||
|
||||
if ($unclaimedProfile instanceof ExternalProfile) {
|
||||
return tap(
|
||||
$unclaimedProfile,
|
||||
fn ($unclaimedProfile) => $unclaimedProfile->update([
|
||||
ExternalProfile::ATTRIBUTE_USER => Arr::integer($parameters, ExternalProfile::ATTRIBUTE_USER),
|
||||
ExternalProfile::ATTRIBUTE_NAME => $action->getUsername(),
|
||||
ExternalProfile::ATTRIBUTE_VISIBILITY => ExternalProfileVisibility::PRIVATE->value,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/** @var StoreAction<ExternalProfile> $storeAction */
|
||||
$storeAction = new StoreAction();
|
||||
|
||||
return $storeAction->store(ExternalProfile::query(), [
|
||||
ExternalProfile::ATTRIBUTE_EXTERNAL_USER_ID => $userId,
|
||||
ExternalProfile::ATTRIBUTE_USER => Arr::integer($parameters, ExternalProfile::ATTRIBUTE_USER),
|
||||
ExternalProfile::ATTRIBUTE_NAME => $action->getUsername(),
|
||||
ExternalProfile::ATTRIBUTE_SITE => $site->value,
|
||||
ExternalProfile::ATTRIBUTE_VISIBILITY => ExternalProfileVisibility::PRIVATE->value,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mapping for the entries token class.
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function getActionClass(ExternalProfileSite $site, ExternalToken $token): BaseExternalEntryClaimedAction
|
||||
{
|
||||
return match ($site) {
|
||||
ExternalProfileSite::ANILIST => new AnilistExternalEntryClaimedAction($token),
|
||||
ExternalProfileSite::MAL => new MalExternalEntryClaimedAction($token),
|
||||
default => throw new RuntimeException("External entry token action not configured for site {$site->localize()}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\List\External;
|
||||
|
||||
use App\Actions\Http\Api\StoreAction;
|
||||
use App\Actions\Models\List\External\Entry\BaseExternalEntryUnclaimedAction;
|
||||
use App\Actions\Models\List\External\Entry\Unclaimed\AnilistExternalEntryUnclaimedAction;
|
||||
use App\Enums\Models\List\ExternalProfileSite;
|
||||
use App\Enums\Models\List\ExternalProfileVisibility;
|
||||
use App\Models\List\ExternalProfile;
|
||||
use Exception;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use RuntimeException;
|
||||
|
||||
class StoreExternalProfileUnclaimedAction
|
||||
{
|
||||
/**
|
||||
* Get the first record or store an external profile and its entries given determined username.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function firstOrCreate(Builder $builder, array $profileParameters): ExternalProfile
|
||||
{
|
||||
$name = Arr::get($profileParameters, ExternalProfile::ATTRIBUTE_NAME);
|
||||
$site = ExternalProfileSite::fromLocalizedName(Arr::get($profileParameters, ExternalProfile::ATTRIBUTE_SITE));
|
||||
$visibility = ExternalProfileVisibility::fromLocalizedName(Arr::get($profileParameters, ExternalProfile::ATTRIBUTE_VISIBILITY));
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$findProfile = ExternalProfile::query()
|
||||
->where(ExternalProfile::ATTRIBUTE_NAME, $name)
|
||||
->where(ExternalProfile::ATTRIBUTE_SITE, $site->value)
|
||||
->first();
|
||||
|
||||
if ($findProfile instanceof ExternalProfile) {
|
||||
throw_if($findProfile->isClaimed(), AuthorizationException::class, "The external profile '{$findProfile->getName()}' is already claimed.");
|
||||
|
||||
DB::rollBack();
|
||||
|
||||
return $findProfile;
|
||||
}
|
||||
|
||||
$action = static::getActionClass($site, $profileParameters);
|
||||
|
||||
/** @var StoreAction<ExternalProfile> $storeAction */
|
||||
$storeAction = new StoreAction();
|
||||
|
||||
$profile = $storeAction->store($builder, [
|
||||
ExternalProfile::ATTRIBUTE_EXTERNAL_USER_ID => $action->getUserId(),
|
||||
ExternalProfile::ATTRIBUTE_NAME => $name,
|
||||
ExternalProfile::ATTRIBUTE_SITE => $site->value,
|
||||
ExternalProfile::ATTRIBUTE_VISIBILITY => $visibility->value,
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
return $profile;
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
DB::rollBack();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mapping for the entries class.
|
||||
*
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function getActionClass(ExternalProfileSite $site, ExternalProfile|array $profile): BaseExternalEntryUnclaimedAction
|
||||
{
|
||||
return match ($site) {
|
||||
ExternalProfileSite::ANILIST => new AnilistExternalEntryUnclaimedAction($profile),
|
||||
default => throw new RuntimeException("External entry action not configured for site {$site->localize()}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\List\External;
|
||||
|
||||
use App\Actions\Models\List\External\Entry\BaseExternalEntryClaimedAction;
|
||||
use App\Actions\Models\List\External\Entry\BaseExternalEntryUnclaimedAction;
|
||||
use App\Enums\Models\List\ExternalProfileSite;
|
||||
use App\Events\List\ExternalProfile\ExternalProfileSynced;
|
||||
use App\Models\List\External\ExternalEntry;
|
||||
use App\Models\List\ExternalProfile;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use Exception;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SyncExternalProfileAction
|
||||
{
|
||||
protected Collection $resources;
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function handle(ExternalProfile $profile): ExternalProfile
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
// A claimed profile will be synced using its external token.
|
||||
$action = $profile->isClaimed()
|
||||
? $this->getClaimedActionClass($profile)
|
||||
: $this->getUnclaimedActionClass($profile);
|
||||
|
||||
$entries = $action->getEntries();
|
||||
|
||||
$this->cacheResources($profile->site);
|
||||
|
||||
$externalEntries = [];
|
||||
foreach ($entries as $entry) {
|
||||
$externalId = Arr::get($entry, 'external_id');
|
||||
|
||||
foreach ($this->getAnimesByExternalId($externalId) as $anime) {
|
||||
$externalEntries[] = [
|
||||
ExternalEntry::ATTRIBUTE_SCORE => Arr::get($entry, ExternalEntry::ATTRIBUTE_SCORE),
|
||||
ExternalEntry::ATTRIBUTE_IS_FAVORITE => Arr::get($entry, ExternalEntry::ATTRIBUTE_IS_FAVORITE),
|
||||
ExternalEntry::ATTRIBUTE_STATUS => Arr::get($entry, ExternalEntry::ATTRIBUTE_STATUS),
|
||||
ExternalEntry::ATTRIBUTE_ANIME => $anime,
|
||||
ExternalEntry::ATTRIBUTE_PROFILE => $profile->getKey(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the external entries or update its status filtering by anime id.
|
||||
ExternalEntry::query()
|
||||
->upsert(
|
||||
$externalEntries,
|
||||
[ExternalEntry::ATTRIBUTE_ANIME, ExternalEntry::ATTRIBUTE_PROFILE],
|
||||
ExternalEntry::fieldsForUpdate(),
|
||||
);
|
||||
|
||||
// An external entry that is not in the external list anymore should be removed.
|
||||
ExternalEntry::query()
|
||||
->where(ExternalEntry::ATTRIBUTE_PROFILE, $profile->getKey())
|
||||
->whereNotIn(ExternalEntry::ATTRIBUTE_ANIME, Arr::map($externalEntries, fn ($value) => $value[ExternalEntry::ATTRIBUTE_ANIME]))
|
||||
->delete();
|
||||
|
||||
$profile->update([ExternalProfile::ATTRIBUTE_SYNCED_AT => now()]);
|
||||
|
||||
DB::commit();
|
||||
|
||||
ExternalProfileSynced::dispatch($profile);
|
||||
|
||||
return $profile;
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
DB::rollBack();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mapping for the entries token class.
|
||||
*/
|
||||
protected function getClaimedActionClass(ExternalProfile $profile): BaseExternalEntryClaimedAction
|
||||
{
|
||||
return StoreExternalProfileClaimedAction::getActionClass($profile->site, $profile->externaltoken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mapping for the entries class.
|
||||
*/
|
||||
protected function getUnclaimedActionClass(ExternalProfile $profile): BaseExternalEntryUnclaimedAction
|
||||
{
|
||||
return StoreExternalProfileUnclaimedAction::getActionClass($profile->site, $profile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache the resources for performance proposals.
|
||||
*/
|
||||
protected function cacheResources(ExternalProfileSite $profileSite): void
|
||||
{
|
||||
// External resources are only added by mods so it doesn't change too often.
|
||||
$this->resources = Cache::flexible("resources_{$profileSite->name}", [60, 300], fn () => ExternalResource::query()
|
||||
->where(ExternalResource::ATTRIBUTE_SITE, $profileSite->getResourceSite()->value)
|
||||
->with([ExternalResource::RELATION_ANIME => fn ($query) => $query->select([Anime::TABLE.'.'.Anime::ATTRIBUTE_ID])])
|
||||
->whereHas(ExternalResource::RELATION_ANIME)
|
||||
->get()
|
||||
->mapWithKeys(fn (ExternalResource $resource): array => [$resource->external_id => $resource->anime->map(fn (Anime $anime) => $anime->getKey())]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, int>
|
||||
*/
|
||||
protected function getAnimesByExternalId(int $externalId): Collection
|
||||
{
|
||||
return $this->resources->get($externalId) ?? collect();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\List\External\Token;
|
||||
|
||||
use App\Models\List\External\ExternalToken;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
|
||||
abstract class BaseExternalTokenAction
|
||||
{
|
||||
/**
|
||||
* Use the authorization code to get the tokens and store them.
|
||||
*
|
||||
* @throws RequestException
|
||||
*/
|
||||
abstract public function store(array $parameters): ExternalToken;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\List\External\Token\Site;
|
||||
|
||||
use App\Actions\Models\List\External\Token\BaseExternalTokenAction;
|
||||
use App\Constants\Config\ServiceConstants;
|
||||
use App\Models\List\External\ExternalToken;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class AnilistExternalTokenAction extends BaseExternalTokenAction
|
||||
{
|
||||
/**
|
||||
* Use the authorization code to get the tokens and store them.
|
||||
*
|
||||
* @param array{code: string} $parameters
|
||||
*/
|
||||
public function store(array $parameters): ExternalToken
|
||||
{
|
||||
$code = Arr::string($parameters, 'code');
|
||||
|
||||
try {
|
||||
$response = Http::asForm()
|
||||
->acceptJson()
|
||||
->post('https://anilist.co/api/v2/oauth/token', [
|
||||
'grant_type' => 'authorization_code',
|
||||
'client_id' => Config::get(ServiceConstants::ANILIST_CLIENT_ID),
|
||||
'client_secret' => Config::get(ServiceConstants::ANILIST_CLIENT_SECRET),
|
||||
'redirect_uri' => Config::get(ServiceConstants::ANILIST_REDIRECT_URI),
|
||||
'code' => $code,
|
||||
])
|
||||
->throw()
|
||||
->json();
|
||||
|
||||
$token = Arr::string($response, 'access_token');
|
||||
|
||||
return ExternalToken::query()->create([
|
||||
ExternalToken::ATTRIBUTE_ACCESS_TOKEN => Crypt::encrypt($token),
|
||||
]);
|
||||
} catch (RequestException $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\List\External\Token\Site;
|
||||
|
||||
use App\Actions\Models\List\External\Token\BaseExternalTokenAction;
|
||||
use App\Constants\Config\ServiceConstants;
|
||||
use App\Models\List\External\ExternalToken;
|
||||
use Exception;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class MalExternalTokenAction extends BaseExternalTokenAction
|
||||
{
|
||||
/**
|
||||
* Use the authorization code to get the tokens and store them.
|
||||
*
|
||||
* @param array{code: string, state: string} $parameters
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function store(array $parameters): ExternalToken
|
||||
{
|
||||
$code = Arr::string($parameters, 'code');
|
||||
$state = Arr::string($parameters, 'state');
|
||||
|
||||
$codeVerifier = Cache::get("mal-external-token-request-{$state}");
|
||||
|
||||
try {
|
||||
$response = Http::asForm()
|
||||
->acceptJson()
|
||||
->post('https://myanimelist.net/v1/oauth2/token', [
|
||||
'grant_type' => 'authorization_code',
|
||||
'client_id' => Config::string(ServiceConstants::MAL_CLIENT_ID),
|
||||
'client_secret' => Config::string(ServiceConstants::MAL_CLIENT_SECRET),
|
||||
'redirect_uri' => Config::string(ServiceConstants::MAL_REDIRECT_URI),
|
||||
'code' => $code,
|
||||
'code_verifier' => $codeVerifier,
|
||||
])
|
||||
->throw()
|
||||
->json();
|
||||
|
||||
$token = Arr::string($response, 'access_token');
|
||||
$refreshToken = Arr::string($response, 'refresh_token');
|
||||
|
||||
return ExternalToken::query()->create([
|
||||
ExternalToken::ATTRIBUTE_ACCESS_TOKEN => Crypt::encrypt($token),
|
||||
ExternalToken::ATTRIBUTE_REFRESH_TOKEN => Crypt::encrypt($refreshToken),
|
||||
]);
|
||||
} catch (RequestException $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
throw $e;
|
||||
} finally {
|
||||
Cache::forget("mal-external-token-request-{$state}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\List;
|
||||
|
||||
use App\Actions\Models\List\External\StoreExternalProfileClaimedAction;
|
||||
use App\Actions\Models\List\External\Token\BaseExternalTokenAction;
|
||||
use App\Actions\Models\List\External\Token\Site\AnilistExternalTokenAction;
|
||||
use App\Actions\Models\List\External\Token\Site\MalExternalTokenAction;
|
||||
use App\Enums\Models\List\ExternalProfileSite;
|
||||
use App\Models\List\External\ExternalToken;
|
||||
use App\Models\List\ExternalProfile;
|
||||
use Exception;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use RuntimeException;
|
||||
|
||||
class ExternalTokenCallbackAction
|
||||
{
|
||||
/**
|
||||
* We should store the token and the profile.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function store(array $parameters): ExternalProfile
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$site = ExternalProfileSite::fromLocalizedName(Arr::get($parameters, ExternalProfile::ATTRIBUTE_SITE));
|
||||
|
||||
$externalToken = ExternalToken::query()
|
||||
->whereRelation(ExternalToken::RELATION_PROFILE, ExternalProfile::ATTRIBUTE_USER, Arr::integer($parameters, ExternalProfile::ATTRIBUTE_USER))
|
||||
->whereRelation(ExternalToken::RELATION_PROFILE, ExternalProfile::ATTRIBUTE_SITE, $site->value)
|
||||
->first();
|
||||
|
||||
if (! $externalToken instanceof ExternalToken) {
|
||||
$externalToken = $this->getActionClass($site)->store($parameters);
|
||||
}
|
||||
|
||||
$profileAction = new StoreExternalProfileClaimedAction();
|
||||
|
||||
$profile = $profileAction->firstOrCreate($externalToken, $parameters);
|
||||
|
||||
$profile->externaltoken()->save($externalToken);
|
||||
|
||||
DB::commit();
|
||||
|
||||
return $profile;
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
DB::rollBack();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mapping for the token class.
|
||||
*
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function getActionClass(ExternalProfileSite $site): BaseExternalTokenAction
|
||||
{
|
||||
return match ($site) {
|
||||
ExternalProfileSite::ANILIST => new AnilistExternalTokenAction(),
|
||||
ExternalProfileSite::MAL => new MalExternalTokenAction(),
|
||||
default => throw new RuntimeException("External token action not configured for site {$site->localize()}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\List\Playlist;
|
||||
|
||||
use App\Models\List\Playlist;
|
||||
use App\Models\List\Playlist\PlaylistTrack;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class FixPlaylistAction
|
||||
{
|
||||
public function handle(Playlist $playlist, mixed $context = null): int
|
||||
{
|
||||
// Fetch all tracks in the playlist and index them by track_id
|
||||
$this->sendMessage("Fetching tracks for playlist ID: {$playlist->getKey()}...", $context, 'info');
|
||||
|
||||
/** @var Collection<int, PlaylistTrack> $tracks */
|
||||
$tracks = PlaylistTrack::query()->where(PlaylistTrack::ATTRIBUTE_PLAYLIST, $playlist->getKey())
|
||||
->orderBy(PlaylistTrack::ATTRIBUTE_ID)
|
||||
->get()
|
||||
->keyBy(PlaylistTrack::ATTRIBUTE_ID);
|
||||
|
||||
// Find the playlist and get the first_id
|
||||
$this->sendMessage('Fetching playlist details...', $context, 'info');
|
||||
|
||||
$first_id = $playlist->first_id;
|
||||
|
||||
// Initialize arrays for ordered tracks and visited tracks
|
||||
$orderedTracks = [];
|
||||
$visitedTracks = [];
|
||||
|
||||
// Start at the first_id and follow the next_id chain
|
||||
$this->sendMessage('Reconstructing the playlist order...', $context, 'info');
|
||||
|
||||
$current_id = $first_id;
|
||||
|
||||
while ($current_id !== null) {
|
||||
if (isset($visitedTracks[$current_id])) {
|
||||
// If we encounter a previously visited track, we've detected a cycle
|
||||
$this->sendMessage("Cycle detected at track ID: $current_id, stopping...", $context, 'warn');
|
||||
break;
|
||||
}
|
||||
|
||||
// Mark the current track as visited and add to ordered tracks
|
||||
$visitedTracks[$current_id] = true;
|
||||
$orderedTracks[] = $current_id;
|
||||
|
||||
// Get the next track in sequence
|
||||
$currentTrack = $tracks[$current_id];
|
||||
$current_id = $currentTrack->next_id;
|
||||
}
|
||||
|
||||
// Check if we have traversed all tracks; if not, some tracks are broken
|
||||
if (count($orderedTracks) !== count($tracks)) {
|
||||
$this->sendMessage('Detected broken tracks, attempting to add them to the end of the list...', $context, 'warn');
|
||||
|
||||
// Find all tracks that were not visited (potentially broken)
|
||||
$remainingTracks = $tracks->reject(fn ($track): bool => isset($visitedTracks[$track->track_id]));
|
||||
|
||||
// Add remaining tracks to the end, preserving the existing order as much as possible
|
||||
foreach ($remainingTracks as $remainingTrack) {
|
||||
$orderedTracks[] = $remainingTrack->track_id;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the previous_id and next_id for all tracks in the correct order
|
||||
$this->sendMessage('Updating track links (previous_id and next_id)...', $context, 'info');
|
||||
DB::transaction(function () use ($orderedTracks, $tracks, $playlist, $context): void {
|
||||
$previous_id = null;
|
||||
|
||||
foreach ($orderedTracks as $index => $track_id) {
|
||||
$track = $tracks[$track_id];
|
||||
|
||||
// Get the next track in the sequence or set null if it's the last one
|
||||
$next_id = $orderedTracks[$index + 1] ?? null;
|
||||
|
||||
// Update the previous_id and next_id for the track
|
||||
$track->previous_id = $previous_id;
|
||||
$track->next_id = $next_id;
|
||||
$track->save();
|
||||
|
||||
// Move the previous_id pointer forward
|
||||
$previous_id = $track_id;
|
||||
}
|
||||
|
||||
// Update the playlist's first_id and last_id
|
||||
$playlist->first_id = $orderedTracks[0];
|
||||
$playlist->last_id = $orderedTracks[count($orderedTracks) - 1];
|
||||
$playlist->save();
|
||||
|
||||
$this->sendMessage('Playlist first_id and last_id updated successfully.', $context, 'info');
|
||||
});
|
||||
|
||||
$this->sendMessage('Playlist fixed successfully.', $context, 'info');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the message given the context.
|
||||
*/
|
||||
protected function sendMessage(string $message, mixed $context, string $type): void
|
||||
{
|
||||
if (is_null($context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$translatedType = match (true) {
|
||||
$context === 'log' => match ($type) {
|
||||
'warn' => 'warning',
|
||||
default => $type,
|
||||
},
|
||||
default => $type,
|
||||
};
|
||||
|
||||
match (true) {
|
||||
$context instanceof Command => $context->$translatedType($message),
|
||||
$context === 'log' => Log::$translatedType($message),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\List\Playlist;
|
||||
|
||||
use App\Models\List\Playlist;
|
||||
use App\Models\List\Playlist\PlaylistTrack;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class InsertTrackAction
|
||||
{
|
||||
/**
|
||||
* Append track to playlist.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function insert(Playlist $playlist, PlaylistTrack $track): void
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
if ($playlist->first()->doesntExist()) {
|
||||
$playlist->first()->associate($track)->save();
|
||||
}
|
||||
|
||||
$last = $playlist->last;
|
||||
|
||||
$playlist->last()->associate($track)->save();
|
||||
|
||||
$last?->next()?->associate($track)?->save();
|
||||
$track->previous()->associate($last);
|
||||
|
||||
$track->next()->disassociate()->save();
|
||||
|
||||
DB::commit();
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
DB::rollBack();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\List\Playlist;
|
||||
|
||||
use App\Models\List\Playlist;
|
||||
use App\Models\List\Playlist\PlaylistTrack;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class InsertTrackAfterAction
|
||||
{
|
||||
/**
|
||||
* Insert track after previous track.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function insertAfter(Playlist $playlist, PlaylistTrack $track, PlaylistTrack $previous): void
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
if ($playlist->last()->is($previous)) {
|
||||
$playlist->last()->associate($track)->save();
|
||||
}
|
||||
|
||||
$next = $previous->next;
|
||||
$next?->previous()?->associate($track)?->save();
|
||||
$track->next()->associate($next);
|
||||
|
||||
$previous->next()->associate($track)->save();
|
||||
$track->previous()->associate($previous)->save();
|
||||
|
||||
$track->moveAfter($previous);
|
||||
|
||||
DB::commit();
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
DB::rollBack();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\List\Playlist;
|
||||
|
||||
use App\Models\List\Playlist;
|
||||
use App\Models\List\Playlist\PlaylistTrack;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class InsertTrackBeforeAction
|
||||
{
|
||||
/**
|
||||
* Insert track before next track.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function insertBefore(Playlist $playlist, PlaylistTrack $track, PlaylistTrack $next): void
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
if ($playlist->first()->is($next)) {
|
||||
$playlist->first()->associate($track)->save();
|
||||
}
|
||||
|
||||
$previous = $next->previous;
|
||||
$previous?->next()?->associate($track)?->save();
|
||||
$track->previous()->associate($previous);
|
||||
|
||||
$next->previous()->associate($track)->save();
|
||||
$track->next()->associate($next)->save();
|
||||
|
||||
$track->moveBefore($next);
|
||||
|
||||
DB::commit();
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
DB::rollBack();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\List\Playlist;
|
||||
|
||||
use App\Models\List\Playlist;
|
||||
use App\Models\List\Playlist\PlaylistTrack;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class RemoveTrackAction
|
||||
{
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function remove(Playlist $playlist, PlaylistTrack $track): void
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$previous = $track->previous;
|
||||
$next = $track->next;
|
||||
|
||||
if ($playlist->first()->is($track)) {
|
||||
$playlist->first()->associate($next);
|
||||
}
|
||||
if ($playlist->last()->is($track)) {
|
||||
$playlist->last()->associate($previous);
|
||||
}
|
||||
$playlist->save();
|
||||
|
||||
$previous?->next()?->associate($next)?->save();
|
||||
$next?->previous()?->associate($previous)?->save();
|
||||
|
||||
$track->previous()->disassociate();
|
||||
$track->next()->disassociate()->save();
|
||||
|
||||
PlaylistTrack::query()
|
||||
->whereBelongsTo($playlist)
|
||||
->where(PlaylistTrack::ATTRIBUTE_POSITION, '>', $track->position)
|
||||
->decrement(PlaylistTrack::ATTRIBUTE_POSITION);
|
||||
|
||||
DB::commit();
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
DB::rollBack();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki\Anime\ExternalApi;
|
||||
|
||||
use App\Actions\Models\Wiki\ExternalApiAction;
|
||||
use App\Contracts\Actions\Models\Wiki\BackfillResources;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class AniZipAnimeExternalApiAction extends ExternalApiAction implements BackfillResources
|
||||
{
|
||||
public function getSite(): ResourceSite
|
||||
{
|
||||
return ResourceSite::ANILIST;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the response after the request.
|
||||
*
|
||||
* @param BelongsToMany<ExternalResource, Anime> $resources
|
||||
*/
|
||||
public function handle(BelongsToMany $resources): static
|
||||
{
|
||||
$resource = $resources->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST->value);
|
||||
|
||||
if ($resource instanceof ExternalResource) {
|
||||
$id = $resource->external_id;
|
||||
|
||||
try {
|
||||
$this->response = Http::get("https://api.ani.zip/v1/mappings?anilist_id=$id")
|
||||
->throw()
|
||||
->json();
|
||||
} catch (RequestException $e) {
|
||||
Log::error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mapped resources.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getResources(): array
|
||||
{
|
||||
$resources = [];
|
||||
|
||||
if ($links = $this->response) {
|
||||
foreach ($this->getResourcesMapping() as $site => $key) {
|
||||
$id = Arr::get($links, $key);
|
||||
|
||||
if ($id !== null) {
|
||||
$resources[$site] = ResourceSite::from($site)
|
||||
->formatResourceLink(
|
||||
Anime::class,
|
||||
is_numeric($id) ? (int) $id : null,
|
||||
is_string($id) ? $id : null
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $resources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available sites to backfill.
|
||||
*/
|
||||
public function getResourcesMapping(): array
|
||||
{
|
||||
return [
|
||||
ResourceSite::ANIME_PLANET->value => 'animeplanet_id',
|
||||
ResourceSite::ANIDB->value => 'anidb_id',
|
||||
ResourceSite::ANILIST->value => 'anilist_id',
|
||||
ResourceSite::KITSU->value => 'kitsu_id',
|
||||
ResourceSite::LIVECHART->value => 'livechart_id',
|
||||
ResourceSite::MAL->value => 'mal_id',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki\Anime\ExternalApi;
|
||||
|
||||
use App\Actions\Models\Wiki\ExternalApiAction;
|
||||
use App\Contracts\Actions\Models\Wiki\BackfillImages;
|
||||
use App\Contracts\Actions\Models\Wiki\BackfillResources;
|
||||
use App\Contracts\Actions\Models\Wiki\BackfillSynonyms;
|
||||
use App\Enums\Models\Wiki\AnimeSynonymType;
|
||||
use App\Enums\Models\Wiki\ImageFacet;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class AnilistAnimeExternalApiAction extends ExternalApiAction implements BackfillImages, BackfillResources, BackfillSynonyms
|
||||
{
|
||||
public function getSite(): ResourceSite
|
||||
{
|
||||
return ResourceSite::ANILIST;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the response after the request.
|
||||
*
|
||||
* @param BelongsToMany<ExternalResource, Anime> $resources
|
||||
*/
|
||||
public function handle(BelongsToMany $resources): static
|
||||
{
|
||||
$resource = $resources->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::ANILIST->value);
|
||||
|
||||
if ($resource instanceof ExternalResource) {
|
||||
$query = '
|
||||
query ($id: Int) {
|
||||
Media (id: $id, type: ANIME) {
|
||||
id
|
||||
title {
|
||||
romaji
|
||||
english
|
||||
native
|
||||
}
|
||||
coverImage {
|
||||
extraLarge
|
||||
medium
|
||||
}
|
||||
externalLinks {
|
||||
url
|
||||
site
|
||||
language
|
||||
}
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$variables = [
|
||||
'id' => $resource->external_id,
|
||||
];
|
||||
|
||||
try {
|
||||
$this->response = Http::post('https://graphql.anilist.co', [
|
||||
'query' => $query,
|
||||
'variables' => $variables,
|
||||
])
|
||||
->throw()
|
||||
->json();
|
||||
} catch (RequestException $e) {
|
||||
Log::error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mapped resources.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getResources(): array
|
||||
{
|
||||
$resources = [];
|
||||
|
||||
if ($response = $this->response) {
|
||||
foreach (Arr::get($response, 'data.Media.externalLinks') as $link) {
|
||||
$url = Arr::get($link, 'url');
|
||||
$siteAnilist = Arr::get($link, 'site');
|
||||
$language = Arr::get($link, 'language');
|
||||
|
||||
foreach ($this->getResourcesMapping() as $site => $key) {
|
||||
if ($siteAnilist === $key) {
|
||||
if (in_array($siteAnilist, ['Official Site', 'Twitter']) && ! in_array($language, ['Japanese', null])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$resources[$site] = $url;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $resources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available sites to backfill.
|
||||
*/
|
||||
public function getResourcesMapping(): array
|
||||
{
|
||||
return [
|
||||
ResourceSite::X->value => 'Twitter',
|
||||
ResourceSite::OFFICIAL_SITE->value => 'Official Site',
|
||||
ResourceSite::NETFLIX->value => 'Netflix',
|
||||
ResourceSite::CRUNCHYROLL->value => 'Crunchyroll',
|
||||
ResourceSite::HIDIVE->value => 'HIDIVE',
|
||||
ResourceSite::AMAZON_PRIME_VIDEO->value => 'Amazon Prime Video',
|
||||
ResourceSite::HULU->value => 'Hulu',
|
||||
ResourceSite::DISNEY_PLUS->value => 'Disney Plus',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mapping for the images.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getImagesMapping(): array
|
||||
{
|
||||
return [
|
||||
ImageFacet::SMALL_COVER->value => 'data.Media.coverImage.medium',
|
||||
ImageFacet::LARGE_COVER->value => 'data.Media.coverImage.extraLarge',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mapping for the synonyms.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSynonymsMapping(): array
|
||||
{
|
||||
return [
|
||||
AnimeSynonymType::ENGLISH->value => 'data.Media.title.english',
|
||||
AnimeSynonymType::NATIVE->value => 'data.Media.title.native',
|
||||
AnimeSynonymType::OTHER->value => 'data.Media.title.romaji',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki\Anime\ExternalApi;
|
||||
|
||||
use App\Actions\Models\Wiki\ExternalApiAction;
|
||||
use App\Contracts\Actions\Models\Wiki\BackfillResources;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class JikanAnimeExternalApiAction extends ExternalApiAction implements BackfillResources
|
||||
{
|
||||
public function getSite(): ResourceSite
|
||||
{
|
||||
return ResourceSite::MAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the response after the request.
|
||||
*
|
||||
* @param BelongsToMany<ExternalResource, Anime> $resources
|
||||
*/
|
||||
public function handle(BelongsToMany $resources): static
|
||||
{
|
||||
$resource = $resources->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL->value);
|
||||
|
||||
if ($resource instanceof ExternalResource) {
|
||||
$id = $resource->external_id;
|
||||
|
||||
try {
|
||||
$this->response = Http::get("https://api.jikan.moe/v4/anime/$id/external")
|
||||
->throw()
|
||||
->json();
|
||||
} catch (RequestException $e) {
|
||||
Log::error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mapped resources.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getResources(): array
|
||||
{
|
||||
$resources = [];
|
||||
|
||||
$reversed = array_flip($this->getResourcesMapping());
|
||||
|
||||
if ($response = $this->response) {
|
||||
$links = Arr::get($response, 'data');
|
||||
|
||||
foreach ($links as $link) {
|
||||
$siteMal = Arr::get($link, 'name');
|
||||
$url = Arr::get($link, 'url');
|
||||
|
||||
foreach ($this->getResourcesMapping() as $site => $key) {
|
||||
if ($siteMal === $key) {
|
||||
$resourceSite = ResourceSite::from(Arr::get($reversed, $siteMal));
|
||||
$resources[$site] = $resourceSite->formatResourceLink(Anime::class, intval(ResourceSite::parseIdFromLink($url)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $resources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the available sites to backfill.
|
||||
*/
|
||||
public function getResourcesMapping(): array
|
||||
{
|
||||
return [
|
||||
ResourceSite::ANIDB->value => 'AniDB',
|
||||
ResourceSite::ANN->value => 'ANN',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki\Anime\ExternalApi;
|
||||
|
||||
use App\Actions\Models\Wiki\ExternalApiAction;
|
||||
use App\Contracts\Actions\Models\Wiki\BackfillResources;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class LivechartAnimeExternalApiAction extends ExternalApiAction implements BackfillResources
|
||||
{
|
||||
public function getSite(): ResourceSite
|
||||
{
|
||||
return ResourceSite::LIVECHART;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the response after the request.
|
||||
*
|
||||
* @param BelongsToMany<ExternalResource, Anime> $resources
|
||||
*/
|
||||
public function handle(BelongsToMany $resources): static
|
||||
{
|
||||
$resource = $resources->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::LIVECHART->value);
|
||||
|
||||
if ($resource instanceof ExternalResource) {
|
||||
$id = $resource->external_id;
|
||||
|
||||
try {
|
||||
$this->response = Http::get("https://www.livechart.me/api/v1/anime/$id")
|
||||
->throw()
|
||||
->json();
|
||||
} catch (RequestException $e) {
|
||||
Log::error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mapping for the resources.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getResourcesMapping(): array
|
||||
{
|
||||
return [
|
||||
ResourceSite::ANIDB->value => 'anidb_url',
|
||||
ResourceSite::ANILIST->value => 'anilist_url',
|
||||
ResourceSite::ANIME_PLANET->value => 'anime_planet_url',
|
||||
ResourceSite::ANN->value => 'ann_url',
|
||||
ResourceSite::KITSU->value => 'kitsu_url',
|
||||
ResourceSite::MAL->value => 'mal_url',
|
||||
ResourceSite::OFFICIAL_SITE->value => 'website_url',
|
||||
ResourceSite::X->value => 'twitter_url',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki\Anime\ExternalApi;
|
||||
|
||||
use App\Actions\Models\Wiki\ExternalApiAction;
|
||||
use App\Constants\Config\ServiceConstants;
|
||||
use App\Contracts\Actions\Models\Wiki\BackfillStudios;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class MalAnimeExternalApiAction extends ExternalApiAction implements BackfillStudios
|
||||
{
|
||||
public function getSite(): ResourceSite
|
||||
{
|
||||
return ResourceSite::MAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the response after the request.
|
||||
*
|
||||
* @param BelongsToMany<ExternalResource, Anime> $resources
|
||||
*/
|
||||
public function handle(BelongsToMany $resources): static
|
||||
{
|
||||
$resource = $resources->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL->value);
|
||||
|
||||
if ($resource instanceof ExternalResource) {
|
||||
try {
|
||||
$this->response = Http::withHeaders(['X-MAL-CLIENT-ID' => Config::get(ServiceConstants::MAL_CLIENT_ID)])
|
||||
->get("https://api.myanimelist.net/v2/anime/$resource->external_id", [
|
||||
'fields' => 'studios',
|
||||
])
|
||||
->throw()
|
||||
->json();
|
||||
} catch (RequestException $e) {
|
||||
Log::error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mapped studios.
|
||||
*/
|
||||
public function getStudios(): array
|
||||
{
|
||||
return Arr::get($this->response, 'studios', []);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Actions\Storage\Wiki\Image\UploadImageAction;
|
||||
use App\Contracts\Models\HasImages;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use App\Enums\Models\Wiki\ImageFacet;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class AttachImageAction
|
||||
{
|
||||
/**
|
||||
* @param ImageFacet[] $facets
|
||||
*/
|
||||
public function handle(BaseModel&HasImages $model, array $fields, array $facets): ActionResult
|
||||
{
|
||||
foreach ($facets as $facet) {
|
||||
$image = Arr::get($fields, $facet->name);
|
||||
|
||||
if (blank($image)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
UploadImageAction::createImageFromFile($image, $facet, $model);
|
||||
}
|
||||
|
||||
return new ActionResult(ActionStatus::PASSED);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Concerns\Models\CanCreateExternalResource;
|
||||
use App\Contracts\Models\HasResources;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\BaseModel;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class AttachResourceAction
|
||||
{
|
||||
use CanCreateExternalResource;
|
||||
|
||||
/**
|
||||
* @param ResourceSite[] $sites
|
||||
*/
|
||||
public function handle(BaseModel&HasResources $model, array $fields, array $sites): ActionResult
|
||||
{
|
||||
foreach ($sites as $resourceSite) {
|
||||
$url = Arr::get($fields, $resourceSite->name);
|
||||
|
||||
if (blank($url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->createResource($url, $resourceSite, $model);
|
||||
}
|
||||
|
||||
return new ActionResult(ActionStatus::PASSED);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Actions\Models\BackfillWikiAction;
|
||||
use App\Actions\Models\Wiki\Anime\ExternalApi\AnilistAnimeExternalApiAction;
|
||||
use App\Actions\Models\Wiki\Anime\ExternalApi\AniZipAnimeExternalApiAction;
|
||||
use App\Actions\Models\Wiki\Anime\ExternalApi\JikanAnimeExternalApiAction;
|
||||
use App\Actions\Models\Wiki\Anime\ExternalApi\LivechartAnimeExternalApiAction;
|
||||
use App\Actions\Models\Wiki\Anime\ExternalApi\MalAnimeExternalApiAction;
|
||||
use App\Concerns\Models\CanCreateStudio;
|
||||
use App\Concerns\Models\HasLabel;
|
||||
use App\Contracts\Actions\Models\Wiki\BackfillImages;
|
||||
use App\Contracts\Actions\Models\Wiki\BackfillResources;
|
||||
use App\Contracts\Actions\Models\Wiki\BackfillStudios;
|
||||
use App\Contracts\Actions\Models\Wiki\BackfillSynonyms;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use App\Enums\Models\Wiki\SynonymType;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\Synonym;
|
||||
use Exception;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class BackfillAnimeAction extends BackfillWikiAction
|
||||
{
|
||||
use CanCreateStudio;
|
||||
use HasLabel;
|
||||
|
||||
final public const string STUDIOS = 'studios';
|
||||
final public const string SYNONYMS = 'synonyms';
|
||||
|
||||
public function __construct(protected Anime $anime, protected array $toBackfill)
|
||||
{
|
||||
parent::__construct($anime, $toBackfill);
|
||||
}
|
||||
|
||||
public function handle(): ActionResult
|
||||
{
|
||||
try {
|
||||
foreach ($this->getExternalApiActions() as $api) {
|
||||
DB::beginTransaction();
|
||||
|
||||
if (
|
||||
count($this->toBackfill[self::RESOURCES]) === 0
|
||||
&& count($this->toBackfill[self::IMAGES]) === 0
|
||||
&& ! $this->toBackfill[self::STUDIOS]
|
||||
&& ! $this->toBackfill[self::SYNONYMS]
|
||||
) {
|
||||
// Don't make other requests if everything is backfilled
|
||||
Log::info("Backfill action finished for Anime {$this->getModel()->getName()}");
|
||||
DB::rollBack();
|
||||
break;
|
||||
}
|
||||
|
||||
$response = $api->handle($this->getModel()->resources());
|
||||
|
||||
if ($response instanceof BackfillResources) {
|
||||
$this->forResources($response);
|
||||
}
|
||||
|
||||
if ($response instanceof BackfillImages) {
|
||||
$this->forImages($response);
|
||||
}
|
||||
|
||||
if ($response instanceof BackfillStudios) {
|
||||
$this->forStudios($response);
|
||||
}
|
||||
|
||||
if ($response instanceof BackfillSynonyms) {
|
||||
$this->forSynonyms($response);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
DB::rollBack();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return new ActionResult(ActionStatus::PASSED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the external API actions available for the backfill action.
|
||||
*
|
||||
* @return ExternalApiAction[]
|
||||
*/
|
||||
protected function getExternalApiActions(): array
|
||||
{
|
||||
return [
|
||||
new AniZipAnimeExternalApiAction(),
|
||||
new LivechartAnimeExternalApiAction(),
|
||||
new AnilistAnimeExternalApiAction(),
|
||||
new JikanAnimeExternalApiAction(),
|
||||
new MalAnimeExternalApiAction(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the studios given the response.
|
||||
*/
|
||||
protected function forStudios(ExternalApiAction&BackfillStudios $response): void
|
||||
{
|
||||
$studios = $response->getStudios();
|
||||
|
||||
if (! $this->toBackfill[self::STUDIOS]) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($studios as $studio) {
|
||||
$id = Arr::get($studio, 'id');
|
||||
$name = Arr::get($studio, 'name');
|
||||
|
||||
if (blank($name) || blank($id)) {
|
||||
Log::info("Skipping empty studio of name '$name' and id '$id''");
|
||||
continue;
|
||||
}
|
||||
|
||||
$studio = $this->getOrCreateStudio($name);
|
||||
|
||||
Log::info("Attaching Studio of name '$name' to Anime {$this->getModel()->getName()}");
|
||||
$this->getModel()->studios()->attach($studio);
|
||||
|
||||
$this->ensureStudioHasResource($studio, $response->getSite(), $id);
|
||||
}
|
||||
|
||||
if ($this->getModel()->studios()->exists()) {
|
||||
$this->toBackfill[self::STUDIOS] = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the synonyms given the response.
|
||||
*/
|
||||
protected function forSynonyms(ExternalApiAction&BackfillSynonyms $api): void
|
||||
{
|
||||
if (! $this->toBackfill[self::SYNONYMS]) {
|
||||
return;
|
||||
}
|
||||
|
||||
$synonyms = $api->getSynonyms()
|
||||
->reject(
|
||||
fn (string $text, int $type): bool => $type === SynonymType::OTHER->value && Str::is($text, $this->getModel()->getName(), true)
|
||||
)
|
||||
->all();
|
||||
|
||||
foreach ($synonyms as $type => $text) {
|
||||
$this->getModel()->synonyms()->create([
|
||||
Synonym::ATTRIBUTE_TEXT => $text,
|
||||
Synonym::ATTRIBUTE_TYPE => $type,
|
||||
]);
|
||||
|
||||
$this->toBackfill[self::SYNONYMS] = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function getModel(): Anime
|
||||
{
|
||||
return $this->anime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Actions\Models\BackfillAction;
|
||||
use App\Concerns\Models\CanCreateExternalResource;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\Song;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Uri;
|
||||
|
||||
/**
|
||||
* @extends BackfillAction<Song>
|
||||
*/
|
||||
class BackfillSongAction extends BackfillAction
|
||||
{
|
||||
use CanCreateExternalResource;
|
||||
|
||||
public function __construct(
|
||||
Song $song,
|
||||
protected readonly string $lnkto,
|
||||
) {
|
||||
parent::__construct($song);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function handle(): ActionResult
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
// Request to lnk.to site
|
||||
$response = Http::get($this->lnkto)
|
||||
->throw();
|
||||
|
||||
$pattern = '/<a[^>]*class="[^"]*music-service-list__link[^"]*js-redirect[^"]*"[^>]*href="([^"]+)"[^>]*data-label="([^"]*)"[^>]*>/i';
|
||||
|
||||
preg_match_all($pattern, (string) $response->body(), $matches);
|
||||
|
||||
foreach ($matches[1] as $key => $link) {
|
||||
$label = $matches[2][$key];
|
||||
$resourceSite = $this->getMappingFromExternalSite($label);
|
||||
|
||||
if (! $resourceSite instanceof ResourceSite) {
|
||||
Log::info("Skipping {$label} for Song {$this->getModel()->getName()}");
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->createResource(Uri::of($link), $resourceSite, $this->getModel());
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
DB::rollBack();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return new ActionResult(ActionStatus::PASSED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resource site according the external label.
|
||||
*/
|
||||
protected function getMappingFromExternalSite(string $label): ?ResourceSite
|
||||
{
|
||||
return match ($label) {
|
||||
'applemusic' => ResourceSite::APPLE_MUSIC,
|
||||
'spotify' => ResourceSite::SPOTIFY,
|
||||
'youtubemusic' => ResourceSite::YOUTUBE_MUSIC,
|
||||
'amazonmp3' => ResourceSite::AMAZON_MUSIC,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model the action is handling.
|
||||
*/
|
||||
protected function getModel(): Song
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relation to resources.
|
||||
*/
|
||||
protected function relation(): BelongsToMany
|
||||
{
|
||||
return $this->getModel()->resources();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Actions\Models\BackfillWikiAction;
|
||||
use App\Actions\Models\Wiki\Studio\ExternalApi\MalStudioExternalApiAction;
|
||||
use App\Contracts\Actions\Models\Wiki\BackfillImages;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use App\Models\Wiki\Studio;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class BackfillStudioAction extends BackfillWikiAction
|
||||
{
|
||||
public function __construct(protected Studio $studio, protected array $toBackfill)
|
||||
{
|
||||
parent::__construct($studio, $toBackfill);
|
||||
}
|
||||
|
||||
public function handle(): ActionResult
|
||||
{
|
||||
foreach ($this->getExternalApiActions() as $api) {
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
if (count($this->toBackfill[self::IMAGES]) === 0) {
|
||||
// Don't make other requests if everything is backfilled
|
||||
Log::info("Backfill action finished for Studio {$this->getModel()->getName()}");
|
||||
DB::rollBack();
|
||||
|
||||
return new ActionResult(ActionStatus::SKIPPED);
|
||||
}
|
||||
|
||||
$response = $api->handle($this->getModel()->resources());
|
||||
|
||||
if ($api instanceof BackfillImages) {
|
||||
$this->forImages($response);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
DB::rollBack();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
return new ActionResult(ActionStatus::PASSED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the external API actions available for the backfill action.
|
||||
*
|
||||
* @return ExternalApiAction[]
|
||||
*/
|
||||
protected function getExternalApiActions(): array
|
||||
{
|
||||
return [
|
||||
new MalStudioExternalApiAction(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getModel(): Studio
|
||||
{
|
||||
return $this->studio;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki;
|
||||
|
||||
use App\Contracts\Actions\Models\Wiki\BackfillImages;
|
||||
use App\Contracts\Actions\Models\Wiki\BackfillResources;
|
||||
use App\Contracts\Actions\Models\Wiki\BackfillSynonyms;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Enums\Models\Wiki\SynonymType;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
abstract class ExternalApiAction
|
||||
{
|
||||
public ?array $response = null;
|
||||
|
||||
abstract public function getSite(): ResourceSite;
|
||||
|
||||
/**
|
||||
* Set the response after the request.
|
||||
*/
|
||||
abstract public function handle(BelongsToMany $resources): static;
|
||||
|
||||
/**
|
||||
* Get the mapped resources.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getResources(): array
|
||||
{
|
||||
$resources = [];
|
||||
|
||||
if ($this instanceof BackfillResources && $this->response) {
|
||||
foreach ($this->getResourcesMapping() as $site => $key) {
|
||||
if (Arr::get($this->response, $key) !== null) {
|
||||
$resources[$site] = Arr::get($this->response, $key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $resources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mapped images.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getImages(): array
|
||||
{
|
||||
$images = [];
|
||||
|
||||
if ($this instanceof BackfillImages && $this->response) {
|
||||
foreach ($this->getImagesMapping() as $facet => $key) {
|
||||
if (Arr::get($this->response, $key) !== null) {
|
||||
$images[$facet] = Arr::get($this->response, $key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mapped synonyms.
|
||||
*
|
||||
* @return Collection<int, string>
|
||||
*/
|
||||
public function getSynonyms(): Collection
|
||||
{
|
||||
$synonyms = collect();
|
||||
|
||||
if ($this instanceof BackfillSynonyms && $this->response) {
|
||||
foreach ($this->getSynonymsMapping() as $type => $key) {
|
||||
$synonyms->put($type, Arr::get($this->response, $key));
|
||||
}
|
||||
}
|
||||
|
||||
return $synonyms
|
||||
->filter(fn (?string $text): bool => filled($text))
|
||||
->reject(function (string $text, int $type) use ($synonyms) {
|
||||
if ($type !== SynonymType::OTHER->value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $synonyms
|
||||
->except([SynonymType::OTHER->value])
|
||||
->containsStrict($text);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki\Image;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Constants\Config\ImageConstants;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use App\Models\Wiki\Image;
|
||||
use Exception;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class OptimizeImageAction
|
||||
{
|
||||
public function __construct(
|
||||
protected readonly Image $image,
|
||||
protected readonly ?string $extension = null,
|
||||
protected readonly ?int $width = null,
|
||||
protected readonly ?int $height = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function handle(): ActionResult
|
||||
{
|
||||
if ($this->extension === null && $this->width === null && $this->height === null) {
|
||||
return new ActionResult(
|
||||
ActionStatus::SKIPPED,
|
||||
'No optimization parameters provided, nothing to process.'
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$directory = File::dirname($this->image->path);
|
||||
|
||||
$optimizedImage = $this->handleFFmpeg();
|
||||
|
||||
if ($optimizedImage === null) {
|
||||
DB::rollBack();
|
||||
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED,
|
||||
"Failed to optimize image '{$this->image->path}'.",
|
||||
);
|
||||
}
|
||||
|
||||
$path = $this->uploadImage(
|
||||
Storage::disk('local')->path($optimizedImage),
|
||||
$directory
|
||||
);
|
||||
|
||||
// Delete the old image from the bucket and local.
|
||||
Storage::disk(Config::get(ImageConstants::DISKS_QUALIFIED))->delete($this->image->path);
|
||||
Storage::disk('local')->delete($optimizedImage);
|
||||
|
||||
$this->image->update([
|
||||
Image::ATTRIBUTE_PATH => $path,
|
||||
]);
|
||||
|
||||
DB::commit();
|
||||
} catch (Exception $e) {
|
||||
DB::rollBack();
|
||||
|
||||
Log::error($e->getMessage());
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return new ActionResult(ActionStatus::PASSED);
|
||||
}
|
||||
|
||||
protected function handleFFmpeg(): ?string
|
||||
{
|
||||
$filesToDelete = [];
|
||||
$imagePath = null;
|
||||
|
||||
try {
|
||||
Storage::disk('local')->put(
|
||||
$this->image->path,
|
||||
Storage::disk(Config::get(ImageConstants::DISKS_QUALIFIED))->get($this->image->path),
|
||||
);
|
||||
|
||||
$imagePath = $this->image->path;
|
||||
|
||||
$filesToDelete[] = $imagePath;
|
||||
|
||||
if ($this->extension !== null) {
|
||||
$imagePath = $this->convertImage($imagePath);
|
||||
$filesToDelete[] = $imagePath;
|
||||
}
|
||||
|
||||
if ($this->width !== null && $this->height !== null) {
|
||||
$imagePath = $this->downscaleImage($imagePath);
|
||||
$filesToDelete[] = $imagePath;
|
||||
}
|
||||
|
||||
return $imagePath;
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
} finally {
|
||||
Storage::disk('local')->delete(Arr::where($filesToDelete, fn (string $file): bool => $file !== $imagePath));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the image to given extension.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function convertImage(string $path): string
|
||||
{
|
||||
[$command, $imagePath] = match ($this->extension) {
|
||||
'avif' => static::getAvifCommand($path),
|
||||
default => throw new Exception("Unsupported image extension: {$this->extension}"),
|
||||
};
|
||||
|
||||
Process::run($command)->throw();
|
||||
|
||||
return $imagePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Downscale the image to given width and height.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function downscaleImage(string $path): string
|
||||
{
|
||||
[$command, $imagePath] = static::getDownscaleCommand($path, $this->width, $this->height);
|
||||
|
||||
Process::run($command)->throw();
|
||||
|
||||
return $imagePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload the image to the bucket.
|
||||
*/
|
||||
protected function uploadImage(string $image, string $directory): string
|
||||
{
|
||||
/** @var \Illuminate\Filesystem\FilesystemAdapter $fs */
|
||||
$fs = Storage::disk(Config::get(ImageConstants::DISKS_QUALIFIED));
|
||||
|
||||
$fsFile = $fs->putFile($directory, $image);
|
||||
|
||||
Log::info("Uploading optimized Image {$fsFile}");
|
||||
|
||||
return $fsFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0:array<int, string>, 1:string}
|
||||
*/
|
||||
public static function getAvifCommand(string $path): array
|
||||
{
|
||||
$imagePath = Storage::disk('local')->path(
|
||||
$newPath = Str::replaceLast(File::extension($path), 'avif', $path)
|
||||
);
|
||||
|
||||
return [
|
||||
[
|
||||
'ffmpeg',
|
||||
'-i',
|
||||
Storage::disk('local')->path($path),
|
||||
'-c:v',
|
||||
'libaom-av1',
|
||||
'-crf',
|
||||
'30',
|
||||
'-pix_fmt',
|
||||
'yuv420p',
|
||||
$imagePath,
|
||||
],
|
||||
$newPath,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0:array<int, string>, 1:string}
|
||||
*/
|
||||
public static function getDownscaleCommand(string $path, int $width = 100, int $height = 150): array
|
||||
{
|
||||
$tempPath = Storage::disk('local')->path(
|
||||
$newPath = Str::replaceLast('.', '_scaled.', $path)
|
||||
);
|
||||
|
||||
return [
|
||||
[
|
||||
'ffmpeg',
|
||||
'-y',
|
||||
'-i',
|
||||
Storage::disk('local')->path($path),
|
||||
'-vf',
|
||||
"scale={$width}:{$height}",
|
||||
$tempPath,
|
||||
],
|
||||
$newPath,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki\Song;
|
||||
|
||||
use App\Models\Wiki\Song;
|
||||
use App\Models\Wiki\Song\Performance;
|
||||
use App\Pivots\Wiki\ArtistMember;
|
||||
use Exception;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ManageSongPerformances
|
||||
{
|
||||
protected Song $song;
|
||||
|
||||
public function __construct(
|
||||
Song|int $song,
|
||||
/** @var Collection<int, non-empty-array<string, mixed>> */
|
||||
protected Collection $performances = new Collection(),
|
||||
/** @var Collection<int, array<string, mixed>> */
|
||||
protected Collection $members = new Collection(),
|
||||
) {
|
||||
$this->song = $song instanceof Song ? $song : Song::query()->find($song);
|
||||
}
|
||||
|
||||
public function addArtist(
|
||||
int $artist,
|
||||
?int $member = null,
|
||||
?string $alias = null,
|
||||
?string $as = null,
|
||||
?string $memberAlias = null,
|
||||
?string $memberAs = null
|
||||
): static {
|
||||
$this->performances->push([
|
||||
Performance::ATTRIBUTE_SONG => $this->song->getKey(),
|
||||
Performance::ATTRIBUTE_ARTIST => $artist,
|
||||
Performance::ATTRIBUTE_MEMBER => $member,
|
||||
Performance::ATTRIBUTE_ALIAS => filled($alias) ? trim($alias) : null,
|
||||
Performance::ATTRIBUTE_AS => filled($as) ? trim($as) : null,
|
||||
Performance::ATTRIBUTE_MEMBER_ALIAS => filled($memberAlias) ? trim($memberAlias) : null,
|
||||
Performance::ATTRIBUTE_MEMBER_AS => filled($memberAs) ? trim($memberAs) : null,
|
||||
]);
|
||||
|
||||
if ($member !== null) {
|
||||
$this->members->put($member, [
|
||||
ArtistMember::ATTRIBUTE_ARTIST => $artist,
|
||||
ArtistMember::ATTRIBUTE_MEMBER => $member,
|
||||
ArtistMember::ATTRIBUTE_ALIAS => filled($memberAlias) ? trim($memberAlias) : null,
|
||||
ArtistMember::ATTRIBUTE_AS => filled($memberAs) ? trim($memberAs) : null,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function commit(): static
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$new = collect($this->performances)
|
||||
->keyBy(fn (array $p): string => $p[Performance::ATTRIBUTE_ARTIST].':'.($p[Performance::ATTRIBUTE_MEMBER] ?? ''));
|
||||
|
||||
$existing = Performance::query()
|
||||
->whereBelongsTo($this->song)
|
||||
->get()
|
||||
->keyBy(fn (Performance $p): string => $p->artist_id.':'.($p->member_id ?? ''));
|
||||
|
||||
$models = $new->map(
|
||||
fn (array $performance) => Performance::query()->updateOrCreate(
|
||||
Arr::only($performance, [Performance::ATTRIBUTE_SONG, Performance::ATTRIBUTE_ARTIST, Performance::ATTRIBUTE_MEMBER]),
|
||||
Arr::only($performance, [Performance::ATTRIBUTE_ALIAS, Performance::ATTRIBUTE_AS, Performance::ATTRIBUTE_MEMBER_ALIAS, Performance::ATTRIBUTE_MEMBER_AS])
|
||||
)
|
||||
);
|
||||
|
||||
$existing->diffKeys($new)->each->delete();
|
||||
|
||||
Performance::setNewOrder($models->pluck(Performance::ATTRIBUTE_ID)->all());
|
||||
|
||||
// Update artist_member table to match member performances
|
||||
ArtistMember::query()->upsert(
|
||||
$this->members->all(),
|
||||
[ArtistMember::ATTRIBUTE_ARTIST, ArtistMember::ATTRIBUTE_MEMBER],
|
||||
[ArtistMember::ATTRIBUTE_ALIAS, ArtistMember::ATTRIBUTE_AS],
|
||||
);
|
||||
|
||||
static::setArtistMemberRelevance();
|
||||
|
||||
DB::commit();
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
DB::rollBack();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complex query to set relevance for artist members based on created_at order.
|
||||
*/
|
||||
public static function setArtistMemberRelevance(): void
|
||||
{
|
||||
DB::statement('
|
||||
WITH base AS (
|
||||
SELECT
|
||||
artist_id,
|
||||
COALESCE(MAX(relevance), 0) AS max_rel
|
||||
FROM artist_member
|
||||
GROUP BY artist_id
|
||||
),
|
||||
ranked AS (
|
||||
SELECT
|
||||
artist_id,
|
||||
member_id,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY artist_id
|
||||
ORDER BY created_at
|
||||
) AS rn
|
||||
FROM artist_member
|
||||
WHERE relevance IS NULL
|
||||
)
|
||||
UPDATE artist_member am
|
||||
JOIN ranked r
|
||||
ON am.artist_id = r.artist_id
|
||||
AND am.member_id = r.member_id
|
||||
JOIN base b
|
||||
ON b.artist_id = r.artist_id
|
||||
SET am.relevance = r.rn + b.max_rel;
|
||||
');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki\Studio\ExternalApi;
|
||||
|
||||
use App\Actions\Models\Wiki\ExternalApiAction;
|
||||
use App\Contracts\Actions\Models\Wiki\BackfillImages;
|
||||
use App\Enums\Models\Wiki\ImageFacet;
|
||||
use App\Enums\Models\Wiki\ResourceSite;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Models\Wiki\Studio;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
class MalStudioExternalApiAction extends ExternalApiAction implements BackfillImages
|
||||
{
|
||||
public function getSite(): ResourceSite
|
||||
{
|
||||
return ResourceSite::MAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the response after the request.
|
||||
*
|
||||
* @param BelongsToMany<ExternalResource, Studio> $resources
|
||||
*/
|
||||
public function handle(BelongsToMany $resources): static
|
||||
{
|
||||
$resource = $resources->firstWhere(ExternalResource::ATTRIBUTE_SITE, ResourceSite::MAL->value);
|
||||
|
||||
if ($resource instanceof ExternalResource) {
|
||||
$this->response = [
|
||||
'images' => [
|
||||
'large' => "https://cdn.myanimelist.net/images/company/$resource->external_id.png",
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mapping for the images.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getImagesMapping(): array
|
||||
{
|
||||
return [
|
||||
ImageFacet::LARGE_COVER->value => 'images.large',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Models\Wiki\Video\Audio;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Actions\Models\BackfillAction;
|
||||
use App\Actions\Storage\Wiki\Audio\MoveAudioAction;
|
||||
use App\Actions\Storage\Wiki\Audio\UploadAudioAction;
|
||||
use App\Constants\Config\VideoConstants;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use App\Enums\Actions\Models\Wiki\Video\DeriveSourceVideo;
|
||||
use App\Enums\Actions\Models\Wiki\Video\OverwriteAudio;
|
||||
use App\Enums\Actions\Models\Wiki\Video\ReplaceRelatedAudio;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\Anime\AnimeTheme;
|
||||
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
|
||||
use App\Models\Wiki\Audio;
|
||||
use App\Models\Wiki\Video;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends BackfillAction<Video>
|
||||
*/
|
||||
class BackfillAudioAction extends BackfillAction
|
||||
{
|
||||
public function __construct(
|
||||
Video $video,
|
||||
protected readonly DeriveSourceVideo $deriveSourceVideo = DeriveSourceVideo::YES,
|
||||
protected readonly OverwriteAudio $overwriteAudio = OverwriteAudio::NO,
|
||||
protected readonly ReplaceRelatedAudio $replaceRelatedAudio = ReplaceRelatedAudio::NO,
|
||||
) {
|
||||
parent::__construct($video);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function handle(): ActionResult
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
if ($this->relation()->getQuery()->exists() && ! $this->overwriteAudio()) {
|
||||
DB::rollBack();
|
||||
Log::info("{$this->label()} '{$this->getModel()->getName()}' already has Audio'.");
|
||||
|
||||
return new ActionResult(ActionStatus::SKIPPED);
|
||||
}
|
||||
|
||||
$audio = $this->getAudio();
|
||||
|
||||
if ($audio instanceof Audio) {
|
||||
$this->attachAudio($audio);
|
||||
}
|
||||
|
||||
if ($this->relation()->getQuery()->doesntExist()) {
|
||||
DB::rollBack();
|
||||
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED,
|
||||
"{$this->label()} '{$this->getModel()->getName()}' has no Audio after backfilling. Please review."
|
||||
);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
DB::rollBack();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return new ActionResult(ActionStatus::PASSED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model the action is handling.
|
||||
*/
|
||||
protected function getModel(): Video
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relation to audio.
|
||||
*/
|
||||
protected function relation(): BelongsTo
|
||||
{
|
||||
return $this->getModel()->audio();
|
||||
}
|
||||
|
||||
protected function deriveSourceVideo(): bool
|
||||
{
|
||||
return $this->deriveSourceVideo === DeriveSourceVideo::YES;
|
||||
}
|
||||
|
||||
protected function overwriteAudio(): bool
|
||||
{
|
||||
return $this->overwriteAudio === OverwriteAudio::YES;
|
||||
}
|
||||
|
||||
protected function replaceRelatedAudio(): bool
|
||||
{
|
||||
return $this->replaceRelatedAudio === ReplaceRelatedAudio::YES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or Create Audio.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function getAudio(): ?Audio
|
||||
{
|
||||
// Allow bypassing of source video derivation
|
||||
if ($this->replaceRelatedAudio()) {
|
||||
$sourceVideo = $this->getSourceVideo(false);
|
||||
} elseif ($this->deriveSourceVideo()) {
|
||||
$sourceVideo = $this->getSourceVideo();
|
||||
} else {
|
||||
$sourceVideo = $this->getModel();
|
||||
}
|
||||
|
||||
// It's possible that the video is not attached to any themes, exit early.
|
||||
if (! $sourceVideo instanceof Video) {
|
||||
Log::error('Could not derive source video');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// First, attempt to set audio from the source video
|
||||
$audio = $sourceVideo->audio;
|
||||
|
||||
// When uploading a BD version we should get the parent audio of a WEB version and
|
||||
// move the file overwriting the content later. Therefore, the old model is not deleted.
|
||||
if ($this->replaceRelatedAudio() && $audio instanceof Audio) {
|
||||
$moveAction = new MoveAudioAction($audio, Str::replace('webm', 'ogg', $this->getModel()->path()));
|
||||
|
||||
$storageResults = $moveAction->handle();
|
||||
|
||||
$audio = $moveAction->then($storageResults);
|
||||
}
|
||||
|
||||
// Anticipate audio path for FFmpeg save file
|
||||
$audioPath = $audio === null
|
||||
? Str::replace('webm', 'ogg', $sourceVideo->path)
|
||||
: $audio->path;
|
||||
|
||||
// Second, attempt to set audio from path
|
||||
if ($audio === null) {
|
||||
$audio = Audio::query()->firstWhere(Audio::ATTRIBUTE_PATH, $audioPath);
|
||||
}
|
||||
|
||||
// Finally, extract audio from the source video
|
||||
if ($audio === null || $this->overwriteAudio() || $this->replaceRelatedAudio()) {
|
||||
if ($this->replaceRelatedAudio()) {
|
||||
$sourceVideo = $this->getModel();
|
||||
}
|
||||
|
||||
return $this->extractAudio($sourceVideo);
|
||||
}
|
||||
|
||||
return $audio;
|
||||
}
|
||||
|
||||
protected function getSourceVideo(bool $descending = true): ?Video
|
||||
{
|
||||
return $this->getAdjacentVideos()->sortBy(
|
||||
fn (Video $video) => $video->getAttribute(Video::ATTRIBUTE_PRIORITY),
|
||||
descending: $descending
|
||||
)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Video>
|
||||
*/
|
||||
protected function getAdjacentVideos(): Collection
|
||||
{
|
||||
$builder = AnimeTheme::query();
|
||||
|
||||
$sortRelation = $builder->getRelation(AnimeTheme::RELATION_ANIME);
|
||||
|
||||
$orderByNameQuery = $sortRelation->getRelationExistenceQuery($sortRelation->getQuery(), $builder, [Anime::ATTRIBUTE_NAME]);
|
||||
$orderBySeasonQuery = $sortRelation->getRelationExistenceQuery($sortRelation->getQuery(), $builder, [Anime::ATTRIBUTE_SEASON]);
|
||||
$orderByYearQuery = $sortRelation->getRelationExistenceQuery($sortRelation->getQuery(), $builder, [Anime::ATTRIBUTE_YEAR]);
|
||||
$orderByMediaFormatQuery = $sortRelation->getRelationExistenceQuery($sortRelation->getQuery(), $builder, [Anime::ATTRIBUTE_FORMAT]);
|
||||
|
||||
return $builder->whereHas(AnimeTheme::RELATION_VIDEOS, fn (Builder $relationBuilder) => $relationBuilder->whereKey($this->getModel()))
|
||||
->orderBy($orderByMediaFormatQuery->toBase())
|
||||
->orderBy($orderByYearQuery->toBase())
|
||||
->orderBy($orderBySeasonQuery->toBase())
|
||||
->orderBy($orderByNameQuery->toBase())
|
||||
->with([
|
||||
AnimeTheme::RELATION_ANIME,
|
||||
AnimeTheme::RELATION_AUDIO,
|
||||
AnimeTheme::RELATION_ENTRIES => fn (Relation $relation) => $relation->getQuery()->orderBy(AnimeThemeEntry::ATTRIBUTE_VERSION),
|
||||
])
|
||||
->get()
|
||||
->flatMap(fn (AnimeTheme $theme) => $theme->animethemeentries)
|
||||
->flatMap(fn (AnimeThemeEntry $entry) => $entry->videos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract audio stream from video and store in filesystem.
|
||||
*/
|
||||
protected function extractAudio(Video $video): ?Audio
|
||||
{
|
||||
$audioBasename = Str::replace('webm', 'ogg', $video->basename);
|
||||
$audioPath = Storage::disk('local')->path($audioBasename);
|
||||
|
||||
try {
|
||||
Storage::disk('local')->put(
|
||||
$video->basename,
|
||||
Storage::disk(Config::get(VideoConstants::DEFAULT_DISK_QUALIFIED))->get($video->path)
|
||||
);
|
||||
|
||||
Process::run([
|
||||
'ffmpeg',
|
||||
'-v',
|
||||
'quiet',
|
||||
'-i',
|
||||
Storage::disk('local')->path($video->basename),
|
||||
'-vn',
|
||||
'-acodec',
|
||||
'copy',
|
||||
'-f',
|
||||
'ogg',
|
||||
'-y',
|
||||
$audioPath,
|
||||
])
|
||||
->throw();
|
||||
|
||||
$uploadAudio = new UploadAudioAction(
|
||||
new UploadedFile($audioPath, $audioBasename),
|
||||
File::dirname($video->path)
|
||||
);
|
||||
|
||||
$storageResults = $uploadAudio->handle();
|
||||
|
||||
return $uploadAudio->then($storageResults);
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
} finally {
|
||||
Storage::disk('local')->delete([$video->basename, $audioBasename]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function attachAudio(Audio $audio): void
|
||||
{
|
||||
if ($this->relation()->isNot($audio)) {
|
||||
Log::info("Associating Audio '{$audio->getName()}' with Video '{$this->getModel()->getName()}'");
|
||||
$this->relation()->associate($audio)->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories\Admin\Dump;
|
||||
|
||||
use App\Actions\Repositories\ReconcileRepositoriesAction;
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Models\Admin\Dump;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @extends ReconcileRepositoriesAction<Dump>
|
||||
*/
|
||||
class ReconcileDumpRepositoriesAction extends ReconcileRepositoriesAction
|
||||
{
|
||||
/**
|
||||
* The columns used for create and delete set operations.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function columnsForCreateDelete(): array
|
||||
{
|
||||
return [
|
||||
Dump::ATTRIBUTE_ID,
|
||||
Dump::ATTRIBUTE_PATH,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for create and delete set operation item comparison.
|
||||
*/
|
||||
protected function diffCallbackForCreateDelete(): Closure
|
||||
{
|
||||
return fn (Dump $first, Dump $second): int => [$first->path] <=> [$second->path];
|
||||
}
|
||||
|
||||
/**
|
||||
* The columns used for update set operation.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function columnsForUpdate(): array
|
||||
{
|
||||
return ['*'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for update set operation item comparison.
|
||||
*/
|
||||
protected function diffCallbackForUpdate(): Closure
|
||||
{
|
||||
return fn (): int => 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source model that has been updated for destination model.
|
||||
*/
|
||||
protected function resolveUpdatedModel(Collection $sourceModels, Model $destinationModel): ?Model
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reconciliation results.
|
||||
*/
|
||||
protected function getResults(Collection $created, Collection $deleted, Collection $updated): ReconcileResults
|
||||
{
|
||||
return new ReconcileDumpResults($created, $deleted, $updated);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories\Admin\Dump;
|
||||
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Models\Admin\Dump;
|
||||
|
||||
/**
|
||||
* @extends ReconcileResults<Dump>
|
||||
*/
|
||||
class ReconcileDumpResults extends ReconcileResults
|
||||
{
|
||||
/**
|
||||
* Get the model of the reconciliation results.
|
||||
*
|
||||
* @return class-string<Dump>
|
||||
*/
|
||||
protected function model(): string
|
||||
{
|
||||
return Dump::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories;
|
||||
|
||||
use App\Contracts\Repositories\RepositoryInterface;
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* @template TModel of \App\Models\BaseModel
|
||||
*/
|
||||
abstract class ReconcileRepositoriesAction
|
||||
{
|
||||
/**
|
||||
* Perform set reconciliation between source and destination repositories.
|
||||
*
|
||||
* @param RepositoryInterface<TModel> $source
|
||||
* @param RepositoryInterface<TModel> $destination
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
public function reconcileRepositories(RepositoryInterface $source, RepositoryInterface $destination): ReconcileResults
|
||||
{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
$sourceModels = $source->get();
|
||||
|
||||
$destinationModels = $destination->get($this->columnsForCreateDelete());
|
||||
|
||||
$created = $this->createModelsFromSource($destination, $sourceModels, $destinationModels);
|
||||
|
||||
$deleted = $this->deleteModelsFromDestination($destination, $sourceModels, $destinationModels);
|
||||
|
||||
$destinationModels = $destination->get($this->columnsForUpdate());
|
||||
|
||||
$updated = $this->updateDestinationModels($destination, $sourceModels, $destinationModels);
|
||||
|
||||
DB::commit();
|
||||
|
||||
return $this->getResults($created, $deleted, $updated);
|
||||
} catch (Exception $e) {
|
||||
Log::error($e->getMessage());
|
||||
|
||||
DB::rollBack();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The columns used for create and delete set operations.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
abstract protected function columnsForCreateDelete(): array;
|
||||
|
||||
/**
|
||||
* Callback for create and delete set operation item comparison.
|
||||
*/
|
||||
abstract protected function diffCallbackForCreateDelete(): Closure;
|
||||
|
||||
/**
|
||||
* Create models that exist in source but not in destination.
|
||||
*
|
||||
* @param RepositoryInterface<TModel> $destination
|
||||
*/
|
||||
protected function createModelsFromSource(
|
||||
RepositoryInterface $destination,
|
||||
Collection $sourceModels,
|
||||
Collection $destinationModels
|
||||
): Collection {
|
||||
$createModels = $sourceModels->diffUsing($destinationModels, $this->diffCallbackForCreateDelete());
|
||||
|
||||
return $createModels->each(fn (Model $createModel): bool => $destination->save($createModel));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete models that exist in destination but not in source.
|
||||
*
|
||||
* @param RepositoryInterface<TModel> $destination
|
||||
*/
|
||||
protected function deleteModelsFromDestination(
|
||||
RepositoryInterface $destination,
|
||||
Collection $sourceModels,
|
||||
Collection $destinationModels
|
||||
): Collection {
|
||||
$deleteModels = $destinationModels->diffUsing($sourceModels, $this->diffCallbackForCreateDelete());
|
||||
|
||||
return $deleteModels->each(fn (Model $deleteModel): bool => $destination->delete($deleteModel));
|
||||
}
|
||||
|
||||
/**
|
||||
* The columns used for update set operation.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
abstract protected function columnsForUpdate(): array;
|
||||
|
||||
/**
|
||||
* Callback for update set operation item comparison.
|
||||
*/
|
||||
abstract protected function diffCallbackForUpdate(): Closure;
|
||||
|
||||
/**
|
||||
* Get source model that has been updated for destination model.
|
||||
*/
|
||||
abstract protected function resolveUpdatedModel(Collection $sourceModels, Model $destinationModel): ?Model;
|
||||
|
||||
/**
|
||||
* Update destination models that have changed in source.
|
||||
*
|
||||
* @param RepositoryInterface<TModel> $destination
|
||||
*/
|
||||
protected function updateDestinationModels(
|
||||
RepositoryInterface $destination,
|
||||
Collection $sourceModels,
|
||||
Collection $destinationModels
|
||||
): Collection {
|
||||
$updatedModels = $destinationModels->diffUsing($sourceModels, $this->diffCallbackForUpdate());
|
||||
|
||||
return $updatedModels->each(function (Model $updatedModel) use ($sourceModels, $destination): void {
|
||||
$sourceModel = $this->resolveUpdatedModel($sourceModels, $updatedModel);
|
||||
if ($sourceModel instanceof Model) {
|
||||
$destination->update($updatedModel, $sourceModel->withoutAppends()->toArray());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reconciliation results.
|
||||
*/
|
||||
abstract protected function getResults(Collection $created, Collection $deleted, Collection $updated): ReconcileResults;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use App\Models\BaseModel;
|
||||
use Countable;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @template TModel of \App\Models\BaseModel
|
||||
*/
|
||||
abstract class ReconcileResults extends ActionResult
|
||||
{
|
||||
public function __construct(
|
||||
protected readonly Collection $created = new Collection(),
|
||||
protected readonly Collection $deleted = new Collection(),
|
||||
protected readonly Collection $updated = new Collection()
|
||||
) {
|
||||
parent::__construct(ActionStatus::PASSED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get created models.
|
||||
*/
|
||||
public function getCreated(): Collection
|
||||
{
|
||||
return $this->created;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get deleted models.
|
||||
*/
|
||||
public function getDeleted(): Collection
|
||||
{
|
||||
return $this->deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get updated models.
|
||||
*/
|
||||
public function getUpdated(): Collection
|
||||
{
|
||||
return $this->updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if any successful changes were made during reconciliation.
|
||||
*/
|
||||
public function hasChanges(): bool
|
||||
{
|
||||
if ($this->created->isNotEmpty()) {
|
||||
return true;
|
||||
}
|
||||
if ($this->deleted->isNotEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->updated->isNotEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write reconcile results to log.
|
||||
*
|
||||
* @noinspection PhpMissingParentCallCommonInspection
|
||||
*/
|
||||
public function toLog(): void
|
||||
{
|
||||
$this->created->each(fn (BaseModel $model) => Log::info("{$this->label()} '{$model->getName()}' created"));
|
||||
$this->deleted->each(fn (BaseModel $model) => Log::info("{$this->label()} '{$model->getName()}' deleted"));
|
||||
$this->updated->each(fn (BaseModel $model) => Log::info("{$this->label()} '{$model->getName()}' updated"));
|
||||
|
||||
Log::info($this->getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Write reconcile results to console output.
|
||||
*
|
||||
* @noinspection PhpMissingParentCallCommonInspection
|
||||
*/
|
||||
public function toConsole(Command $command): void
|
||||
{
|
||||
$this->created->each(fn (BaseModel $model) => $command->info("{$this->label()} '{$model->getName()}' created"));
|
||||
$this->deleted->each(fn (BaseModel $model) => $command->info("{$this->label()} '{$model->getName()}' deleted"));
|
||||
$this->updated->each(fn (BaseModel $model) => $command->info("{$this->label()} '{$model->getName()}' updated"));
|
||||
|
||||
$command->info($this->getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the action result message.
|
||||
*
|
||||
* @noinspection PhpMissingParentCallCommonInspection
|
||||
*/
|
||||
public function getMessage(): ?string
|
||||
{
|
||||
if ($this->hasChanges()) {
|
||||
return "{$this->created->count()} {$this->label($this->created)} created, {$this->deleted->count()} {$this->label($this->deleted)} deleted, {$this->updated->count()} {$this->label($this->updated)} updated";
|
||||
}
|
||||
|
||||
return "No {$this->label($this->created)} created or deleted or updated";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model of the reconciliation results.
|
||||
*
|
||||
* @return class-string<TModel>
|
||||
*/
|
||||
abstract protected function model(): string;
|
||||
|
||||
/**
|
||||
* Get the user-friendly label for the model class name.
|
||||
*/
|
||||
protected function label(int|array|Countable $models = 1): string
|
||||
{
|
||||
return Str::plural(Str::headline(class_basename($this->model())), $models);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories\Wiki\Audio;
|
||||
|
||||
use App\Actions\Repositories\ReconcileRepositoriesAction;
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Models\Wiki\Audio;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @extends ReconcileRepositoriesAction<Audio>
|
||||
*/
|
||||
class ReconcileAudioRepositoriesAction extends ReconcileRepositoriesAction
|
||||
{
|
||||
/**
|
||||
* The columns used for create and delete set operations.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function columnsForCreateDelete(): array
|
||||
{
|
||||
return [
|
||||
Audio::ATTRIBUTE_BASENAME,
|
||||
Audio::ATTRIBUTE_ID,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for create and delete set operation item comparison.
|
||||
*/
|
||||
protected function diffCallbackForCreateDelete(): Closure
|
||||
{
|
||||
return fn (Audio $first, Audio $second): int => $first->basename <=> $second->basename;
|
||||
}
|
||||
|
||||
/**
|
||||
* The columns used for update set operation.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function columnsForUpdate(): array
|
||||
{
|
||||
return [
|
||||
Audio::ATTRIBUTE_BASENAME,
|
||||
Audio::ATTRIBUTE_FILENAME,
|
||||
Audio::ATTRIBUTE_ID,
|
||||
Audio::ATTRIBUTE_MIMETYPE,
|
||||
Audio::ATTRIBUTE_PATH,
|
||||
Audio::ATTRIBUTE_SIZE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for update set operation item comparison.
|
||||
*/
|
||||
protected function diffCallbackForUpdate(): Closure
|
||||
{
|
||||
return fn (Audio $first, Audio $second): int => [$first->basename, $first->path, $first->size] <=> [$second->basename, $second->path, $second->size];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source model that has been updated for destination model.
|
||||
*/
|
||||
protected function resolveUpdatedModel(Collection $sourceModels, Model $destinationModel): ?Model
|
||||
{
|
||||
return $sourceModels->firstWhere(
|
||||
Audio::ATTRIBUTE_BASENAME,
|
||||
$destinationModel->getAttribute(Audio::ATTRIBUTE_BASENAME)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reconciliation results.
|
||||
*/
|
||||
protected function getResults(Collection $created, Collection $deleted, Collection $updated): ReconcileResults
|
||||
{
|
||||
return new ReconcileAudioResults($created, $deleted, $updated);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories\Wiki\Audio;
|
||||
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Models\Wiki\Audio;
|
||||
|
||||
/**
|
||||
* @extends ReconcileResults<Audio>
|
||||
*/
|
||||
class ReconcileAudioResults extends ReconcileResults
|
||||
{
|
||||
/**
|
||||
* Get the model of the reconciliation results.
|
||||
*
|
||||
* @return class-string<Audio>
|
||||
*/
|
||||
protected function model(): string
|
||||
{
|
||||
return Audio::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories\Wiki\Video;
|
||||
|
||||
use App\Actions\Repositories\ReconcileRepositoriesAction;
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Models\Wiki\Video;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @extends ReconcileRepositoriesAction<Video>
|
||||
*/
|
||||
class ReconcileVideoRepositoriesAction extends ReconcileRepositoriesAction
|
||||
{
|
||||
/**
|
||||
* The columns used for create and delete set operations.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function columnsForCreateDelete(): array
|
||||
{
|
||||
return [
|
||||
Video::ATTRIBUTE_BASENAME,
|
||||
Video::ATTRIBUTE_ID,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for create and delete set operation item comparison.
|
||||
*/
|
||||
protected function diffCallbackForCreateDelete(): Closure
|
||||
{
|
||||
return fn (Video $first, Video $second): int => $first->basename <=> $second->basename;
|
||||
}
|
||||
|
||||
/**
|
||||
* The columns used for update set operation.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function columnsForUpdate(): array
|
||||
{
|
||||
return [
|
||||
Video::ATTRIBUTE_BASENAME,
|
||||
Video::ATTRIBUTE_FILENAME,
|
||||
Video::ATTRIBUTE_ID,
|
||||
Video::ATTRIBUTE_MIMETYPE,
|
||||
Video::ATTRIBUTE_PATH,
|
||||
Video::ATTRIBUTE_SIZE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for update set operation item comparison.
|
||||
*/
|
||||
protected function diffCallbackForUpdate(): Closure
|
||||
{
|
||||
return fn (Video $first, Video $second): int => [$first->basename, $first->path, $first->size] <=> [$second->basename, $second->path, $second->size];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source model that has been updated for destination model.
|
||||
*/
|
||||
protected function resolveUpdatedModel(Collection $sourceModels, Model $destinationModel): ?Model
|
||||
{
|
||||
return $sourceModels->firstWhere(
|
||||
Video::ATTRIBUTE_BASENAME,
|
||||
$destinationModel->getAttribute(Video::ATTRIBUTE_BASENAME)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reconciliation results.
|
||||
*/
|
||||
protected function getResults(Collection $created, Collection $deleted, Collection $updated): ReconcileResults
|
||||
{
|
||||
return new ReconcileVideoResults($created, $deleted, $updated);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories\Wiki\Video;
|
||||
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Models\Wiki\Video;
|
||||
|
||||
/**
|
||||
* @extends ReconcileResults<Video>
|
||||
*/
|
||||
class ReconcileVideoResults extends ReconcileResults
|
||||
{
|
||||
/**
|
||||
* Get the model of the reconciliation results.
|
||||
*
|
||||
* @return class-string<Video>
|
||||
*/
|
||||
protected function model(): string
|
||||
{
|
||||
return Video::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories\Wiki\Video\Script;
|
||||
|
||||
use App\Actions\Repositories\ReconcileRepositoriesAction;
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Models\Wiki\Video\VideoScript;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* @extends ReconcileRepositoriesAction<VideoScript>
|
||||
*/
|
||||
class ReconcileScriptRepositoriesAction extends ReconcileRepositoriesAction
|
||||
{
|
||||
/**
|
||||
* The columns used for create and delete set operations.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function columnsForCreateDelete(): array
|
||||
{
|
||||
return [
|
||||
VideoScript::ATTRIBUTE_ID,
|
||||
VideoScript::ATTRIBUTE_PATH,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for create and delete set operation item comparison.
|
||||
*/
|
||||
protected function diffCallbackForCreateDelete(): Closure
|
||||
{
|
||||
return fn (VideoScript $first, VideoScript $second): int => [$first->path] <=> [$second->path];
|
||||
}
|
||||
|
||||
/**
|
||||
* The columns used for update set operation.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
protected function columnsForUpdate(): array
|
||||
{
|
||||
return ['*'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for update set operation item comparison.
|
||||
*/
|
||||
protected function diffCallbackForUpdate(): Closure
|
||||
{
|
||||
return fn (): int => 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get source model that has been updated for destination model.
|
||||
*/
|
||||
protected function resolveUpdatedModel(Collection $sourceModels, Model $destinationModel): ?Model
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reconciliation results.
|
||||
*/
|
||||
protected function getResults(Collection $created, Collection $deleted, Collection $updated): ReconcileResults
|
||||
{
|
||||
return new ReconcileScriptResults($created, $deleted, $updated);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Repositories\Wiki\Video\Script;
|
||||
|
||||
use App\Actions\Repositories\ReconcileResults;
|
||||
use App\Models\Wiki\Video\VideoScript;
|
||||
|
||||
/**
|
||||
* @extends ReconcileResults<VideoScript>
|
||||
*/
|
||||
class ReconcileScriptResults extends ReconcileResults
|
||||
{
|
||||
/**
|
||||
* Get the model of the reconciliation results.
|
||||
*
|
||||
* @return class-string<VideoScript>
|
||||
*/
|
||||
protected function model(): string
|
||||
{
|
||||
return VideoScript::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Admin\Dump;
|
||||
|
||||
use App\Actions\ActionResult;
|
||||
use App\Constants\Config\DumpConstants;
|
||||
use App\Enums\Actions\ActionStatus;
|
||||
use App\Models\Admin\Dump;
|
||||
use Exception;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Database\MySqlConnection;
|
||||
use Illuminate\Database\PostgresConnection;
|
||||
use Illuminate\Database\SQLiteConnection;
|
||||
use Illuminate\Filesystem\FilesystemAdapter;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use PDO;
|
||||
use RuntimeException;
|
||||
use Spatie\DbDumper\Databases\MySql;
|
||||
use Spatie\DbDumper\Databases\PostgreSql;
|
||||
use Spatie\DbDumper\Databases\Sqlite;
|
||||
use Spatie\DbDumper\DbDumper;
|
||||
use Spatie\DbDumper\Exceptions\CannotSetParameter;
|
||||
|
||||
abstract class DumpAction
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function __construct(protected readonly array $options = []) {}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function handle(): ActionResult
|
||||
{
|
||||
$dumpFile = $this->getDumpFile();
|
||||
|
||||
try {
|
||||
$connection = DB::connection();
|
||||
|
||||
$dumper = $this->getDumper($connection);
|
||||
if (! $dumper instanceof DbDumper) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED,
|
||||
"Unrecognized connection '{$connection->getName()}'"
|
||||
);
|
||||
}
|
||||
|
||||
// First, Dump file to temporary location
|
||||
// Writing to disks is not fully supported
|
||||
$dumper->dumpToFile($dumpFile);
|
||||
|
||||
// Then, store dump file in intended location
|
||||
/** @var FilesystemAdapter $fs */
|
||||
$fs = Storage::disk(Config::get(DumpConstants::DISK_QUALIFIED));
|
||||
$path = $fs->putFileAs('', $dumpFile, File::basename($dumpFile));
|
||||
|
||||
// Finally, delete the temporary file
|
||||
File::delete($dumpFile);
|
||||
} catch (Exception $e) {
|
||||
return new ActionResult(
|
||||
ActionStatus::FAILED,
|
||||
$e->getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
Dump::query()->create([
|
||||
Dump::ATTRIBUTE_PATH => $path,
|
||||
Dump::ATTRIBUTE_PUBLIC => $this->isPublic(),
|
||||
]);
|
||||
|
||||
return new ActionResult(
|
||||
ActionStatus::PASSED,
|
||||
"Database dump '$dumpFile' has been created",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dumper for the database connection.
|
||||
*
|
||||
* @throws CannotSetParameter
|
||||
*/
|
||||
protected function getDumper(Connection $connection): ?DbDumper
|
||||
{
|
||||
if ($connection instanceof SQLiteConnection) {
|
||||
return $this->prepareSqliteDumper($connection);
|
||||
}
|
||||
|
||||
if ($connection instanceof MySqlConnection) {
|
||||
return $this->prepareMySqlDumper($connection);
|
||||
}
|
||||
|
||||
if ($connection instanceof PostgresConnection) {
|
||||
return $this->preparePostgreSqlDumper($connection);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure Sqlite database dumper.
|
||||
*
|
||||
* @throws RuntimeException
|
||||
* @throws CannotSetParameter
|
||||
*/
|
||||
protected function prepareSqliteDumper(Connection $connection): Sqlite
|
||||
{
|
||||
throw_if(version_compare($connection->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION), '3.32.0', '<'), RuntimeException::class, 'DB connection does not support includeTables option');
|
||||
|
||||
$dumper = Sqlite::create();
|
||||
|
||||
$dumper->setDbName($connection->getDatabaseName());
|
||||
$dumper->includeTables(static::allowedTables());
|
||||
|
||||
return $dumper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure MySQL database dumper.
|
||||
*
|
||||
* @throws CannotSetParameter
|
||||
*/
|
||||
protected function prepareMySqlDumper(Connection $connection): MySql
|
||||
{
|
||||
$dumper = MySql::create();
|
||||
|
||||
$dumper->setDbName($connection->getDatabaseName());
|
||||
$dumper->setUserName(strval($connection->getConfig('username')));
|
||||
$dumper->setPassword(strval($connection->getConfig('password')));
|
||||
|
||||
$hostConfig = $connection->getConfig('host');
|
||||
$socketConfig = $connection->getConfig('unix_socket');
|
||||
if ($socketConfig !== null) {
|
||||
$dumper->setSocket(strval($socketConfig));
|
||||
} elseif ($hostConfig !== null) {
|
||||
$dumper->setHost(collect($hostConfig)->first());
|
||||
}
|
||||
|
||||
$port = $connection->getConfig('port');
|
||||
if (is_numeric($port)) {
|
||||
$dumper->setPort(intval($port));
|
||||
}
|
||||
|
||||
$dumper->includeTables(static::allowedTables());
|
||||
|
||||
if ($this->option('comments')) {
|
||||
$dumper->dontSkipComments();
|
||||
}
|
||||
|
||||
if ($this->option('skip-comments')) {
|
||||
$dumper->skipComments();
|
||||
}
|
||||
|
||||
if ($this->option('extended-insert')) {
|
||||
$dumper->useExtendedInserts();
|
||||
}
|
||||
|
||||
if ($this->option('skip-extended-insert')) {
|
||||
$dumper->dontUseExtendedInserts();
|
||||
}
|
||||
|
||||
$this->option('single-transaction')
|
||||
? $dumper->useSingleTransaction()
|
||||
: $dumper->dontUseSingleTransaction();
|
||||
|
||||
if ($this->option('lock-tables')) {
|
||||
$dumper->dontSkipLockTables();
|
||||
}
|
||||
|
||||
if ($this->option('skip-lock-tables')) {
|
||||
$dumper->skipLockTables();
|
||||
}
|
||||
|
||||
if ($this->option('skip-column-statistics')) {
|
||||
$dumper->doNotUseColumnStatistics();
|
||||
}
|
||||
|
||||
if ($this->option('quick')) {
|
||||
$dumper->useQuick();
|
||||
}
|
||||
|
||||
if ($this->option('skip-quick')) {
|
||||
$dumper->dontUseQuick();
|
||||
}
|
||||
|
||||
if ($this->hasOption('default-character-set')) {
|
||||
$dumper->setDefaultCharacterSet($this->option('default-character-set'));
|
||||
}
|
||||
|
||||
if ($this->hasOption('set-gtid-purged')) {
|
||||
$dumper->setGtidPurged($this->option('set-gtid-purged'));
|
||||
}
|
||||
|
||||
if ($this->option('no-create-info')) {
|
||||
$dumper->doNotCreateTables();
|
||||
}
|
||||
|
||||
return $dumper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure PostgreSql database dumper.
|
||||
*
|
||||
* @throws CannotSetParameter
|
||||
*/
|
||||
protected function preparePostgreSqlDumper(Connection $connection): PostgreSql
|
||||
{
|
||||
$dumper = PostgreSql::create();
|
||||
|
||||
$dumper->setDbName($connection->getDatabaseName());
|
||||
$dumper->setUserName(strval($connection->getConfig('username')));
|
||||
$dumper->setPassword(strval($connection->getConfig('password')));
|
||||
|
||||
$host = $connection->getConfig('host');
|
||||
if ($host !== null) {
|
||||
$dumper->setHost(collect($host)->first());
|
||||
}
|
||||
|
||||
$port = $connection->getConfig('port');
|
||||
if (is_int($port)) {
|
||||
$dumper->setPort($port);
|
||||
}
|
||||
|
||||
$dumper->includeTables(static::allowedTables());
|
||||
|
||||
if ($this->option('inserts')) {
|
||||
$dumper->useInserts();
|
||||
}
|
||||
|
||||
if ($this->option('data-only')) {
|
||||
$dumper->doNotCreateTables();
|
||||
}
|
||||
|
||||
return $dumper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the string option is set.
|
||||
*/
|
||||
protected function hasOption(string $key): bool
|
||||
{
|
||||
return is_string(Arr::get($this->options, $key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the option by key.
|
||||
*/
|
||||
protected function option(string $key): mixed
|
||||
{
|
||||
return Arr::get($this->options, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine wheter the dump should be public.
|
||||
*/
|
||||
protected function isPublic(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The temporary path for the database dump.
|
||||
* Note: The dumper library does not support writing to disk, so we have to write to the local filesystem first.
|
||||
* Pattern: "animethemes-db-dump-{concern}-{milliseconds from epoch}.sql".
|
||||
*/
|
||||
abstract protected function getDumpFile(): string;
|
||||
|
||||
/**
|
||||
* The list of tables to include in the dump.
|
||||
*/
|
||||
abstract public static function allowedTables(): array;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Admin\Dump;
|
||||
|
||||
use App\Concerns\Repositories\Admin\ReconcilesDumpRepositories;
|
||||
use App\Models\Admin\ActionLog;
|
||||
use App\Models\Admin\Announcement;
|
||||
use App\Models\Admin\Dump;
|
||||
use App\Models\Admin\Feature;
|
||||
use App\Models\Admin\FeaturedTheme;
|
||||
use Illuminate\Support\Facades\Date;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DumpAdminAction extends DumpAction
|
||||
{
|
||||
use ReconcilesDumpRepositories;
|
||||
|
||||
final public const string FILENAME_PREFIX = 'animethemes-db-dump-admin-';
|
||||
|
||||
/**
|
||||
* The list of tables to include in the dump.
|
||||
*/
|
||||
public static function allowedTables(): array
|
||||
{
|
||||
return [
|
||||
'action_events', // Nova events
|
||||
ActionLog::TABLE,
|
||||
Announcement::TABLE,
|
||||
Dump::TABLE,
|
||||
Feature::TABLE,
|
||||
FeaturedTheme::TABLE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The temporary path for the database dump.
|
||||
* Note: The dumper library does not support writing to disk, so we have to write to the local filesystem first.
|
||||
* Pattern: "animethemes-db-dump-admin-{milliseconds from epoch}.sql".
|
||||
*/
|
||||
protected function getDumpFile(): string
|
||||
{
|
||||
$filesystem = Storage::disk('local');
|
||||
|
||||
return Str::of($filesystem->path(''))
|
||||
->append(DumpAdminAction::FILENAME_PREFIX)
|
||||
->append(strval(Date::now()->valueOf()))
|
||||
->append('.sql')
|
||||
->__toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Admin\Dump;
|
||||
|
||||
use App\Concerns\Repositories\Admin\ReconcilesDumpRepositories;
|
||||
use App\Models\Auth\Permission;
|
||||
use App\Models\Auth\Role;
|
||||
use App\Models\Auth\User;
|
||||
use App\Models\List\External\ExternalToken;
|
||||
use Illuminate\Support\Facades\Date;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DumpAuthAction extends DumpAction
|
||||
{
|
||||
use ReconcilesDumpRepositories;
|
||||
|
||||
final public const string FILENAME_PREFIX = 'animethemes-db-dump-auth-';
|
||||
|
||||
/**
|
||||
* The list of tables to include in the dump.
|
||||
*/
|
||||
public static function allowedTables(): array
|
||||
{
|
||||
return [
|
||||
// This table stores tokens which are sensitive data.
|
||||
ExternalToken::TABLE,
|
||||
|
||||
Permission::TABLE,
|
||||
Role::TABLE,
|
||||
User::TABLE,
|
||||
'model_has_permissions',
|
||||
'model_has_roles',
|
||||
'password_reset_tokens',
|
||||
'personal_access_tokens',
|
||||
'role_has_permissions',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The temporary path for the database dump.
|
||||
* Note: The dumper library does not support writing to disk, so we have to write to the local filesystem first.
|
||||
* Pattern: "animethemes-db-dump-auth-{milliseconds from epoch}.sql".
|
||||
*/
|
||||
protected function getDumpFile(): string
|
||||
{
|
||||
$filesystem = Storage::disk('local');
|
||||
|
||||
return Str::of($filesystem->path(''))
|
||||
->append(DumpAuthAction::FILENAME_PREFIX)
|
||||
->append(strval(Date::now()->valueOf()))
|
||||
->append('.sql')
|
||||
->__toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Admin\Dump;
|
||||
|
||||
use App\Concerns\Repositories\Admin\ReconcilesDumpRepositories;
|
||||
use App\Models\Wiki\Anime;
|
||||
use App\Models\Wiki\Anime\AnimeSynonym;
|
||||
use App\Models\Wiki\Anime\AnimeTheme;
|
||||
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
|
||||
use App\Models\Wiki\Artist;
|
||||
use App\Models\Wiki\Audio;
|
||||
use App\Models\Wiki\ExternalResource;
|
||||
use App\Models\Wiki\Group;
|
||||
use App\Models\Wiki\Image;
|
||||
use App\Models\Wiki\Series;
|
||||
use App\Models\Wiki\Song;
|
||||
use App\Models\Wiki\Song\Performance;
|
||||
use App\Models\Wiki\Studio;
|
||||
use App\Models\Wiki\Synonym;
|
||||
use App\Models\Wiki\Video;
|
||||
use App\Models\Wiki\Video\VideoScript;
|
||||
use App\Pivots\Morph\Imageable;
|
||||
use App\Pivots\Morph\Resourceable;
|
||||
use App\Pivots\Wiki\AnimeSeries;
|
||||
use App\Pivots\Wiki\AnimeStudio;
|
||||
use App\Pivots\Wiki\AnimeThemeEntryVideo;
|
||||
use App\Pivots\Wiki\ArtistMember;
|
||||
use Illuminate\Support\Facades\Date;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DumpContentAction extends DumpAction
|
||||
{
|
||||
use ReconcilesDumpRepositories;
|
||||
|
||||
final public const string FILENAME_PREFIX = 'animethemes-db-dump-wiki-';
|
||||
|
||||
/**
|
||||
* The list of tables to include in the dump.
|
||||
*/
|
||||
public static function allowedTables(): array
|
||||
{
|
||||
return [
|
||||
Anime::TABLE,
|
||||
AnimeSeries::TABLE,
|
||||
AnimeStudio::TABLE,
|
||||
AnimeSynonym::TABLE,
|
||||
AnimeTheme::TABLE,
|
||||
AnimeThemeEntry::TABLE,
|
||||
AnimeThemeEntryVideo::TABLE,
|
||||
Artist::TABLE,
|
||||
ArtistMember::TABLE,
|
||||
'artist_song',
|
||||
Audio::TABLE,
|
||||
ExternalResource::TABLE,
|
||||
Group::TABLE,
|
||||
Image::TABLE,
|
||||
Imageable::TABLE,
|
||||
'memberships',
|
||||
Performance::TABLE,
|
||||
Resourceable::TABLE,
|
||||
Series::TABLE,
|
||||
Song::TABLE,
|
||||
Studio::TABLE,
|
||||
Synonym::TABLE,
|
||||
Video::TABLE,
|
||||
VideoScript::TABLE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The temporary path for the database dump.
|
||||
* Note: The dumper library does not support writing to disk, so we have to write to the local filesystem first.
|
||||
* Pattern: "animethemes-db-dump-wiki-{milliseconds from epoch}.sql".
|
||||
*/
|
||||
protected function getDumpFile(): string
|
||||
{
|
||||
$filesystem = Storage::disk('local');
|
||||
|
||||
return Str::of($filesystem->path(''))
|
||||
->append(DumpContentAction::FILENAME_PREFIX)
|
||||
->append(strval(Date::now()->valueOf()))
|
||||
->append('.sql')
|
||||
->__toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine wheter the dump should be public.
|
||||
*/
|
||||
protected function isPublic(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Admin\Dump;
|
||||
|
||||
use App\Concerns\Repositories\Admin\ReconcilesDumpRepositories;
|
||||
use App\Models\Discord\DiscordThread;
|
||||
use Illuminate\Support\Facades\Date;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DumpDiscordAction extends DumpAction
|
||||
{
|
||||
use ReconcilesDumpRepositories;
|
||||
|
||||
final public const string FILENAME_PREFIX = 'animethemes-db-dump-discord-';
|
||||
|
||||
/**
|
||||
* The list of tables to include in the dump.
|
||||
*/
|
||||
public static function allowedTables(): array
|
||||
{
|
||||
return [
|
||||
DiscordThread::TABLE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The temporary path for the database dump.
|
||||
* Note: The dumper library does not support writing to disk, so we have to write to the local filesystem first.
|
||||
* Pattern: "animethemes-db-dump-discord-{milliseconds from epoch}.sql".
|
||||
*/
|
||||
protected function getDumpFile(): string
|
||||
{
|
||||
$filesystem = Storage::disk('local');
|
||||
|
||||
return Str::of($filesystem->path(''))
|
||||
->append(DumpDiscordAction::FILENAME_PREFIX)
|
||||
->append(strval(Date::now()->valueOf()))
|
||||
->append('.sql')
|
||||
->__toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Admin\Dump;
|
||||
|
||||
use App\Concerns\Repositories\Admin\ReconcilesDumpRepositories;
|
||||
use App\Models\Document\Page;
|
||||
use App\Pivots\Document\PageRole;
|
||||
use Illuminate\Support\Facades\Date;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DumpDocumentAction extends DumpAction
|
||||
{
|
||||
use ReconcilesDumpRepositories;
|
||||
|
||||
final public const string FILENAME_PREFIX = 'animethemes-db-dump-document-';
|
||||
|
||||
/**
|
||||
* The list of tables to include in the dump.
|
||||
*/
|
||||
public static function allowedTables(): array
|
||||
{
|
||||
return [
|
||||
Page::TABLE,
|
||||
PageRole::TABLE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The temporary path for the database dump.
|
||||
* Note: The dumper library does not support writing to disk, so we have to write to the local filesystem first.
|
||||
* Pattern: "animethemes-db-dump-document-{milliseconds from epoch}.sql".
|
||||
*/
|
||||
protected function getDumpFile(): string
|
||||
{
|
||||
$filesystem = Storage::disk('local');
|
||||
|
||||
return Str::of($filesystem->path(''))
|
||||
->append(DumpDocumentAction::FILENAME_PREFIX)
|
||||
->append(strval(Date::now()->valueOf()))
|
||||
->append('.sql')
|
||||
->__toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Admin\Dump;
|
||||
|
||||
use App\Concerns\Repositories\Admin\ReconcilesDumpRepositories;
|
||||
use App\Models\List\External\ExternalEntry;
|
||||
use App\Models\List\ExternalProfile;
|
||||
use App\Models\List\Playlist;
|
||||
use App\Models\List\Playlist\PlaylistTrack;
|
||||
use Illuminate\Support\Facades\Date;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DumpListAction extends DumpAction
|
||||
{
|
||||
use ReconcilesDumpRepositories;
|
||||
|
||||
final public const string FILENAME_PREFIX = 'animethemes-db-dump-list-';
|
||||
|
||||
/**
|
||||
* The list of tables to include in the dump.
|
||||
*/
|
||||
public static function allowedTables(): array
|
||||
{
|
||||
return [
|
||||
ExternalEntry::TABLE,
|
||||
ExternalProfile::TABLE,
|
||||
PlaylistTrack::TABLE,
|
||||
Playlist::TABLE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The temporary path for the database dump.
|
||||
* Note: The dumper library does not support writing to disk, so we have to write to the local filesystem first.
|
||||
* Pattern: "animethemes-db-dump-list-{milliseconds from epoch}.sql".
|
||||
*/
|
||||
protected function getDumpFile(): string
|
||||
{
|
||||
$filesystem = Storage::disk('local');
|
||||
|
||||
return Str::of($filesystem->path(''))
|
||||
->append(DumpListAction::FILENAME_PREFIX)
|
||||
->append(strval(Date::now()->valueOf()))
|
||||
->append('.sql')
|
||||
->__toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Actions\Storage\Admin\Dump;
|
||||
|
||||
use App\Concerns\Repositories\Admin\ReconcilesDumpRepositories;
|
||||
use App\Models\Aggregate\LikeAggregate;
|
||||
use App\Models\User\Like;
|
||||
use App\Models\User\Notification;
|
||||
use App\Models\User\Submission;
|
||||
use Illuminate\Support\Facades\Date;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DumpUserAction extends DumpAction
|
||||
{
|
||||
use ReconcilesDumpRepositories;
|
||||
|
||||
final public const string FILENAME_PREFIX = 'animethemes-db-dump-user-';
|
||||
|
||||
/**
|
||||
* The list of tables to include in the dump.
|
||||
*/
|
||||
public static function allowedTables(): array
|
||||
{
|
||||
return [
|
||||
Like::TABLE,
|
||||
LikeAggregate::TABLE,
|
||||
Notification::TABLE,
|
||||
Submission::TABLE,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The temporary path for the database dump.
|
||||
* Note: The dumper library does not support writing to disk, so we have to write to the local filesystem first.
|
||||
* Pattern: "animethemes-db-dump-user-{milliseconds from epoch}.sql".
|
||||
*/
|
||||
protected function getDumpFile(): string
|
||||
{
|
||||
$filesystem = Storage::disk('local');
|
||||
|
||||
return Str::of($filesystem->path(''))
|
||||
->append(DumpUserAction::FILENAME_PREFIX)
|
||||
->append(strval(Date::now()->valueOf()))
|
||||
->append('.sql')
|
||||
->__toString();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user