Compare commits

...

434 commits
1.0.0 ... main

Author SHA1 Message Date
yggverse
d72bda1e71 remove deprecated info 2025-08-05 21:46:49 +03:00
yggverse
085e6174d9 update readme 2025-08-05 21:45:08 +03:00
yggverse
7bcc52fcae update readme 2025-08-05 21:44:31 +03:00
yggverse
5a1ada42e7 update readme 2025-08-05 21:42:17 +03:00
yggverse
ee9b7be6ac fix github markdown 2025-08-05 21:30:58 +03:00
yggverse
09811cd801 add reference to btracker project 2025-08-05 21:30:28 +03:00
yggverse
1281acea22 remove deprecated info 2025-08-05 21:27:26 +03:00
ghost
95addf0c48 add crawler 2024-02-01 16:59:14 +02:00
ghost
e780c5b4b5 fix description variables 2023-12-23 08:18:25 +02:00
ghost
6c775f822c fix torrent description filters 2023-12-23 07:56:54 +02:00
ghost
bff1962071 undefined variable 2023-12-10 22:15:59 +02:00
ghost
10181a04f1 hide header description #32 2023-12-10 01:55:49 +02:00
ghost
7eb02d06cc update version 2023-12-08 20:18:53 +02:00
ghost
2f4dbff90d add trim filters 2023-12-08 20:18:28 +02:00
ghost
3c6b1d6ab7 allow markdown from whitelist only 2023-12-08 20:06:47 +02:00
ghost
9d596de610 update version 2023-12-08 04:10:51 +02:00
ghost
6603790aba disable markdown as unsafe for remote content without additional filters implementation 2023-12-08 04:09:38 +02:00
ghost
fe608cff8f update version 2023-12-08 02:19:01 +02:00
ghost
9814a56135 composer update 2023-12-07 02:30:15 +02:00
ghost
27e598fded add rel/nofollow for wanted links 2023-12-03 02:22:53 +02:00
ghost
f5d4c19eb9 add new crawler 2023-12-03 02:11:08 +02:00
ghost
9081acebb3 update readme 2023-12-01 15:57:00 +02:00
ghost
3b832c94bc add rel="nofollow" to the action links 2023-11-19 23:05:47 +02:00
ghost
110976c619 add clickable links support for software info 2023-11-19 19:25:27 +02:00
ghost
2afaf2f618 fix links regex condition 2023-11-19 19:24:23 +02:00
ghost
8aff756e30 apply search filter for RSS #35 2023-11-18 09:27:33 +02:00
ghost
7177cdb4fe add torrent private info 2023-11-15 09:52:49 +02:00
ghost
3ce3dfe77b fix contributors list by integrate new features 2023-11-13 19:38:05 +02:00
ghost
5523034307 add search filters counter #35 2023-11-13 11:23:37 +02:00
ghost
e3503bc4bd add rel/nofollow to the filter link #35 2023-11-13 10:50:41 +02:00
ghost
002a41da87 add filter link #35 2023-11-13 10:37:16 +02:00
ghost
deb35d5013 add filter attributes support for pagination and tag links #35 2023-11-13 10:11:48 +02:00
ghost
1ae5d324c2 remove h2 tag from filter headers #35 2023-11-13 09:39:10 +02:00
ghost
5a0342a998 init extended search feature #35 2023-11-13 09:35:04 +02:00
ghost
7a1fa12271 fix locales sort order 2023-11-13 08:24:12 +02:00
ghost
77616c3c8a fix sensitive access condition and disable on direct request #37 2023-11-11 04:17:57 +02:00
ghost
b96ed08694 apply Bencode library update 2023-11-04 15:02:38 +02:00
ghost
ff565ac33b add categories feature translation #26 2023-11-04 13:51:41 +02:00
ghost
d1e9e72401 disallow toggleStar method for crawlers 2023-11-04 11:04:05 +02:00
ghost
178ab23031 update version 2023-11-04 09:58:16 +02:00
ghost
e1e3bfb2ce update json api #26 2023-11-04 09:57:42 +02:00
ghost
31aad63399 add category edition events support #26 2023-11-04 07:45:58 +02:00
ghost
2e9b119733 fix comma separator 2023-11-04 07:03:53 +02:00
ghost
0247e0e064 add new localization 2023-11-04 06:35:09 +02:00
ghost
701b448cd6 init torrent categories feature #26 2023-11-04 06:26:01 +02:00
ghost
35babed517 add missed locale 2023-11-04 03:30:32 +02:00
ghost
f8e0890966 update composer 2023-11-03 15:36:35 +02:00
ghost
261031dc50 unify syntax style 2023-11-03 15:36:24 +02:00
ghost
6dcbd6de40 make description links clickable #30 2023-11-03 15:35:46 +02:00
ghost
ad5b075878 add poster settings to the user profile page 2023-11-03 14:54:12 +02:00
ghost
7796aba342 append torrent filename to the manual wanted downloads 2023-11-03 14:06:15 +02:00
ghost
2e4129927d set app IDs in the filename postfix, remove lowercase 2023-11-03 13:59:50 +02:00
ghost
258d206f2e unify torrent filenames to prevent encoding issues on FTP connection 2023-11-03 13:44:49 +02:00
ghost
4f2879fdef set users approved by default 2023-11-02 19:15:01 +02:00
ghost
e788744a0f add configuration tip for upload_max_filesize 2023-11-02 01:38:55 +02:00
ghost
986f6678f8 add new locale 2023-11-01 23:23:43 +02:00
ghost
514b1ebc5d fix event message 2023-11-01 05:11:25 +02:00
ghost
3589d2eef4 set new users as sensitive by default 2023-10-31 23:05:48 +02:00
ghost
69309b9a98 update readme 2023-10-31 20:20:26 +02:00
ghost
5c76a17df5 implement poster position settings #18 2023-10-31 02:06:58 +02:00
ghost
cfeeabee72 fix posters event translation #18 2023-10-30 21:04:11 +02:00
ghost
cef76daa49 fix deleted event id info #18 2023-10-30 19:40:49 +02:00
ghost
446da9eb0b fix method data type #18 2023-10-30 19:32:58 +02:00
ghost
99ea797699 fix torrent poster update on last poster delete #18 2023-10-30 19:12:09 +02:00
ghost
8742c91f9f fix poster sub-directories initiation #18 2023-10-30 07:00:38 +02:00
ghost
ed3803df95 update version 2023-10-30 05:37:26 +02:00
ghost
1b8deb439b update strings translation #18 2023-10-30 05:36:01 +02:00
ghost
706ea40eec add poster event support #18 2023-10-30 05:14:46 +02:00
ghost
bd5191e894 implement torrent posters feature #18 2023-10-30 04:44:44 +02:00
ghost
8ae1b3f0b7 init torrent poster database #18 2023-10-29 20:00:27 +02:00
ghost
997666ab8e implement transliteration word forms in search #33 2023-10-28 00:03:42 +03:00
ghost
c7c5d7340c implement additional torrent fields index, add indexer configuration #31 2023-10-27 21:22:16 +03:00
ghost
afcacebe26 update version 2023-10-27 01:40:07 +03:00
ghost
989f2f3311 implement torrent statuses management #28 2023-10-27 01:36:50 +03:00
ghost
3cbc6ea90f add NL locale 2023-10-26 19:05:19 +03:00
ghost
dbc37da63d fix sensitive settings description 2023-10-26 18:37:48 +03:00
ghost
be248963e9 rollback commit 246fdda4fb causes error 2023-10-26 18:31:21 +03:00
ghost
246fdda4fb fix sensitive content visibility for publication owners 2023-10-26 06:57:10 +03:00
ghost
5c4e949269 update file tree view 2023-10-26 06:37:41 +03:00
ghost
0b77a5d171 add line break support for torrent comments 2023-10-26 05:52:08 +03:00
ghost
79886c0d77 fix request scrape update on torrent download 2023-10-26 04:09:42 +03:00
ghost
a47893ff9b request scrape update on torrent download 2023-10-26 04:02:35 +03:00
ghost
83ea09adad fix event margin 2023-10-26 03:58:51 +03:00
ghost
82f69c64a6 fix line break for wanted label 2023-10-26 02:32:11 +03:00
ghost
4969d33f57 fix long filename out of container width 2023-10-25 19:34:07 +03:00
ghost
1d6a7b03b2 fix long filename out of container width 2023-10-25 19:27:43 +03:00
ghost
1c9de9b275 fix activity template 2023-10-25 18:49:56 +03:00
ghost
6bc2c360a3 fix activity template 2023-10-25 18:48:31 +03:00
ghost
341c3a70a5 update readme 2023-10-25 02:16:22 +03:00
ghost
fa1f1f18c6 update readme 2023-10-24 04:59:36 +03:00
ghost
b4c6a98d49 fix announcement generation for wanted torrents #27 2023-10-24 01:48:26 +03:00
ghost
0d96cd3e3a update version 2023-10-24 01:13:48 +03:00
ghost
5ecf8461b4 update string translations, fix activity margin #27 2023-10-24 01:13:20 +03:00
ghost
a72413706d fix dependencies 2023-10-24 01:08:53 +03:00
ghost
dd04922cc0 update readme 2023-10-24 00:58:25 +03:00
ghost
24c58d4301 add wanted events support #27 2023-10-24 00:12:13 +03:00
ghost
5f4a14ebe2 add APP_TORRENT_WANTED_FTP_FOLDER setting #27 2023-10-23 23:37:15 +03:00
ghost
78a7134ced update readme 2023-10-23 23:24:52 +03:00
ghost
09bd7ecf34 update readme 2023-10-23 23:17:26 +03:00
ghost
7373f622e4 update readme 2023-10-23 23:14:11 +03:00
ghost
35bf1a8814 update readme 2023-10-23 23:10:18 +03:00
ghost
d40436db00 update readme 2023-10-23 23:09:21 +03:00
ghost
6bfd230915 add header description (bridge tip) #27 2023-10-23 22:26:00 +03:00
ghost
8190fc1914 implement FTP storage for wanted feature #27 2023-10-23 21:16:40 +03:00
ghost
c70205e204 remove duplicates from announcement list 2023-10-23 07:40:03 +03:00
ghost
a128cb7cb3 add wanted raw file with download feature 2023-10-23 07:31:07 +03:00
ghost
2d2c6be016 improve yggdrasil filters on torrent/magnet download 2023-10-23 07:07:43 +03:00
ghost
8affbe8644 update version 2023-10-20 02:08:59 +03:00
ghost
e6fac3d298 update readme 2023-10-20 01:58:15 +03:00
ghost
371dda9d31 update readme 2023-10-20 00:22:18 +03:00
ghost
5d8988719a add qBittorrent search plugin link 2023-10-20 00:12:20 +03:00
ghost
0b316734b8 fix warning notice 2023-10-20 00:12:15 +03:00
ghost
314320c554 add Semantic Versioning 2.0.0 notice 2023-10-19 05:14:06 +03:00
ghost
51ee02201a add magnet urn link #25 2023-10-19 04:58:29 +03:00
ghost
31bed20b4b update download routes #25 2023-10-19 04:41:19 +03:00
ghost
4c519a56ba add torrent / magnet download links, remove locale references #25 2023-10-19 04:36:12 +03:00
ghost
d794e48a54 implement torrents API #25 2023-10-19 03:53:32 +03:00
ghost
3a14f29b38 add search results RSS feed support 2023-10-18 22:25:49 +03:00
ghost
b9111213b2 fix comment 2023-10-18 19:55:50 +03:00
ghost
891868eccd implement sitemap 2023-10-18 19:54:57 +03:00
ghost
d3cdbc831c fix offset/limit 2023-10-18 19:41:08 +03:00
ghost
33e950bb42 add yggo crawler address 2023-10-18 18:01:27 +03:00
ghost
4a801fa809 disable activity log by crawler requests #24 2023-10-18 15:17:41 +03:00
ghost
2524a30476 add meta keywords support 2023-10-17 03:24:24 +03:00
ghost
306ccb6078 fix margins 2023-10-16 21:27:01 +03:00
ghost
1c869fd78e update link 2023-10-16 20:39:55 +03:00
ghost
29553c75c8 update readme 2023-10-16 20:35:02 +03:00
ghost
70ce180765 update translations 2023-10-16 18:03:16 +03:00
ghost
2acfc06ca6 add missed bittorrent protocol info on search page 2023-10-16 18:00:12 +03:00
ghost
4cafc51b67 remove opacity style from sensitive content 2023-10-16 17:56:06 +03:00
ghost
99eb0ddcb2 update translations 2023-10-16 04:18:14 +03:00
ghost
8e069c7997 update translation strings 2023-10-16 04:08:46 +03:00
ghost
8c48e3b40e update string translation 2023-10-16 04:04:59 +03:00
ghost
fd17185a9e fix activity event margins 2023-10-16 04:01:01 +03:00
ghost
5161becd60 pull new translations 2023-10-16 04:00:46 +03:00
ghost
9c0b10b283 update version 2023-10-16 02:55:50 +03:00
ghost
b7238eaf9f fix plural forms in ukrainian translation 2023-10-16 02:37:23 +03:00
ghost
453d70b7cb add cyrillic plural forms support 2023-10-16 02:30:55 +03:00
ghost
3005b16c94 update translation strings 2023-10-16 01:45:12 +03:00
ghost
1f51c56c0d fix time ago strings interpretation 2023-10-16 01:42:42 +03:00
ghost
8e204312e0 update menu position 2023-10-16 01:35:52 +03:00
ghost
1cd252b610 remove deprecated construction 2023-10-16 01:23:13 +03:00
ghost
bc67ae2198 require allowed locales in routing requests 2023-10-16 01:19:54 +03:00
ghost
7dfb133328 add localization badge 2023-10-16 01:08:45 +03:00
ghost
111fe6a7b5 add new strings translation 2023-10-16 01:06:18 +03:00
ghost
7587b38831 extract new strings 2023-10-16 00:42:38 +03:00
ghost
69a463f0b0 complete ukrainian localization 2023-10-16 00:37:11 +03:00
ghost
dfcd5c438e update 404 message string 2023-10-15 23:07:53 +03:00
ghost
9925d4f57c implement 404, 500 error pages #20 2023-10-15 23:02:28 +03:00
ghost
0923eb2234 fix contributors selection 2023-10-15 22:15:09 +03:00
ghost
6bda9c733c update localization strings 2023-10-15 21:42:50 +03:00
ghost
b0f7202c82 add ukrainian translation 2023-10-15 21:33:16 +03:00
ghost
4c521aef99 add bittorrent protocol version labels notice 2023-10-15 18:47:52 +03:00
ghost
8cf9905846 order tags by file size 2023-10-15 07:03:23 +03:00
ghost
6ab4dc87c2 add rel/nofollow attributes to the action links 2023-10-15 06:51:22 +03:00
ghost
8308ab28f9 order keywords by quantity matches in content 2023-10-15 06:37:22 +03:00
ghost
2682666b94 decrease listings padding 2023-10-15 05:47:38 +03:00
ghost
bd7fe4cf04 generate keywords based on content extensions 2023-10-15 05:39:32 +03:00
ghost
07262f4486 require app key on crontab/tool http requests 2023-10-15 04:30:19 +03:00
ghost
f5d8759dfe add filename to keywords index 2023-10-15 03:36:18 +03:00
ghost
5168280519 replace keywords match mode from OR to AND condition 2023-10-15 03:32:07 +03:00
ghost
d0096cbfe2 make search keywords case insensitive 2023-10-15 03:28:56 +03:00
ghost
a285aa3158 fix cyrillic tags highlight 2023-10-15 03:25:23 +03:00
ghost
06dd75eb1e fix cyrilic keywords extraction, add reindex all torrents toolkit 2023-10-15 03:21:55 +03:00
ghost
e5855d06b5 add /tool/torrent/reindex 2023-10-15 03:20:56 +03:00
d47081
6a3c810da7
Update README.md 2023-10-15 02:37:12 +03:00
ghost
be34befe96 fix errors 2023-10-14 22:01:10 +03:00
ghost
20dc122ab2 call urldecode on request provided only 2023-10-14 21:11:44 +03:00
ghost
68fbafaefa generate keywords on file parsed only 2023-10-14 20:50:48 +03:00
ghost
e97c4ec27f set log formatter by default 2023-10-14 20:16:34 +03:00
ghost
8b42df4041 write errors to file by default 2023-10-14 20:15:05 +03:00
ghost
60503becdf add info hash to torrent keywords index 2023-10-14 19:49:19 +03:00
ghost
f45bda4e58 update action paddings 2023-10-14 19:30:27 +03:00
ghost
ef25487854 fix search button highlight 2023-10-14 18:57:43 +03:00
ghost
d9ecec44c0 fix pagination 2023-10-14 18:41:23 +03:00
ghost
851efe0392 fix variable name 2023-10-14 17:59:08 +03:00
ghost
924137f09a fix session variables 2023-10-14 17:25:50 +03:00
ghost
60e8a57dce fix scrape wanted condition 2023-10-14 17:13:51 +03:00
ghost
e0410390aa update readme 2023-10-14 17:01:50 +03:00
ghost
f8de41ffab update readme 2023-10-14 07:48:39 +03:00
d47081
e21561c1d0
Update README.md 2023-10-14 07:46:36 +03:00
ghost
5fde6c2d1c update readme 2023-10-14 07:32:54 +03:00
ghost
a930ca4df7 update readme 2023-10-14 07:32:14 +03:00
ghost
4799b0dcd4 shift version 2023-10-14 07:31:04 +03:00
ghost
616ee54ad7 remove deprecated constructions 2023-10-14 07:21:10 +03:00
ghost
401c38ff6a add yggtracker prefix 2023-10-14 05:00:14 +03:00
ghost
94a86e5acc update readme 2023-10-14 04:59:30 +03:00
ghost
91c9415976 generate translations 2023-10-14 00:48:27 +03:00
ghost
92d715d303 update menu padding 2023-10-14 00:09:33 +03:00
ghost
643d643f46 implement recent uploads RSS 2023-10-13 23:52:37 +03:00
ghost
7c28eaadd2 update string 2023-10-13 23:31:35 +03:00
ghost
3ef090257a update rss links position 2023-10-13 23:30:14 +03:00
ghost
35b84546ff implement activity RSS feed 2023-10-13 23:06:20 +03:00
ghost
60a5593446 implement approved/sensitive access behavior 2023-10-13 18:28:27 +03:00
ghost
130add0904 fix torrent filter status 2023-10-13 16:01:17 +03:00
ghost
b1db78554c update settings form 2023-10-13 03:37:41 +03:00
ghost
daf8c63c51 update torrent forms 2023-10-13 03:32:40 +03:00
ghost
f8e7bd8c44 update announce trackers list 2023-10-13 03:12:24 +03:00
ghost
df6896f3e5 change activity button 2023-10-13 02:41:33 +03:00
ghost
ed6c4ea415 add torrent approved moderation tools 2023-10-13 02:28:50 +03:00
ghost
92c2a56bbf update strings 2023-10-13 00:28:54 +03:00
ghost
ffaae984d6 update string 2023-10-13 00:27:51 +03:00
ghost
4d231a799f remove commented code 2023-10-13 00:26:43 +03:00
ghost
ea9f7f1589 remove article drafts (feature moved to new releases) 2023-10-13 00:26:09 +03:00
ghost
da1e869be5 update torrent locales/sensitive values on user approve 2023-10-13 00:20:28 +03:00
ghost
e713c17333 update torrent features 2023-10-13 00:08:45 +03:00
ghost
d1f8c126b0 update margins 2023-10-12 04:56:44 +03:00
ghost
5e25b15de3 fix margin 2023-10-12 04:39:40 +03:00
ghost
038abfbb52 fix event text margins 2023-10-12 04:38:53 +03:00
ghost
bdb563dace hide duplicating data 2023-10-12 04:33:49 +03:00
ghost
c3414e132b update string translation 2023-10-12 04:32:15 +03:00
ghost
63dfeb9d4c update torrent page 2023-10-12 04:30:43 +03:00
ghost
311ecd0481 fix submit link 2023-10-12 03:38:05 +03:00
ghost
ab217b240a draft pagination 2023-10-12 03:18:57 +03:00
ghost
3aa7e5ff3c apply sensitive / locale filters for search results 2023-10-12 03:15:12 +03:00
ghost
d2cb66f51d remove page attribute from url sef routing 2023-10-12 02:53:01 +03:00
ghost
cf9b8de29f fix events grid 2023-10-12 02:44:58 +03:00
ghost
f56f07ac29 remove manual torrent/page relations 2023-10-12 02:33:43 +03:00
ghost
995d4bde54 implement events pagination 2023-10-12 02:19:57 +03:00
ghost
cc6c68957c add last user activity on profile page 2023-10-11 23:03:45 +03:00
ghost
9c9dc5b5a4 show events by user settings whitelist only 2023-10-11 22:45:08 +03:00
ghost
7aa2c03abc implement event settings 2023-10-11 22:34:47 +03:00
ghost
d4fbb4b592 fix form action action 2023-10-11 21:21:21 +03:00
ghost
c1c5c7fa59 make common initUser method 2023-10-11 17:01:08 +03:00
ghost
28f21d09c6 add user join event #4 2023-10-11 16:38:01 +03:00
ghost
0339ee9f23 add user moderation events #4 2023-10-11 04:04:02 +03:00
ghost
6ea0024b94 update container width 2023-10-11 03:34:30 +03:00
ghost
855929f592 add torrent download events #4 2023-10-11 01:42:19 +03:00
ghost
964dae97aa define torrent add event #4 2023-10-11 01:34:04 +03:00
ghost
46d5e25a5f update status codes #4 2023-10-11 01:17:02 +03:00
ghost
b1445ce541 update strings #4 2023-10-11 01:12:43 +03:00
ghost
a3dd5a81a9 add torrent star event support #4 2023-10-11 01:09:34 +03:00
ghost
ef84fefca3 init activity features #4 2023-10-10 23:42:45 +03:00
ghost
c47c8ad83b fix dependencies 2023-10-10 23:39:39 +03:00
ghost
cf204d09d0 fix last editions sort ordering 2023-10-10 22:58:30 +03:00
ghost
87f6661423 update user menu style 2023-10-10 17:42:59 +03:00
ghost
ac49397a1a fix strings case registry 2023-10-10 15:48:50 +03:00
ghost
1f7d8d0810 prevend duplicates upload by md5 file check #11 2023-10-10 15:21:16 +03:00
ghost
bd772f87d1 rename page to article 2023-10-10 14:43:55 +03:00
ghost
c747166a30 rename page to article 2023-10-10 14:43:44 +03:00
ghost
649838d4ee update paddings 2023-10-10 04:53:11 +03:00
ghost
4dcdd177ee update paddings 2023-10-10 04:52:08 +03:00
ghost
247003a366 update paddings 2023-10-10 04:37:41 +03:00
ghost
22df1dbbd0 update icons size 2023-10-10 04:29:37 +03:00
ghost
a5d5f95dce increase container width 2023-10-10 04:15:20 +03:00
ghost
f8969ba66e update padding 2023-10-10 04:11:57 +03:00
ghost
8ab4c0b9cf implement torrent search features 2023-10-10 04:07:59 +03:00
ghost
e9375f9127 update user menu style 2023-10-10 04:03:49 +03:00
ghost
1495378a4b argument pass optiomization 2023-10-09 23:41:59 +03:00
ghost
f88f1c63f2 add active search type selection 2023-10-09 23:41:29 +03:00
ghost
4b2b239b76 allow null value 2023-10-09 23:27:25 +03:00
ghost
c0f593ebec add search content type selector 2023-10-09 22:59:29 +03:00
ghost
b25cc173c5 remove torrent file limits 2023-10-09 22:58:55 +03:00
ghost
e591fe36a1 change form title 2023-10-09 22:25:31 +03:00
ghost
44927e2d07 add profile settings link 2023-10-09 21:05:30 +03:00
ghost
4f24ec22d2 make identicon clickable 2023-10-09 20:20:10 +03:00
ghost
285a5104e2 make common methods optimization 2023-10-09 20:04:52 +03:00
ghost
8d258c677b implement user moderation tools 2023-10-09 19:31:25 +03:00
ghost
42cef39589 uppercase theme name 2023-10-09 17:03:05 +03:00
ghost
c3ba8930f4 add alt locales view 2023-10-09 16:59:02 +03:00
ghost
b1679f3f65 add sensitive filter settings #17 2023-10-09 16:53:08 +03:00
ghost
6effb4cad2 fix methods position 2023-10-09 16:36:35 +03:00
ghost
5a940541ee implement theme settings #17 2023-10-09 16:33:07 +03:00
ghost
0ab282e1c4 hide header id 2023-10-09 16:10:11 +03:00
ghost
7b3ab7de7b fix user id route 2023-10-09 16:09:01 +03:00
ghost
0a218cfd3a implement user stars feature 2023-10-09 16:07:28 +03:00
ghost
d97a678952 update method name 2023-10-09 15:59:06 +03:00
ghost
f260bda7ee refactor bookmarks to stars 2023-10-09 15:35:32 +03:00
ghost
997d9db562 add torrent contributors info #11 2023-10-09 15:28:02 +03:00
ghost
3a7a87a89e update crontab task url 2023-10-09 15:17:22 +03:00
ghost
68fdc3c28c fix scrape totals, reset counters on zero results 2023-10-09 05:36:49 +03:00
ghost
1be80ca34e extract new localization strings 2023-10-09 05:17:20 +03:00
ghost
1b722812e8 implement torrent scrape queue 2023-10-09 05:09:18 +03:00
ghost
d2f7fff24e change method name 2023-10-09 02:27:54 +03:00
ghost
2f06c2a7e5 update method names 2023-10-09 02:03:05 +03:00
ghost
6752b7b93e update method names 2023-10-09 01:57:03 +03:00
ghost
ffa59d6f82 add publisher link #11 2023-10-09 01:49:50 +03:00
ghost
4a6dd85c60 update header style 2023-10-09 01:44:41 +03:00
ghost
dec2383a04 update torrent file name #11 2023-10-09 01:26:15 +03:00
ghost
69214e8058 fix single file torrents display #11 2023-10-09 01:19:20 +03:00
ghost
0609d5775f add scrape info #11 2023-10-09 01:14:41 +03:00
ghost
62ad522286 implement torrent/magnet download feature 2023-10-09 00:30:13 +03:00
ghost
5b0a7bcb69 implement bookmarks total counter 2023-10-08 20:37:50 +03:00
ghost
3d74818303 implement torrent bookmarks feature 2023-10-08 20:08:30 +03:00
ghost
a8b08bed06 update title tag 2023-10-08 16:28:18 +03:00
ghost
41b46b6604 fix moderation tip messages 2023-10-08 16:17:15 +03:00
ghost
b81d973331 implement torrent sensitive edit features #11 2023-10-08 16:02:38 +03:00
ghost
cbda078c38 allow users delete own content 2023-10-08 14:35:34 +03:00
ghost
d34ff8ecc6 implement locales moderation methods 2023-10-08 14:21:15 +03:00
ghost
fafca13b32 change meta title 2023-10-08 14:20:50 +03:00
ghost
8bdb115a26 show edit links always 2023-10-08 04:33:54 +03:00
ghost
9335b01ad6 fix returned data type 2023-10-08 04:27:42 +03:00
ghost
f919a7ed85 add torrent file validation 2023-10-08 04:25:38 +03:00
ghost
5aa20e6a22 draft torrent sensitive features 2023-10-08 03:30:57 +03:00
ghost
6fbce1678d update service methods 2023-10-08 03:15:38 +03:00
ghost
9af7117206 update torrent library 2023-10-08 02:05:56 +03:00
ghost
57085f8167 add approved fields 2023-10-08 02:05:14 +03:00
ghost
23b74a800a add userId/added fields 2023-10-08 01:44:01 +03:00
ghost
f0a38e1bf5 update class names 2023-10-08 01:20:15 +03:00
ghost
16696bc1d2 increase margin 2023-10-08 01:13:15 +03:00
ghost
bf08fa6191 implement torrent locales form #11 2023-10-08 01:03:27 +03:00
ghost
6031ce59a2 set initial user as approved 2023-10-08 01:01:18 +03:00
ghost
a0b59500fc set initial user as moderator 2023-10-07 23:31:12 +03:00
ghost
df253ba1b0 add format_ago filter, remove time service 2023-10-07 22:14:57 +03:00
ghost
e772955eb2 update readme 2023-10-07 04:44:19 +03:00
ghost
86e1455c6b replace bencode library to rhilip/bencode, fix files tree builder #11 2023-10-07 04:37:43 +03:00
ghost
387acb59b6 draft torrent info page with related features #11 2023-10-07 01:27:11 +03:00
ghost
21ffd8aa01 add default app trackers 2023-10-06 23:32:08 +03:00
ghost
ab2c310ec5 update font size 2023-10-06 16:42:11 +03:00
ghost
1c1e5b02c1 add torrents FS storage #11 2023-10-06 16:11:28 +03:00
ghost
6655fd907f add borders 2023-10-06 14:57:52 +03:00
ghost
3f083cb1b7 change input label 2023-10-06 04:27:37 +03:00
ghost
36b1bf25f4 change active options color 2023-10-06 04:27:21 +03:00
ghost
c0cc029350 update page form dependencies #19 2023-10-06 04:12:14 +03:00
ghost
8df29ef605 draft torrent submit form #11 2023-10-06 03:28:36 +03:00
ghost
b1fe274ae8 add paddings 2023-10-06 00:48:57 +03:00
ghost
edc78531d3 add new locales #19 2023-10-05 15:32:12 +03:00
ghost
eade8260d7 update translation #19 2023-10-05 15:04:55 +03:00
ghost
12e56bf6d1 update readme 2023-10-05 14:53:20 +03:00
ghost
f6e6a0f114 add Crowdin link #19 2023-10-05 14:51:39 +03:00
ghost
fe7e33f7ef update translation strings #19 2023-10-05 14:30:05 +03:00
ghost
731624d886 draft submit form #14 2023-10-05 00:15:00 +03:00
ghost
ffa568275f init user session on dashboard page 2023-10-05 00:12:48 +03:00
ghost
82a4860e0c remove translation link 2023-10-04 23:12:59 +03:00
ghost
1fffab732c remove address from profile info 2023-10-04 22:09:18 +03:00
ghost
2d6fa05081 increase textarea size 2023-10-04 20:50:02 +03:00
ghost
5f5da0ded4 add app version to css 2023-10-04 20:49:52 +03:00
ghost
b9a2804132 remove deprecated methods 2023-10-04 19:22:59 +03:00
ghost
d73086231f draft activity feature #4 2023-10-04 19:20:34 +03:00
ghost
8ee25d3a30 move dashboard relations to the user controller 2023-10-04 19:17:43 +03:00
ghost
02e56e4d08 decrease container width 2023-10-04 19:15:33 +03:00
ghost
8680718714 add more languages, init crowdin driver #19 2023-10-04 19:15:17 +03:00
ghost
fbeb793f6d move containers inside the main_content block 2023-10-04 19:02:25 +03:00
ghost
36329b0b81 update paddings 2023-10-04 17:47:36 +03:00
ghost
8eb5a05f29 move container inside the main_content block 2023-10-04 17:44:56 +03:00
ghost
0aa38c16dd add identicon alt text translation #19 2023-10-04 17:19:54 +03:00
ghost
af72fa14c4 remove row paddings by default 2023-10-04 17:09:15 +03:00
ghost
4b1b80b875 add dev project initiation example 2023-10-04 15:52:43 +03:00
ghost
90f28cfd8b add missed locales whitelist validation #19 2023-10-04 15:35:54 +03:00
ghost
4852882243 add more locales #19 2023-10-03 23:58:43 +03:00
ghost
b1e5d524ac init alt translation dump #19 2023-10-03 23:40:49 +03:00
ghost
0224fbf23a integrate twig/intl-extra to make locale codes translation #19 2023-10-03 23:30:50 +03:00
ghost
7750a214e0 add crowdin link #19 2023-10-03 23:10:29 +03:00
ghost
b863d145a0 remove demo templates 2023-10-03 23:00:10 +03:00
ghost
9fcc5a451b make redirect to user locale selected #19 2023-10-03 22:52:26 +03:00
ghost
f85414fd2b rename method 2023-10-03 22:42:35 +03:00
ghost
6b2e67f04b redirect to env default language on empty _lang request #19 2023-10-03 22:41:42 +03:00
ghost
ae8ec4823a implement profile page #17 2023-10-03 22:35:13 +03:00
ghost
737b79b608 fix static call 2023-10-03 17:24:34 +03:00
ghost
8d1c35360b init locale settings #19 2023-10-03 17:02:00 +03:00
ghost
4720b34e9c remove center alignment 2023-10-03 00:30:35 +03:00
ghost
89ac72b77d implement time service 2023-10-03 00:25:48 +03:00
ghost
06e0694739 update readme 2023-10-02 16:15:51 +03:00
ghost
380377b27c init symfony framework #14 2023-10-02 16:13:55 +03:00
ghost
3c233fcfad remove deprecated model 2023-09-29 20:03:09 +03:00
ghost
7d7629488a add sef configuration example 2023-09-29 20:02:56 +03:00
ghost
ca50f85626 separate url settings to sheme/host/port/path parts 2023-09-29 20:01:34 +03:00
ghost
cfc9c721ff fix variable names 2023-09-29 17:34:30 +03:00
ghost
7f892b0772 add value definition support 2023-09-29 16:15:42 +03:00
ghost
3a4e498c34 add server environment support 2023-09-29 16:13:41 +03:00
ghost
1f6c439c92 update error message 2023-09-29 14:42:11 +03:00
ghost
e6293f90b2 add user settings fields 2023-09-27 15:41:05 +03:00
ghost
2eb0a30ed2 update profile module 2023-09-27 15:40:49 +03:00
ghost
f51ad8d5de add theme settings #17 2023-09-27 14:20:51 +03:00
ghost
a4dae88575 remove user account type selection features #14 2023-09-27 13:05:30 +03:00
ghost
1dba3a4126 draft page form progress 2023-09-27 00:44:11 +03:00
ghost
1c79b7c0b6 fix variable names 2023-09-26 22:57:49 +03:00
ghost
aa4d2821b1 add pageId field 2023-09-26 22:53:11 +03:00
ghost
d3e978dbde add new methods 2023-09-26 22:52:49 +03:00
ghost
d25634299e add data types 2023-09-26 22:41:41 +03:00
ghost
44e37cfbe6 fix variable name 2023-09-26 22:41:16 +03:00
ghost
00c39f49c1 add AppModelRequest dependency 2023-09-26 22:40:31 +03:00
ghost
451df36fc2 add default locale settings 2023-09-26 22:40:17 +03:00
ghost
f39d3d8ea4 update AppModelLocale 2023-09-26 22:39:55 +03:00
ghost
0227f0c9ec implement request model 2023-09-26 22:33:03 +03:00
ghost
2279e0d44e add locale methods 2023-09-26 17:25:02 +03:00
ghost
4a1f06fd82 add text methods 2023-09-26 14:46:21 +03:00
ghost
f1086bfbc9 rename data table to text 2023-09-26 14:45:21 +03:00
ghost
c05abad02f update user profile module dependencies 2023-09-26 04:49:32 +03:00
ghost
86587db9e9 remove deprecated construction 2023-09-26 04:21:25 +03:00
ghost
623375484e update dependencies 2023-09-26 04:21:02 +03:00
ghost
246944e74e draft page controller 2023-09-26 04:11:46 +03:00
ghost
35ced66093 update url 2023-09-26 04:11:34 +03:00
ghost
e5a0ac92cc draft submit form 2023-09-26 04:11:22 +03:00
ghost
e560c38c1f add page methods, add user.approved field 2023-09-26 04:11:04 +03:00
ghost
3dd619baa7 remove deprecated file 2023-09-26 04:10:29 +03:00
ghost
fe48055f82 add localeKeyExists method #14 2023-09-26 04:07:01 +03:00
ghost
d955446b99 add getDefaultUserStatus method #14 2023-09-26 03:54:38 +03:00
ghost
0672f0349e add default user status settings #14 2023-09-26 03:52:36 +03:00
ghost
0065ef476c update bootstrap #14 2023-09-26 03:42:54 +03:00
ghost
5e31e0e972 init locale model #14 2023-09-26 03:41:55 +03:00
ghost
b269e24561 init session model #14 2023-09-26 03:41:45 +03:00
ghost
f1fedfbbcb implement website model #14 2023-09-26 03:03:32 +03:00
ghost
982f532841 add user.status field #14 2023-09-26 02:42:12 +03:00
ghost
aa0be166d1 relate magnet KT value with data table #14 2023-09-26 00:24:28 +03:00
ghost
da89328ce5 fix field names #14 2023-09-26 00:19:51 +03:00
ghost
1233527e49 update db model #14 2023-09-26 00:14:53 +03:00
ghost
c05eae8c9c delete page preview field #14 2023-09-25 16:42:10 +03:00
ghost
bbd54b3a14 add content locale filter #14 2023-09-25 16:02:25 +03:00
ghost
0c26c0ac9b delete deprecated template file 2023-09-25 15:49:43 +03:00
ghost
f9eb917149 fix link uri 2023-09-25 15:38:41 +03:00
ghost
8b8eb74835 update dependencies 2023-09-25 15:38:04 +03:00
ghost
0d840a5ab5 add content language selection #14 2023-09-25 01:30:28 +03:00
ghost
d77ad74d32 add locales registry #14 2023-09-25 01:08:42 +03:00
ghost
5a3ac70fd2 update bootstrap 2023-09-25 01:01:48 +03:00
ghost
d949474737 force object return 2023-09-25 00:35:16 +03:00
ghost
9aaa5d5989 update config dependencies to json 2023-09-25 00:32:41 +03:00
ghost
3a858648de add settings 2023-09-25 00:32:04 +03:00
ghost
556a135b59 add host rules 2023-09-25 00:31:49 +03:00
ghost
e4d1215a53 remove deprecated controller 2023-09-24 23:49:01 +03:00
ghost
32c1bbe4a2 rewrite config files to JSON, refactor environment bootstrap #14 2023-09-24 23:18:51 +03:00
ghost
e1054cd69e add Environment library 2023-09-24 23:05:50 +03:00
ghost
6b112d441c draft images feature #14 2023-09-24 18:56:21 +03:00
ghost
947e359976 delete deprecated components #14 2023-09-24 18:07:40 +03:00
ghost
eebfefbb3d fix padding 2023-09-24 18:07:03 +03:00
ghost
1512bfe3c8 change icon size 2023-09-24 18:06:34 +03:00
ghost
f3b32a713f update form validation 2023-09-24 17:26:10 +03:00
ghost
d8f6b6d27e add required fields configuration, draft post request processing #14 2023-09-24 16:36:43 +03:00
ghost
762d72e913 change menu order #15 2023-09-24 15:59:01 +03:00
ghost
31b8a0a5fb draft submit form #14 2023-09-24 15:57:08 +03:00
ghost
4697519663 change search form placeholder #14 2023-09-24 15:54:38 +03:00
ghost
9b0bfcb76c move validator to model, init separated config file in JSON format #14 2023-09-24 15:25:44 +03:00
ghost
919bc0a66c change block size 2023-09-24 02:25:36 +03:00
ghost
8740e7a63f add user avatar #14 #15 2023-09-24 00:18:16 +03:00
ghost
a600a08a28 init MVC framework refactory #14 2023-09-23 21:37:52 +03:00
ghost
c4f5409ffa rename methods #15 2023-09-23 21:31:43 +03:00
ghost
cbf0902677 add edit history menu item #15 2023-09-23 20:06:59 +03:00
ghost
41a557cbb6 implement profile module #14 #15 2023-09-23 19:34:49 +03:00
ghost
a9f67243ba change css version #14 2023-09-23 17:24:56 +03:00
ghost
67921a0d3e create separated search page #14 2023-09-21 15:37:10 +03:00
ghost
fe2a7a575f add API version check #13, #14 2023-09-21 15:22:02 +03:00
ghost
3507abc35f update nodes description 2023-09-21 15:03:10 +03:00
ghost
34ae82532f draft v.2 database model #14 2023-09-21 01:19:56 +03:00
ghost
2add05557d add link of traffic-oriented public peers 2023-09-20 19:20:19 +03:00
216 changed files with 42977 additions and 13170 deletions

126
.env Normal file
View file

@ -0,0 +1,126 @@
# In all environments, the following files are loaded if they exist,
# the latter taking precedence over the former:
#
# * .env contains default values for the environment variables needed by the app
# * .env.local uncommitted file with local overrides
# * .env.$APP_ENV committed environment-specific defaults
# * .env.$APP_ENV.local uncommitted environment-specific overrides
#
# Real environment variables win over .env files.
#
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
# https://symfony.com/doc/current/configuration/secrets.html
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=EDITME
APP_KEY=EDITME
###< symfony/framework-bundle ###
###> doctrine/doctrine-bundle ###
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
#
DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
# DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=15&charset=utf8"
###< doctrine/doctrine-bundle ###
###> symfony/messenger ###
# Choose one of the transports below
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
###< symfony/messenger ###
###> symfony/mailer ###
# MAILER_DSN=null://null
###< symfony/mailer ###
###> symfony/crowdin-translation-provider ###
# CROWDIN_DSN=crowdin://PROJECT_ID:API_TOKEN@ORGANIZATION_DOMAIN.default
###< symfony/crowdin-translation-provider ###
# YGGtracker
# Application version, used for API and media cache
APP_VERSION=2.6.2
# Application name
APP_NAME=YGGtracker
# Default locale
APP_LOCALE=en
# Supported locales for interface and content filters
APP_LOCALES=en|cs|nl|eo|fr|ja|ka|de|he|it|lv|pl|pt|ru|es|uk
# Content categories, lowercase, enabled by default for new users
# src/Twig/AppExtension.php:transCategory
APP_CATEGORIES=movie|series|tv|animation|music|game|audiobook|podcast|book|archive|picture|software|other
# Items per page on pagination
APP_PAGINATION=10
# Default application theme
APP_THEME=default
# Additional themes, stored in /src/templates, /public/asset
APP_THEMES=default
# Default sensitive status for new users
APP_SENSITIVE=1
# Default approved status for new users
APP_APPROVED=1
# Default Yggdrasil filters status for new users
APP_YGGDRASIL=1
# Default posters status for new users
APP_POSTERS=1
# Build-in trackers append to downloads
APP_TRACKERS=http://[201:23b4:991a:634d:8359:4521:5576:15b7]:2023/announce|http://[200:1e2f:e608:eb3a:2bf:1e62:87ba:e2f7]/announce|http://[316:c51a:62a3:8b9::5]/announce
# List of crawlers where ignored in actions and activity features
APP_CRAWLERS=201:23b4:991a:634d:8359:4521:5576:15b7|30a:5fad::e|202:f2bc:f800:7cc4:c109:7857:5cae:6630|200:1554:e730:4030:605b:47be:6fb6:7b11
# Max torrent filesize for uploads (check upload_max_filesize in the php.ini)
APP_TORRENT_FILE_SIZE_MAX=1024000
# Max torrent poster filesize for uploads (check upload_max_filesize in the php.ini)
APP_TORRENT_POSTER_FILE_SIZE_MAX=10240000
# Store wanted torrent files in /app/var/ftp by /app/crontab/torrent/scrape/{key}
APP_TORRENT_WANTED_FTP_ENABLED=1
APP_TORRENT_WANTED_FTP_FOLDER=/yggtracker
APP_TORRENT_WANTED_FTP_APPROVED_ONLY=1
# Enable search index for torrent name
APP_INDEX_TORRENT_NAME_ENABLED=1
# Enable search index for torrent info hash v1
APP_INDEX_TORRENT_HASH_V1_ENABLED=1
# Enable search index for torrent info hash v2
APP_INDEX_TORRENT_HASH_V2_ENABLED=1
# Enable search index for torrent filenames
APP_INDEX_TORRENT_FILENAMES_ENABLED=1
# Enable search index for torrent source
APP_INDEX_TORRENT_SOURCE_ENABLED=1
# Enable search index for torrent comment
APP_INDEX_TORRENT_COMMENT_ENABLED=1
# Enable search index for words length greater than N chars
APP_INDEX_WORD_LENGTH_MIN=3
# Enable search index for words length not greater than N chars
APP_INDEX_WORD_LENGTH_MAX=255

6
.env.test Normal file
View file

@ -0,0 +1,6 @@
# define your env variables for the test env here
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
SYMFONY_DEPRECATIONS_HELPER=999999
PANTHER_APP_ENV=panther
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots

33
.gitignore vendored
View file

@ -1,21 +1,22 @@
/.vscode/
###> symfony/framework-bundle ###
/.env.local
/.env.local.php
/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
/public/bundles/
/var/
/vendor/ /vendor/
###< symfony/framework-bundle ###
/database/yggtracker.mwb.bak ###> phpunit/phpunit ###
/phpunit.xml
.phpunit.result.cache
###< phpunit/phpunit ###
/src/public/api/*.json ###> symfony/phpunit-bridge ###
.phpunit.result.cache
/phpunit.xml
###< symfony/phpunit-bridge ###
/src/config/* .vscode
!/src/config/bootstrap.json
!/src/config/nodes.json
!/src/config/trackers.json
!/src/config/peers.json
/src/public/sitemap.xml
/src/storage/log/*.log
/composer.lock
*test*

218
README.md
View file

@ -1,55 +1,76 @@
# YGGtracker # YGGtracker
Distributed BitTorrent Registry for Yggdrasil > [!NOTE]
> Take a look at [βtracker](https://github.com/yggverse/btracker) - the modern aggregation alternative written in Rust!
YGGtracker uses [Yggdrasil](https://github.com/yggdrasil-network/yggdrasil-go) IPv6 addresses to identify users without registration. A social-oriented BitTorrent catalog for the [Yggdrasil](https://github.com/yggdrasil-network) network, written in the Symfony framework.
#### Nodes online YGGtracker is a manually operated catalog and social network that allows users to share their torrents in the local network. Engine uses IPv6 `0200::/7` addresses to identify users without registration.
YGGtracker is distributed index engine, default nodes list defined in [nodes.json](https://github.com/YGGverse/YGGtracker/blob/main/src/config/nodes.json) #### [Showcase](https://github.com/YGGverse/YGGtracker/wiki/Showcase)
If you have launched new one, feel free to participate by PR. ![Pasted image 1](https://github.com/YGGverse/YGGtracker/assets/108541346/962f7850-01e1-4add-9dbe-c11b80108a75)
#### Trackers
Open trackers defined in [trackers.json](https://github.com/YGGverse/YGGtracker/blob/main/src/config/trackers.json)
* Application appends initial trackers to all download links and magnet forms
* Trackers not in list will be cropped by the application filter
* Feel free to PR new yggdrasil tracker!
#### Requirements
```
php8^
php-pdo
php-mysql
php-curl
php-memcached
sphinxsearch
memcached
```
#### Installation #### Installation
```
symfony check:requirements
```
##### Production ##### Production
* `composer create-project yggverse/yggtracker` Install stable release
```
composer create-project yggverse/yggtracker
```
##### Development ##### Development
* `git clone https://github.com/YGGverse/YGGtracker.git` Latest codebase available in repository
* `cd YGGtracker`
* `composer update`
#### Setup ```
* Server configuration `/example/environment` git clone https://github.com/YGGverse/YGGtracker.git
* The web root dir is `/src/public` cd YGGtracker
* Deploy the database using [MySQL Workbench](https://www.mysql.com/products/workbench) project presented in the `/database` folder composer update
* Install [Sphinx Search Server](https://sphinxsearch.com) symfony server:start
* Configuration examples presented at `/example/environment` folder. On first app launch, configuration file will be auto-generated in `/src/config` ```
* Make sure `/src/api` folder writable
#### Contribute ##### Database
New installation
```
php bin/console doctrine:schema:update --force
```
Existing DB upgrade
```
php bin/console doctrine:migrations:migrate
```
##### Crontab
* `* * * * * /crontab/torrent/scrape/{%app.key%}` - update seeding stats
##### FTP
Setup anonymous read-only access to `/var/ftp` catalog ([read more](https://github.com/YGGverse/YGGtracker/wiki/Features#the-wanted))
##### App settings
Custom settings could be provided in the `/.env.local` file by overwriting default `/.env` values
#### Localization
[![Crowdin](https://badges.crowdin.net/yggtracker/localized.svg)](https://crowdin.com/project/yggtracker)
#### API
[Wiki reference](https://github.com/YGGverse/YGGtracker/wiki/API)
#### Contribution
Please make new branch for each PR Please make new branch for each PR
@ -58,127 +79,38 @@ git checkout main
git checkout -b my-pr-branch-name git checkout -b my-pr-branch-name
``` ```
#### Roadmap
* [ ] BitTorrent protocol
+ [ ] Protocol
+ [ ] announce
+ [ ] announce-list
+ [ ] comment
+ [ ] created by
+ [ ] creation date
+ [ ] info
+ [ ] file-duration
+ [ ] file-media
+ [ ] files
+ [ ] name
+ [ ] piece length
+ [ ] pieces
+ [ ] private
+ [ ] profiles
* [ ] Magnet protocol
+ [x] Exact Topic / xt
+ [x] Display Name / dn
+ [x] eXact Length / xl
+ [x] Address Tracker / rt
+ [x] Web Seed / ws
+ [x] Acceptable Source / as
+ [x] eXact Source / xs
+ [x] Keyword Topic / kt
+ [ ] Manifest Topic / mt
+ [ ] Select Only / so
+ [ ] PEer / x.pe
* [ ] Catalog
+ [x] Public levels
+ [x] Sensitive filter
+ [x] Comments
+ [x] Scrape trackers
+ [x] Peers
+ [x] Completed
+ [x] Leechers
+ [x] Stars
+ [x] Views
+ [x] Downloads
+ [x] Wanted
+ [x] Threading comments
+ [ ] Forks
* [ ] Profile
+ [ ] Listing
+ [ ] Uploads
+ [ ] Downloads
+ [ ] Stars
+ [ ] Following
+ [ ] Followers
+ [ ] Comments
+ [ ] Settings
+ [ ] Public name
+ [ ] Downloads customization
+ [ ] Address Tracker
+ [ ] Web Seed
+ [ ] Acceptable Source
+ [ ] eXact Source
+ [ ] Content filters
* [x] API
+ [x] Active (push)
+ [x] Magnet
+ [x] Edit
+ [x] Download
+ [x] Comment
+ [x] Star
+ [x] View
+ [x] Passive (feed)
+ [x] Manifest
+ [x] Users
+ [x] Magnets
+ [x] Downloads
+ [x] Comments
+ [x] Stars
+ [x] Views
* [x] Export
+ [x] Sitemap
+ [x] RSS
+ [x] Magnets
+ [x] Comments
* [x] Other
+ [x] Moderation
+ [x] UI
+ [ ] CLI
+ [ ] Installation tools
#### Donate to contributors
* @d47081:
+ ![wakatime](https://wakatime.com/badge/user/0b7fe6c1-b091-4c98-b930-75cfee17c7a5/project/059ec567-2c65-4c65-a48e-51dcc366f1a0.svg)
+ [BTC](https://www.blockchain.com/explorer/addresses/btc/bc1qngdf2kwty6djjqpk0ynkpq9wmlrmtm7e0c534y) | [LTC](https://live.blockcypher.com/ltc/address/LUSiqzKsfB1vBLvpu515DZktG9ioKqLyj7) | [XMR](835gSR1Uvka19gnWPkU2pyRozZugRZSPHDuFL6YajaAqjEtMwSPr4jafM8idRuBWo7AWD3pwFQSYRMRW9XezqrK4BEXBgXE) | [ZEPH](ZEPHsADHXqnhfWhXrRcXnyBQMucE3NM7Ng5ZVB99XwA38PTnbjLKpCwcQVgoie8EJuWozKgBiTmDFW4iY7fNEgSEWyAy4dotqtX)
+ Support our server by order [Linux VPS](https://www.yourserver.se/portal/aff.php?aff=610)
+ Inspiration by [SomaFM Deep Space One](https://somafm.com/deepspaceone/)
#### License #### License
* Engine sources [MIT License](https://github.com/YGGverse/YGGtracker/blob/main/LICENSE) * Engine sources [MIT License](https://github.com/YGGverse/YGGtracker/blob/main/LICENSE)
#### Versioning
[Semantic Versioning 2.0.0](https://semver.org/#semantic-versioning-200)
#### Components #### Components
* [Symfony Framework](https://symfony.com)
* [SVG icons](https://icons.getbootstrap.com) * [SVG icons](https://icons.getbootstrap.com)
* [PHP Scrapper](https://github.com/medariox/scrapeer) * [Scrapper](https://github.com/medariox/scrapeer) / [Composer Edition](https://github.com/YGGverse/scrapeer)
* [Bencode](https://github.com/Rhilip/Bencode)
* [Transliteration](https://github.com/ashtokalo/php-translit)
* [Identicons](https://github.com/dmester/jdenticon-php) * [Identicons](https://github.com/dmester/jdenticon-php)
#### Feedback #### Support
[https://github.com/YGGverse/YGGtracker/issues](https://github.com/YGGverse/YGGtracker/issues) * [Issues](https://github.com/YGGverse/YGGtracker/issues)
* [Documentation](https://github.com/YGGverse/YGGtracker/wiki)
* [HowTo Yggdrasil](https://ygg.work.gd/yggdrasil:bittorrent:yggtracker)
#### Community #### Blog
* [Mastodon](https://mastodon.social/@YGGverse) * [Mastodon](https://mastodon.social/@YGGverse)
#### Integrations
* [YGGtracker Search Plugin for qBittorrent](https://github.com/YGGverse/qbittorrent-yggtracker-search-plugin)
* [Crontab script that allows to receive wanted torrents from multiple YGGtracker nodes](https://github.com/YGGverse/yggtracker-wanted-torrents-receiver)
#### See also #### See also
* [YGGo - YGGo! Distributed Web Search Engine ](https://github.com/YGGverse/YGGo) * [YGGo - YGGo! Distributed Web Search Engine ](https://github.com/YGGverse/YGGo)

17
bin/console Executable file
View file

@ -0,0 +1,17 @@
#!/usr/bin/env php
<?php
use App\Kernel;
use Symfony\Bundle\FrameworkBundle\Console\Application;
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
}
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
return new Application($kernel);
};

19
bin/phpunit Executable file
View file

@ -0,0 +1,19 @@
#!/usr/bin/env php
<?php
if (!ini_get('date.timezone')) {
ini_set('date.timezone', 'UTC');
}
if (is_file(dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit')) {
define('PHPUNIT_COMPOSER_INSTALL', dirname(__DIR__).'/vendor/autoload.php');
require PHPUNIT_COMPOSER_INSTALL;
PHPUnit\TextUI\Command::main();
} else {
if (!is_file(dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php')) {
echo "Unable to find the `simple-phpunit.php` script in `vendor/symfony/phpunit-bridge/bin/`.\n";
exit(1);
}
require dirname(__DIR__).'/vendor/symfony/phpunit-bridge/bin/simple-phpunit.php';
}

View file

@ -1,24 +1,114 @@
{ {
"name": "yggverse/yggtracker", "name": "yggverse/yggtracker",
"description": "Public BitTorrent tracker for Yggdrasil network", "description": "BitTorrent tracker for Yggdrasil network",
"type": "project", "type": "project",
"require": {
"php": "^8.1",
"yggverse/cache": ">=0.3.0",
"yggverse/parser": ">=0.4.0",
"jdenticon/jdenticon": "^1.0",
"christeredvartsen/php-bittorrent": "^2.0"
},
"license": "MIT", "license": "MIT",
"minimum-stability": "stable",
"prefer-stable": true,
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"ashtokalo/php-translit": "^0.2.0",
"doctrine/annotations": "^2.0",
"doctrine/doctrine-bundle": "^2.10",
"doctrine/doctrine-migrations-bundle": "^3.2",
"doctrine/orm": "^2.16",
"jdenticon/jdenticon": "^1.0",
"league/commonmark": "^2.4",
"phpdocumentor/reflection-docblock": "^5.3",
"phpstan/phpdoc-parser": "^1.24",
"rhilip/bencode": "^2.3",
"symfony/asset": "6.3.*",
"symfony/console": "6.3.*",
"symfony/crowdin-translation-provider": "6.3.*",
"symfony/doctrine-messenger": "6.3.*",
"symfony/dotenv": "6.3.*",
"symfony/expression-language": "6.3.*",
"symfony/flex": "^2",
"symfony/form": "6.3.*",
"symfony/framework-bundle": "6.3.*",
"symfony/http-client": "6.3.*",
"symfony/intl": "6.3.*",
"symfony/mailer": "6.3.*",
"symfony/mime": "6.3.*",
"symfony/monolog-bundle": "^3.0",
"symfony/notifier": "6.3.*",
"symfony/process": "6.3.*",
"symfony/property-access": "6.3.*",
"symfony/property-info": "6.3.*",
"symfony/runtime": "6.3.*",
"symfony/security-bundle": "6.3.*",
"symfony/serializer": "6.3.*",
"symfony/string": "6.3.*",
"symfony/translation": "6.3.*",
"symfony/twig-bundle": "6.3.*",
"symfony/validator": "6.3.*",
"symfony/web-link": "6.3.*",
"symfony/yaml": "6.3.*",
"twig/extra-bundle": "^3.7",
"twig/intl-extra": "^3.7",
"twig/markdown-extra": "^3.7",
"twig/string-extra": "^3.7",
"twig/twig": "^2.12|^3.0",
"yggverse/scrapeer": "^0.5.4"
},
"config": {
"allow-plugins": {
"php-http/discovery": true,
"symfony/flex": true,
"symfony/runtime": true
},
"sort-packages": true
},
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Yggverse\\Yggtracker\\": "src/" "App\\": "src/"
} }
}, },
"authors": [ "autoload-dev": {
{ "psr-4": {
"name": "YGGverse" "App\\Tests\\": "tests/"
} }
},
"replace": {
"symfony/polyfill-ctype": "*",
"symfony/polyfill-iconv": "*",
"symfony/polyfill-php72": "*",
"symfony/polyfill-php73": "*",
"symfony/polyfill-php74": "*",
"symfony/polyfill-php80": "*",
"symfony/polyfill-php81": "*"
},
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"
], ],
"minimum-stability": "alpha" "post-update-cmd": [
"@auto-scripts"
]
},
"conflict": {
"symfony/symfony": "*"
},
"extra": {
"symfony": {
"allow-contrib": false,
"require": "6.3.*"
}
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"symfony/browser-kit": "6.3.*",
"symfony/css-selector": "6.3.*",
"symfony/debug-bundle": "6.3.*",
"symfony/maker-bundle": "^1.0",
"symfony/phpunit-bridge": "^6.3",
"symfony/stopwatch": "6.3.*",
"symfony/web-profiler-bundle": "6.3.*"
}
} }

10456
composer.lock generated Normal file

File diff suppressed because it is too large Load diff

14
config/bundles.php Normal file
View file

@ -0,0 +1,14 @@
<?php
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
];

View file

@ -0,0 +1,19 @@
framework:
cache:
# Unique name of your app: used to compute stable namespaces for cache keys.
#prefix_seed: your_vendor_name/app_name
# The "app" cache stores to the filesystem by default.
# The data in this cache should persist between deploys.
# Other options include:
# Redis
#app: cache.adapter.redis
#default_redis_provider: redis://localhost
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
#app: cache.adapter.apcu
# Namespaced pools use the above "app" backend by default
#pools:
#my.dedicated.cache: null

View file

@ -0,0 +1,5 @@
when@dev:
debug:
# Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
# See the "server:dump" command to start a new server.
dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"

View file

@ -0,0 +1,48 @@
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
# IMPORTANT: You MUST configure your server version,
# either here or in the DATABASE_URL env var (see .env file)
#server_version: '15'
profiling_collect_backtrace: '%kernel.debug%'
orm:
auto_generate_proxy_classes: true
enable_lazy_ghost_objects: true
report_fields_where_declared: true
validate_xml_mapping: true
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
auto_mapping: true
mappings:
App:
is_bundle: false
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
when@test:
doctrine:
dbal:
# "TEST_TOKEN" is typically set by ParaTest
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
when@prod:
doctrine:
orm:
auto_generate_proxy_classes: false
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
query_cache_driver:
type: pool
pool: doctrine.system_cache_pool
result_cache_driver:
type: pool
pool: doctrine.result_cache_pool
framework:
cache:
pools:
doctrine.result_cache_pool:
adapter: cache.app
doctrine.system_cache_pool:
adapter: cache.system

View file

@ -0,0 +1,6 @@
doctrine_migrations:
migrations_paths:
# namespace is arbitrary but should be different from App\Migrations
# as migrations classes should NOT be autoloaded
'DoctrineMigrations': '%kernel.project_dir%/migrations'
enable_profiler: false

View file

@ -0,0 +1,25 @@
# see https://symfony.com/doc/current/reference/configuration/framework.html
framework:
secret: '%env(APP_SECRET)%'
#csrf_protection: true
http_method_override: false
handle_all_throwables: true
# Enables session support. Note that the session will ONLY be started if you read or write from it.
# Remove or comment this section to explicitly disable session support.
session:
handler_id: null
cookie_secure: auto
cookie_samesite: lax
storage_factory_id: session.storage.factory.native
#esi: true
#fragments: true
php_errors:
log: true
when@test:
framework:
test: true
session:
storage_factory_id: session.storage.factory.mock_file

View file

@ -0,0 +1,3 @@
framework:
mailer:
dsn: '%env(MAILER_DSN)%'

View file

@ -0,0 +1,24 @@
framework:
messenger:
failure_transport: failed
transports:
# https://symfony.com/doc/current/messenger.html#transport-configuration
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
options:
use_notify: true
check_delayed_interval: 60000
retry_strategy:
max_retries: 3
multiplier: 2
failed: 'doctrine://default?queue_name=failed'
# sync: 'sync://'
routing:
Symfony\Component\Mailer\Messenger\SendEmailMessage: async
Symfony\Component\Notifier\Message\ChatMessage: async
Symfony\Component\Notifier\Message\SmsMessage: async
# Route your messages to the transports
# 'App\Message\YourMessage': async

View file

@ -0,0 +1,62 @@
monolog:
channels:
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
when@dev:
monolog:
handlers:
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
channels: ["!event"]
# uncomment to get logging in your browser
# you may have to allow bigger header sizes in your Web server configuration
#firephp:
# type: firephp
# level: info
#chromephp:
# type: chromephp
# level: info
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!console"]
when@test:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
channels: ["!event"]
nested:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
when@prod:
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
excluded_http_codes: [404, 405]
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
nested:
type: stream
# path: php://stderr
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
# formatter: monolog.formatter.json
console:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine"]
deprecation:
type: stream
channels: [deprecation]
path: php://stderr

View file

@ -0,0 +1,13 @@
framework:
notifier:
chatter_transports:
texter_transports:
crowdin: '%env(CROWDIN_DSN)%'
channel_policy:
# use chat/slack, chat/telegram, sms/twilio or sms/nexmo
urgent: ['email']
high: ['email']
medium: ['email']
low: ['email']
admin_recipients:
- { email: admin@example.com }

View file

@ -0,0 +1,12 @@
framework:
router:
utf8: true
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
#default_uri: http://localhost
when@prod:
framework:
router:
strict_requirements: null

View file

@ -0,0 +1,39 @@
security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
users_in_memory: { memory: null }
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: users_in_memory
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
when@test:
security:
password_hashers:
# By default, password hashers are resource intensive and take time. This is
# important to generate secure password hashes. In tests however, secure hashes
# are not important, waste resources and increase test times. The following
# reduces the work factor to the lowest possible values.
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: auto
cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon

View file

@ -0,0 +1,17 @@
framework:
default_locale: en
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks:
- en
providers:
crowdin:
dsn: '%env(CROWDIN_DSN)%'
domains: ['messages']
locales: ['en','cs','nl','eo','fr', 'ja', 'ka','de','he','it','lv','pl','pt','ru','es','uk']
# loco:
# dsn: '%env(LOCO_DSN)%'
# lokalise:
# dsn: '%env(LOKALISE_DSN)%'
# phrase:
# dsn: '%env(PHRASE_DSN)%'

10
config/packages/twig.yaml Normal file
View file

@ -0,0 +1,10 @@
twig:
default_path: '%kernel.project_dir%/templates'
globals:
version: '%app.version%'
name: '%app.name%'
theme: '%app.theme%'
when@test:
twig:
strict_variables: true

View file

@ -0,0 +1,13 @@
framework:
validation:
email_validation_mode: html5
# Enables validator auto-mapping support.
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
#auto_mapping:
# App\Entity\: []
when@test:
framework:
validation:
not_compromised_password: false

View file

@ -0,0 +1,17 @@
when@dev:
web_profiler:
toolbar: true
intercept_redirects: false
framework:
profiler:
only_exceptions: false
collect_serializer_data: true
when@test:
web_profiler:
toolbar: false
intercept_redirects: false
framework:
profiler: { collect: false }

5
config/preload.php Normal file
View file

@ -0,0 +1,5 @@
<?php
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
}

5
config/routes.yaml Normal file
View file

@ -0,0 +1,5 @@
controllers:
resource:
path: ../src/Controller/
namespace: App\Controller
type: attribute

View file

@ -0,0 +1,4 @@
when@dev:
_errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.xml'
prefix: /_error

View file

@ -0,0 +1,8 @@
when@dev:
web_profiler_wdt:
resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml'
prefix: /_wdt
web_profiler_profiler:
resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml'
prefix: /_profiler

58
config/services.yaml Normal file
View file

@ -0,0 +1,58 @@
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:
app.version: '%env(APP_VERSION)%'
app.name: '%env(APP_NAME)%'
app.key: '%env(APP_KEY)%'
app.pagination: '%env(APP_PAGINATION)%'
app.trackers: '%env(APP_TRACKERS)%'
app.crawlers: '%env(APP_CRAWLERS)%'
app.locales: '%env(APP_LOCALES)%'
app.categories: '%env(APP_CATEGORIES)%'
app.themes: '%env(APP_THEMES)%'
app.locale: '%env(APP_LOCALE)%'
app.theme: '%env(APP_THEME)%'
app.sensitive: '%env(APP_SENSITIVE)%'
app.approved: '%env(APP_APPROVED)%'
app.yggdrasil: '%env(APP_YGGDRASIL)%'
app.posters: '%env(APP_POSTERS)%'
app.torrent.size.max: '%env(APP_TORRENT_FILE_SIZE_MAX)%'
app.torrent.poster.size.max: '%env(APP_TORRENT_POSTER_FILE_SIZE_MAX)%'
app.torrent.wanted.ftp.enabled: '%env(APP_TORRENT_WANTED_FTP_ENABLED)%'
app.torrent.wanted.ftp.folder: '%env(APP_TORRENT_WANTED_FTP_FOLDER)%'
app.torrent.wanted.ftp.approved: '%env(APP_TORRENT_WANTED_FTP_APPROVED_ONLY)%'
app.index.torrent.name.enabled: '%env(APP_INDEX_TORRENT_NAME_ENABLED)%'
app.index.torrent.filenames.enabled: '%env(APP_INDEX_TORRENT_FILENAMES_ENABLED)%'
app.index.torrent.hash.v1.enabled: '%env(APP_INDEX_TORRENT_HASH_V1_ENABLED)%'
app.index.torrent.hash.v2.enabled: '%env(APP_INDEX_TORRENT_HASH_V2_ENABLED)%'
app.index.torrent.source.enabled: '%env(APP_INDEX_TORRENT_SOURCE_ENABLED)%'
app.index.torrent.comment.enabled: '%env(APP_INDEX_TORRENT_COMMENT_ENABLED)%'
app.index.word.length.min: '%env(APP_INDEX_WORD_LENGTH_MIN)%'
app.index.word.length.max: '%env(APP_INDEX_WORD_LENGTH_MAX)%'
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
App\Twig\AppExtension:
arguments:
- '@service_container'
tags:
- { name: twig.extension}

Binary file not shown.

View file

@ -0,0 +1,14 @@
version: '3'
services:
###> doctrine/doctrine-bundle ###
database:
ports:
- "5432"
###< doctrine/doctrine-bundle ###
###> symfony/mailer ###
mailer:
image: schickling/mailcatcher
ports: ["1025", "1080"]
###< symfony/mailer ###

21
docker-compose.yml Normal file
View file

@ -0,0 +1,21 @@
version: '3'
services:
###> doctrine/doctrine-bundle ###
database:
image: postgres:${POSTGRES_VERSION:-15}-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB:-app}
# You should definitely change the password in production
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!}
POSTGRES_USER: ${POSTGRES_USER:-app}
volumes:
- database_data:/var/lib/postgresql/data:rw
# You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data!
# - ./docker/db/data:/var/lib/postgresql/data:rw
###< doctrine/doctrine-bundle ###
volumes:
###> doctrine/doctrine-bundle ###
database_data:
###< doctrine/doctrine-bundle ###

View file

@ -1,10 +0,0 @@
@reboot searchd
@reboot indexer --all --rotate
* * * * * indexer magnet --rotate > /dev/null 2>&1
* * * * * /usr/bin/php /YGGtracker/src/crontab/scrape.php > /dev/null 2>&1
* * * * * /usr/bin/php /YGGtracker/src/crontab/export/push.php > /dev/null 2>&1
0 5 * * * /usr/bin/php /YGGtracker/src/crontab/import/feed.php > /dev/null 2>&1
0 0 * * * /usr/bin/php /YGGtracker/src/crontab/export/feed.php > /dev/null 2>&1
0 0 * * * /usr/bin/php /YGGtracker/src/crontab/sitemap.php > /dev/null 2>&1

View file

@ -1,218 +0,0 @@
<?php
/*
* MIT License
*
* Copyright (c) 2023 YGGverse
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* Project home page
* https://github.com/YGGverse/YGGtracker
*
* Get support
* https://github.com/YGGverse/YGGtracker/issues
*/
// Debug
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
error_reporting(E_ALL);
// Database
define('DB_PORT', 3306);
define('DB_HOST', 'localhost');
define('DB_NAME', '');
define('DB_USERNAME', '');
define('DB_PASSWORD', '');
// Sphinx
define('SPHINX_HOST', '127.0.0.1');
define('SPHINX_PORT', 9306);
// Memcached
define('MEMCACHED_PORT', 11211);
define('MEMCACHED_HOST', 'localhost');
define('MEMCACHED_NAMESPACE', 'yggtracker');
define('MEMCACHED_TIMEOUT', 60 * 5);
// Webapp
define('WEBSITE_URL', '');
define('WEBSITE_NAME', 'YGGtracker');
define('WEBSITE_CSS_VERSION', 1);
define('WEBSITE_PAGINATION_LIMIT', 20);
// Moderation
define('MODERATOR_IP_LIST', (array)
[
'127.0.0.1',
// ...
]
);
// User
define('USER_DEFAULT_APPROVED', false);
define('USER_AUTO_APPROVE_ON_MAGNET_APPROVE', true);
define('USER_AUTO_APPROVE_ON_COMMENT_APPROVE', true);
define('USER_AUTO_APPROVE_ON_IMPORT_APPROVED', false);
define('USER_DEFAULT_IDENTICON', 'jidenticon'); // jidenticon|false
define('USER_IDENTICON_FIELD', 'address'); // address|userId|...
// Magnet
define('MAGNET_DEFAULT_APPROVED', USER_DEFAULT_APPROVED);
define('MAGNET_DEFAULT_PUBLIC', false);
define('MAGNET_DEFAULT_COMMENTS', true);
define('MAGNET_DEFAULT_SENSITIVE', false);
define('MAGNET_AUTO_APPROVE_ON_IMPORT_APPROVED', true);
define('MAGNET_EDITOR_LOCK_TIMEOUT', 60*60);
define('MAGNET_TITLE_MIN_LENGTH', 10);
define('MAGNET_TITLE_MAX_LENGTH', 140);
define('MAGNET_TITLE_REGEX', '/.*/ui');
define('MAGNET_PREVIEW_MIN_LENGTH', 0);
define('MAGNET_PREVIEW_MAX_LENGTH', 255);
define('MAGNET_PREVIEW_REGEX', '/.*/ui');
define('MAGNET_DESCRIPTION_MIN_LENGTH', 0);
define('MAGNET_DESCRIPTION_MAX_LENGTH', 10000);
define('MAGNET_DESCRIPTION_REGEX', '/.*/ui');
define('MAGNET_DN_MIN_LENGTH', 2);
define('MAGNET_DN_MAX_LENGTH', 255);
define('MAGNET_DN_REGEX', '/.*/ui');
define('MAGNET_KT_MIN_LENGTH', 2);
define('MAGNET_KT_MAX_LENGTH', 140);
define('MAGNET_KT_REGEX', '/[\w]+/ui');
define('MAGNET_KT_MIN_QUANTITY', 0);
define('MAGNET_KT_MAX_QUANTITY', 20);
define('MAGNET_TR_MIN_QUANTITY', 1);
define('MAGNET_TR_MAX_QUANTITY', 50);
define('MAGNET_AS_MIN_QUANTITY', 0);
define('MAGNET_AS_MAX_QUANTITY', 50);
define('MAGNET_WS_MIN_QUANTITY', 0);
define('MAGNET_WS_MAX_QUANTITY', 50);
define('MAGNET_STOP_WORDS_SIMILAR',
[
'series',
'season',
'discography',
// ...
]
);
// Magnet comment
define('MAGNET_COMMENT_DEFAULT_APPROVED', false);
define('MAGNET_COMMENT_DEFAULT_PUBLIC', false);
define('MAGNET_COMMENT_MIN_LENGTH', 1);
define('MAGNET_COMMENT_MAX_LENGTH', 1000);
// Torrent
define('TORRENT_ANNOUNCE_MIN_QUANTITY', 1);
define('TORRENT_ANNOUNCE_MAX_QUANTITY', 50);
define('TORRENT_COMMENT_MIN_LENGTH', 0);
define('TORRENT_COMMENT_MAX_LENGTH', 255);
define('TORRENT_COMMENT_REGEX', '/.*/ui');
define('TORRENT_INFO_NAME_MIN_LENGTH', 0);
define('TORRENT_INFO_NAME_MAX_LENGTH', 255);
define('TORRENT_INFO_NAME_REGEX', '/.*/ui');
define('TORRENT_INFO_SOURCE_MIN_LENGTH', 0);
define('TORRENT_INFO_SOURCE_MAX_LENGTH', 255);
define('TORRENT_INFO_SOURCE_REGEX', '/.*/ui');
define('TORRENT_CREATED_BY_MIN_LENGTH', 0);
define('TORRENT_CREATED_BY_MAX_LENGTH', 255);
define('TORRENT_CREATED_BY_REGEX', '/.*/ui');
// Yggdrasil
define('YGGDRASIL_HOST_REGEX', '/^0{0,1}[2-3][a-f0-9]{0,2}:/'); // thanks to @ygguser (https://github.com/YGGverse/YGGo/issues/1#issuecomment-1498182228 )
// Crawler
define('CRAWLER_SCRAPE_QUEUE_LIMIT', 1);
define('CRAWLER_SCRAPE_TIME_OFFLINE_TIMEOUT', 60*60*24);
// Node
define('NODE_RULE_SUBJECT', 'Common');
define('NODE_RULE_LANGUAGES', 'All');
// API
define('API_VERSION', '1.0.0');
define('API_USER_AGENT', WEBSITE_NAME);
/// Export
define('API_EXPORT_ENABLED', true);
define('API_EXPORT_PUSH_ENABLED', true); // depends of API_EXPORT_ENABLED
define('API_EXPORT_USERS_ENABLED', true); // depends of API_EXPORT_ENABLED
define('API_EXPORT_MAGNETS_ENABLED', true); // depends of API_EXPORT_ENABLED, API_EXPORT_USERS_ENABLED
define('API_EXPORT_MAGNET_DOWNLOADS_ENABLED', true); // depends of API_EXPORT_ENABLED, API_EXPORT_USERS_ENABLED, API_EXPORT_MAGNETS_ENABLED
define('API_EXPORT_MAGNET_COMMENTS_ENABLED', true); // depends of API_EXPORT_ENABLED, API_EXPORT_USERS_ENABLED, API_EXPORT_MAGNETS_ENABLED
define('API_EXPORT_MAGNET_STARS_ENABLED', true); // depends of API_EXPORT_ENABLED, API_EXPORT_USERS_ENABLED, API_EXPORT_MAGNETS_ENABLED
define('API_EXPORT_MAGNET_VIEWS_ENABLED', true); // depends of API_EXPORT_ENABLED, API_EXPORT_USERS_ENABLED, API_EXPORT_MAGNETS_ENABLED
/// Import
define('API_IMPORT_ENABLED', true);
define('API_IMPORT_PUSH_ENABLED', true); // depends of API_IMPORT_ENABLED
define('API_IMPORT_USERS_ENABLED', true); // depends of API_IMPORT_ENABLED
define('API_IMPORT_USERS_APPROVED_ONLY', false); // depends of API_IMPORT_ENABLED, API_IMPORT_USERS_ENABLED
define('API_IMPORT_MAGNETS_ENABLED', true); // depends of API_IMPORT_ENABLED, API_IMPORT_USERS_ENABLED
define('API_IMPORT_MAGNETS_APPROVED_ONLY', false); // depends of API_IMPORT_ENABLED, API_IMPORT_USERS_ENABLED, API_IMPORT_MAGNETS_ENABLED
define('API_IMPORT_MAGNET_DOWNLOADS_ENABLED', true); // depends of API_IMPORT_ENABLED, API_IMPORT_USERS_ENABLED, API_IMPORT_MAGNETS_ENABLED
define('API_IMPORT_MAGNET_COMMENTS_ENABLED', true); // depends of API_IMPORT_ENABLED, API_IMPORT_USERS_ENABLED, API_IMPORT_MAGNETS_ENABLED
define('API_IMPORT_MAGNET_COMMENTS_APPROVED_ONLY', false); // depends of API_IMPORT_ENABLED, API_IMPORT_USERS_ENABLED, API_IMPORT_MAGNETS_ENABLED, API_IMPORT_MAGNET_COMMENTS_ENABLED
define('API_IMPORT_MAGNET_STARS_ENABLED', true); // depends of API_IMPORT_ENABLED, API_IMPORT_USERS_ENABLED, API_IMPORT_MAGNETS_ENABLED
define('API_IMPORT_MAGNET_VIEWS_ENABLED', true); // depends of API_IMPORT_ENABLED, API_IMPORT_USERS_ENABLED, API_IMPORT_MAGNETS_ENABLED
// Logs
define('LOG_DIRECTORY', __DIR__ . '/../storage/log');
define('LOG_CRONTAB_SCRAPE_ENABLED', true);
define('LOG_CRONTAB_SCRAPE_FILENAME', sprintf('crontab_scrape_%s.log', date('Y-m-d')));
define('LOG_CRONTAB_SITEMAP_ENABLED', true);
define('LOG_CRONTAB_SITEMAP_FILENAME', sprintf('crontab_sitemap_%s.log', date('Y-m-d')));
define('LOG_CRONTAB_EXPORT_FEED_ENABLED', true);
define('LOG_CRONTAB_EXPORT_FEED_FILENAME', sprintf('crontab_export_feed_%s.log', date('Y-m-d')));
define('LOG_CRONTAB_EXPORT_PUSH_ENABLED', true);
define('LOG_CRONTAB_EXPORT_PUSH_FILENAME', sprintf('crontab_export_push_%s.log', date('Y-m-d')));
define('LOG_CRONTAB_IMPORT_FEED_ENABLED', true);
define('LOG_CRONTAB_IMPORT_FEED_FILENAME', sprintf('crontab_import_feed_%s.log', date('Y-m-d')));
define('LOG_API_PUSH_ENABLED', true);
define('LOG_API_PUSH_FILENAME', sprintf('api_push_%s.log', date('Y-m-d')));

View file

@ -1,25 +0,0 @@
server {
listen [::]:80 default;
allow 0200::/7;
deny all;
root /var/www/html;
index index.html index.htm index.nginx-debian.html index.php;
server_name _;
location / {
try_files $uri $uri/ =404;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
}
location ~ /\. {
deny all;
}
}

View file

@ -1,71 +0,0 @@
source yggtracker
{
type = mysql
sql_port = 3306
sql_host = localhost
sql_user =
sql_pass =
sql_db =
}
source magnet : yggtracker
{
sql_query = \
SELECT `magnet`.`timeAdded`, \
`magnet`.`timeUpdated`, \
`magnet`.`magnetId`, \
`magnet`.`title`, \
`magnet`.`preview`, \
`magnet`.`description`, \
`magnet`.`dn`, \
(SELECT GROUP_CONCAT(DISTINCT `infoHash`.`value`) \
FROM `infoHash` \
JOIN `magnetToInfoHash` ON (`magnetToInfoHash`.`magnetId` = `magnet`.`magnetId`) \
WHERE `infoHash`.`infoHashId` = `magnetToInfoHash`.`infoHashId`) AS `infoHash`, \
(SELECT GROUP_CONCAT(DISTINCT `keywordTopic`.`value`) \
FROM `keywordTopic` \
JOIN `magnetToKeywordTopic` ON (`magnetToKeywordTopic`.`magnetId` = `magnet`.`magnetId`) \
WHERE `keywordTopic`.`keywordTopicId` = `magnetToKeywordTopic`.`keywordTopicId`) AS `keywords`, \
(SELECT GROUP_CONCAT(DISTINCT `magnetComment`.`value`) \
FROM `magnetComment` \
WHERE `magnetComment`.`magnetId` = `magnet`.`magnetId`) AS `comments` \
FROM `magnet`\
sql_attr_uint = magnetId
}
index magnet
{
source = magnet
path = /var/lib/sphinxsearch/data/magnet
morphology = stem_cz, stem_ar, stem_enru
min_word_len = 2
min_prefix_len = 2
html_strip = 1
index_exact_words = 1
}
indexer
{
mem_limit = 256M
}
searchd
{
listen = 127.0.0.1:9306:mysql41
log = /var/log/sphinxsearch/searchd.log
query_log = /var/log/sphinxsearch/query.log
pid_file = /run/sphinxsearch/searchd.pid
binlog_path = /var/lib/sphinxsearch/data
read_timeout = 5
max_children = 30
seamless_rotate = 1
preopen_indexes = 1
unlink_old = 1
workers = threads # for RT to work
}

View file

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20231026163131 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE torrent ADD COLUMN status BOOLEAN DEFAULT 1');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TEMPORARY TABLE __temp__torrent AS SELECT id, user_id, added, scraped, locales, sensitive, approved, md5file, keywords, seeders, peers, leechers FROM torrent');
$this->addSql('DROP TABLE torrent');
$this->addSql('CREATE TABLE torrent (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER NOT NULL, added INTEGER NOT NULL, scraped INTEGER DEFAULT NULL, locales CLOB NOT NULL --(DC2Type:simple_array)
, sensitive BOOLEAN NOT NULL, approved BOOLEAN NOT NULL, md5file VARCHAR(32) NOT NULL, keywords CLOB DEFAULT NULL --(DC2Type:simple_array)
, seeders INTEGER DEFAULT NULL, peers INTEGER DEFAULT NULL, leechers INTEGER DEFAULT NULL)');
$this->addSql('INSERT INTO torrent (id, user_id, added, scraped, locales, sensitive, approved, md5file, keywords, seeders, peers, leechers) SELECT id, user_id, added, scraped, locales, sensitive, approved, md5file, keywords, seeders, peers, leechers FROM __temp__torrent');
$this->addSql('DROP TABLE __temp__torrent');
}
}

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20231029184600 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE torrent_poster (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, torrent_id INTEGER NOT NULL, user_id INTEGER NOT NULL, added INTEGER NOT NULL, approved BOOLEAN NOT NULL, md5file VARCHAR(32) NOT NULL)');
$this->addSql('ALTER TABLE user ADD COLUMN posters BOOLEAN NOT NULL DEFAULT 1');
$this->addSql('ALTER TABLE torrent ADD COLUMN torrent_poster_id BOOLEAN NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE torrent_poster');
$this->addSql('ALTER TABLE user DROP COLUMN posters');
$this->addSql('ALTER TABLE torrent DROP COLUMN torrent_poster_id');
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20231030225418 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE torrent_poster ADD COLUMN position BOOLEAN NOT NULL DEFAULT "center"');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE torrent_poster DROP COLUMN position');
}
}

View file

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20231103235504 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE torrent_categories (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, torrent_id INTEGER NOT NULL, user_id INTEGER NOT NULL, added INTEGER NOT NULL, value CLOB NOT NULL --(DC2Type:simple_array)
, approved BOOLEAN NOT NULL)');
$this->addSql('ALTER TABLE user ADD COLUMN categories CLOB DEFAULT "other"');
$this->addSql('ALTER TABLE torrent ADD COLUMN categories CLOB DEFAULT "other"');
$this->addSql('UPDATE user SET categories = "movie,series,tv,animation,music,game,audiobook,podcast,book,archive,picture,software,other"');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE torrent_categories');
$this->addSql('ALTER TABLE user DROP COLUMN categories');
$this->addSql('ALTER TABLE torrent DROP COLUMN categories');
}
}

38
phpunit.xml.dist Normal file
View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="tests/bootstrap.php"
convertDeprecationsToExceptions="false"
>
<php>
<ini name="display_errors" value="1" />
<ini name="error_reporting" value="-1" />
<server name="APP_ENV" value="test" force="true" />
<server name="SHELL_VERBOSITY" value="-1" />
<server name="SYMFONY_PHPUNIT_REMOVE" value="" />
<server name="SYMFONY_PHPUNIT_VERSION" value="9.5" />
</php>
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
<listeners>
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
</listeners>
<extensions>
</extensions>
</phpunit>

View file

@ -0,0 +1,128 @@
* {
border: 0;
margin: 0;
padding: 0;
box-sizing: border-box;
outline: none;
}
:focus,
:focus-within,
:focus-visible,
:active,
:target,
:hover {
opacity: 1;
transition: opacity .2s ease-in-out;
}
body {
background: #282b3c;
color: #ccc;
font-family: Sans-serif;
font-size: 13px;
}
a,
a:visited,
a:active {
color: #96d9a1;
text-decoration: none;
opacity: .9;
}
h1, h2, h3, h4, h5 {
display: inline-block;
font-weight: normal;
}
h1 {
font-size: 16px;
}
h2 {
color: #ccc;
font-size: 14px;
}
input,
button,
select,
textarea {
accent-color: #65916d;
background: #5d627d;
border: #5d627d 1px solid;
color: #ccc;
border-radius: 3px;
padding: 6px 8px;
opacity: .96;
}
input[type="file"] {
padding: 4px 8px;
}
/*
main input,
main button,
main select,
main textarea {
padding: 8px;
}
*/
textarea:focus,
input:focus {
border: #65916d 1px solid;
color: #fff;
}
select[multiple="multiple"] > option {
border-top: 1px #5d627d solid;
border-bottom: 1px #5d627d solid;
color: #fff;
}
select[multiple="multiple"] > option:active,
select[multiple="multiple"] > option:focus,
select[multiple="multiple"] > option:focus-within,
select[multiple="multiple"] > option:checked {
border-top: 1px #65916d solid;
border-bottom: 1px #65916d solid;
background: linear-gradient(#65916d, #65916d);
}
button,
input[type="submit"] {
cursor: pointer;
}
textarea,
select[multiple="multiple"] {
min-height: 180px;
}
textarea::placeholder,
input::placeholder {
color: #9698a5;
opacity: 1;
}
input[type="text"]:hover,
textarea:hover {
background: #636884;
}
td {
padding: 2px 0;
vertical-align: top;
}
header a.logo {
color: #ccc;
font-size: 20px;
}
header a.logo > span {
color: #96d9a1;
}

View file

@ -0,0 +1,553 @@
.container {
position: relative;
overflow: hidden;
max-width: 748px;
margin: 0 auto;
}
.row {
position: relative;
overflow: hidden;
}
.column {
position: relative;
float: left;
}
.word-break {
word-break: break-word;
}
.overflow-auto {
overflow: auto;
}
.float-left {
float: left;
}
.float-right {
float: right;
}
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.text-color-green,
a.text-color-green,
a.text-color-green:active,
a.text-color-green:visited,
a.text-color-green:hover {
color: #96d9a1;
}
.text-color-red,
a.text-color-red,
a.text-color-red:active,
a.text-color-red:visited,
a.text-color-red:hover {
color: #d77575;
}
.text-color-pink,
a.text-color-pink,
a.text-color-pink:active,
a.text-color-pink:visited,
a.text-color-pink:hover {
color: #b55cab;
}
.text-color-default,
a.text-color-default,
a.text-color-default:active,
a.text-color-default:visited,
a.text-color-default:hover {
color: #ccc;
}
.text-color-white,
a.text-color-white,
a.text-color-white:active,
a.text-color-white:visited,
a.text-color-white:hover {
color: #fff;
}
/*
.text-color-pink {
color: #a44399;
}
*/
.text-color-blue {
color: #5785b7;
}
.text-color-night,
a.text-color-night,
a.text-color-night:active,
a.text-color-night:visited,
a.text-color-night:hover {
color: #838695;
}
.label {
padding: 4px 8px;
border-radius: 3px;
}
.label-green,
a.label-green,
a.label-green:active,
a.label-green:visited,
a.label-green:hover {
color: #fff;
background-color: #65916d;
}
.button,
a.button,
a.button:active,
a.button:visited,
a.button:hover {
background: #5d627d;
border: #5d627d 1px solid;
color: #ccc;
padding: 6px 8px;
border-radius: 3px;
opacity: .96;
display: inline-block;
}
.button-green,
a.button-green,
a.button-green:active,
a.button-green:visited,
a.button-green:hover {
color: #fff;
background-color: #65916d;
border: #65916d 1px solid;
}
.button-green:hover {
color: #fff;
background-color: #709e79;
}
.position-relative {
position: relative;
}
.position-fixed {
position: fixed;
}
.position-absolute {
position: absolute;
}
.vertical-align-middle {
vertical-align: middle;
}
.top-2-px {
top: 2px;
}
.line-height-20-px {
line-height: 20px;
}
.line-height-26-px {
line-height: 26px;
}
.border-radius-3-px {
border-radius: 3px;
}
.border-radius-50 {
border-radius: 50%;
}
.border-color-pink-light {
border: 1px #9b6895 solid;
}
.border-color-pink {
border: 1px #a44399 solid;
}
.border-color-green {
border: 1px #65916d solid;
}
.border-color-pink {
border-bottom: 1px #a44399 solid;
}
.border-color-default {
border: 1px rgba(93, 98, 125, .6) solid;
}
.border-bottom-default {
border-bottom: 1px rgba(93, 98, 125, .6) solid;
}
.border-top-default {
border-top: 1px rgba(93, 98, 125, .6) solid;
}
.border-bottom-dashed {
border-bottom: 1px rgba(93, 98, 125, .6) dashed;
}
.border-top-dashed {
border-top: 1px rgba(93, 98, 125, .6) dashed;
}
.border-width-2-px {
border-width: 2px;
}
.background-poster {
background-position: center;
background-repeat: no-repeat;
background-size: cover;
background-blend-mode: soft-light;
}
.background-color-night {
background-color: #34384f;
}
.background-color-night-light {
background-color: #3d4159;
}
.background-color-green {
background-color: #65916d;
}
.background-color-hover-night-light:hover,
a.background-color-hover-night-light:hover,
a:active.background-color-hover-night-light:hover,
a:visited.background-color-hover-night-light:hover {
/*color: #fff;*/
background-color: #3d4159;
}
.background-color-red {
background-color: #9b4a4a;
}
.cursor-default {
cursor: default;
}
.cursor-help {
cursor: help;
}
.font-weight-normal {
font-weight: normal
}
.font-weight-200 {
font-weight: 200
}
.font-size-10-px {
font-size: 10px;
}
.font-size-12-px {
font-size: 12px;
}
.font-size-22-px {
font-size: 22px;
}
.padding-0 {
padding: 0;
}
.padding-x-0 {
padding-left: 0;
padding-right: 0;
}
.padding-4-px {
padding: 4px;
}
.padding-l-4-px {
padding-left: 4px;
}
.padding-t-4-px {
padding-top: 4px;
}
.padding-y-4-px {
padding-top: 4px;
padding-bottom: 4px;
}
.padding-x-4-px {
padding-left: 4px;
padding-right: 4px;
}
.padding-x-8-px {
padding-left: 8px;
padding-right: 8px;
}
.padding-y-6-px {
padding-top: 6px;
padding-bottom: 6px;
}
.padding-8-px {
padding: 8px;
}
.padding-l-8-px {
padding-left: 8px;
}
.padding-t-8-px {
padding-top: 8px;
}
.padding-b-8-px {
padding-bottom: 8px;
}
.padding-y-8-px {
padding-top: 8px;
padding-bottom: 8px;
}
.padding-y-12-px {
padding-top: 12px;
padding-bottom: 12px;
}
.padding-b-16-px {
padding-bottom: 16px;
}
.padding-t-16-px {
padding-top: 16px;
}
.padding-y-16-px {
padding-top: 16px;
padding-bottom: 16px;
}
.padding-x-16-px {
padding-left: 16px;
padding-right: 16px;
}
.padding-16-px {
padding: 16px;
}
.padding-24-px {
padding: 24px;
}
.padding-x-24-px {
padding-left: 24px;
padding-right: 24px;
}
.padding-y-24-px {
padding-top: 24px;
padding-bottom: 24px;
}
.margin-t-4-px {
margin-top: 4px;
}
.margin-l-4-px {
margin-left: 4px;
}
.margin-8-px {
margin: 8px;
}
.margin-x-8-px {
margin-left: 8px;
margin-right: 8px;
}
.margin-l-8-px {
margin-left: 8px;
}
.margin-16-px {
margin: 16px;
}
.margin-l-16-px {
margin-left: 16px;
}
.margin-x-4-px {
margin-left: 4px;
margin-right: 4px;
}
.margin-b-4-px {
margin-bottom: 4px;
}
.margin-r-4-px {
margin-right: 4px;
}
.margin-r-8-px {
margin-right: 8px;
}
.margin-l-12-px {
margin-left: 12px;
}
.margin-l-96-px {
margin-left: 96px;
}
.margin-l--48-px {
margin-left: -48px;
}
.margin-y-8-px {
margin-top: 8px;
margin-bottom: 8px;
}
.margin-t-8-px {
margin-top: 8px;
}
.margin-b-8-px {
margin-bottom: 8px;
}
.margin-y-16-px {
margin-top: 16px;
margin-bottom: 16px;
}
.margin-t-16-px {
margin-top: 16px;
}
.margin-b-16-px {
margin-bottom: 16px;
}
.margin-b-24-px {
margin-bottom: 24px;
}
.display-block {
display: block;
}
.display-inline-block {
display: inline-block;
}
.display-flex {
display: flex;
}
.opacity-0 {
opacity: 0;
}
.opacity-06 {
opacity: .6;
}
.opacity-hover-1:hover {
opacity: 1;
transition: opacity .2s;
}
*:hover > .parent-hover-opacity-09 {
opacity: .9;
transition: opacity .2s;
}
.blur-2 {
filter: blur(2px);
}
.blur-hover-0:hover {
filter: blur(0);
}
/* responsive rules */
.width-100 {
width: 100%;
}
.width-50 {
width: 50%;
}
.width-20 {
width: 20%;
}
.width-80 {
width: 80%;
}
.width-80-px {
width: 80px;
}
.min-width-120-px {
min-width: 120px;
}
.min-width-200-px {
min-width: 200px;
}
@media (max-width: 1220px) {
.width-tablet-100 {
width: 100%;
}
}
@media (max-width: 512px) {
.width-mobile-100 {
width: 100%;
}
}

9
public/index.php Normal file
View file

@ -0,0 +1,9 @@
<?php
use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};

0
src/Controller/.gitignore vendored Normal file
View file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,164 @@
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use App\Service\UserService;
use App\Service\TorrentService;
use App\Service\ActivityService;
class SearchController extends AbstractController
{
public function module(
Request $request,
UserService $userService,
TorrentService $torrentService,
ActivityService $activityService
): Response
{
// Defaults
$locales = [];
$categories = [];
$sensitive = [];
// Request
$query = $request->get('query') ? urldecode($request->get('query')) : '';
$filter = $request->get('filter') ? true : false;
// Extended search
if ($filter)
{
// Init user
$user = $this->initUser(
$request,
$userService,
$activityService
);
// Keywords
$keywords = explode(' ', $query);
// Locales
foreach (explode('|', $this->getParameter('app.locales')) as $locale)
{
if ($request->get('locales'))
{
$checked = in_array($locale, (array) $request->get('locales'));
}
else
{
$checked = in_array($locale, $user->getLocales());
}
$locales[] =
[
'value' => $locale,
'checked' => $checked,
'total' => $torrentService->findTorrentsTotal(
0,
$keywords,
[$locale],
$request->get('categories') ? $request->get('categories') : $user->getCategories(),
$request->get('sensitive') ? null : false,
!$user->isModerator() ? true : null,
!$user->isModerator() ? true : null,
)
];
}
// Categories
foreach (explode('|', $this->getParameter('app.categories')) as $category)
{
if ($request->get('categories'))
{
$checked = in_array($category, (array) $request->get('categories'));
}
else
{
$checked = in_array($category, $user->getCategories());
}
$categories[] =
[
'value' => $category,
'checked' => $checked,
'total' => $torrentService->findTorrentsTotal(
0,
$keywords,
$request->get('locales') ? $request->get('locales') : $user->getLocales(),
[$category],
$request->get('sensitive') ? null : false,
!$user->isModerator() ? true : null,
!$user->isModerator() ? true : null,
)
];
}
// Sensitive
$sensitive =
[
'checked' => $request->get('sensitive'),
'total' => $torrentService->findTorrentsTotal(
0,
$keywords,
$request->get('locales') ? $request->get('locales') : $user->getLocales(),
$request->get('categories') ? $request->get('categories') : $user->getCategories(),
true,
!$user->isModerator() ? true : null,
!$user->isModerator() ? true : null,
)
];
}
return $this->render(
'default/search/module.html.twig',
[
'query' => $query,
'filter' => $filter,
'sensitive' => $sensitive,
'locales' => $locales,
'categories' => $categories,
]
);
}
private function initUser(
Request $request,
UserService $userService,
ActivityService $activityService
): ?\App\Entity\User
{
// Init user
if (!$user = $userService->findUserByAddress($request->getClientIp()))
{
$user = $userService->addUser(
$request->getClientIp(),
time(),
$this->getParameter('app.locale'),
explode('|', $this->getParameter('app.locales')),
$activityService->getEventCodes(),
$this->getParameter('app.theme'),
$this->getParameter('app.sensitive'),
$this->getParameter('app.yggdrasil'),
$this->getParameter('app.posters'),
$this->getParameter('app.approved')
);
// Add user join event
$activityService->addEventUserAdd(
$user->getId(),
time()
);
}
return $user;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,672 @@
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use App\Service\ActivityService;
use App\Service\UserService;
use App\Service\TorrentService;
class UserController extends AbstractController
{
#[Route('/')]
public function root(
Request $request,
UserService $userService,
ActivityService $activityService
): Response
{
// Init user
$user = $this->initUser(
$request,
$userService,
$activityService
);
return $this->redirectToRoute(
'torrent_recent',
[
'_locale' => $user->getLocale()
]
);
}
#[Route(
'/{_locale}/settings',
name: 'user_settings',
requirements: [
'_locale' => '%app.locales%',
],
)]
public function settings(
Request $request,
UserService $userService,
ActivityService $activityService
): Response
{
// Init user
$user = $this->initUser(
$request,
$userService,
$activityService
);
// Process post request
if ($request->isMethod('post'))
{
// Update locale
if (in_array($request->get('locale'), explode('|', $this->getParameter('app.locales'))))
{
$user->setLocale(
$request->get('locale')
);
}
// Update locales
if ($request->get('locales'))
{
$locales = [];
foreach ((array) $request->get('locales') as $locale)
{
if (in_array($locale, explode('|', $this->getParameter('app.locales'))))
{
$locales[] = $locale;
}
}
$user->setLocales(
$locales
);
}
// Update categories
if ($request->get('categories'))
{
$categories = [];
foreach ((array) $request->get('categories') as $category)
{
if (in_array($category, explode('|', $this->getParameter('app.categories'))))
{
$categories[] = $category;
}
}
$user->setCategories(
$categories
);
}
// Update theme
if (in_array($request->get('theme'), explode('|', $this->getParameter('app.themes'))))
{
$user->setTheme(
$request->get('theme')
);
}
// Update events
$events = [];
foreach ((array) $request->get('events') as $event)
{
if (in_array($event, $activityService->getEventCodes()))
{
$events[] = $event;
}
}
$user->setEvents(
$events
);
// Update sensitive
$user->setSensitive(
$request->get('sensitive') === 'true'
);
// Update yggdrasil
$user->setYggdrasil(
$request->get('yggdrasil') === 'true'
);
// Update posters
$user->setPosters(
$request->get('posters') === 'true'
);
// Save changes to DB
$userService->save($user);
// Redirect user to new locale
return $this->redirectToRoute(
'user_settings',
[
'_locale' => $user->getLocale()
]
);
}
// Render template
return $this->render(
'default/user/settings.html.twig',
[
'user' => [
'id' => $user->getId(),
'sensitive' => $user->isSensitive(),
'yggdrasil' => $user->isYggdrasil(),
'posters' => $user->isPosters(),
'locale' => $user->getLocale(),
'locales' => $user->getLocales(),
'categories' => $user->getCategories(),
'events' => $user->getEvents(),
'theme' => $user->getTheme(),
'added' => $user->getAdded()
],
'locales' => explode('|', $this->getParameter('app.locales')),
'categories' => explode('|', $this->getParameter('app.categories')),
'themes' => explode('|', $this->getParameter('app.themes')),
'events' => $activityService->getEventsTree()
]
);
}
#[Route(
'/{_locale}/profile/{userId}',
name: 'user_info',
defaults: [
'userId' => 0,
],
requirements: [
'_locale' => '%app.locales%',
'userId' => '\d+',
],
)]
public function info(
Request $request,
TranslatorInterface $translator,
UserService $userService,
ActivityService $activityService
): Response
{
// Init user
$user = $this->initUser(
$request,
$userService,
$activityService
);
if (!$user->isStatus())
{
// @TODO
throw new \Exception(
$translator->trans('Access denied')
);
}
// Init target user
if (!$userTarget = $userService->getUser(
$request->get('userId') ? $request->get('userId') : $user->getId()
))
{
throw $this->createNotFoundException();
}
// Get total activities
$total = $activityService->findActivitiesTotalByUserId(
$userTarget->getId(),
$user->getEvents()
);
// Init page
$page = $request->get('page') ? (int) $request->get('page') : 1;
// Render template
return $this->render(
'default/user/info.html.twig',
[
'session' =>
[
'user' => $user,
'owner' => $user->getId() === $userTarget->getId(),
'moderator' => $user->isModerator()
],
'user' => [
'id' => $userTarget->getId(),
'address' => $userTarget->getAddress(),
'moderator' => $userTarget->isModerator(),
'approved' => $userTarget->isApproved(),
'status' => $userTarget->isStatus(),
'posters' => $userTarget->isPosters(),
'sensitive' => $userTarget->isSensitive(),
'yggdrasil' => $userTarget->isYggdrasil(),
'locale' => $userTarget->getLocale(),
'locales' => $userTarget->getLocales(),
'categories' => $user->getCategories(),
'events' => $userTarget->getEvents(),
'theme' => $userTarget->getTheme(),
'added' => $userTarget->getAdded(),
'identicon' => $userService->identicon(
$userTarget->getAddress(),
48
),
'owner' => $user->getId() === $userTarget->getId(),
'star' =>
[
'exist' => (bool) $userService->findUserStar(
$user->getId(),
$userTarget->getId()
),
'total' => $userService->findUserStarsTotalByUserIdTarget(
$userTarget->getId()
)
],
'activities' => $activityService->findLastActivitiesByUserId(
$userTarget->getId(),
$userTarget->getEvents(),
$this->getParameter('app.pagination'),
($page - 1) * $this->getParameter('app.pagination')
)
],
'events' => $activityService->getEventsTree(),
'pagination' =>
[
'page' => $page,
'pages' => ceil($total / $this->getParameter('app.pagination')),
'total' => $total
]
]
);
}
#[Route(
'/{_locale}/user/star/toggle/{userId}',
name: 'user_star_toggle',
requirements:
[
'_locale' => '%app.locales%',
'userId' => '\d+',
],
methods:
[
'GET'
]
)]
public function toggleStar(
Request $request,
TranslatorInterface $translator,
UserService $userService,
ActivityService $activityService
): Response
{
// Init user
$user = $this->initUser(
$request,
$userService,
$activityService
);
if (!$user->isStatus())
{
// @TODO
throw new \Exception(
$translator->trans('Access denied')
);
}
// Block crawler requests
if (in_array($request->getClientIp(), explode('|', $this->getParameter('app.crawlers'))))
{
throw $this->createNotFoundException();
}
// Init target user
if (!$userTarget = $userService->getUser($request->get('userId')))
{
throw $this->createNotFoundException();
}
// Update
$value = $userService->toggleUserStar(
$user->getId(),
$userTarget->getId(),
time()
);
// Add activity event
if ($value)
{
$activityService->addEventUserStarAdd(
$user->getId(),
time(),
$userTarget->getId()
);
}
else
{
$activityService->addEventUserStarDelete(
$user->getId(),
time(),
$userTarget->getId()
);
}
// Redirect
return $this->redirectToRoute(
'user_info',
[
'_locale' => $request->get('_locale'),
'userId' => $userTarget->getId()
]
);
}
#[Route(
'/{_locale}/user/{userId}/moderator/toggle',
name: 'user_moderator_toggle',
requirements:
[
'_locale' => '%app.locales%',
'userId' => '\d+',
],
methods:
[
'GET'
]
)]
public function toggleModerator(
Request $request,
TranslatorInterface $translator,
UserService $userService,
ActivityService $activityService
): Response
{
// Init user
$user = $this->initUser(
$request,
$userService,
$activityService
);
if (!$user->isModerator())
{
// @TODO
throw new \Exception(
$translator->trans('Access denied')
);
}
// Init target user
if (!$userTarget = $userService->getUser($request->get('userId')))
{
throw $this->createNotFoundException();
}
// Update user moderator
$value = $userService->toggleUserModerator(
$userTarget->getId()
)->isModerator();
// Add activity event
if ($value)
{
$activityService->addEventUserModeratorAdd(
$user->getId(),
time(),
$userTarget->getId()
);
}
else
{
$activityService->addEventUserModeratorDelete(
$user->getId(),
time(),
$userTarget->getId()
);
}
// Redirect
return $this->redirectToRoute(
'user_info',
[
'_locale' => $request->get('_locale'),
'userId' => $userTarget->getId()
]
);
}
#[Route(
'/{_locale}/user/{userId}/status/toggle',
name: 'user_status_toggle',
requirements:
[
'_locale' => '%app.locales%',
'userId' => '\d+',
],
methods:
[
'GET'
]
)]
public function toggleStatus(
Request $request,
TranslatorInterface $translator,
UserService $userService,
ActivityService $activityService
): Response
{
// Init user
$user = $this->initUser(
$request,
$userService,
$activityService
);
if (!$user->isModerator())
{
// @TODO
throw new \Exception(
$translator->trans('Access denied')
);
}
// Init target user
if (!$userTarget = $userService->getUser($request->get('userId')))
{
throw $this->createNotFoundException();
}
// Update user status
$value = $userService->toggleUserStatus(
$userTarget->getId()
)->isStatus();
// Add activity event
if ($value)
{
$activityService->addEventUserStatusAdd(
$user->getId(),
time(),
$userTarget->getId()
);
}
else
{
$activityService->addEventUserStatusDelete(
$user->getId(),
time(),
$userTarget->getId()
);
}
// Redirect
return $this->redirectToRoute(
'user_info',
[
'_locale' => $request->get('_locale'),
'userId' => $userTarget->getId()
]
);
}
#[Route(
'/{_locale}/user/{userId}/approved/toggle',
name: 'user_approved_toggle',
requirements:
[
'_locale' => '%app.locales%',
'userId' => '\d+',
],
methods:
[
'GET'
]
)]
public function toggleApproved(
Request $request,
TranslatorInterface $translator,
UserService $userService,
TorrentService $torrentService,
ActivityService $activityService
): Response
{
// Init user
$user = $this->initUser(
$request,
$userService,
$activityService
);
if (!$user->isModerator())
{
// @TODO
throw new \Exception(
$translator->trans('Access denied')
);
}
// Init target user
if (!$userTarget = $userService->getUser($request->get('userId')))
{
throw $this->createNotFoundException();
}
// Auto-approve all related content on user approve
if (!$userTarget->isApproved())
{
$torrentService->setTorrentsApprovedByUserId(
$userTarget->getId(),
true
);
$torrentService->setTorrentLocalesApprovedByUserId(
$userTarget->getId(),
true
);
$torrentService->setTorrentCategoriesApprovedByUserId(
$userTarget->getId(),
true
);
$torrentService->setTorrentSensitivesApprovedByUserId(
$userTarget->getId(),
true
);
$torrentService->setTorrentPostersApprovedByUserId(
$userTarget->getId(),
true
);
// @TODO make event for each item
}
// Update user approved
$value = $userService->toggleUserApproved(
$userTarget->getId()
)->isApproved();
// Add activity event
if ($value)
{
$activityService->addEventUserApproveAdd(
$user->getId(),
time(),
$userTarget->getId()
);
}
else
{
$activityService->addEventUserApproveDelete(
$user->getId(),
time(),
$userTarget->getId()
);
}
// Redirect
return $this->redirectToRoute(
'user_info',
[
'_locale' => $request->get('_locale'),
'userId' => $userTarget->getId()
]
);
}
public function module(?string $route): Response
{
return $this->render(
'default/user/module.html.twig',
[
'route' => $route,
'stars' => 0,
'views' => 0,
'comments' => 0,
'downloads' => 0,
'editions' => 0,
]
);
}
private function initUser(
Request $request,
UserService $userService,
ActivityService $activityService
): ?\App\Entity\User
{
// Init user
if (!$user = $userService->findUserByAddress($request->getClientIp()))
{
$user = $userService->addUser(
$request->getClientIp(),
time(),
$this->getParameter('app.locale'),
explode('|', $this->getParameter('app.locales')),
$activityService->getEventCodes(),
$this->getParameter('app.theme'),
$this->getParameter('app.sensitive'),
$this->getParameter('app.yggdrasil'),
$this->getParameter('app.posters'),
$this->getParameter('app.approved')
);
// Add user join event
$activityService->addEventUserAdd(
$user->getId(),
time()
);
}
return $user;
}
}

0
src/Entity/.gitignore vendored Normal file
View file

160
src/Entity/Activity.php Normal file
View file

@ -0,0 +1,160 @@
<?php
namespace App\Entity;
use App\Repository\ActivityRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ActivityRepository::class)]
class Activity
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column]
private ?int $event = null;
// Event codes
/// User
public const EVENT_USER_ADD = 1000;
public const EVENT_USER_APPROVE_ADD = 1200;
public const EVENT_USER_APPROVE_DELETE = 1201;
public const EVENT_USER_MODERATOR_ADD = 1300;
public const EVENT_USER_MODERATOR_DELETE = 1301;
public const EVENT_USER_STATUS_ADD = 1400;
public const EVENT_USER_STATUS_DELETE = 1401;
public const EVENT_USER_STAR_ADD = 1500;
public const EVENT_USER_STAR_DELETE = 1501;
/// Torrent
public const EVENT_TORRENT_ADD = 2000;
public const EVENT_TORRENT_APPROVE_ADD = 1100;
public const EVENT_TORRENT_APPROVE_DELETE = 1101;
public const EVENT_TORRENT_LOCALES_ADD = 2200;
public const EVENT_TORRENT_LOCALES_DELETE = 2201;
public const EVENT_TORRENT_LOCALES_APPROVE_ADD = 2210;
public const EVENT_TORRENT_LOCALES_APPROVE_DELETE = 2211;
public const EVENT_TORRENT_SENSITIVE_ADD = 2300;
public const EVENT_TORRENT_SENSITIVE_DELETE = 2301;
public const EVENT_TORRENT_SENSITIVE_APPROVE_ADD = 2310;
public const EVENT_TORRENT_SENSITIVE_APPROVE_DELETE = 2311;
public const EVENT_TORRENT_STAR_ADD = 2400;
public const EVENT_TORRENT_STAR_DELETE = 2401;
public const EVENT_TORRENT_DOWNLOAD_FILE_ADD = 2500;
public const EVENT_TORRENT_DOWNLOAD_MAGNET_ADD = 2600;
public const EVENT_TORRENT_WANTED_ADD = 2700;
public const EVENT_TORRENT_STATUS_ADD = 1800;
public const EVENT_TORRENT_STATUS_DELETE = 1801;
public const EVENT_TORRENT_POSTER_ADD = 2800;
public const EVENT_TORRENT_POSTER_DELETE = 2801;
public const EVENT_TORRENT_POSTER_APPROVE_ADD = 2810;
public const EVENT_TORRENT_POSTER_APPROVE_DELETE = 2811;
public const EVENT_TORRENT_CATEGORIES_ADD = 2900;
public const EVENT_TORRENT_CATEGORIES_DELETE = 2901;
public const EVENT_TORRENT_CATEGORIES_APPROVE_ADD = 2910;
public const EVENT_TORRENT_CATEGORIES_APPROVE_DELETE = 2911;
// ...
#[ORM\Column]
private ?int $userId = null;
#[ORM\Column(nullable: true)]
private ?int $torrentId = null;
#[ORM\Column]
private ?int $added = null;
#[ORM\Column(type: Types::ARRAY)]
private array $data = [];
public function getId(): ?int
{
return $this->id;
}
public function setId(string $id): static
{
$this->id = $id;
return $this;
}
public function getEvent(): ?int
{
return $this->event;
}
public function setEvent(int $event): static
{
$this->event = $event;
return $this;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function setUserId(?int $userId): static
{
$this->userId = $userId;
return $this;
}
public function getTorrentId(): ?int
{
return $this->torrentId;
}
public function setTorrentId(?int $torrentId): static
{
$this->torrentId = $torrentId;
return $this;
}
public function getAdded(): ?int
{
return $this->added;
}
public function setAdded(int $added): static
{
$this->added = $added;
return $this;
}
public function getData(): array
{
return $this->data;
}
public function setData(array $data): static
{
$this->data = $data;
return $this;
}
}

239
src/Entity/Torrent.php Normal file
View file

@ -0,0 +1,239 @@
<?php
namespace App\Entity;
use App\Repository\TorrentRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: TorrentRepository::class)]
class Torrent
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column]
private ?int $userId = null;
#[ORM\Column]
private ?int $added = null;
#[ORM\Column(nullable: true)]
private ?int $scraped = null;
#[ORM\Column(type: Types::SIMPLE_ARRAY)]
private array $locales = [];
#[ORM\Column]
private ?bool $sensitive = null;
#[ORM\Column]
private ?bool $approved = null;
#[ORM\Column]
private ?bool $status = null;
#[ORM\Column(length: 32)]
private ?string $md5file = null;
#[ORM\Column(type: Types::SIMPLE_ARRAY, nullable: true)]
private ?array $keywords = null;
#[ORM\Column(nullable: true)]
private ?int $seeders = null;
#[ORM\Column(nullable: true)]
private ?int $peers = null;
#[ORM\Column(nullable: true)]
private ?int $leechers = null;
#[ORM\Column(nullable: true)]
private ?int $torrentPosterId = null;
#[ORM\Column(type: Types::SIMPLE_ARRAY)]
private ?array $categories = null;
public function getId(): ?int
{
return $this->id;
}
public function setId(string $id): static
{
$this->id = $id;
return $this;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function setUserId(int $userId): static
{
$this->userId = $userId;
return $this;
}
public function getAdded(): ?int
{
return $this->added;
}
public function setAdded(int $added): static
{
$this->added = $added;
return $this;
}
public function getScraped(): ?int
{
return $this->scraped;
}
public function setScraped(int $scraped): static
{
$this->scraped = $scraped;
return $this;
}
public function getMd5file(): ?string
{
return $this->md5file;
}
public function setMd5file(string $md5file): static
{
$this->md5file = $md5file;
return $this;
}
public function getKeywords(): ?array
{
return $this->keywords;
}
public function setKeywords(?array $keywords): static
{
$this->keywords = $keywords;
return $this;
}
public function getLocales(): array
{
return $this->locales;
}
public function setLocales(array $locales): static
{
$this->locales = $locales;
return $this;
}
public function isSensitive(): ?bool
{
return $this->sensitive;
}
public function setSensitive(bool $sensitive): static
{
$this->sensitive = $sensitive;
return $this;
}
public function isApproved(): ?bool
{
return $this->approved;
}
public function setApproved(bool $approved): static
{
$this->approved = $approved;
return $this;
}
public function isStatus(): ?bool
{
return $this->status;
}
public function setStatus(bool $status): static
{
$this->status = $status;
return $this;
}
public function getSeeders(): ?int
{
return $this->seeders;
}
public function setSeeders(?int $seeders): static
{
$this->seeders = $seeders;
return $this;
}
public function getPeers(): ?int
{
return $this->peers;
}
public function setPeers(?int $peers): static
{
$this->peers = $peers;
return $this;
}
public function getLeechers(): ?int
{
return $this->leechers;
}
public function setLeechers(?int $leechers): static
{
$this->leechers = $leechers;
return $this;
}
public function getTorrentPosterId(): ?int
{
return $this->torrentPosterId;
}
public function setTorrentPosterId(?int $torrentPosterId): static
{
$this->torrentPosterId = $torrentPosterId;
return $this;
}
public function getCategories(): ?array
{
return $this->categories;
}
public function setCategories(?array $categories): static
{
$this->categories = $categories;
return $this;
}
}

View file

@ -0,0 +1,103 @@
<?php
namespace App\Entity;
use App\Repository\TorrentCategoriesRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: TorrentCategoriesRepository::class)]
class TorrentCategories
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column]
private ?int $torrentId = null;
#[ORM\Column]
private ?int $userId = null;
#[ORM\Column]
private ?int $added = null;
#[ORM\Column(type: Types::SIMPLE_ARRAY)]
private array $value = [];
#[ORM\Column]
private ?bool $approved = null;
public function getId(): ?int
{
return $this->id;
}
public function setId(string $id): static
{
$this->id = $id;
return $this;
}
public function getTorrentId(): ?int
{
return $this->torrentId;
}
public function setTorrentId(int $torrentId): static
{
$this->torrentId = $torrentId;
return $this;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function setUserId(int $userId): static
{
$this->userId = $userId;
return $this;
}
public function getAdded(): ?int
{
return $this->added;
}
public function setAdded(int $added): static
{
$this->added = $added;
return $this;
}
public function getValue(): array
{
return $this->value;
}
public function setValue(array $value): static
{
$this->value = $value;
return $this;
}
public function isApproved(): ?bool
{
return $this->approved;
}
public function setApproved(bool $approved): static
{
$this->approved = $approved;
return $this;
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace App\Entity;
use App\Repository\TorrentDownloadFileRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: TorrentDownloadFileRepository::class)]
class TorrentDownloadFile
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column]
private ?int $torrentId = null;
#[ORM\Column]
private ?int $userId = null;
#[ORM\Column]
private ?int $added = null;
public function getId(): ?int
{
return $this->id;
}
public function setId(string $id): static
{
$this->id = $id;
return $this;
}
public function getTorrentId(): ?int
{
return $this->torrentId;
}
public function setTorrentId(int $torrentId): static
{
$this->torrentId = $torrentId;
return $this;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function setUserId(int $userId): static
{
$this->userId = $userId;
return $this;
}
public function getAdded(): ?int
{
return $this->added;
}
public function setAdded(int $added): static
{
$this->added = $added;
return $this;
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace App\Entity;
use App\Repository\TorrentDownloadMagnetRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: TorrentDownloadMagnetRepository::class)]
class TorrentDownloadMagnet
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column]
private ?int $torrentId = null;
#[ORM\Column]
private ?int $userId = null;
#[ORM\Column]
private ?int $added = null;
public function getId(): ?int
{
return $this->id;
}
public function setId(string $id): static
{
$this->id = $id;
return $this;
}
public function getTorrentId(): ?int
{
return $this->torrentId;
}
public function setTorrentId(int $torrentId): static
{
$this->torrentId = $torrentId;
return $this;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function setUserId(int $userId): static
{
$this->userId = $userId;
return $this;
}
public function getAdded(): ?int
{
return $this->added;
}
public function setAdded(int $added): static
{
$this->added = $added;
return $this;
}
}

View file

@ -0,0 +1,103 @@
<?php
namespace App\Entity;
use App\Repository\TorrentLocalesRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: TorrentLocalesRepository::class)]
class TorrentLocales
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column]
private ?int $torrentId = null;
#[ORM\Column]
private ?int $userId = null;
#[ORM\Column]
private ?int $added = null;
#[ORM\Column(type: Types::SIMPLE_ARRAY)]
private array $value = [];
#[ORM\Column]
private ?bool $approved = null;
public function getId(): ?int
{
return $this->id;
}
public function setId(string $id): static
{
$this->id = $id;
return $this;
}
public function getTorrentId(): ?int
{
return $this->torrentId;
}
public function setTorrentId(int $torrentId): static
{
$this->torrentId = $torrentId;
return $this;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function setUserId(int $userId): static
{
$this->userId = $userId;
return $this;
}
public function getAdded(): ?int
{
return $this->added;
}
public function setAdded(int $added): static
{
$this->added = $added;
return $this;
}
public function getValue(): array
{
return $this->value;
}
public function setValue(array $value): static
{
$this->value = $value;
return $this;
}
public function isApproved(): ?bool
{
return $this->approved;
}
public function setApproved(bool $approved): static
{
$this->approved = $approved;
return $this;
}
}

View file

@ -0,0 +1,117 @@
<?php
namespace App\Entity;
use App\Repository\TorrentPosterRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: TorrentPosterRepository::class)]
class TorrentPoster
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column]
private ?int $torrentId = null;
#[ORM\Column]
private ?int $userId = null;
#[ORM\Column]
private ?int $added = null;
#[ORM\Column]
private ?bool $approved = null;
#[ORM\Column(length: 32)]
private ?string $md5file = null;
#[ORM\Column(length: 255)]
private ?string $position = null;
public function getId(): ?int
{
return $this->id;
}
public function setId(string $id): static
{
$this->id = $id;
return $this;
}
public function getTorrentId(): ?int
{
return $this->torrentId;
}
public function setTorrentId(int $torrentId): static
{
$this->torrentId = $torrentId;
return $this;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function setUserId(int $userId): static
{
$this->userId = $userId;
return $this;
}
public function getAdded(): ?int
{
return $this->added;
}
public function setAdded(int $added): static
{
$this->added = $added;
return $this;
}
public function isApproved(): ?bool
{
return $this->approved;
}
public function setApproved(bool $approved): static
{
$this->approved = $approved;
return $this;
}
public function getMd5file(): ?string
{
return $this->md5file;
}
public function setMd5file(string $md5file): static
{
$this->md5file = $md5file;
return $this;
}
public function getPosition(): ?string
{
return $this->position;
}
public function setPosition(string $position): static
{
$this->position = $position;
return $this;
}
}

View file

@ -0,0 +1,103 @@
<?php
namespace App\Entity;
use App\Repository\TorrentSensitiveRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: TorrentSensitiveRepository::class)]
class TorrentSensitive
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column]
private ?int $torrentId = null;
#[ORM\Column]
private ?int $userId = null;
#[ORM\Column]
private ?int $added = null;
#[ORM\Column]
private ?bool $value = null;
#[ORM\Column]
private ?bool $approved = null;
public function getId(): ?int
{
return $this->id;
}
public function setId(string $id): static
{
$this->id = $id;
return $this;
}
public function getTorrentId(): ?int
{
return $this->torrentId;
}
public function setTorrentId(int $torrentId): static
{
$this->torrentId = $torrentId;
return $this;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function setUserId(int $userId): static
{
$this->userId = $userId;
return $this;
}
public function getAdded(): ?int
{
return $this->added;
}
public function setAdded(int $added): static
{
$this->added = $added;
return $this;
}
public function isValue(): ?bool
{
return $this->value;
}
public function setValue(bool $value): static
{
$this->value = $value;
return $this;
}
public function isApproved(): ?bool
{
return $this->approved;
}
public function setApproved(bool $approved): static
{
$this->approved = $approved;
return $this;
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace App\Entity;
use App\Repository\TorrentStarRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: TorrentStarRepository::class)]
class TorrentStar
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column]
private ?int $torrentId = null;
#[ORM\Column]
private ?int $userId = null;
#[ORM\Column]
private ?int $added = null;
public function getId(): ?int
{
return $this->id;
}
public function getTorrentId(): ?int
{
return $this->torrentId;
}
public function setTorrentId(int $torrentId): static
{
$this->torrentId = $torrentId;
return $this;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function setUserId(int $userId): static
{
$this->userId = $userId;
return $this;
}
public function getAdded(): ?int
{
return $this->added;
}
public function setAdded(int $added): static
{
$this->added = $added;
return $this;
}
}

223
src/Entity/User.php Normal file
View file

@ -0,0 +1,223 @@
<?php
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: UserRepository::class)]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $address = null;
#[ORM\Column]
private ?int $added = null;
#[ORM\Column]
private ?bool $moderator = null;
#[ORM\Column]
private ?bool $approved = null;
#[ORM\Column]
private ?bool $status = null;
#[ORM\Column(length: 2)]
private ?string $locale = null;
#[ORM\Column(type: Types::SIMPLE_ARRAY)]
private array $locales = [];
#[ORM\Column(type: Types::SIMPLE_ARRAY)]
private array $events = [];
#[ORM\Column(length: 255)]
private ?string $theme = null;
#[ORM\Column]
private ?bool $sensitive = null;
#[ORM\Column]
private ?bool $yggdrasil = null;
#[ORM\Column]
private ?bool $posters = null;
#[ORM\Column(type: Types::SIMPLE_ARRAY)]
private ?array $categories = null;
public function getId(): ?int
{
return $this->id;
}
public function setId(string $id): static
{
$this->id = $id;
return $this;
}
public function getAddress(): ?string
{
return $this->address;
}
public function setAddress(string $address): static
{
$this->address = $address;
return $this;
}
public function getAdded(): ?int
{
return $this->added;
}
public function setAdded(int $added): static
{
$this->added = $added;
return $this;
}
public function isModerator(): ?bool
{
return $this->moderator;
}
public function setModerator(bool $moderator): static
{
$this->moderator = $moderator;
return $this;
}
public function isApproved(): ?bool
{
return $this->approved;
}
public function setApproved(bool $approved): static
{
$this->approved = $approved;
return $this;
}
public function isStatus(): ?bool
{
return $this->status;
}
public function setStatus(bool $status): static
{
$this->status = $status;
return $this;
}
public function getLocale(): ?string
{
return $this->locale;
}
public function setLocale(string $locale): static
{
$this->locale = $locale;
return $this;
}
public function getLocales(): array
{
return $this->locales;
}
public function setLocales(array $locales): static
{
$this->locales = $locales;
return $this;
}
public function getEvents(): array
{
return $this->events;
}
public function setEvents(array $events): static
{
$this->events = $events;
return $this;
}
public function getTheme(): ?string
{
return $this->theme;
}
public function setTheme(string $theme): static
{
$this->theme = $theme;
return $this;
}
public function isSensitive(): ?bool
{
return $this->sensitive;
}
public function setSensitive(bool $sensitive): static
{
$this->sensitive = $sensitive;
return $this;
}
public function isYggdrasil(): ?bool
{
return $this->yggdrasil;
}
public function setYggdrasil(bool $yggdrasil): static
{
$this->yggdrasil = $yggdrasil;
return $this;
}
public function isPosters(): ?bool
{
return $this->posters;
}
public function setPosters(bool $posters): static
{
$this->posters = $posters;
return $this;
}
public function getCategories(): ?array
{
return $this->categories;
}
public function setCategories(?array $categories): static
{
$this->categories = $categories;
return $this;
}
}

65
src/Entity/UserStar.php Normal file
View file

@ -0,0 +1,65 @@
<?php
namespace App\Entity;
use App\Repository\UserStarRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: UserStarRepository::class)]
class UserStar
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column]
private ?int $userId = null;
#[ORM\Column]
private ?int $userIdTarget = null;
#[ORM\Column]
private ?int $added = null;
public function getId(): ?int
{
return $this->id;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function setUserId(int $userId): static
{
$this->userId = $userId;
return $this;
}
public function getUserIdTarget(): ?int
{
return $this->userIdTarget;
}
public function setUserIdTarget(int $userIdTarget): static
{
$this->userIdTarget = $userIdTarget;
return $this;
}
public function getAdded(): ?int
{
return $this->added;
}
public function setAdded(int $added): static
{
$this->added = $added;
return $this;
}
}

11
src/Kernel.php Normal file
View file

@ -0,0 +1,11 @@
<?php
namespace App;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
}

0
src/Repository/.gitignore vendored Normal file
View file

View file

@ -0,0 +1,68 @@
<?php
namespace App\Repository;
use App\Entity\Activity;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Activity>
*
* @method Activity|null find($id, $lockMode = null, $lockVersion = null)
* @method Activity|null findOneBy(array $criteria, array $orderBy = null)
* @method Activity[] findAll()
* @method Activity[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class ActivityRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Activity::class);
}
public function findActivitiesTotal(
array $whitelist
): int
{
return $this->createQueryBuilder('a')
->select('count(a.id)')
->where('a.event IN (:event)')
->setParameter(':event', $whitelist)
->getQuery()
->getSingleScalarResult()
;
}
public function findActivitiesTotalByUserId(
int $userId,
array $whitelist
): int
{
return $this->createQueryBuilder('a')
->select('count(a.id)')
->where('a.userId = :userId')
->andWhere('a.event IN (:event)')
->setParameter(':userId', $userId)
->setParameter(':event', $whitelist)
->getQuery()
->getSingleScalarResult()
;
}
public function findActivitiesTotalByTorrentId(
int $torrentId,
array $whitelist
): int
{
return $this->createQueryBuilder('a')
->select('count(a.id)')
->where('a.torrentId = :torrentId')
->andWhere('a.event IN (:event)')
->setParameter(':torrentId', $torrentId)
->setParameter(':event', $whitelist)
->getQuery()
->getSingleScalarResult()
;
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace App\Repository;
use App\Entity\TorrentCategories;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<TorrentCategories>
*
* @method TorrentCategories|null find($id, $lockMode = null, $lockVersion = null)
* @method TorrentCategories|null findOneBy(array $criteria, array $orderBy = null)
* @method TorrentCategories[] findAll()
* @method TorrentCategories[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class TorrentCategoriesRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, TorrentCategories::class);
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace App\Repository;
use App\Entity\TorrentDownloadFile;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<TorrentDownloadFile>
*
* @method TorrentDownloadFile|null find($id, $lockMode = null, $lockVersion = null)
* @method TorrentDownloadFile|null findOneBy(array $criteria, array $orderBy = null)
* @method TorrentDownloadFile[] findAll()
* @method TorrentDownloadFile[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class TorrentDownloadFileRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, TorrentDownloadFile::class);
}
public function findTorrentDownloadFilesTotalByTorrentId(
int $torrentId
): int
{
return $this->createQueryBuilder('tdf')
->select('count(tdf.id)')
->where('tdf.torrentId = :torrentId')
->setParameter('torrentId', $torrentId)
->getQuery()
->getSingleScalarResult()
;
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace App\Repository;
use App\Entity\TorrentDownloadMagnet;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<TorrentDownloadMagnet>
*
* @method TorrentDownloadMagnet|null find($id, $lockMode = null, $lockVersion = null)
* @method TorrentDownloadMagnet|null findOneBy(array $criteria, array $orderBy = null)
* @method TorrentDownloadMagnet[] findAll()
* @method TorrentDownloadMagnet[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class TorrentDownloadMagnetRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, TorrentDownloadMagnet::class);
}
public function findTorrentDownloadMagnetsTotalByTorrentId(
int $torrentId
): int
{
return $this->createQueryBuilder('tdm')
->select('count(tdm.id)')
->where('tdm.torrentId = :torrentId')
->setParameter('torrentId', $torrentId)
->getQuery()
->getSingleScalarResult()
;
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace App\Repository;
use App\Entity\TorrentLocales;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<TorrentLocales>
*
* @method TorrentLocales|null find($id, $lockMode = null, $lockVersion = null)
* @method TorrentLocales|null findOneBy(array $criteria, array $orderBy = null)
* @method TorrentLocales[] findAll()
* @method TorrentLocales[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class TorrentLocalesRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, TorrentLocales::class);
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace App\Repository;
use App\Entity\TorrentPoster;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<TorrentPoster>
*
* @method TorrentPoster|null find($id, $lockMode = null, $lockVersion = null)
* @method TorrentPoster|null findOneBy(array $criteria, array $orderBy = null)
* @method TorrentPoster[] findAll()
* @method TorrentPoster[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class TorrentPosterRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, TorrentPoster::class);
}
}

View file

@ -0,0 +1,241 @@
<?php
namespace App\Repository;
use App\Entity\Torrent;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Torrent>
*
* @method Torrent|null find($id, $lockMode = null, $lockVersion = null)
* @method Torrent|null findOneBy(array $criteria, array $orderBy = null)
* @method Torrent[] findAll()
* @method Torrent[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class TorrentRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Torrent::class);
}
public function findTorrentsTotal(
int $userId,
array $keywords,
?array $locales,
?array $categories,
?bool $sensitive = null,
?bool $approved = null,
?bool $status = null,
int $limit = 10,
int $offset = 0
): int
{
return $this->getTorrentsQueryByFilter(
$userId,
$keywords,
$locales,
$categories,
$sensitive,
$approved,
$status,
)->select('count(t.id)')
->getQuery()
->getSingleScalarResult();
}
public function findTorrents(
int $userId,
array $keywords,
?array $locales,
?array $categories,
?bool $sensitive = null,
?bool $approved = null,
?bool $status = null,
int $limit = 10,
int $offset = 0
): array
{
return $this->getTorrentsQueryByFilter(
$userId,
$keywords,
$locales,
$categories,
$sensitive,
$approved,
$status,
)->setMaxResults($limit)
->setFirstResult($offset)
->orderBy('t.id', 'DESC') // same as t.added
->getQuery()
->getResult();
}
private function getTorrentsQueryByFilter(
int $userId,
?array $keywords,
?array $locales,
?array $categories,
?bool $sensitive = null,
?bool $approved = null,
?bool $status = null
): \Doctrine\ORM\QueryBuilder
{
$query = $this->createQueryBuilder('t');
if (is_array($keywords))
{
foreach ($keywords as $i => $keyword)
{
// Make query to the index case insensitive
$keyword = mb_strtolower($keyword);
// Init OR condition for each word form
$orKeywords = $query->expr()->orX();
$orKeywords->add("t.keywords LIKE :keyword{$i}");
$query->setParameter(":keyword{$i}", "%{$keyword}%");
// Generate word forms for each transliteration locale #33
foreach ($this->generateWordForms($keyword) as $j => $wordForm)
{
$orKeywords->add("t.keywords LIKE :keyword{$i}{$j}");
$query->setParameter(":keyword{$i}{$j}", "%{$wordForm}%");
}
// Append AND condition
$query->andWhere($orKeywords);
}
}
if (is_array($locales))
{
$orLocales = $query->expr()->orX();
foreach ($locales as $i => $locale)
{
$orLocales->add("t.locales LIKE :locale{$i}");
$orLocales->add("t.userId = :userId");
$query->setParameter(":locale{$i}", "%{$locale}%");
$query->setParameter('userId', $userId);
}
$query->andWhere($orLocales);
}
if (is_array($categories))
{
$orCategories = $query->expr()->orX();
foreach ($categories as $i => $category)
{
$orCategories->add("t.categories LIKE :category{$i}");
$orCategories->add("t.userId = :userId");
$query->setParameter(":category{$i}", "%{$category}%");
$query->setParameter('userId', $userId);
}
$query->andWhere($orCategories);
}
if (is_bool($sensitive))
{
$orSensitive = $query->expr()->orX();
$orSensitive->add("t.sensitive = :sensitive");
$orSensitive->add("t.userId = :userId");
$query->setParameter('sensitive', $sensitive);
$query->setParameter('userId', $userId);
$query->andWhere($orSensitive);
}
if (is_bool($approved))
{
$orApproved = $query->expr()->orX();
$orApproved->add("t.approved = :approved");
$orApproved->add("t.userId = :userId");
$query->setParameter('approved', $approved);
$query->setParameter('userId', $userId);
$query->andWhere($orApproved);
}
if (is_bool($status))
{
$orStatus = $query->expr()->orX();
$orStatus->add("t.status = :status");
$orStatus->add("t.userId = :userId");
$query->setParameter('status', $status);
$query->setParameter('userId', $userId);
$query->andWhere($orStatus);
}
return $query;
}
// Word forms generator to improve search results
// e.g. transliteration rules for latin filenames
private function generateWordForms(
string $keyword,
// #33 supported locales:
// https://github.com/ashtokalo/php-translit
array $transliteration = [
'be',
'bg',
'el',
'hy',
'kk',
'mk',
'ru',
'ka',
'uk'
],
// Additional char forms
array $charForms =
[
'c' => 'k',
'k' => 'c',
]
): array
{
$wordForms = [];
// Apply transliteration
foreach ($transliteration as $locale)
{
$wordForms[] = \ashtokalo\translit\Translit::object()->convert(
$keyword,
$locale
);
}
// Apply char forms
foreach ($wordForms as $wordForm)
{
foreach ($charForms as $from => $to)
{
$wordForms[] = str_replace(
$from,
$to,
$wordForm
);
}
}
// Remove duplicates
return array_unique(
$wordForms
);
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace App\Repository;
use App\Entity\TorrentSensitive;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<TorrentSensitive>
*
* @method TorrentSensitive|null find($id, $lockMode = null, $lockVersion = null)
* @method TorrentSensitive|null findOneBy(array $criteria, array $orderBy = null)
* @method TorrentSensitive[] findAll()
* @method TorrentSensitive[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class TorrentSensitiveRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, TorrentSensitive::class);
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace App\Repository;
use App\Entity\TorrentStar;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<TorrentStar>
*
* @method TorrentStar|null find($id, $lockMode = null, $lockVersion = null)
* @method TorrentStar|null findOneBy(array $criteria, array $orderBy = null)
* @method TorrentStar[] findAll()
* @method TorrentStar[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class TorrentStarRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, TorrentStar::class);
}
public function findTorrentStarsTotalByTorrentId(
int $torrentId
): int
{
return $this->createQueryBuilder('ts')
->select('count(ts.id)')
->where('ts.torrentId = :torrentId')
->setParameter('torrentId', $torrentId)
->getQuery()
->getSingleScalarResult()
;
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace App\Repository;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<User>
*
* @method User|null find($id, $lockMode = null, $lockVersion = null)
* @method User|null findOneBy(array $criteria, array $orderBy = null)
* @method User[] findAll()
* @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class UserRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, User::class);
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace App\Repository;
use App\Entity\UserStar;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<UserStar>
*
* @method UserStar|null find($id, $lockMode = null, $lockVersion = null)
* @method UserStar|null findOneBy(array $criteria, array $orderBy = null)
* @method UserStar[] findAll()
* @method UserStar[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class UserStarRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, UserStar::class);
}
public function findUserStarsTotalByUserIdTarget(
int $userIdTarget
): int
{
return $this->createQueryBuilder('us')
->select('count(us.userId)')
->where('us.userIdTarget = :userIdTarget')
->setParameter('userIdTarget', $userIdTarget)
->getQuery()
->getSingleScalarResult()
;
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

256
src/Service/UserService.php Normal file
View file

@ -0,0 +1,256 @@
<?php
namespace App\Service;
use App\Entity\User;
use App\Entity\UserStar;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
class UserService
{
private EntityManagerInterface $entityManagerInterface;
private ParameterBagInterface $parameterBagInterface;
public function __construct(
EntityManagerInterface $entityManagerInterface,
ParameterBagInterface $parameterBagInterface
)
{
$this->entityManagerInterface = $entityManagerInterface;
$this->parameterBagInterface = $parameterBagInterface;
}
public function addUser(
string $address,
string $added,
string $locale,
array $locales,
array $events,
string $theme,
bool $sensitive = true,
bool $yggdrasil = true,
bool $posters = true,
bool $approved = false,
bool $moderator = false,
bool $status = true
): ?User
{
// Create new user
$user = new User();
$user->setAddress(
$address
);
$user->setAdded(
$added
);
$user->setApproved(
$approved
);
$user->setModerator(
$moderator
);
$user->setStatus(
$status
);
$user->setLocale(
$locale
);
$user->setLocales(
$locales
);
$user->setTheme(
$theme
);
$user->setEvents(
$events
);
$user->setSensitive(
$sensitive
);
$user->setYggdrasil(
$yggdrasil
);
$user->setPosters(
$posters
);
$this->entityManagerInterface->persist($user);
$this->entityManagerInterface->flush();
// Set initial user as approved & moderator
if (1 === $user->getId())
{
$user->setApproved(true);
$user->setModerator(true);
$user->setSensitive(false);
$this->entityManagerInterface->persist($user);
$this->entityManagerInterface->flush();
}
// Return user data
return $user;
}
public function getUser(int $userId): ?User
{
return $this->entityManagerInterface
->getRepository(User::class)
->find($userId);
}
public function findUserByAddress(string $address): ?User
{
return $this->entityManagerInterface
->getRepository(User::class)
->findOneBy(
[
'address' => $address
]
);
}
public function identicon(
mixed $value,
int $size = 16,
array $style =
[
'backgroundColor' => 'rgba(255, 255, 255, 0)',
'padding' => 0
],
string $format = 'webp'
): string
{
$identicon = new \Jdenticon\Identicon();
$identicon->setValue($value);
$identicon->setSize($size);
$identicon->setStyle($style);
return $identicon->getImageDataUri($format);
}
public function save(User $user) : void // @TODO delete
{
$this->entityManagerInterface->persist($user);
$this->entityManagerInterface->flush();
}
// User star
public function findUserStar(
int $userId,
int $userIdTarget
): ?UserStar
{
return $this->entityManagerInterface
->getRepository(UserStar::class)
->findOneBy(
[
'userId' => $userId,
'userIdTarget' => $userIdTarget
]
);
}
public function findUserStarsTotalByUserIdTarget(int $torrentId): int
{
return $this->entityManagerInterface
->getRepository(UserStar::class)
->findUserStarsTotalByUserIdTarget($torrentId);
}
public function toggleUserStar(
int $userId,
int $userIdTarget,
int $added
): bool
{
if ($userStar = $this->findUserStar($userId, $userIdTarget))
{
$this->entityManagerInterface->remove($userStar);
$this->entityManagerInterface->flush();
return false;
}
else
{
$userStar = new UserStar();
$userStar->setUserId($userId);
$userStar->setUserIdTarget($userIdTarget);
$userStar->setAdded($added);
$this->entityManagerInterface->persist($userStar);
$this->entityManagerInterface->flush();
return true;
}
}
public function toggleUserModerator(
int $userId
): ?User
{
if ($user = $this->getUser($userId))
{
$user->setModerator(
!$user->isModerator()
);
$this->entityManagerInterface->persist($user);
$this->entityManagerInterface->flush();
}
return $user;
}
public function toggleUserStatus(
int $userId
): ?User
{
if ($user = $this->getUser($userId))
{
$user->setStatus(
!$user->isStatus()
);
$this->entityManagerInterface->persist($user);
$this->entityManagerInterface->flush();
}
return $user;
}
public function toggleUserApproved(
int $userId
): ?User
{
if ($user = $this->getUser($userId))
{
$user->setApproved(
!$user->isApproved()
);
$this->entityManagerInterface->persist($user);
$this->entityManagerInterface->flush();
}
return $user;
}
}

192
src/Twig/AppExtension.php Normal file
View file

@ -0,0 +1,192 @@
<?php
namespace App\Twig;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
class AppExtension extends AbstractExtension
{
protected ContainerInterface $container;
protected TranslatorInterface $translator;
public function __construct(
ContainerInterface $container,
TranslatorInterface $translator
)
{
$this->container = $container;
$this->translator = $translator;
}
public function getFilters()
{
return
[
new TwigFilter(
'format_bytes',
[
$this,
'formatBytes'
]
),
new TwigFilter(
'format_ago',
[
$this,
'formatAgo'
]
),
new TwigFilter(
'url_to_markdown',
[
$this,
'urlToMarkdown'
]
),
new TwigFilter(
'trans_category',
[
$this,
'transCategory'
]
),
];
}
public function formatBytes(
int $bytes,
int $precision = 2
): string
{
$size = [
$this->translator->trans('B'),
$this->translator->trans('Kb'),
$this->translator->trans('Mb'),
$this->translator->trans('Gb'),
$this->translator->trans('Tb'),
$this->translator->trans('Pb'),
$this->translator->trans('Eb'),
$this->translator->trans('Zb'),
$this->translator->trans('Yb')
];
$factor = floor((strlen($bytes) - 1) / 3);
return sprintf("%.{$precision}f", $bytes / pow(1024, $factor)) . ' ' . @$size[$factor];
}
public function formatAgo(
int $time,
): string
{
$diff = time() - $time;
if ($diff < 1)
{
return $this->translator->trans('now');
}
$values =
[
365 * 24 * 60 * 60 =>
[
$this->translator->trans('year ago'),
$this->translator->trans('years ago'),
$this->translator->trans(' years ago')
],
30 * 24 * 60 * 60 =>
[
$this->translator->trans('month ago'),
$this->translator->trans('months ago'),
$this->translator->trans(' months ago')
],
24 * 60 * 60 =>
[
$this->translator->trans('day ago'),
$this->translator->trans('days ago'),
$this->translator->trans(' days ago')
],
60 * 60 =>
[
$this->translator->trans('hour ago'),
$this->translator->trans('hours ago'),
$this->translator->trans(' hours ago')
],
60 =>
[
$this->translator->trans('minute ago'),
$this->translator->trans('minutes ago'),
$this->translator->trans(' minutes ago')
],
1 =>
[
$this->translator->trans('second ago'),
$this->translator->trans('seconds ago'),
$this->translator->trans(' seconds ago')
]
];
foreach ($values as $key => $value)
{
$result = $diff / $key;
if ($result >= 1)
{
$round = round($result);
return sprintf(
'%s %s',
$round,
$this->plural(
$round,
$value
)
);
}
}
}
public function urlToMarkdown(
string $text
): string
{
return preg_replace(
'~(https?://(?:www\.)?[^\(\s\)]+)~i',
'[$1]($1)',
$text
);
}
public function transCategory(
string $name
): string
{
switch ($name)
{
case 'movie': return $this->translator->trans('movie');
case 'series': return $this->translator->trans('series');
case 'tv': return $this->translator->trans('tv');
case 'animation': return $this->translator->trans('animation');
case 'music': return $this->translator->trans('music');
case 'game': return $this->translator->trans('game');
case 'audiobook': return $this->translator->trans('audiobook');
case 'podcast': return $this->translator->trans('podcast');
case 'book': return $this->translator->trans('book');
case 'archive': return $this->translator->trans('archive');
case 'picture': return $this->translator->trans('picture');
case 'software': return $this->translator->trans('software');
case 'other': return $this->translator->trans('other');
default: return $name;
}
}
private function plural(int $number, array $texts)
{
$cases = [2, 0, 1, 1, 1, 2];
return $texts[(($number % 100) > 4 && ($number % 100) < 20) ? 2 : $cases[min($number % 10, 5)]];
}
}

View file

@ -1,83 +0,0 @@
<?php
// PHP
declare(strict_types=1);
// Init environment
if (!file_exists(__DIR__ . '/.env'))
{
if ($handle = fopen(__DIR__ . '/.env', 'w+'))
{
fwrite($handle, 'default');
fclose($handle);
chmod(__DIR__ . '/.env', 0770);
}
else exit (_('Could not init environment file. Please check permissions.'));
}
define('PHP_ENV', file_get_contents(__DIR__ . '/.env'));
// Init config
if (!file_exists(__DIR__ . '/env.' . PHP_ENV . '.php'))
{
if (copy(__DIR__ . '/../../example/environment/env.example.php',
__DIR__ . '/env.' . PHP_ENV . '.php'))
{
chmod(__DIR__ . '/env.' . PHP_ENV . '.php', 0770);
}
else exit (_('Could not init configuration file. Please check permissions.'));
}
// Load environment
require_once __DIR__ . '/env.' . PHP_ENV . '.php';
// Local internal dependencies
require_once __DIR__ . '/../library/database.php';
require_once __DIR__ . '/../library/sphinx.php';
require_once __DIR__ . '/../library/scrapeer.php';
require_once __DIR__ . '/../library/time.php';
require_once __DIR__ . '/../library/curl.php';
require_once __DIR__ . '/../library/valid.php';
require_once __DIR__ . '/../library/filter.php';
// Vendors autoload
require_once __DIR__ . '/../../vendor/autoload.php';
// Connect database
try {
$db = new Database(DB_HOST, DB_PORT, DB_NAME, DB_USERNAME, DB_PASSWORD);
} catch (Exception $e) {
var_dump($e);
exit;
}
// Connect Sphinx
try {
$sphinx = new Sphinx(SPHINX_HOST, SPHINX_PORT);
} catch(Exception $e) {
var_dump($e);
exit;
}
// Connect memcached
try {
$memory = new Yggverse\Cache\Memory(MEMCACHED_HOST, MEMCACHED_PORT, MEMCACHED_NAMESPACE, MEMCACHED_TIMEOUT + time());
} catch(Exception $e) {
var_dump($e);
exit;
}

View file

@ -1,12 +0,0 @@
[
{
"description":"YGGtracker instance #1 with main branch updates",
"url":"http://[201:23b4:991a:634d:8359:4521:5576:15b7]/yggtracker",
"manifest":"http://[201:23b4:991a:634d:8359:4521:5576:15b7]/yggtracker/api/manifest.json"
},
{
"description":"YGGtracker instance #2 with main branch updates",
"url":"http://[200:e6fd:bb3c:b354:cd3a:f939:753e:cd72]/yggtracker",
"manifest":"http://[200:e6fd:bb3c:b354:cd3a:f939:753e:cd72]/yggtracker/api/manifest.json"
}
]

View file

@ -1,7 +0,0 @@
[
{
"description":"YGGtracker public peer instance without traffic limit",
"url":"http://[201:23b4:991a:634d:8359:4521:5576:15b7]/yggstate",
"address":"tls://94.140.114.241:4708"
}
]

View file

@ -1,23 +0,0 @@
[
{
"description":"YGGtracker instance, yggdrasil-only connections",
"url":"http://[201:23b4:991a:634d:8359:4521:5576:15b7]/yggtracker",
"announce":"http://[201:23b4:991a:634d:8359:4521:5576:15b7]:2023/announce",
"stats":"http://[201:23b4:991a:634d:8359:4521:5576:15b7]:2023/stats",
"scrape":"http://[201:23b4:991a:634d:8359:4521:5576:15b7]:2023/scrape"
},
{
"description":"Yggdrasil-only torrent tracker, operated by jeff",
"url":false,
"announce":"http://[200:1e2f:e608:eb3a:2bf:1e62:87ba:e2f7]/announce",
"stats":"http://[200:1e2f:e608:eb3a:2bf:1e62:87ba:e2f7]/stats",
"scrape":"http://[200:1e2f:e608:eb3a:2bf:1e62:87ba:e2f7]/scrape"
},
{
"description":"Yggdrasil torrent tracker, operated by R4SAS",
"url":false,
"announce":"http://[316:c51a:62a3:8b9::5]/announce",
"stats":"http://[316:c51a:62a3:8b9::5]/stats",
"scrape":"http://[316:c51a:62a3:8b9::5]/scrape"
}
]

View file

@ -1,558 +0,0 @@
<?php
// Lock multi-thread execution
$semaphore = sem_get(crc32('yggtracker.crontab.export.feed'), 1);
if (false === sem_acquire($semaphore, true))
{
exit (_('yggtracker.crontab.export.feed process locked by another thread.'));
}
// Bootstrap
require_once __DIR__ . '/../../config/bootstrap.php';
// Init Debug
$debug =
[
'dump' => [],
'time' => [
'ISO8601' => date('c'),
'total' => microtime(true),
],
'http' =>
[
'total' => 0,
],
'memory' =>
[
'start' => memory_get_usage(),
'total' => 0,
'peaks' => 0
],
];
// Define public registry
$public = [
'user' => [],
'magnet' => [],
];
// Begin export
try
{
// Init API folder if not exists
@mkdir(__DIR__ . '/../public/api');
// Delete cached feeds
@unlink(__DIR__ . '/../public/api/manifest.json');
@unlink(__DIR__ . '/../public/api/users.json');
@unlink(__DIR__ . '/../public/api/magnets.json');
@unlink(__DIR__ . '/../public/api/magnetComments.json');
@unlink(__DIR__ . '/../public/api/magnetDownloads.json');
@unlink(__DIR__ . '/../public/api/magnetStars.json');
@unlink(__DIR__ . '/../public/api/magnetViews.json');
if (API_EXPORT_ENABLED)
{
// Manifest
$manifest =
[
'updated' => time(),
'settings' => (object)
[
'YGGDRASIL_HOST_REGEX' => (string) YGGDRASIL_HOST_REGEX,
'NODE_RULE_SUBJECT' => (string) NODE_RULE_SUBJECT,
'NODE_RULE_LANGUAGES' => (string) NODE_RULE_LANGUAGES,
'USER_DEFAULT_APPROVED' => (bool) USER_DEFAULT_APPROVED,
'USER_AUTO_APPROVE_ON_MAGNET_APPROVE' => (bool) USER_AUTO_APPROVE_ON_MAGNET_APPROVE,
'USER_AUTO_APPROVE_ON_COMMENT_APPROVE' => (bool) USER_AUTO_APPROVE_ON_COMMENT_APPROVE,
'USER_DEFAULT_IDENTICON' => (string) USER_DEFAULT_IDENTICON,
'USER_IDENTICON_FIELD' => (string) USER_IDENTICON_FIELD,
'MAGNET_DEFAULT_APPROVED' => (bool) MAGNET_DEFAULT_APPROVED,
'MAGNET_DEFAULT_PUBLIC' => (bool) MAGNET_DEFAULT_PUBLIC,
'MAGNET_DEFAULT_COMMENTS' => (bool) MAGNET_DEFAULT_COMMENTS,
'MAGNET_DEFAULT_SENSITIVE' => (bool) MAGNET_DEFAULT_SENSITIVE,
'MAGNET_EDITOR_LOCK_TIMEOUT' => (int) MAGNET_EDITOR_LOCK_TIMEOUT,
'MAGNET_TITLE_MIN_LENGTH' => (int) MAGNET_TITLE_MIN_LENGTH,
'MAGNET_TITLE_MAX_LENGTH' => (int) MAGNET_TITLE_MAX_LENGTH,
'MAGNET_TITLE_REGEX' => (string) MAGNET_TITLE_REGEX,
'MAGNET_PREVIEW_MIN_LENGTH' => (int) MAGNET_PREVIEW_MIN_LENGTH,
'MAGNET_PREVIEW_MAX_LENGTH' => (int) MAGNET_PREVIEW_MAX_LENGTH,
'MAGNET_PREVIEW_REGEX' => (string) MAGNET_PREVIEW_REGEX,
'MAGNET_DESCRIPTION_MIN_LENGTH' => (int) MAGNET_DESCRIPTION_MIN_LENGTH,
'MAGNET_DESCRIPTION_MAX_LENGTH' => (int) MAGNET_DESCRIPTION_MAX_LENGTH,
'MAGNET_DESCRIPTION_REGEX' => (string) MAGNET_DESCRIPTION_REGEX,
'MAGNET_DN_MIN_LENGTH' => (int) MAGNET_DN_MIN_LENGTH,
'MAGNET_DN_MAX_LENGTH' => (int) MAGNET_DN_MAX_LENGTH,
'MAGNET_DN_REGEX' => (string) MAGNET_DN_REGEX,
'MAGNET_KT_MIN_LENGTH' => (int) MAGNET_KT_MIN_LENGTH,
'MAGNET_KT_MAX_LENGTH' => (int) MAGNET_KT_MAX_LENGTH,
'MAGNET_KT_MIN_QUANTITY' => (int) MAGNET_KT_MIN_QUANTITY,
'MAGNET_KT_MAX_QUANTITY' => (int) MAGNET_KT_MAX_QUANTITY,
'MAGNET_KT_REGEX' => (string) MAGNET_KT_REGEX,
'MAGNET_TR_MIN_QUANTITY' => (int) MAGNET_TR_MIN_QUANTITY,
'MAGNET_TR_MAX_QUANTITY' => (int) MAGNET_TR_MAX_QUANTITY,
'MAGNET_AS_MIN_QUANTITY' => (int) MAGNET_AS_MIN_QUANTITY,
'MAGNET_AS_MAX_QUANTITY' => (int) MAGNET_AS_MAX_QUANTITY,
'MAGNET_WS_MIN_QUANTITY' => (int) MAGNET_WS_MIN_QUANTITY,
'MAGNET_WS_MAX_QUANTITY' => (int) MAGNET_WS_MAX_QUANTITY,
'MAGNET_COMMENT_DEFAULT_APPROVED' => (bool) MAGNET_COMMENT_DEFAULT_APPROVED,
'MAGNET_COMMENT_DEFAULT_PUBLIC' => (bool) MAGNET_COMMENT_DEFAULT_PUBLIC,
'MAGNET_COMMENT_DEFAULT_PUBLIC' => (bool) MAGNET_COMMENT_DEFAULT_PUBLIC,
'MAGNET_COMMENT_MIN_LENGTH' => (int) MAGNET_COMMENT_MIN_LENGTH,
'MAGNET_COMMENT_MAX_LENGTH' => (int) MAGNET_COMMENT_MAX_LENGTH,
'MAGNET_STOP_WORDS_SIMILAR' => (object) MAGNET_STOP_WORDS_SIMILAR,
'API_VERSION' => (string) API_VERSION,
'API_USER_AGENT' => (string) API_USER_AGENT,
'API_EXPORT_ENABLED' => (bool) API_EXPORT_ENABLED,
'API_EXPORT_PUSH_ENABLED' => (bool) API_EXPORT_PUSH_ENABLED,
'API_EXPORT_USERS_ENABLED' => (bool) API_EXPORT_USERS_ENABLED,
'API_EXPORT_MAGNETS_ENABLED' => (bool) API_EXPORT_MAGNETS_ENABLED,
'API_EXPORT_MAGNET_DOWNLOADS_ENABLED' => (bool) API_EXPORT_MAGNET_DOWNLOADS_ENABLED,
'API_EXPORT_MAGNET_COMMENTS_ENABLED' => (bool) API_EXPORT_MAGNET_COMMENTS_ENABLED,
'API_EXPORT_MAGNET_STARS_ENABLED' => (bool) API_EXPORT_MAGNET_STARS_ENABLED,
'API_EXPORT_MAGNET_STARS_ENABLED' => (bool) API_EXPORT_MAGNET_STARS_ENABLED,
'API_EXPORT_MAGNET_VIEWS_ENABLED' => (bool) API_EXPORT_MAGNET_VIEWS_ENABLED,
'API_IMPORT_ENABLED' => (bool) API_IMPORT_ENABLED,
'API_IMPORT_PUSH_ENABLED' => (bool) API_IMPORT_PUSH_ENABLED,
'API_IMPORT_USERS_ENABLED' => (bool) API_IMPORT_USERS_ENABLED,
'API_IMPORT_USERS_APPROVED_ONLY' => (bool) API_IMPORT_USERS_APPROVED_ONLY,
'API_IMPORT_MAGNETS_ENABLED' => (bool) API_IMPORT_MAGNETS_ENABLED,
'API_IMPORT_MAGNETS_APPROVED_ONLY' => (bool) API_IMPORT_MAGNETS_APPROVED_ONLY,
'API_IMPORT_MAGNET_DOWNLOADS_ENABLED' => (bool) API_IMPORT_MAGNET_DOWNLOADS_ENABLED,
'API_IMPORT_MAGNET_COMMENTS_ENABLED' => (bool) API_IMPORT_MAGNET_COMMENTS_ENABLED,
'API_IMPORT_MAGNET_COMMENTS_APPROVED_ONLY' => (bool) API_IMPORT_MAGNET_COMMENTS_APPROVED_ONLY,
'API_IMPORT_MAGNET_STARS_ENABLED' => (bool) API_IMPORT_MAGNET_STARS_ENABLED,
'API_IMPORT_MAGNET_VIEWS_ENABLED' => (bool) API_IMPORT_MAGNET_VIEWS_ENABLED,
],
'totals' => (object)
[
'magnets' => (object)
[
'total' => $db->getMagnetsTotal(),
'distributed' => $db->getMagnetsTotalByUsersPublic(true),
'local' => $db->getMagnetsTotalByUsersPublic(false),
],
'downloads' => (object)
[
'total' => $db->getMagnetDownloadsTotal(),
'distributed' => $db->findMagnetDownloadsTotalByUsersPublic(true),
'local' => $db->findMagnetDownloadsTotalByUsersPublic(false),
],
'comments' => (object)
[
'total' => $db->getMagnetCommentsTotal(),
'distributed' => $db->findMagnetCommentsTotalByUsersPublic(true),
'local' => $db->findMagnetCommentsTotalByUsersPublic(false),
],
'stars' => (object)
[
'total' => $db->getMagnetStarsTotal(),
'distributed' => $db->findMagnetStarsTotalByUsersPublic(true),
'local' => $db->findMagnetStarsTotalByUsersPublic(false),
],
'views' => (object)
[
'total' => $db->getMagnetViewsTotal(),
'distributed' => $db->findMagnetViewsTotalByUsersPublic(true),
'local' => $db->findMagnetViewsTotalByUsersPublic(false),
],
],
'import' => (object)
[
'push' => API_IMPORT_PUSH_ENABLED ? sprintf('%s/api/push.php', WEBSITE_URL) : false,
],
'export' => (object)
[
'users' => API_EXPORT_USERS_ENABLED ? sprintf('%s/api/users.json', WEBSITE_URL) : false,
'magnets' => API_EXPORT_MAGNETS_ENABLED ? sprintf('%s/api/magnets.json', WEBSITE_URL) : false,
'magnetDownloads' => API_EXPORT_MAGNET_DOWNLOADS_ENABLED ? sprintf('%s/api/magnetDownloads.json', WEBSITE_URL) : false,
'magnetComments' => API_EXPORT_MAGNET_COMMENTS_ENABLED ? sprintf('%s/api/magnetComments.json', WEBSITE_URL) : false,
'magnetStars' => API_EXPORT_MAGNET_STARS_ENABLED ? sprintf('%s/api/magnetStars.json', WEBSITE_URL) : false,
'magnetViews' => API_EXPORT_MAGNET_VIEWS_ENABLED ? sprintf('%s/api/magnetViews.json', WEBSITE_URL) : false,
],
'trackers' => (object) json_decode(file_get_contents(__DIR__ . '/../../config/trackers.json')),
'nodes' => (object) json_decode(file_get_contents(__DIR__ . '/../../config/nodes.json')),
'peers' => (object) json_decode(file_get_contents(__DIR__ . '/../../config/peers.json')),
];
/// Dump feed
if ($handle = fopen(__DIR__ . '/../../public/api/manifest.json', 'w+'))
{
fwrite($handle, json_encode($manifest));
fclose($handle);
chmod(__DIR__ . '/../../public/api/manifest.json', 0774);
}
// Users
if (API_EXPORT_USERS_ENABLED)
{
$users = [];
foreach ($db->getUsers() as $user)
{
// Dump public data only
if ($user->public)
{
$users[] = (object)
[
'userId' => (int) $user->userId,
'address' => (string) $user->address,
'timeAdded' => (int) $user->timeAdded,
'timeUpdated' => (int) $user->timeUpdated,
'approved' => (bool) $user->approved,
'magnets' => (int) $db->findMagnetsTotalByUserId($user->userId),
'downloads' => (int) $db->findMagnetDownloadsTotalByUserId($user->userId),
'comments' => (int) $db->findMagnetCommentsTotalByUserId($user->userId),
'stars' => (int) $db->findMagnetStarsTotalByUserId($user->userId),
'views' => (int) $db->findMagnetViewsTotalByUserId($user->userId),
];
}
// Cache public status
$public['user'][$user->userId] = (bool) $user->public;
}
/// Dump users feed
if ($handle = fopen(__DIR__ . '/../../public/api/users.json', 'w+'))
{
fwrite($handle, json_encode($users));
fclose($handle);
chmod(__DIR__ . '/../../public/api/users.json', 0774);
}
}
// Magnets
if (API_EXPORT_MAGNETS_ENABLED)
{
$magnets = [];
foreach ($db->getMagnets($user->userId) as $magnet)
{
// Dump public data only
if ($magnet->public &&
$public['user'][$magnet->userId]) // After upgrade, some users have not updated their public status.
// Remote node have warning on import, because user info still hidden to init new profile there.
// Stop magnets export without public profile available, even magnet is public.
{
// Info Hash
$xt = [];
foreach ($db->findMagnetToInfoHashByMagnetId($magnet->magnetId) as $result)
{
if ($infoHash = $db->getInfoHash($result->infoHashId))
{
$xt[] = (object) [
'version' => (float) $infoHash->version,
'value' => (string) $infoHash->value,
];
}
}
// Keyword Topic
$kt = [];
foreach ($db->findKeywordTopicByMagnetId($magnet->magnetId) as $result)
{
$kt[] = $db->getKeywordTopic($result->keywordTopicId)->value;
}
// Address Tracker
$tr = [];
foreach ($db->findAddressTrackerByMagnetId($magnet->magnetId) as $result)
{
$addressTracker = $db->getAddressTracker($result->addressTrackerId);
$scheme = $db->getScheme($addressTracker->schemeId);
$host = $db->getHost($addressTracker->hostId);
$port = $db->getPort($addressTracker->portId);
$uri = $db->getUri($addressTracker->uriId);
// Yggdrasil host only
if (!Valid::host($host->value))
{
continue;
}
$tr[] = $port->value ? sprintf('%s://%s:%s%s', $scheme->value,
$host->value,
$port->value,
$uri->value) : sprintf('%s://%s%s', $scheme->value,
$host->value,
$uri->value);
}
// Acceptable Source
$as = [];
foreach ($db->findAcceptableSourceByMagnetId($magnet->magnetId) as $result)
{
$acceptableSource = $db->getAcceptableSource($result->acceptableSourceId);
$scheme = $db->getScheme($acceptableSource->schemeId);
$host = $db->getHost($acceptableSource->hostId);
$port = $db->getPort($acceptableSource->portId);
$uri = $db->getUri($acceptableSource->uriId);
// Yggdrasil host only
if (!Valid::host($host->value))
{
continue;
}
$as[] = $port->value ? sprintf('%s://%s:%s%s', $scheme->value,
$host->value,
$port->value,
$uri->value) : sprintf('%s://%s%s', $scheme->value,
$host->value,
$uri->value);
}
// Exact Source
$xs = [];
foreach ($db->findExactSourceByMagnetId($magnet->magnetId) as $result)
{
$eXactSource = $db->getExactSource($result->eXactSourceId);
$scheme = $db->getScheme($eXactSource->schemeId);
$host = $db->getHost($eXactSource->hostId);
$port = $db->getPort($eXactSource->portId);
$uri = $db->getUri($eXactSource->uriId);
// Yggdrasil host only
if (!Valid::host($host->value))
{
continue;
}
$xs[] = $port->value ? sprintf('%s://%s:%s%s', $scheme->value,
$host->value,
$port->value,
$uri->value) : sprintf('%s://%s%s', $scheme->value,
$host->value,
$uri->value);
}
$magnets[] = (object)
[
'magnetId' => (int) $magnet->magnetId,
'userId' => (int) $magnet->userId,
'title' => (string) $magnet->title,
'preview' => (string) $magnet->preview,
'description' => (string) $magnet->description,
'comments' => (bool) $magnet->comments,
'sensitive' => (bool) $magnet->sensitive,
'approved' => (bool) $magnet->approved,
'timeAdded' => (int) $magnet->timeAdded,
'timeUpdated' => (int) $magnet->timeUpdated,
'dn' => (string) $magnet->dn,
'xl' => (float) $magnet->xl,
'xt' => (object) $xt,
'kt' => (object) $kt,
'tr' => (object) $tr,
'as' => (object) $as,
'xs' => (object) $xs,
];
}
// Cache public status
if (!empty($public['user'][$magnet->userId]))
{
$public['magnet'][$magnet->magnetId] = (bool) $magnet->public;
} else {
$public['magnet'][$magnet->magnetId] = false;
}
}
/// Dump magnets feed
if ($handle = fopen(__DIR__ . '/../../public/api/magnets.json', 'w+'))
{
fwrite($handle, json_encode($magnets));
fclose($handle);
chmod(__DIR__ . '/../../public/api/magnets.json', 0774);
}
}
// Magnet downloads
if (API_EXPORT_MAGNET_DOWNLOADS_ENABLED)
{
$magnetDownloads = [];
foreach ($db->getMagnetDownloads() as $magnetDownload)
{
// Dump public data only
if (!empty($public['magnet'][$magnetDownload->magnetId]) &&
!empty($public['user'][$magnetDownload->userId]))
{
$magnetDownloads[] = (object)
[
'magnetDownloadId' => (int) $magnetDownload->magnetDownloadId,
'userId' => (int) $magnetDownload->userId,
'magnetId' => (int) $magnetDownload->magnetId,
'timeAdded' => (int) $magnetDownload->timeAdded,
];
}
}
/// Dump feed
if ($handle = fopen(__DIR__ . '/../../public/api/magnetDownloads.json', 'w+'))
{
fwrite($handle, json_encode($magnetDownloads));
fclose($handle);
chmod(__DIR__ . '/../../public/api/magnetDownloads.json', 0774);
}
}
// Magnet comments
if (API_EXPORT_MAGNET_COMMENTS_ENABLED)
{
$magnetComments = [];
foreach ($db->getMagnetComments() as $magnetComment)
{
// Dump public data only
if (!empty($public['magnet'][$magnetComment->magnetId]) &&
!empty($public['user'][$magnetComment->userId]))
{
$magnetComments[] = (object)
[
'magnetCommentId' => (int) $magnetComment->magnetCommentId,
'magnetCommentIdParent' => $magnetComment->magnetCommentIdParent,
'userId' => (int) $magnetComment->userId,
'magnetId' => (int) $magnetComment->magnetId,
'timeAdded' => (int) $magnetComment->timeAdded,
'approved' => (bool) $magnetComment->approved,
'value' => (string) $magnetComment->value
];
}
}
/// Dump feed
if ($handle = fopen(__DIR__ . '/../../public/api/magnetComments.json', 'w+'))
{
fwrite($handle, json_encode($magnetComments));
fclose($handle);
chmod(__DIR__ . '/../../public/api/magnetComments.json', 0774);
}
}
// Magnet stars
if (API_EXPORT_MAGNET_STARS_ENABLED)
{
$magnetStars = [];
foreach ($db->getMagnetStars() as $magnetStar)
{
// Dump public data only
if (!empty($public['magnet'][$magnetStar->magnetId]) &&
!empty($public['user'][$magnetStar->userId]))
{
$magnetStars[] = (object)
[
'magnetStarId' => (int) $magnetStar->magnetStarId,
'userId' => (int) $magnetStar->userId,
'magnetId' => (int) $magnetStar->magnetId,
'value' => (bool) $magnetStar->value,
'timeAdded' => (int) $magnetStar->timeAdded,
];
}
}
/// Dump feed
if ($handle = fopen(__DIR__ . '/../../public/api/magnetStars.json', 'w+'))
{
fwrite($handle, json_encode($magnetStars));
fclose($handle);
chmod(__DIR__ . '/../../public/api/magnetStars.json', 0774);
}
}
// Magnet views
if (API_EXPORT_MAGNET_VIEWS_ENABLED)
{
$magnetViews = [];
foreach ($db->getMagnetViews() as $magnetView)
{
// Dump public data only
if (!empty($public['magnet'][$magnetView->magnetId]) &&
!empty($public['user'][$magnetView->userId]))
{
$magnetViews[] = (object)
[
'magnetViewId' => (int) $magnetView->magnetViewId,
'userId' => (int) $magnetView->userId,
'magnetId' => (int) $magnetView->magnetId,
'timeAdded' => (int) $magnetView->timeAdded,
];
}
}
/// Dump feed
if ($handle = fopen(__DIR__ . '/../../public/api/magnetViews.json', 'w+'))
{
fwrite($handle, json_encode($magnetViews));
fclose($handle);
chmod(__DIR__ . '/../../public/api/magnetViews.json', 0774);
}
}
}
} catch (EXception $e) {
var_dump($e);
}
// Debug output
$debug['time']['total'] = microtime(true) - $debug['time']['total'];
$debug['memory']['total'] = memory_get_usage() - $debug['memory']['start'];
$debug['memory']['peaks'] = memory_get_peak_usage();
$debug['db']['total']['select'] = $db->getDebug()->query->select->total;
$debug['db']['total']['insert'] = $db->getDebug()->query->insert->total;
$debug['db']['total']['update'] = $db->getDebug()->query->update->total;
$debug['db']['total']['delete'] = $db->getDebug()->query->delete->total;
print_r($debug);
// Debug log
if (LOG_CRONTAB_EXPORT_FEED_ENABLED)
{
@mkdir(LOG_DIRECTORY, 0774, true);
if ($handle = fopen(LOG_DIRECTORY . '/' . LOG_CRONTAB_EXPORT_FEED_FILENAME, 'a+'))
{
fwrite($handle, print_r($debug, true));
fclose($handle);
chmod(LOG_DIRECTORY . '/' . LOG_CRONTAB_EXPORT_FEED_FILENAME, 0774);
}
}

View file

@ -1,506 +0,0 @@
<?php
// Lock multi-thread execution
$semaphore = sem_get(crc32('yggtracker.crontab.export.push'), 1);
if (false === sem_acquire($semaphore, true))
{
exit (_('yggtracker.crontab.export.push process locked by another thread.'));
}
// Bootstrap
require_once __DIR__ . '/../../config/bootstrap.php';
// Init Debug
$debug =
[
'dump' => [],
'time' => [
'ISO8601' => date('c'),
'total' => microtime(true),
],
'http' =>
[
'total' => 0,
],
'memory' =>
[
'start' => memory_get_usage(),
'total' => 0,
'peaks' => 0
],
];
// Define public registry
$public = [
'user' => [],
'magnet' => [],
];
// Push export enabled
if (API_EXPORT_PUSH_ENABLED)
{
// Get push queue from memory pool
foreach((array) $memoryApiExportPush = $memory->get('api.export.push') as $id => $push)
{
// Init request
$request = [];
// User request
if (!empty($push->userId) && API_EXPORT_USERS_ENABLED)
{
// Get user info
if ($user = $db->getUser($push->userId))
{
// Dump public data only
if ($user->public)
{
$request['user'] = (object)
[
'userId' => (int) $user->userId,
'address' => (string) $user->address,
'timeAdded' => (int) $user->timeAdded,
'timeUpdated' => (int) $user->timeUpdated,
'approved' => (bool) $user->approved,
];
// Cache public status
$public['user'][$user->userId] = (bool) $user->public;
}
}
}
// Magnet request
if (!empty($push->magnetId) && API_EXPORT_MAGNETS_ENABLED)
{
// Get magnet info
if ($magnet = $db->getMagnet($push->magnetId))
{
if ($magnet->public &&
$public['user'][$magnet->userId]) // After upgrade, some users have not updated their public status.
// Remote node have warning on import, because user info still hidden to init new profile there.
// Stop magnets export without public profile available, even magnet is public.
{
// Info Hash
$xt = [];
foreach ($db->findMagnetToInfoHashByMagnetId($magnet->magnetId) as $result)
{
if ($infoHash = $db->getInfoHash($result->infoHashId))
{
$xt[] = (object) [
'version' => (float) $infoHash->version,
'value' => (string) $infoHash->value,
];
}
}
// Keyword Topic
$kt = [];
foreach ($db->findKeywordTopicByMagnetId($magnet->magnetId) as $result)
{
$kt[] = $db->getKeywordTopic($result->keywordTopicId)->value;
}
// Address Tracker
$tr = [];
foreach ($db->findAddressTrackerByMagnetId($magnet->magnetId) as $result)
{
$addressTracker = $db->getAddressTracker($result->addressTrackerId);
$scheme = $db->getScheme($addressTracker->schemeId);
$host = $db->getHost($addressTracker->hostId);
$port = $db->getPort($addressTracker->portId);
$uri = $db->getUri($addressTracker->uriId);
// Yggdrasil host only
if (!Valid::host($host->value))
{
continue;
}
$tr[] = $port->value ? sprintf('%s://%s:%s%s', $scheme->value,
$host->value,
$port->value,
$uri->value) : sprintf('%s://%s%s', $scheme->value,
$host->value,
$uri->value);
}
// Acceptable Source
$as = [];
foreach ($db->findAcceptableSourceByMagnetId($magnet->magnetId) as $result)
{
$acceptableSource = $db->getAcceptableSource($result->acceptableSourceId);
$scheme = $db->getScheme($acceptableSource->schemeId);
$host = $db->getHost($acceptableSource->hostId);
$port = $db->getPort($acceptableSource->portId);
$uri = $db->getUri($acceptableSource->uriId);
// Yggdrasil host only
if (!Valid::host($host->value))
{
continue;
}
$as[] = $port->value ? sprintf('%s://%s:%s%s', $scheme->value,
$host->value,
$port->value,
$uri->value) : sprintf('%s://%s%s', $scheme->value,
$host->value,
$uri->value);
}
// Exact Source
$xs = [];
foreach ($db->findExactSourceByMagnetId($magnet->magnetId) as $result)
{
$eXactSource = $db->getExactSource($result->eXactSourceId);
$scheme = $db->getScheme($eXactSource->schemeId);
$host = $db->getHost($eXactSource->hostId);
$port = $db->getPort($eXactSource->portId);
$uri = $db->getUri($eXactSource->uriId);
// Yggdrasil host only
if (!Valid::host($host->value))
{
continue;
}
$xs[] = $port->value ? sprintf('%s://%s:%s%s', $scheme->value,
$host->value,
$port->value,
$uri->value) : sprintf('%s://%s%s', $scheme->value,
$host->value,
$uri->value);
}
$request['magnet'] = (object)
[
'magnetId' => (int) $magnet->magnetId,
'userId' => (int) $magnet->userId,
'title' => (string) $magnet->title,
'preview' => (string) $magnet->preview,
'description' => (string) $magnet->description,
'comments' => (bool) $magnet->comments,
'sensitive' => (bool) $magnet->sensitive,
'approved' => (bool) $magnet->approved,
'timeAdded' => (int) $magnet->timeAdded,
'timeUpdated' => (int) $magnet->timeUpdated,
'dn' => (string) $magnet->dn,
'xl' => (float) $magnet->xl,
'xt' => (object) $xt,
'kt' => (object) $kt,
'tr' => (object) $tr,
'as' => (object) $as,
'xs' => (object) $xs,
];
}
// Cache public status
if (!empty($public['user'][$magnet->userId]))
{
$public['magnet'][$magnet->magnetId] = (bool) $magnet->public;
} else {
$public['magnet'][$magnet->magnetId] = false;
}
}
}
// Magnet download request
if (!empty($push->magnetDownloadId) && API_EXPORT_MAGNET_DOWNLOADS_ENABLED)
{
// Get magnet download info
if ($magnetDownload = $db->getMagnetDownload($push->magnetDownloadId))
{
// Dump public data only
if (!empty($public['magnet'][$magnetDownload->magnetId]) &&
!empty($public['user'][$magnetDownload->userId]))
{
$request['magnetDownload'] = (object)
[
'magnetDownloadId' => (int) $magnetDownload->magnetDownloadId,
'userId' => (int) $magnetDownload->userId,
'magnetId' => (int) $magnetDownload->magnetId,
'timeAdded' => (int) $magnetDownload->timeAdded,
];
}
}
}
// Magnet comment request
if (!empty($push->magnetCommentId) && API_EXPORT_MAGNET_COMMENTS_ENABLED)
{
// Get magnet comment info
if ($magnetComment = $db->getMagnetComment($push->magnetCommentId))
{
// Dump public data only
if (!empty($public['magnet'][$magnetComment->magnetId]) &&
!empty($public['user'][$magnetComment->userId]))
{
$request['magnetComment'] = (object)
[
'magnetCommentId' => (int) $magnetComment->magnetCommentId,
'magnetCommentIdParent' => (int) $magnetComment->magnetCommentIdParent,
'userId' => (int) $magnetComment->userId,
'magnetId' => (int) $magnetComment->magnetId,
'timeAdded' => (int) $magnetComment->timeAdded,
'approved' => (bool) $magnetComment->approved,
'value' => (string) $magnetComment->value
];
}
}
}
// Magnet star request
if (!empty($push->magnetStarId) && API_EXPORT_MAGNET_STARS_ENABLED)
{
// Get magnet star info
if ($magnetStar = $db->getMagnetStar($push->magnetStarId))
{
// Dump public data only
if (!empty($public['magnet'][$magnetStar->magnetId]) &&
!empty($public['user'][$magnetStar->userId]))
{
$request['magnetStar'] = (object)
[
'magnetStarId' => (int) $magnetStar->magnetStarId,
'userId' => (int) $magnetStar->userId,
'magnetId' => (int) $magnetStar->magnetId,
'value' => (bool) $magnetStar->value,
'timeAdded' => (int) $magnetStar->timeAdded,
];
}
}
}
// Magnet view request
if (!empty($push->magnetViewId) && API_EXPORT_MAGNET_VIEWS_ENABLED)
{
// Get magnet view info
if ($magnetView = $db->getMagnetView($push->magnetViewId))
{
// Dump public data only
if (!empty($public['magnet'][$magnetView->magnetId]) &&
!empty($public['user'][$magnetView->userId]))
{
$request['magnetView'] = (object)
[
'magnetViewId' => (int) $magnetView->magnetViewId,
'userId' => (int) $magnetView->userId,
'magnetId' => (int) $magnetView->magnetId,
'timeAdded' => (int) $magnetView->timeAdded,
];
}
}
}
// Check request
if (empty($request))
{
// Amy request data match conditions, skip
continue;
}
// Send push data
foreach (json_decode(
file_get_contents(__DIR__ . '/../../config/nodes.json')
) as $node)
{
// Manifest exists
if (empty($node->manifest))
{
$debug['dump']['warning'][] = sprintf(
_('Manifest URL not provided: %s'),
$node
);
continue;
}
// Skip non-condition addresses
$error = [];
if (!Valid::url($node->manifest, $error))
{
$debug['dump'][$node->manifest]['warning'][] = sprintf(
_('Manifest URL invalid: %s'),
print_r(
$error,
true
)
);
continue;
}
// Skip current host
$thisUrl = Yggverse\Parser\Url::parse(WEBSITE_URL);
$manifestUrl = Yggverse\Parser\Url::parse($node->manifest);
if (empty($thisUrl->host->name) ||
empty($manifestUrl->host->name) ||
$manifestUrl->host->name == $thisUrl->host->name) // @TODO some mirrors could be available, improve condition
{
// No debug warnings in this case, continue next item
continue;
}
// Get node manifest
// @TODO cache to prevent extra-queries as usually this script running every minute
$curl = new Curl($node->manifest, API_USER_AGENT);
$debug['http']['total']++;
if (200 != $code = $curl->getCode())
{
$debug['dump'][$node->manifest]['warning'][] = sprintf(
_('Manifest unreachable with code: "%s"'),
$code
);
continue;
}
if (!$manifest = $curl->getResponse())
{
$debug['dump'][$node->manifest]['warning'][] = sprintf(
_('Manifest URL "%s" has broken response'),
$node->manifest
);
continue;
}
// API channel not exists
if (empty($manifest->import))
{
$debug['dump'][$node->manifest]['warning'][] = sprintf(
_('Manifest import URL not provided: %s'),
$node
);
continue;
}
// Push API channel not exists
if (empty($manifest->import->push))
{
$debug['dump'][$manifest->import->push]['warning'][] = sprintf(
_('Manifest import push URL not provided: %s'),
$node
);
continue;
}
// Skip sending to non-condition addresses
$error = [];
if (!Valid::url($manifest->import->push, $error))
{
$debug['dump'][$manifest->import->push]['warning'][] = sprintf(
_('Manifest import push URL invalid: %s'),
print_r(
$error,
true
)
);
continue;
}
// Skip current host
$thisUrl = Yggverse\Parser\Url::parse(WEBSITE_URL);
$pushUrl = Yggverse\Parser\Url::parse($manifest->import->push);
if (empty($thisUrl->host->name) ||
empty($pushUrl->host->name) ||
$pushUrl->host->name == $thisUrl->host->name) // @TODO some mirrors could be available, improve condition
{
// No debug warnings in this case, continue next item
continue;
}
// @TODO add recipient manifest conditions check to not disturb it API without needs
// Send push request
$debug['dump'][$manifest->import->push]['request'][] = $request;
$curl = new Curl(
$manifest->import->push,
API_USER_AGENT,
[
'data' => json_encode($request)
]
);
$debug['http']['total']++;
if (200 != $code = $curl->getCode())
{
$debug['dump'][$manifest->import->push]['warning'][] = sprintf(
_('Server returned code "%s"'),
$code
);
continue;
}
if (!$response = $curl->getResponse())
{
$debug['dump'][$manifest->import->push]['warning'][] = _('Could not receive server response');
continue;
}
$debug['dump'][$manifest->import->push]['response'][] = $response;
}
// Drop processed item from queue
unset($memoryApiExportPush[$id]);
}
// Update memory pool
$memory->set('api.export.push', $memoryApiExportPush, 3600);
}
// Export push disabled, free api.export.push pool
else
{
$memory->delete('api.export.push');
}
// Debug output
$debug['time']['total'] = microtime(true) - $debug['time']['total'];
$debug['memory']['total'] = memory_get_usage() - $debug['memory']['start'];
$debug['memory']['peaks'] = memory_get_peak_usage();
$debug['db']['total']['select'] = $db->getDebug()->query->select->total;
$debug['db']['total']['insert'] = $db->getDebug()->query->insert->total;
$debug['db']['total']['update'] = $db->getDebug()->query->update->total;
$debug['db']['total']['delete'] = $db->getDebug()->query->delete->total;
print_r($debug);
// Debug log
if (LOG_CRONTAB_EXPORT_PUSH_ENABLED)
{
@mkdir(LOG_DIRECTORY, 0770, true);
if ($handle = fopen(LOG_DIRECTORY . '/' . LOG_CRONTAB_EXPORT_PUSH_FILENAME, 'a+'))
{
fwrite($handle, print_r($debug, true));
fclose($handle);
chmod(LOG_DIRECTORY . '/' . LOG_CRONTAB_EXPORT_PUSH_FILENAME, 0770);
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,158 +0,0 @@
<?php
// Lock multi-thread execution
$semaphore = sem_get(crc32('yggtracker.crontab.scrape'), 1);
if (false === sem_acquire($semaphore, true)) {
exit (PHP_EOL . 'yggtracker.crontab.scrape process locked by another thread.' . PHP_EOL);
}
// Bootstrap
require_once __DIR__ . '/../config/bootstrap.php';
// Init Debug
$debug = [
'time' => [
'ISO8601' => date('c'),
'total' => microtime(true),
],
'http' =>
[
'total' => 0,
],
'memory' =>
[
'start' => memory_get_usage(),
'total' => 0,
'peaks' => 0
],
];
// Init Scraper
try {
$scraper = new Scrapeer\Scraper();
} catch(Exception $e) {
var_dump($e);
exit;
}
// Begin
try {
$db->beginTransaction();
// Reset time offline by timeout
$db->resetMagnetToAddressTrackerTimeOfflineByTimeout(
CRAWLER_SCRAPE_TIME_OFFLINE_TIMEOUT
);
foreach ($db->getMagnetToAddressTrackerScrapeQueue(CRAWLER_SCRAPE_QUEUE_LIMIT) as $queue)
{
$hashes = [];
foreach ($db->findMagnetToInfoHashByMagnetId($queue->magnetId) as $result)
{
$hashes[] = $db->getInfoHash($result->infoHashId)->value;
}
if ($addressTracker = $db->getAddressTracker($queue->addressTrackerId))
{
// Build url
$scheme = $db->getScheme($addressTracker->schemeId);
$host = $db->getHost($addressTracker->hostId);
$port = $db->getPort($addressTracker->portId);
$uri = $db->getUri($addressTracker->uriId);
$url = $port->value ? sprintf('%s://%s:%s%s', $scheme->value,
$host->value,
$port->value,
$uri->value) : sprintf('%s://%s%s', $scheme->value,
$host->value,
$uri->value);
foreach ($hashes as $hash)
{
if ($scrape = $scraper->scrape([$hash], [$url], null, 1))
{
$db->updateMagnetToAddressTrackerTimeOffline(
$queue->magnetToAddressTrackerId,
null
);
if (isset($scrape[$hash]['seeders']))
{
$db->updateMagnetToAddressTrackerSeeders(
$queue->magnetToAddressTrackerId,
(int) $scrape[$hash]['seeders'],
time()
);
}
if (isset($scrape[$hash]['completed']))
{
$db->updateMagnetToAddressTrackerCompleted(
$queue->magnetToAddressTrackerId,
(int) $scrape[$hash]['completed'],
time()
);
}
if (isset($scrape[$hash]['leechers']))
{
$db->updateMagnetToAddressTrackerLeechers(
$queue->magnetToAddressTrackerId,
(int) $scrape[$hash]['leechers'],
time()
);
}
}
else
{
$db->updateMagnetToAddressTrackerTimeOffline(
$queue->magnetToAddressTrackerId,
time()
);
}
}
}
}
$db->commit();
} catch (EXception $e) {
$db->rollback();
var_dump($e);
}
// Debug output
$debug['time']['total'] = microtime(true) - $debug['time']['total'];
$debug['memory']['total'] = memory_get_usage() - $debug['memory']['start'];
$debug['memory']['peaks'] = memory_get_peak_usage();
$debug['db']['total']['select'] = $db->getDebug()->query->select->total;
$debug['db']['total']['insert'] = $db->getDebug()->query->insert->total;
$debug['db']['total']['update'] = $db->getDebug()->query->update->total;
$debug['db']['total']['delete'] = $db->getDebug()->query->delete->total;
print_r($debug);
// Debug log
if (LOG_CRONTAB_SCRAPE_ENABLED)
{
@mkdir(LOG_DIRECTORY, 0774, true);
if ($handle = fopen(LOG_DIRECTORY . '/' . LOG_CRONTAB_SCRAPE_FILENAME, 'a+'))
{
fwrite($handle, print_r($debug, true));
fclose($handle);
chmod(LOG_DIRECTORY . '/' . LOG_CRONTAB_SCRAPE_FILENAME, 0774);
}
}

View file

@ -1,86 +0,0 @@
<?php
// Lock multi-thread execution
$semaphore = sem_get(crc32('yggtracker.crontab.sitemap'), 1);
if (false === sem_acquire($semaphore, true)) {
exit (PHP_EOL . 'yggtracker.crontab.sitemap process locked by another thread.' . PHP_EOL);
}
// Bootstrap
require_once __DIR__ . '/../config/bootstrap.php';
// Init Debug
$debug = [
'time' => [
'ISO8601' => date('c'),
'total' => microtime(true),
],
'http' =>
[
'total' => 0,
],
'memory' =>
[
'start' => memory_get_usage(),
'total' => 0,
'peaks' => 0
],
];
// Begin
try {
// Delete cache
@unlink(__DIR__ . '/../public/sitemap.xml');
if ($handle = fopen(__DIR__ . '/../public/sitemap.xml', 'w+'))
{
fwrite($handle, '<?xml version="1.0" encoding="UTF-8"?>');
fwrite($handle, '<urlset>');
foreach ($db->getMagnets() as $magnet)
{
if ($magnet->public && $magnet->approved)
{
fwrite($handle, sprintf('<url><loc>%s/magnet.php?magnetId=%s</loc></url>', WEBSITE_URL, $magnet->magnetId));
}
}
fwrite($handle, '</urlset>');
fclose($handle);
}
} catch (EXception $e) {
var_dump($e);
}
// Debug output
$debug['time']['total'] = microtime(true) - $debug['time']['total'];
$debug['memory']['total'] = memory_get_usage() - $debug['memory']['start'];
$debug['memory']['peaks'] = memory_get_peak_usage();
$debug['db']['total']['select'] = $db->getDebug()->query->select->total;
$debug['db']['total']['insert'] = $db->getDebug()->query->insert->total;
$debug['db']['total']['update'] = $db->getDebug()->query->update->total;
$debug['db']['total']['delete'] = $db->getDebug()->query->delete->total;
print_r($debug);
// Debug log
if (LOG_CRONTAB_SITEMAP_ENABLED)
{
@mkdir(LOG_DIRECTORY, 0774, true);
if ($handle = fopen(LOG_DIRECTORY . '/' . LOG_CRONTAB_SITEMAP_FILENAME, 'a+'))
{
fwrite($handle, print_r($debug, true));
fclose($handle);
chmod(LOG_DIRECTORY . '/' . LOG_CRONTAB_SITEMAP_FILENAME, 0774);
}
}

View file

@ -1,97 +0,0 @@
<?php
class Curl
{
private $_connection;
private $_response;
public function __construct(string $url,
string $userAgent = 'YGGtracker',
array $post = [],
int $connectTimeout = 10,
bool $header = false,
bool $followLocation = false,
int $maxRedirects = 10,
bool $sslVerifyHost = false,
bool $sslVerifyPeer = false)
{
$this->_connection = curl_init($url);
if ($userAgent)
{
curl_setopt($this->_connection, CURLOPT_USERAGENT, $userAgent);
}
if (!empty($post))
{
curl_setopt($this->_connection, CURLOPT_POST, true);
curl_setopt($this->_connection, CURLOPT_POSTFIELDS, http_build_query($post));
}
if ($header) {
curl_setopt($this->_connection, CURLOPT_HEADER, true);
}
if ($followLocation) {
curl_setopt($this->_connection, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($this->_connection, CURLOPT_MAXREDIRS, $maxRedirects);
}
curl_setopt($this->_connection, CURLOPT_FRESH_CONNECT, true);
curl_setopt($this->_connection, CURLOPT_RETURNTRANSFER, true);
curl_setopt($this->_connection, CURLOPT_CONNECTTIMEOUT, $connectTimeout);
curl_setopt($this->_connection, CURLOPT_TIMEOUT, $connectTimeout);
curl_setopt($this->_connection, CURLOPT_SSL_VERIFYHOST, $sslVerifyHost);
curl_setopt($this->_connection, CURLOPT_SSL_VERIFYPEER, $sslVerifyPeer);
$this->_response = curl_exec($this->_connection);
}
public function __destruct()
{
curl_close($this->_connection);
}
public function getError()
{
if (curl_errno($this->_connection))
{
return curl_errno($this->_connection);
}
else
{
return false;
}
}
public function getCode()
{
return curl_getinfo($this->_connection, CURLINFO_HTTP_CODE);
}
public function getContentType()
{
return curl_getinfo($this->_connection, CURLINFO_CONTENT_TYPE);
}
public function getSizeDownload()
{
return curl_getinfo($this->_connection, CURLINFO_SIZE_DOWNLOAD);
}
public function getSizeRequest()
{
return curl_getinfo($this->_connection, CURLINFO_REQUEST_SIZE);
}
public function getTotalTime()
{
return curl_getinfo($this->_connection, CURLINFO_TOTAL_TIME_T);
}
public function getResponse(bool $json = true)
{
return $json ? json_decode($this->_response) : $this->_response;
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,48 +0,0 @@
<?php
class Filter
{
public static function magnetTitle(mixed $value) : string
{
$value = trim(
strip_tags(
html_entity_decode($value)
)
);
return (string) $value;
}
public static function magnetPreview(mixed $value) : string
{
$value = trim(
strip_tags(
html_entity_decode($value)
)
);
return (string) $value;
}
public static function magnetDescription(mixed $value) : string
{
$value = trim(
strip_tags(
html_entity_decode($value)
)
);
return (string) $value;
}
public static function magnetDn(mixed $value) : string
{
$value = trim(
strip_tags(
html_entity_decode($value)
)
);
return (string) $value;
}
}

View file

@ -1,692 +0,0 @@
<?php
/**
* Scrapeer, a tiny PHP library that lets you scrape
* HTTP(S) and UDP trackers for torrent information.
*
* This file is extensively based on Johannes Zinnau's
* work, which can be found at https://goo.gl/7hyjde
*
* Licensed under a Creative Commons
* Attribution-ShareAlike 3.0 Unported License
* http://creativecommons.org/licenses/by-sa/3.0
*
* @package Scrapeer
*/
namespace Scrapeer;
/**
* The one and only class you'll ever need.
*/
class Scraper {
/**
* Current version of Scrapeer
*
* @var string
*/
const VERSION = '0.5.4';
/**
* Array of errors
*
* @var array
*/
private $errors = array();
/**
* Array of infohashes to scrape
*
* @var array
*/
private $infohashes = array();
/**
* Timeout for a single tracker
*
* @var int
*/
private $timeout;
/**
* Initiates the scraper
*
* @throws \RangeException In case of invalid amount of info-hashes.
*
* @param array|string $hashes List (>1) or string of infohash(es).
* @param array|string $trackers List (>1) or string of tracker(s).
* @param int|null $max_trackers Optional. Maximum number of trackers to be scraped, Default all.
* @param int $timeout Optional. Maximum time for each tracker scrape in seconds, Default 2.
* @param bool $announce Optional. Use announce instead of scrape, Default false.
* @return array List of results.
*/
public function scrape( $hashes, $trackers, $max_trackers = null, $timeout = 2, $announce = false ) {
$final_result = array();
if ( empty( $trackers ) ) {
$this->errors[] = 'No tracker specified, aborting.';
return $final_result;
} else if ( ! is_array( $trackers ) ) {
$trackers = array( $trackers );
}
if ( is_int( $timeout ) ) {
$this->timeout = $timeout;
} else {
$this->timeout = 2;
$this->errors[] = 'Timeout must be an integer. Using default value.';
}
try {
$this->infohashes = $this->normalize_infohashes( $hashes );
} catch ( \RangeException $e ) {
$this->errors[] = $e->getMessage();
return $final_result;
}
$max_iterations = is_int( $max_trackers ) ? $max_trackers : count( $trackers );
foreach ( $trackers as $index => $tracker ) {
if ( ! empty( $this->infohashes ) && $index < $max_iterations ) {
$info = parse_url( $tracker );
$protocol = $info['scheme'];
$host = $info['host'];
if ( empty( $protocol ) || empty( $host ) ) {
$this->errors[] = 'Skipping invalid tracker (' . $tracker . ').';
continue;
}
$port = isset( $info['port'] ) ? $info['port'] : null;
$path = isset( $info['path'] ) ? $info['path'] : null;
$passkey = $this->get_passkey( $path );
$result = $this->try_scrape( $protocol, $host, $port, $passkey, $announce );
$final_result = array_merge( $final_result, $result );
continue;
}
break;
}
return $final_result;
}
/**
* Normalizes the given hashes
*
* @throws \RangeException If amount of valid infohashes > 64 or < 1.
*
* @param array $infohashes List of infohash(es).
* @return array Normalized infohash(es).
*/
private function normalize_infohashes( $infohashes ) {
if ( ! is_array( $infohashes ) ) {
$infohashes = array( $infohashes );
}
foreach ( $infohashes as $index => $infohash ) {
if ( ! preg_match( '/^[a-f0-9]{40}$/i', $infohash ) ) {
$this->errors[] = 'Invalid infohash skipped (' . $infohash . ').';
unset( $infohashes[ $index ] );
}
}
$total_infohashes = count( $infohashes );
if ( $total_infohashes > 64 || $total_infohashes < 1 ) {
throw new \RangeException( 'Invalid amount of valid infohashes (' . $total_infohashes . ').' );
}
$infohashes = array_values( $infohashes );
return $infohashes;
}
/**
* Returns the passkey found in the scrape request.
*
* @param string $path Path from the scrape request.
* @return string Passkey or empty string.
*/
private function get_passkey( $path ) {
if ( ! is_null( $path ) && preg_match( '/[a-z0-9]{32}/i', $path, $matches ) ) {
return '/' . $matches[0];
}
return '';
}
/**
* Tries to scrape with a single tracker.
*
* @throws \Exception In case of unsupported protocol.
*
* @param string $protocol Protocol of the tracker.
* @param string $host Domain or address of the tracker.
* @param int $port Optional. Port number of the tracker.
* @param string $passkey Optional. Passkey provided in the scrape request.
* @param bool $announce Optional. Use announce instead of scrape, Default false.
* @return array List of results.
*/
private function try_scrape( $protocol, $host, $port, $passkey, $announce ) {
$infohashes = $this->infohashes;
$this->infohashes = array();
$results = array();
try {
switch ( $protocol ) {
case 'udp':
$port = isset( $port ) ? $port : 80;
$results = $this->scrape_udp( $infohashes, $host, $port, $announce );
break;
case 'http':
$port = isset( $port ) ? $port : 80;
$results = $this->scrape_http( $infohashes, $protocol, $host, $port, $passkey, $announce );
break;
case 'https':
$port = isset( $port ) ? $port : 443;
$results = $this->scrape_http( $infohashes, $protocol, $host, $port, $passkey, $announce );
break;
default:
throw new \Exception( 'Unsupported protocol (' . $protocol . '://' . $host . ').' );
}
} catch ( \Exception $e ) {
$this->infohashes = $infohashes;
$this->errors[] = $e->getMessage();
}
return $results;
}
/**
* Initiates the HTTP(S) scraping
*
* @param array|string $infohashes List (>1) or string of infohash(es).
* @param string $protocol Protocol to use for the scraping.
* @param string $host Domain or IP address of the tracker.
* @param int $port Optional. Port number of the tracker, Default 80 (HTTP) or 443 (HTTPS).
* @param string $passkey Optional. Passkey provided in the scrape request.
* @param bool $announce Optional. Use announce instead of scrape, Default false.
* @return array List of results.
*/
private function scrape_http( $infohashes, $protocol, $host, $port, $passkey, $announce ) {
if ( true === $announce ) {
$response = $this->http_announce( $infohashes, $protocol, $host, $port, $passkey );
} else {
$query = $this->http_query( $infohashes, $protocol, $host, $port, $passkey );
$response = $this->http_request( $query, $host, $port );
}
$results = $this->http_data( $response, $infohashes, $host );
return $results;
}
/**
* Builds the HTTP(S) query
*
* @param array|string $infohashes List (>1) or string of infohash(es).
* @param string $protocol Protocol to use for the scraping.
* @param string $host Domain or IP address of the tracker.
* @param int $port Port number of the tracker, Default 80 (HTTP) or 443 (HTTPS).
* @param string $passkey Optional. Passkey provided in the scrape request.
* @return string Request query.
*/
private function http_query( $infohashes, $protocol, $host, $port, $passkey ) {
$tracker_url = $protocol . '://' . $host . ':' . $port . $passkey;
$scrape_query = '';
foreach ( $infohashes as $index => $infohash ) {
if ( $index > 0 ) {
$scrape_query .= '&info_hash=' . urlencode( pack( 'H*', $infohash ) );
} else {
$scrape_query .= '/scrape?info_hash=' . urlencode( pack( 'H*', $infohash ) );
}
}
$request_query = $tracker_url . $scrape_query;
return $request_query;
}
/**
* Executes the query and returns the result
*
* @throws \Exception If the connection can't be established.
* @throws \Exception If the response isn't valid.
*
* @param string $query The query that will be executed.
* @param string $host Domain or IP address of the tracker.
* @param int $port Port number of the tracker, Default 80 (HTTP) or 443 (HTTPS).
* @return string Request response.
*/
private function http_request( $query, $host, $port ) {
$context = stream_context_create( array(
'http' => array(
'timeout' => $this->timeout,
),
));
if ( false === ( $response = @file_get_contents( $query, false, $context ) ) ) {
throw new \Exception( 'Invalid scrape connection (' . $host . ':' . $port . ').' );
}
if ( substr( $response, 0, 12 ) !== 'd5:filesd20:' ) {
throw new \Exception( 'Invalid scrape response (' . $host . ':' . $port . ').' );
}
return $response;
}
/**
* Builds the query, sends the announce request and returns the data
*
* @throws \Exception If the connection can't be established.
*
* @param array|string $infohashes List (>1) or string of infohash(es).
* @param string $protocol Protocol to use for the scraping.
* @param string $host Domain or IP address of the tracker.
* @param int $port Port number of the tracker, Default 80 (HTTP) or 443 (HTTPS).
* @param string $passkey Optional. Passkey provided in the scrape request.
* @return string Request response.
*/
private function http_announce( $infohashes, $protocol, $host, $port, $passkey ) {
$tracker_url = $protocol . '://' . $host . ':' . $port . $passkey;
$context = stream_context_create( array(
'http' => array(
'timeout' => $this->timeout,
),
));
$response_data = '';
foreach ( $infohashes as $infohash ) {
$query = $tracker_url . '/announce?info_hash=' . urlencode( pack( 'H*', $infohash ) );
if ( false === ( $response = @file_get_contents( $query, false, $context ) ) ) {
throw new \Exception( 'Invalid announce connection (' . $host . ':' . $port . ').' );
}
if ( substr( $response, 0, 12 ) !== 'd8:completei' ||
substr( $response, 0, 46 ) === 'd8:completei0e10:downloadedi0e10:incompletei1e' ) {
continue;
}
$ben_hash = '20:' . pack( 'H*', $infohash ) . 'd';
$response_data .= $ben_hash . $response;
}
return $response_data;
}
/**
* Parses the response and returns the data
*
* @param string $response The response that will be parsed.
* @param array $infohashes List of infohash(es).
* @param string $host Domain or IP address of the tracker.
* @return array Parsed data.
*/
private function http_data( $response, $infohashes, $host ) {
$torrents_data = array();
foreach ( $infohashes as $infohash ) {
$ben_hash = '20:' . pack( 'H*', $infohash ) . 'd';
$start_pos = strpos( $response, $ben_hash );
if ( false !== $start_pos ) {
$start = $start_pos + 24;
$head = substr( $response, $start );
$end = strpos( $head, 'ee' ) + 1;
$data = substr( $response, $start, $end );
$seeders = '8:completei';
$torrent_info['seeders'] = $this->get_information( $data, $seeders, 'e' );
$completed = '10:downloadedi';
$torrent_info['completed'] = $this->get_information( $data, $completed, 'e' );
$leechers = '10:incompletei';
$torrent_info['leechers'] = $this->get_information( $data, $leechers, 'e' );
$torrents_data[ $infohash ] = $torrent_info;
} else {
$this->collect_infohash( $infohash );
$this->errors[] = 'Invalid infohash (' . $infohash . ') for tracker: ' . $host . '.';
}
}
return $torrents_data;
}
/**
* Parses a string and returns the data between $start and $end.
*
* @param string $data The data that will be parsed.
* @param string $start Beginning part of the data.
* @param string $end Ending part of the data.
* @return int Parsed information or 0.
*/
private function get_information( $data, $start, $end ) {
$start_pos = strpos( $data, $start );
if ( false !== $start_pos ) {
$start = $start_pos + strlen( $start );
$head = substr( $data, $start );
$end = strpos( $head, $end );
$information = substr( $data, $start, $end );
return (int) $information;
}
return 0;
}
/**
* Initiates the UDP scraping
*
* @param array|string $infohashes List (>1) or string of infohash(es).
* @param string $host Domain or IP address of the tracker.
* @param int $port Optional. Port number of the tracker, Default 80.
* @param bool $announce Optional. Use announce instead of scrape, Default false.
* @return array List of results.
*/
private function scrape_udp( $infohashes, $host, $port, $announce ) {
list( $socket, $transaction_id, $connection_id ) = $this->prepare_udp( $host, $port );
if ( true === $announce ) {
$response = $this->udp_announce( $socket, $infohashes, $connection_id );
$keys = 'Nleechers/Nseeders';
$start = 12;
$end = 16;
$offset = 20;
} else {
$response = $this->udp_scrape( $socket, $infohashes, $connection_id, $transaction_id, $host, $port );
$keys = 'Nseeders/Ncompleted/Nleechers';
$start = 8;
$end = $offset = 12;
}
$results = $this->udp_scrape_data( $response, $infohashes, $host, $keys, $start, $end, $offset );
return $results;
}
/**
* Prepares the UDP connection
*
* @param string $host Domain or IP address of the tracker.
* @param int $port Optional. Port number of the tracker, Default 80.
* @return array Created socket, transaction ID and connection ID.
*/
private function prepare_udp( $host, $port ) {
$socket = $this->udp_create_connection( $host, $port );
$transaction_id = $this->udp_connection_request( $socket );
$connection_id = $this->udp_connection_response( $socket, $transaction_id, $host, $port );
return array( $socket, $transaction_id, $connection_id );
}
/**
* Creates the UDP socket and establishes the connection
*
* @throws \Exception If the socket couldn't be created or connected to.
*
* @param string $host Domain or IP address of the tracker.
* @param int $port Port number of the tracker, Default 80.
* @return resource $socket Created and connected socket.
*/
private function udp_create_connection( $host, $port ) {
if ( false === ( $socket = @socket_create( AF_INET, SOCK_DGRAM, SOL_UDP ) ) ) {
throw new \Exception( "Couldn't create socket." );
}
$timeout = $this->timeout;
socket_set_option( $socket, SOL_SOCKET, SO_RCVTIMEO, array( 'sec' => $timeout, 'usec' => 0 ) );
socket_set_option( $socket, SOL_SOCKET, SO_SNDTIMEO, array( 'sec' => $timeout, 'usec' => 0 ) );
if ( false === @socket_connect( $socket, $host, $port ) ) {
throw new \Exception( "Couldn't connect to socket." );
}
return $socket;
}
/**
* Writes to the connected socket and returns the transaction ID
*
* @throws \Exception If the socket couldn't be written to.
*
* @param resource $socket The socket resource.
* @return int The transaction ID.
*/
private function udp_connection_request( $socket ) {
$connection_id = "\x00\x00\x04\x17\x27\x10\x19\x80";
$action = pack( 'N', 0 );
$transaction_id = mt_rand( 0, 2147483647 );
$buffer = $connection_id . $action . pack( 'N', $transaction_id );
if ( false === @socket_write( $socket, $buffer, strlen( $buffer ) ) ) {
socket_close( $socket );
throw new \Exception( "Couldn't write to socket." );
}
return $transaction_id;
}
/**
* Reads the connection response and returns the connection ID
*
* @throws \Exception If anything fails with the scraping.
*
* @param resource $socket The socket resource.
* @param int $transaction_id The transaction ID.
* @param string $host Domain or IP address of the tracker.
* @param int $port Port number of the tracker, Default 80.
* @return string The connection ID.
*/
private function udp_connection_response( $socket, $transaction_id, $host, $port ) {
if ( false === ( $response = @socket_read( $socket, 16 ) ) ) {
socket_close( $socket );
throw new \Exception( 'Invalid scrape connection! (' . $host . ':' . $port . ').' );
}
if ( strlen( $response ) < 16 ) {
socket_close( $socket );
throw new \Exception( 'Invalid scrape response (' . $host . ':' . $port . ').' );
}
$result = unpack( 'Naction/Ntransaction_id', $response );
if ( 0 !== $result['action'] || $result['transaction_id'] !== $transaction_id ) {
socket_close( $socket );
throw new \Exception( 'Invalid scrape result (' . $host . ':' . $port . ').' );
}
$connection_id = substr( $response, 8, 8 );
return $connection_id;
}
/**
* Reads the socket response and returns the torrent data
*
* @throws \Exception If anything fails while reading the response.
*
* @param resource $socket The socket resource.
* @param array $hashes List (>1) or string of infohash(es).
* @param string $connection_id The connection ID.
* @param int $transaction_id The transaction ID.
* @param string $host Domain or IP address of the tracker.
* @param int $port Port number of the tracker, Default 80.
* @return string Response data.
*/
private function udp_scrape( $socket, $hashes, $connection_id, $transaction_id, $host, $port ) {
$this->udp_scrape_request( $socket, $hashes, $connection_id, $transaction_id );
$read_length = 8 + ( 12 * count( $hashes ) );
if ( false === ( $response = @socket_read( $socket, $read_length ) ) ) {
socket_close( $socket );
throw new \Exception( 'Invalid scrape connection (' . $host . ':' . $port . ').' );
}
socket_close( $socket );
if ( strlen( $response ) < $read_length ) {
throw new \Exception( 'Invalid scrape response (' . $host . ':' . $port . ').' );
}
$result = unpack( 'Naction/Ntransaction_id', $response );
if ( 2 !== $result['action'] || $result['transaction_id'] !== $transaction_id ) {
throw new \Exception( 'Invalid scrape result (' . $host . ':' . $port . ').' );
}
return $response;
}
/**
* Writes to the connected socket
*
* @throws \Exception If the socket couldn't be written to.
*
* @param resource $socket The socket resource.
* @param array $hashes List (>1) or string of infohash(es).
* @param string $connection_id The connection ID.
* @param int $transaction_id The transaction ID.
*/
private function udp_scrape_request( $socket, $hashes, $connection_id, $transaction_id ) {
$action = pack( 'N', 2 );
$infohashes = '';
foreach ( $hashes as $infohash ) {
$infohashes .= pack( 'H*', $infohash );
}
$buffer = $connection_id . $action . pack( 'N', $transaction_id ) . $infohashes;
if ( false === @socket_write( $socket, $buffer, strlen( $buffer ) ) ) {
socket_close( $socket );
throw new \Exception( "Couldn't write to socket." );
}
}
/**
* Writes the announce to the connected socket
*
* @throws \Exception If the socket couldn't be written to.
*
* @param resource $socket The socket resource.
* @param array $hashes List (>1) or string of infohash(es).
* @param string $connection_id The connection ID.
* @return string Torrent(s) data.
*/
private function udp_announce( $socket, $hashes, $connection_id ) {
$action = pack( 'N', 1 );
$downloaded = $left = $uploaded = "\x30\x30\x30\x30\x30\x30\x30\x30";
$peer_id = $this->random_peer_id();
$event = pack( 'N', 3 );
$ip_addr = pack( 'N', 0 );
$key = pack( 'N', mt_rand( 0, 2147483647 ) );
$num_want = -1;
$ann_port = pack( 'N', mt_rand( 0, 255 ) );
$response_data = '';
foreach ( $hashes as $infohash ) {
$transaction_id = mt_rand( 0, 2147483647 );
$buffer = $connection_id . $action . pack( 'N', $transaction_id ) . pack( 'H*', $infohash ) .
$peer_id . $downloaded . $left . $uploaded . $event . $ip_addr . $key . $num_want . $ann_port;
if ( false === @socket_write( $socket, $buffer, strlen( $buffer ) ) ) {
socket_close( $socket );
throw new \Exception( "Couldn't write announce to socket." );
}
$response = $this->udp_verify_announce( $socket, $transaction_id );
if ( false === $response ) {
continue;
}
$response_data .= $response;
}
socket_close( $socket );
return $response_data;
}
/**
* Generates a random peer ID
*
* @return string Generated peer ID.
*/
private function random_peer_id() {
$identifier = '-SP0054-';
$chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
$peer_id = $identifier . substr( str_shuffle( $chars ), 0, 12 );
return $peer_id;
}
/**
* Verifies the correctness of the announce response
*
* @param resource $socket The socket resource.
* @param int $transaction_id The transaction ID.
* @return string Response data.
*/
private function udp_verify_announce( $socket, $transaction_id ) {
if ( false === ( $response = @socket_read( $socket, 20 ) ) ) {
return false;
}
if ( strlen( $response ) < 20 ) {
return false;
}
$result = unpack( 'Naction/Ntransaction_id', $response );
if ( 1 !== $result['action'] || $result['transaction_id'] !== $transaction_id ) {
return false;
}
return $response;
}
/**
* Reads the socket response and returns the torrent data
*
* @param string $response Data from the request response.
* @param array $hashes List (>1) or string of infohash(es).
* @param string $host Domain or IP address of the tracker.
* @param string $keys Keys for the unpacked information.
* @param int $start Start of the content we want to unpack.
* @param int $end End of the content we want to unpack.
* @param int $offset Offset to the next content part.
* @return array Scraped torrent data.
*/
private function udp_scrape_data( $response, $hashes, $host, $keys, $start, $end, $offset ) {
$torrents_data = array();
foreach ( $hashes as $infohash ) {
$byte_string = substr( $response, $start, $end );
$data = unpack( 'N', $byte_string );
$content = $data[1];
if ( ! empty( $content ) ) {
$results = unpack( $keys, $byte_string );
$torrents_data[ $infohash ] = $results;
} else {
$this->collect_infohash( $infohash );
$this->errors[] = 'Invalid infohash (' . $infohash . ') for tracker: ' . $host . '.';
}
$start += $offset;
}
return $torrents_data;
}
/**
* Collects info-hashes that couldn't be scraped.
*
* @param string $infohash Infohash that wasn't scraped.
*/
private function collect_infohash( $infohash ) {
$this->infohashes[] = $infohash;
}
/**
* Checks if there are any errors
*
* @return bool True or false, depending if errors are present or not.
*/
public function has_errors() {
return ! empty( $this->errors );
}
/**
* Returns all the errors that were logged
*
* @return array All the logged errors.
*/
public function get_errors() {
return $this->errors;
}
}

View file

@ -1,111 +0,0 @@
<?php
class Sphinx {
private $_sphinx;
public function __construct(string $host, int $port)
{
$this->_sphinx = new PDO('mysql:host=' . $host . ';port=' . $port . ';charset=utf8', false, false, [PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8']);
$this->_sphinx->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->_sphinx->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ);
}
public function searchMagnetsTotal(string $keyword, string $mode = 'default', array $stopWords = []) : int
{
$query = $this->_sphinx->prepare('SELECT COUNT(*) AS `total` FROM `magnet` WHERE MATCH(?)');
$query->execute(
[
self::_match($keyword, $mode, $stopWords)
]
);
return $query->fetch()->total;
}
public function searchMagnets(string $keyword, int $start, int $limit, int $maxMatches, string $mode = 'default', array $stopWords = [])
{
$query = $this->_sphinx->prepare("SELECT *
FROM `magnet`
WHERE MATCH(?)
ORDER BY `magnetId` DESC, WEIGHT() DESC
LIMIT " . (int) ($start >= $maxMatches ? ($maxMatches > 0 ? $maxMatches - 1 : 0) : $start) . "," . (int) $limit . "
OPTION `max_matches`=" . (int) ($maxMatches >= 1 ? $maxMatches : 1));
$query->execute(
[
self::_match($keyword, $mode, $stopWords)
]
);
return $query->fetchAll();
}
private static function _match(string $keyword, string $mode = 'default', array $stopWords = []) : string
{
$keyword = trim($keyword);
if (empty($keyword))
{
return $keyword;
}
$keyword = str_replace(['"'], ' ', $keyword);
$keyword = preg_replace('/[\W]/ui', ' ', $keyword);
$keyword = preg_replace('/[\s]+/ui', ' ', $keyword);
$keyword = trim($keyword);
switch ($mode)
{
case 'similar':
$result = [];
$keyword = preg_replace('/[\d]/ui', ' ', $keyword);
$keyword = preg_replace('/[\s]+/ui', ' ', $keyword);
$keyword = trim($keyword);
foreach ((array) explode(' ', $keyword) as $value)
{
if (mb_strlen($value) > 5)
{
if (!in_array(mb_strtolower($value), array_map('strtolower', $stopWords)))
{
$result[] = sprintf('@title "%s" | @dn "%s"', $value, $value);
}
}
}
if (empty($result))
{
return '*';
}
else
{
return implode(' | ', $result);
}
break;
default:
$result = [];
foreach ((array) explode(' ', $keyword) as $value)
{
if (!in_array(mb_strtolower($value), $stopWords))
{
$result[] = sprintf('@"*%s*"', $value);
}
}
return implode(' | ', $result);
}
}
}

View file

@ -1,45 +0,0 @@
<?php
class Time
{
public static function ago(int $time)
{
$diff = time() - $time;
if ($diff < 1)
{
return _('now');
}
$values =
[
365 * 24 * 60 * 60 => _('year'),
30 * 24 * 60 * 60 => _('month'),
24 * 60 * 60 => _('day'),
60 * 60 => _('hour'),
60 => _('minute'),
1 => _('second')
];
$plural = [
_('year') => _('years'),
_('month') => _('months'),
_('day') => _('days'),
_('hour') => _('hours'),
_('minute') => _('minutes'),
_('second') => _('seconds')
];
foreach ($values as $key => $value)
{
$result = $diff / $key;
if ($result >= 1)
{
$round = round($result);
return sprintf('%s %s ago', $round, $round > 1 ? $plural[$value] : $value);
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,670 +0,0 @@
<?php
// Bootstrap
require_once __DIR__ . '/../config/bootstrap.php';
// Define response
$response = (object)
[
'success' => true,
'message' => _('Internal server error'),
'title' => sprintf(_('Oops - %s'), WEBSITE_NAME)
];
// Begin action request
switch (isset($_GET['target']) ? urldecode($_GET['target']) : false)
{
case 'profile':
switch (isset($_GET['toggle']) ? $_GET['toggle'] : false)
{
case 'jidenticon':
// Yggdrasil connections only
if (!Valid::host($_SERVER['REMOTE_ADDR']))
{
$response->success = false;
$response->message = _('Yggdrasil connection required for this action');
}
// Init session
else if (!$userId = $db->initUserId($_SERVER['REMOTE_ADDR'], USER_DEFAULT_APPROVED, time()))
{
$response->success = false;
$response->message = _('Could not init user session');
}
// Get user
else if (!$user = $db->getUser($userId))
{
$response->success = false;
$response->message = _('Could not init user info');
}
// On first visit, redirect user to the welcome page with access level question
else if (is_null($user->public))
{
header(
sprintf('Location: %s/welcome.php', WEBSITE_URL)
);
}
// Render icon
else
{
header('Cache-Control: max-age=604800');
$icon = new Jdenticon\Identicon();
$icon->setValue($user->{USER_IDENTICON_FIELD});
$icon->setSize(empty($_GET['size']) ? 100 : (int) $_GET['size']);
$icon->setStyle(
[
'backgroundColor' => 'rgba(255, 255, 255, 0)',
]
);
$icon->displayImage('webp');
}
break;
}
break;
case 'comment':
switch (isset($_GET['toggle']) ? $_GET['toggle'] : false)
{
case 'approved':
// Yggdrasil connections only
if (!Valid::host($_SERVER['REMOTE_ADDR']))
{
$response->success = false;
$response->message = _('Yggdrasil connection required for this action');
}
// Init session
else if (!$userId = $db->initUserId($_SERVER['REMOTE_ADDR'], USER_DEFAULT_APPROVED, time()))
{
$response->success = false;
$response->message = _('Could not init user session');
}
// Get user
else if (!$user = $db->getUser($userId))
{
$response->success = false;
$response->message = _('Could not init user info');
}
// On first visit, redirect user to the welcome page with access level question
else if (is_null($user->public))
{
header(
sprintf('Location: %s/welcome.php', WEBSITE_URL)
);
}
// Magnet comment exists
else if (!$magnetComment = $db->getMagnetComment(isset($_GET['magnetCommentId']) && $_GET['magnetCommentId'] > 0 ? (int) $_GET['magnetCommentId'] : 0))
{
$response->success = false;
$response->message = _('Requested magnet comment not found');
}
// Access allowed
else if (!in_array($user->address, MODERATOR_IP_LIST)) {
$response->success = false;
$response->message = _('Access denied');
}
// Validate callback
else if (empty($_GET['callback']))
{
$response->success = false;
$response->message = _('Callback required');
}
// Validate base64
else if (!$callback = (string) @base64_decode($_GET['callback']))
{
$response->success = false;
$response->message = _('Invalid callback encoding');
}
// Request valid
else
{
if ($magnetComment->approved)
{
$db->updateMagnetCommentApproved($magnetComment->magnetCommentId, false);
if (USER_AUTO_APPROVE_ON_COMMENT_APPROVE)
{
$db->updateUserApproved($magnetComment->userId, false, time());
}
}
else
{
$db->updateMagnetCommentApproved($magnetComment->magnetCommentId, true);
if (USER_AUTO_APPROVE_ON_COMMENT_APPROVE)
{
$db->updateUserApproved($magnetComment->userId, true, time());
}
}
// Redirect to edit page
header(
sprintf('Location: %s', $callback)
);
}
break;
case 'new':
// Yggdrasil connections only
if (!Valid::host($_SERVER['REMOTE_ADDR']))
{
$response->success = false;
$response->message = _('Yggdrasil connection required for this action');
}
// Init session
else if (!$userId = $db->initUserId($_SERVER['REMOTE_ADDR'], USER_DEFAULT_APPROVED, time()))
{
$response->success = false;
$response->message = _('Could not init user session');
}
// Get user
else if (!$user = $db->getUser($userId))
{
$response->success = false;
$response->message = _('Could not init user info');
}
// On first visit, redirect user to the welcome page with access level question
else if (is_null($user->public))
{
header(
sprintf('Location: %s/welcome.php', WEBSITE_URL)
);
}
// Magnet exists
else if (!$magnet = $db->getMagnet(isset($_GET['magnetId']) && $_GET['magnetId'] > 0 ? (int) $_GET['magnetId'] : 0))
{
$response->success = false;
$response->message = _('Requested magnet not found');
}
// Access allowed
else if (!($user->address == $db->getUser($magnet->userId)->address || in_array($user->address, MODERATOR_IP_LIST) || ($magnet->public && $magnet->approved))) {
$response->success = false;
$response->message = _('Magnet not available for this action');
}
// Validate callback
else if (empty($_GET['callback']))
{
$response->success = false;
$response->message = _('Callback required');
}
// Validate base64
else if (!$callback = (string) @base64_decode($_GET['callback']))
{
$response->success = false;
$response->message = _('Invalid callback encoding');
}
// Validate comment value
else if (empty($_POST['comment']) ||
mb_strlen($_POST['comment']) < MAGNET_COMMENT_MIN_LENGTH ||
mb_strlen($_POST['comment']) > MAGNET_COMMENT_MAX_LENGTH)
{
$response->success = false;
$response->message = sprintf(_('Valid comment value required, %s-%s chars allowed'), MAGNET_COMMENT_MIN_LENGTH, MAGNET_COMMENT_MAX_LENGTH);
}
// Request valid
else
{
if ($magnetCommentId = $db->addMagnetComment($magnet->magnetId,
$user->userId,
null, // @TODO implement threads
trim($_POST['comment']),
$user->approved || in_array($user->address, MODERATOR_IP_LIST) ? true : MAGNET_COMMENT_DEFAULT_APPROVED,
MAGNET_COMMENT_DEFAULT_PUBLIC,
time()))
{
// Push event to other nodes
if (API_EXPORT_ENABLED &&
API_EXPORT_PUSH_ENABLED &&
API_EXPORT_USERS_ENABLED &&
API_EXPORT_MAGNETS_ENABLED &&
API_EXPORT_MAGNET_COMMENTS_ENABLED)
{
if (!$memoryApiExportPush = $memory->get('api.export.push'))
{
$memoryApiExportPush = [];
}
$memoryApiExportPush[] = (object)
[
'time' => time(),
'userId' => $user->userId,
'magnetId' => $magnet->magnetId,
'magnetCommentId' => $magnetCommentId
];
$memory->set('api.export.push', $memoryApiExportPush, 3600);
}
// Redirect to referrer page
header(
sprintf('Location: %s#comment-%s', $callback, $magnetCommentId)
);
}
}
break;
default:
header(
sprintf('Location: %s', WEBSITE_URL)
);
}
break;
case 'magnet':
switch (isset($_GET['toggle']) ? $_GET['toggle'] : false)
{
case 'star':
// Yggdrasil connections only
if (!Valid::host($_SERVER['REMOTE_ADDR']))
{
$response->success = false;
$response->message = _('Yggdrasil connection required for this action');
}
// Init session
else if (!$userId = $db->initUserId($_SERVER['REMOTE_ADDR'], USER_DEFAULT_APPROVED, time()))
{
$response->success = false;
$response->message = _('Could not init user session');
}
// Get user
else if (!$user = $db->getUser($userId))
{
$response->success = false;
$response->message = _('Could not init user info');
}
// On first visit, redirect user to the welcome page with access level question
else if (is_null($user->public))
{
header(
sprintf('Location: %s/welcome.php', WEBSITE_URL)
);
}
// Magnet exists
else if (!$magnet = $db->getMagnet(isset($_GET['magnetId']) && $_GET['magnetId'] > 0 ? (int) $_GET['magnetId'] : 0))
{
$response->success = false;
$response->message = _('Requested magnet not found');
}
// Access allowed
else if (!($_SERVER['REMOTE_ADDR'] == $db->getUser($magnet->userId)->address || in_array($_SERVER['REMOTE_ADDR'], MODERATOR_IP_LIST) || ($magnet->public && $magnet->approved))) {
$response->success = false;
$response->message = _('Magnet not available for this action');
}
// Validate callback
else if (empty($_GET['callback']))
{
$response->success = false;
$response->message = _('Callback required');
}
// Validate base64
else if (!$callback = (string) @base64_decode($_GET['callback']))
{
$response->success = false;
$response->message = _('Invalid callback encoding');
}
// Request valid
else
{
// Save star
if ($magnetStarId = $db->addMagnetStar( $magnet->magnetId,
$user->userId,
!$db->findLastMagnetStarValue($magnet->magnetId, $user->userId),
time()))
{
// Push event to other nodes
if (API_EXPORT_ENABLED &&
API_EXPORT_PUSH_ENABLED &&
API_EXPORT_USERS_ENABLED &&
API_EXPORT_MAGNETS_ENABLED &&
API_EXPORT_MAGNET_STARS_ENABLED)
{
if (!$memoryApiExportPush = $memory->get('api.export.push'))
{
$memoryApiExportPush = [];
}
$memoryApiExportPush[] = (object)
[
'time' => time(),
'userId' => $user->userId,
'magnetId' => $magnet->magnetId,
'magnetStarId' => $magnetStarId
];
$memory->set('api.export.push', $memoryApiExportPush, 3600);
}
// Redirect to edit page
header(
sprintf('Location: %s', $callback)
);
}
}
break;
case 'new':
// Yggdrasil connections only
if (!Valid::host($_SERVER['REMOTE_ADDR']))
{
$response->success = false;
$response->message = _('Yggdrasil connection required for this action');
}
// Init session
else if (!$userId = $db->initUserId($_SERVER['REMOTE_ADDR'], USER_DEFAULT_APPROVED, time()))
{
$response->success = false;
$response->message = _('Could not init user session');
}
// Get user
else if (!$user = $db->getUser($userId))
{
$response->success = false;
$response->message = _('Could not init user info');
}
// On first visit, redirect user to the welcome page with access level question
else if (is_null($user->public))
{
header(
sprintf('Location: %s/welcome.php', WEBSITE_URL)
);
}
// Validate link
if (empty($_GET['magnet']))
{
$response->success = false;
$response->message = _('Link required');
}
// Validate magnet
else if (!$magnet = Yggverse\Parser\Magnet::parse($_GET['magnet']))
{
$response->success = false;
$response->message = _('Invalid magnet link');
}
// Request valid
else
{
// Begin magnet registration
try
{
$db->beginTransaction();
// Init magnet
if ($magnetId = $db->addMagnet( $user->userId,
$magnet->xl,
$magnet->dn,
'', // @TODO deprecated, remove
MAGNET_DEFAULT_PUBLIC,
MAGNET_DEFAULT_COMMENTS,
MAGNET_DEFAULT_SENSITIVE,
$user->approved ? true : MAGNET_DEFAULT_APPROVED,
time()))
{
foreach ($magnet as $key => $value)
{
switch ($key)
{
case 'xt':
foreach ($value as $xt)
{
if (Yggverse\Parser\Magnet::isXTv1($xt))
{
$db->addMagnetToInfoHash(
$magnetId,
$db->initInfoHashId(
Yggverse\Parser\Magnet::filterInfoHash($xt), 1
)
);
}
if (Yggverse\Parser\Magnet::isXTv2($xt))
{
$db->addMagnetToInfoHash(
$magnetId,
$db->initInfoHashId(
Yggverse\Parser\Magnet::filterInfoHash($xt), 2
)
);
}
}
break;
case 'tr':
foreach ($value as $tr)
{
if (Valid::url($tr))
{
if ($url = Yggverse\Parser\Url::parse($tr))
{
$db->initMagnetToAddressTrackerId(
$magnetId,
$db->initAddressTrackerId(
$db->initSchemeId($url->host->scheme),
$db->initHostId($url->host->name),
$db->initPortId($url->host->port),
$db->initUriId($url->page->uri)
)
);
}
}
}
break;
case 'ws':
foreach ($value as $ws)
{
// @TODO
}
break;
case 'as':
foreach ($value as $as)
{
if (Valid::url($as))
{
if ($url = Yggverse\Parser\Url::parse($as))
{
$db->initMagnetToAcceptableSourceId(
$magnetId,
$db->initAcceptableSourceId(
$db->initSchemeId($url->host->scheme),
$db->initHostId($url->host->name),
$db->initPortId($url->host->port),
$db->initUriId($url->page->uri)
)
);
}
}
}
break;
case 'xs':
foreach ($value as $xs)
{
if (Valid::url($xs))
{
if ($url = Yggverse\Parser\Url::parse($xs))
{
$db->initMagnetToExactSourceId(
$magnetId,
$db->initExactSourceId(
$db->initSchemeId($url->host->scheme),
$db->initHostId($url->host->name),
$db->initPortId($url->host->port),
$db->initUriId($url->page->uri)
)
);
}
}
}
break;
case 'mt':
foreach ($value as $mt)
{
// @TODO
}
break;
case 'x.pe':
foreach ($value as $xPe)
{
// @TODO
}
break;
case 'kt':
foreach ($value as $kt)
{
$db->initMagnetToKeywordTopicId(
$magnetId,
$db->initKeywordTopicId(trim(mb_strtolower(strip_tags(html_entity_decode($kt)))))
);
}
break;
}
}
$db->commit();
// Redirect to edit page
header(sprintf('Location: %s/edit.php?magnetId=%s', trim(WEBSITE_URL, '/'), $magnetId));
}
} catch (Exception $e) {
var_dump($e);
$db->rollBack();
}
}
break;
}
break;
}
?>
<!DOCTYPE html>
<html lang="en-US">
<head>
<link rel="stylesheet" type="text/css" href="<?php echo WEBSITE_URL ?>/assets/theme/default/css/common.css?<?php echo WEBSITE_CSS_VERSION ?>" />
<link rel="stylesheet" type="text/css" href="<?php echo WEBSITE_URL ?>/assets/theme/default/css/framework.css?<?php echo WEBSITE_CSS_VERSION ?>" />
<title>
<?php echo $response->title ?>
</title>
<meta name="robots" content="noindex,nofollow"/>
<meta name="author" content="YGGtracker" />
<meta charset="UTF-8" />
</head>
<body>
<header>
<div class="container">
<div class="row margin-t-8 text-center">
<a class="logo" href="<?php echo WEBSITE_URL ?>"><?php echo str_replace('YGG', '<span>YGG</span>', WEBSITE_NAME) ?></a>
<form class="margin-t-8" name="search" method="get" action="<?php echo WEBSITE_URL ?>/index.php">
<input type="text" name="query" value="" placeholder="<?php echo _('search or submit magnet link') ?>" />
<input type="submit" value="<?php echo _('submit') ?>" />
</form>
</div>
</div>
</header>
<main>
<div class="container">
<div class="row">
<div class="column width-100">
<div class="padding-16 margin-y-8 border-radius-3 background-color-night">
<div class="text-center"><?php echo $response->message ?></div>
</div>
</div>
</div>
<?php if (!empty($_SERVER['HTTP_REFERER']) && false !== strpos($_SERVER['HTTP_REFERER'], WEBSITE_URL)) { ?>
<div class="row">
<div class="column width-100 text-right">
<a class="button margin-l-8"
rel="nofollow"
href="<?php echo $_SERVER['HTTP_REFERER'] ?>">
<?php echo _('back') ?>
</a>
</div>
</div>
<?php } ?>
</div>
</main>
<footer>
<div class="container">
<div class="row">
<div class="column width-100 text-center margin-y-8">
<?php foreach (json_decode(file_get_contents(__DIR__ . '/../config/trackers.json')) as $i => $tracker) { ?>
<?php if (!empty($tracker->announce) && !empty($tracker->stats)) { ?>
<a href="<?php echo $tracker->announce ?>"><?php echo sprintf('Tracker %s', $i + 1) ?></a>
/
<a href="<?php echo $tracker->stats ?>"><?php echo _('Stats') ?></a>
|
<?php } ?>
<?php } ?>
<a href="<?php echo WEBSITE_URL ?>/faq.php"><?php echo _('F.A.Q') ?></a>
|
<a href="<?php echo WEBSITE_URL ?>/node.php"><?php echo _('Node') ?></a>
|
<a rel="nofollow" href="<?php echo WEBSITE_URL ?>/index.php?rss"><?php echo _('RSS') ?></a>
<?php if (API_EXPORT_ENABLED) { ?>
|
<a rel="nofollow" href="<?php echo WEBSITE_URL ?>/api/manifest.json"><?php echo _('API') ?></a>
<?php } ?>
|
<a href="https://github.com/YGGverse/YGGtracker"><?php echo _('GitHub') ?></a>
</div>
</div>
</div>
</footer>
</body>
</html>

View file

@ -1,938 +0,0 @@
<?php
// Bootstrap
require_once __DIR__ . '/../../config/bootstrap.php';
// Init Debug
$debug =
[
'time' => [
'ISO8601' => date('c'),
'total' => microtime(true),
],
'memory' =>
[
'start' => memory_get_usage(),
'total' => 0,
'peaks' => 0
],
'exception' => []
];
// Define response
$response =
[
'status' => false,
'message' => _('Internal server error'),
'data' => [
'user' => [],
'magnet' => [],
'magnetDownload' => [],
'magnetComment' => [],
'magnetView' => [],
'magnetStar' => [],
]
];
// Init connections whitelist
$connectionWhiteList = [];
foreach (json_decode(file_get_contents(__DIR__ . '/../../config/nodes.json')) as $node)
{
// Skip non-condition addresses
if (!Valid::url($node->manifest))
{
continue;
}
// Skip current host
$thisUrl = Yggverse\Parser\Url::parse(WEBSITE_URL);
$manifestUrl = Yggverse\Parser\Url::parse($node->manifest);
if (empty($thisUrl->host->name) ||
empty($manifestUrl->host->name) ||
$manifestUrl->host->name == $thisUrl->host->name) // @TODO some mirrors could be available on same host sub-folders, improve condition
{
continue;
}
$connectionWhiteList[] = str_replace(['[',']'], false, $manifestUrl->host->name);
}
// API import enabled
$error = [];
if (!API_IMPORT_ENABLED)
{
$response =
[
'status' => false,
'message' => _('Import API disabled')
];
}
// Push API import enabled
else if (!API_IMPORT_PUSH_ENABLED)
{
$response =
[
'status' => false,
'message' => _('Push API import disabled')
];
}
// Yggdrasil connections only
else if (!Valid::host($_SERVER['REMOTE_ADDR'], $error))
{
$response =
[
'status' => false,
'message' => $error
];
}
// Init session
else if (!in_array($_SERVER['REMOTE_ADDR'], $connectionWhiteList))
{
$response =
[
'status' => false,
'message' => sprintf(
_('Push API access denied for host "%s"'),
$_SERVER['REMOTE_ADDR']
)
];
}
// Init session
else if (!$userId = $db->initUserId($_SERVER['REMOTE_ADDR'], USER_DEFAULT_APPROVED, time()))
{
$response =
[
'status' => false,
'message' => _('Could not init user session for this connection')
];
}
// Validate required fields
else if (empty($_POST['data']))
{
$response =
[
'status' => false,
'message' => _('Request protocol invalid')
];
}
// Validate required fields
else if (false === $data = json_decode($_POST['data']))
{
$response =
[
'status' => false,
'message' => _('Could not decode data requested')
];
}
// Import begin
else
{
$response =
[
'status' => true,
'message' => sprintf(
_('Connection for "%s" established'),
$_SERVER['REMOTE_ADDR']
)
];
// Init alias registry
$aliasUserId = [];
$aliasMagnetId = [];
try {
// Transaction begin
$db->beginTransaction();
// Process request
foreach ((object) $data as $field => $remote)
{
// Process alias fields
switch ($field)
{
case 'user':
if (!API_IMPORT_USERS_ENABLED)
{
$response['user'][] = [
'status' => false,
'message' => _('Users import disabled on this node. Related content skipped.')
];
continue 2;
}
// Validate remote fields
$error = [];
if (!Valid::user($remote, $error))
{
$response['user'][] = [
'status' => false,
'message' => sprintf(
_('User data mismatch protocol with error: %s'),
print_r($error, true)
),
];
continue 2;
}
// Skip import on user approved required
if (API_IMPORT_USERS_APPROVED_ONLY && !$remote->approved)
{
$response['user'][] = [
'status' => false,
'message' => _('Node accepting approved users only')
];
continue 2;
}
// Init local user by remote address
if (!$local = $db->getUser($db->initUserId($remote->address,
USER_AUTO_APPROVE_ON_IMPORT_APPROVED ? $remote->approved : USER_DEFAULT_APPROVED,
$remote->timeAdded)))
{
$response['user'][] = [
'status' => false,
'message' => _('Could not init user profile')
];
continue 2;
}
else
{
$response['user'][] = [
'status' => true,
'message' => sprintf(
_('User profile successfully associated with ID "%s"'),
$local->userId
)
];
}
// Register user alias
$aliasUserId[$remote->userId] = $local->userId;
// Update time added if newer
if ($local->timeAdded < $remote->timeAdded)
{
$db->updateUserTimeAdded(
$local->userId,
$remote->timeAdded
);
$response['user'][] = [
'status' => true,
'message' => sprintf(
_('Field "timeAdded" changed to newer value for user ID "%s"'),
$local->userId
)
];
}
// Update user info if newer
if ($local->timeUpdated < $remote->timeUpdated)
{
// Update time updated
$db->updateUserTimeUpdated(
$local->userId,
$remote->timeUpdated
);
$response['user'][] = [
'status' => true,
'message' => sprintf(
_('Field "timeUpdated" changed to newer value for user ID "%s"'),
$local->userId
)
];
// Update approved for existing user
if (USER_AUTO_APPROVE_ON_IMPORT_APPROVED && $local->approved !== $remote->approved && $remote->approved)
{
$db->updateUserApproved(
$local->userId,
$remote->approved,
$remote->timeUpdated
);
$response['user'][] = [
'status' => true,
'message' => sprintf(
_('Field "approved" changed to newer value for user ID "%s"'),
$local->userId
)
];
}
// Set public as received remotely
if (!$local->public)
{
$db->updateUserPublic(
$local->userId,
true,
$remote->timeUpdated
);
$response['user'][] = [
'status' => true,
'message' => sprintf(
_('Field "public" changed to newer value for user ID "%s"'),
$local->userId
)
];
}
}
break;
case 'magnet':
if (!API_IMPORT_MAGNETS_ENABLED)
{
$response['magnet'][] = [
'status' => false,
'message' => _('Magnets import disabled on this node. Related content skipped.')
];
continue 2;
}
// Validate remote fields
$error = [];
if (!Valid::magnet($remote, $error))
{
$response['magnet'][] = [
'status' => false,
'message' => sprintf(
_('Magnet data mismatch protocol with error: %s'),
print_r($error, true)
),
];
continue 2;
}
// User local alias required
if (!isset($aliasUserId[$remote->userId]))
{
$response['magnet'][] = [
'status' => false,
'message' => _('User data relation not found for magnet'),
];
continue 2;
}
// Skip import on magnet approved required
if (API_IMPORT_MAGNETS_APPROVED_ONLY && !$remote->approved)
{
$response['magnet'][] = [
'status' => false,
'message' => _('Node accepting approved magnets only')
];
continue 2;
}
/// Add new magnet if not exist by timestamp added for this user
if ($local = $db->findMagnet($aliasUserId[$remote->userId], $remote->timeAdded))
{
$response['magnet'][] = [
'status' => true,
'message' => sprintf(
_('Magnet successfully associated with ID "%s"'),
$local->magnetId
)
];
}
/// Add and init new magnet if not exist
else if ($local = $db->getMagnet(
$db->addMagnet(
$aliasUserId[$remote->userId],
$remote->xl,
$remote->dn,
'', // @TODO linkSource used for debug only, will be deleted later
true,
$remote->comments,
$remote->sensitive,
MAGNET_AUTO_APPROVE_ON_IMPORT_APPROVED ? $remote->approved : MAGNET_DEFAULT_APPROVED,
$remote->timeAdded
)
)
)
{
$response['magnet'][] = [
'status' => true,
'message' => sprintf(
_('Magnet successfully synced with ID "%s"'),
$local->magnetId
)
];
}
else
{
$response['magnet'][] = [
'status' => false,
'message' => sprintf(
_('Could not init magnet: %s'),
$remote
)
];
continue 2;
}
/// Add magnet alias for this host
$aliasMagnetId[$remote->magnetId] = $local->magnetId;
/// Update time added if newer
if ($local->timeAdded < $remote->timeAdded)
{
$db->updateMagnetTimeAdded(
$local->magnetId,
$remote->timeAdded
);
$response['magnet'][] = [
'status' => true,
'message' => sprintf(
_('Field "timeAdded" changed to newer value for magnet ID "%s"'),
$local->magnetId
)
];
}
/// Update info if remote newer
if ($local->timeUpdated < $remote->timeUpdated)
{
// Magnet fields
$db->updateMagnetXl($local->magnetId, $remote->xl, $remote->timeUpdated);
$db->updateMagnetDn($local->magnetId, $remote->dn, $remote->timeUpdated);
$db->updateMagnetTitle($local->magnetId, $remote->title, $remote->timeUpdated);
$db->updateMagnetPreview($local->magnetId, $remote->preview, $remote->timeUpdated);
$db->updateMagnetDescription($local->magnetId, $remote->description, $remote->timeUpdated);
$db->updateMagnetComments($local->magnetId, $remote->comments, $remote->timeUpdated);
$db->updateMagnetSensitive($local->magnetId, $remote->sensitive, $remote->timeUpdated);
if (MAGNET_AUTO_APPROVE_ON_IMPORT_APPROVED && $local->approved !== $remote->approved && $remote->approved)
{
$db->updateMagnetApproved($local->magnetId, $remote->approved, $remote->timeUpdated);
}
// xt
foreach ((array) $remote->xt as $xt)
{
switch ($xt->version)
{
case 1:
$exist = false;
foreach ($db->findMagnetToInfoHashByMagnetId($local->magnetId) as $result)
{
if ($infoHash = $db->getInfoHash($result->infoHashId))
{
if ($infoHash->version == 1)
{
$exist = true;
}
}
}
if (!$exist)
{
$db->addMagnetToInfoHash(
$local->magnetId,
$db->initInfoHashId(
$xt->value, 1
)
);
}
break;
case 2:
$exist = false;
foreach ($db->findMagnetToInfoHashByMagnetId($local->magnetId) as $result)
{
if ($infoHash = $db->getInfoHash($result->infoHashId))
{
if ($infoHash->version == 2)
{
$exist = true;
}
}
}
if (!$exist)
{
$db->addMagnetToInfoHash(
$local->magnetId,
$db->initInfoHashId(
$xt->value, 2
)
);
}
break;
}
}
// kt
$db->deleteMagnetToKeywordTopicByMagnetId($local->magnetId);
foreach ($remote->kt as $kt)
{
$db->initMagnetToKeywordTopicId(
$local->magnetId,
$db->initKeywordTopicId(trim(mb_strtolower($kt)))
);
}
// tr
$db->deleteMagnetToAddressTrackerByMagnetId($local->magnetId);
foreach ($remote->tr as $tr)
{
if ($url = Yggverse\Parser\Url::parse($tr))
{
$db->initMagnetToAddressTrackerId(
$local->magnetId,
$db->initAddressTrackerId(
$db->initSchemeId($url->host->scheme),
$db->initHostId($url->host->name),
$db->initPortId($url->host->port),
$db->initUriId($url->page->uri)
)
);
}
}
// as
$db->deleteMagnetToAcceptableSourceByMagnetId($local->magnetId);
foreach ($remote->as as $as)
{
if ($url = Yggverse\Parser\Url::parse($as))
{
$db->initMagnetToAcceptableSourceId(
$local->magnetId,
$db->initAcceptableSourceId(
$db->initSchemeId($url->host->scheme),
$db->initHostId($url->host->name),
$db->initPortId($url->host->port),
$db->initUriId($url->page->uri)
)
);
}
}
// xs
$db->deleteMagnetToExactSourceByMagnetId($local->magnetId);
foreach ($remote->xs as $xs)
{
if ($url = Yggverse\Parser\Url::parse($xs))
{
$db->initMagnetToExactSourceId(
$local->magnetId,
$db->initExactSourceId(
$db->initSchemeId($url->host->scheme),
$db->initHostId($url->host->name),
$db->initPortId($url->host->port),
$db->initUriId($url->page->uri)
)
);
}
}
$response['magnet'][] = [
'status' => true,
'message' => sprintf(
_('Magnet fields updated to newer version for magnet ID "%s"'),
$local->magnetId
)
];
}
break;
case 'magnetComment':
if (!API_IMPORT_MAGNET_COMMENTS_ENABLED)
{
$response['magnetComment'][] = [
'status' => false,
'message' => _('Magnet comments import disabled on this node')
];
continue 2;
}
// Validate
$error = [];
if (!Valid::magnetComment($remote, $error))
{
$response['magnetComment'][] = [
'status' => false,
'message' => sprintf(
_('Magnet comment data mismatch protocol with error: %s'),
print_r($error, true)
),
];
continue 2;
}
// Skip import on magnet approved required
if (API_IMPORT_MAGNET_COMMENTS_APPROVED_ONLY && !$remote->approved)
{
$response['magnetComment'][] = [
'status' => false,
'message' => _('Node accepting approved magnet comments only: %s')
];
continue 2;
}
// User local alias required
if (!isset($aliasUserId[$remote->userId]) || !isset($aliasMagnetId[$remote->magnetId]))
{
$response['magnetComment'][] = [
'status' => false,
'message' => _('Magnet comment data relation not found for: %s')
];
continue 2;
}
// Parent comment provided
if (is_int($remote->magnetCommentIdParent))
{
$localMagnetCommentIdParent = null; // @TODO feature not in use yet
}
else
{
$localMagnetCommentIdParent = null;
}
// Magnet comment exists by timestamp added for this user
if ($local = $db->findMagnetComment($aliasMagnetId[$remote->magnetId],
$aliasUserId[$remote->userId],
$remote->timeAdded))
{
$response['magnetComment'][] = [
'status' => true,
'message' => sprintf(
_('Magnet comment successfully associated with ID "%s"'),
$local->magnetCommentId
)
];
}
// Magnet comment exists by timestamp added for this user, register new one
else if ($magnetCommentId = $db->addMagnetComment($aliasMagnetId[$remote->magnetId],
$aliasUserId[$remote->userId],
$localMagnetCommentIdParent,
$remote->value,
$remote->approved,
true,
$remote->timeAdded))
{
$response['magnetComment'][] = [
'status' => true,
'message' => sprintf(
_('Magnet comment successfully synced with ID "%s"'),
$magnetCommentId
)
];
}
break;
case 'magnetDownload':
// Magnet downloads
if (!API_IMPORT_MAGNET_DOWNLOADS_ENABLED)
{
$response['magnetDownload'][] = [
'status' => false,
'message' => _('Magnet downloads import disabled on this node')
];
continue 2;
}
// Validate
$error = [];
if (!Valid::magnetDownload($remote, $error))
{
$response['magnetDownload'][] = [
'status' => false,
'message' => sprintf(
_('Magnet download data mismatch protocol with error: %s'),
print_r($error, true)
),
];
continue 2;
}
// User local alias required
if (!isset($aliasUserId[$remote->userId]) || !isset($aliasMagnetId[$remote->magnetId]))
{
$response['magnetDownload'][] = [
'status' => false,
'message' => _('Magnet download data relation not found')
];
continue 2;
}
// Magnet download exists by timestamp added for this user
if ($local = $db->findMagnetDownload($aliasMagnetId[$remote->magnetId],
$aliasUserId[$remote->userId],
$remote->timeAdded))
{
$response['magnetDownload'][] = [
'status' => true,
'message' => sprintf(
_('Magnet download successfully associated with ID "%s"'),
$local->magnetDownloadId
)
];
}
// Magnet download exists by timestamp added for this user, register new one
else if ($magnetDownloadId = $db->addMagnetDownload($aliasMagnetId[$remote->magnetId],
$aliasUserId[$remote->userId],
$remote->timeAdded))
{
$response['magnetDownload'][] = [
'status' => true,
'message' => sprintf(
_('Magnet download successfully synced with ID "%s"'),
$magnetDownloadId
)
];
}
break;
case 'magnetStar':
if (!API_IMPORT_MAGNET_STARS_ENABLED)
{
$response['magnetStar'][] = [
'status' => false,
'message' => _('Magnet stars import disabled on this node')
];
continue 2;
}
// Validate
$error = [];
if (!Valid::magnetStar($remote, $error))
{
$response['magnetStar'][] = [
'status' => false,
'message' => sprintf(
_('Magnet star data mismatch protocol with error: %s'),
print_r($error, true)
),
];
continue 2;
}
// User local alias required
if (!isset($aliasUserId[$remote->userId]) || !isset($aliasMagnetId[$remote->magnetId]))
{
$response['magnetStar'][] = [
'status' => false,
'message' => _('Magnet star data relation not found')
];
continue 2;
}
// Magnet star exists by timestamp added for this user
if ($local = $db->findMagnetStar($aliasMagnetId[$remote->magnetId],
$aliasUserId[$remote->userId],
$remote->timeAdded))
{
$response['magnetStar'][] = [
'status' => true,
'message' => sprintf(
_('Magnet star successfully associated with ID "%s"'),
$local->magnetStarId
)
];
}
// Magnet star exists by timestamp added for this user, register new one
else if ($magnetStarId = $db->addMagnetStar($aliasMagnetId[$remote->magnetId],
$aliasUserId[$remote->userId],
$remote->value,
$remote->timeAdded))
{
$response['magnetStar'][] = [
'status' => true,
'message' => sprintf(
_('Magnet star successfully synced with ID "%s"'),
$magnetStarId
)
];
}
break;
case 'magnetView':
if (!API_IMPORT_MAGNET_VIEWS_ENABLED)
{
$response['magnetView'][] = [
'status' => false,
'message' => _('Magnet views import disabled on this node')
];
continue 2;
}
// Validate
$error = [];
if (!Valid::magnetView($remote, $error))
{
$response['magnetView'][] = [
'status' => false,
'message' => sprintf(
_('Magnet view data mismatch protocol with error: %s'),
print_r($error, true)
),
];
continue 2;
}
// User local alias required
if (!isset($aliasUserId[$remote->userId]) || !isset($aliasMagnetId[$remote->magnetId]))
{
$response['magnetView'][] = [
'status' => false,
'message' => _('Magnet view data relation not found for: %s')
];
continue 2;
}
// Magnet view exists by timestamp added for this user
if ($local = $db->findMagnetView($aliasMagnetId[$remote->magnetId],
$aliasUserId[$remote->userId],
$remote->timeAdded))
{
$response['magnetView'][] = [
'status' => true,
'message' => sprintf(
_('Magnet view successfully associated with ID "%s"'),
$local->magnetViewId
)
];
}
// Magnet view exists by timestamp added for this user, register new one
else if ($magnetViewId = $db->addMagnetView($aliasMagnetId[$remote->magnetId],
$aliasUserId[$remote->userId],
$remote->timeAdded))
{
$response['magnetView'][] = [
'status' => true,
'message' => sprintf(
_('Magnet view successfully synced with ID "%s"'),
$magnetViewId
)
];
}
break;
default:
$response[$field][] =
[
'status' => false,
'message' => _('Field "%s" not supported by protocol')
];
continue 2;
}
}
$db->commit();
}
catch (Exception $error)
{
$debug['exception'][] = print_r($error, true);
$db->rollBack();
}
}
// Debug log
if (LOG_API_PUSH_ENABLED)
{
@mkdir(LOG_DIRECTORY, 0770, true);
if ($handle = fopen(LOG_DIRECTORY . '/' . LOG_API_PUSH_FILENAME, 'a+'))
{
$debug['time']['total'] = microtime(true) - $debug['time']['total'];
$debug['memory']['total'] = memory_get_usage() - $debug['memory']['start'];
$debug['memory']['peaks'] = memory_get_peak_usage();
$debug['db']['total']['select'] = $db->getDebug()->query->select->total;
$debug['db']['total']['insert'] = $db->getDebug()->query->insert->total;
$debug['db']['total']['update'] = $db->getDebug()->query->update->total;
$debug['db']['total']['delete'] = $db->getDebug()->query->delete->total;
fwrite(
$handle,
print_r(
[
'response' => $response,
'debug' => $debug
],
true
)
);
fclose($handle);
chmod(LOG_DIRECTORY . '/' . LOG_API_PUSH_FILENAME, 0770);
}
}
// Output
header('Content-Type: application/json; charset=utf-8');
echo json_encode($response);

View file

@ -1,115 +0,0 @@
* {
border: 0;
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #282b3c;
color: #ccc;
font-family: Sans-serif;
font-size: 13px;
}
a,
a:visited,
a:active {
color: #96d9a1;
text-decoration: none;
opacity: .9;
}
a:hover {
opacity: 1;
transition: opacity .5s ease-in-out;
}
h1, h2, h3, h4, h5 {
display: inline-block;
font-weight: normal;
}
h1 {
font-size: 16px;
}
h2 {
color: #ccc;
font-size: 16px;
}
a h2,
a:visited h2,
a:active h2 {
/* @TODO doubts
color: #a4d4ff;
*/
}
input,
textarea {
background: #5d627d;
color: #ccc;
border: 0;
border-radius: 3px;
padding: 6px 8px;
font-size: 13px;
}
textarea:focus,
input:focus {
outline: none;
color: #fff;
}
textarea {
min-height: 86px;
}
/* @TODO improve focus out
textarea:focus {
min-height: 120px;
}
*/
textarea::placeholder,
input::placeholder {
color: #9698a5;
opacity: 1;
}
input:hover,
textarea:hover {
background: #636884;
}
input[type="submit"] {
cursor: pointer;
}
td {
padding: 2px 0;
}
header a.logo {
color: #ccc;
font-size: 22px;
}
header a.logo > span {
color: #96d9a1;
}
a.button,
a.button:visited,
a.button:active,
a.button:hover,
.button {
background: #5d627d;
color: #ccc;
border: 0;
border-radius: 3px;
padding: 6px 8px;
font-size: 13px;
}

View file

@ -1,338 +0,0 @@
.container {
position: relative;
overflow: hidden;
max-width: 748px;
margin: 0 auto;
}
.row {
position: relative;
overflow: hidden;
padding: 8px;
}
.column {
position: relative;
float: left;
}
.float-right {
float: right;
}
.text-center {
text-align: center;
}
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.text-color-green {
color: #96d9a1;
}
.text-color-red {
color: #d77575;
}
.text-color-pink {
color: #b55cab;
}
.text-color-default {
color: #ccc;
}
/*
.text-color-pink {
color: #a44399;
}
*/
.text-color-blue {
color: #5785b7;
}
.text-color-night {
color: #838695;
}
.label {
padding: 4px 8px;
border-radius: 3px;
}
.label-green {
color: #fff;
background-color: #65916d;
}
.position-relative {
position: relative;
}
.top--2 {
top: -2px;
}
.top-2 {
top: 2px;
}
.line-height-26 {
line-height: 26px;
}
.border-radius-3 {
border-radius: 3px;
}
.border-pink-light {
border: 1px #9b6895 solid;
}
.border-pink {
border: 1px #a44399 solid;
}
.border-bottom-pink {
border-bottom: 1px #a44399 solid;
}
.border-default {
border: 1px #5d627d solid;
}
.border-bottom-default {
border-bottom: 1px #5d627d solid;
}
.border-top-default {
border-top: 1px #5d627d solid;
}
.background-color-night {
background-color: #34384f;
}
/*
.background-color-hover-night-light:hover {
background-color: #363a51;
}
*/
.background-color-red {
background-color: #9b4a4a;
}
.cursor-default {
cursor: default;
}
.cursor-help {
cursor: help;
}
.font-width-normal {
font-weight: normal;
}
.font-size-10 {
font-size: 10px;
}
.font-size-12 {
font-size: 12px;
}
.font-size-22 {
font-size: 22px;
}
.padding-0 {
padding: 0;
}
.padding-x-0 {
padding-left: 0;
padding-right: 0;
}
.padding-4 {
padding: 4px;
}
.padding-t-4 {
padding-top: 4px;
}
.padding-y-4 {
padding-top: 4px;
padding-bottom: 4px;
}
.padding-x-4 {
padding-left: 4px;
padding-right: 4px;
}
.padding-x-8 {
padding-left: 8px;
padding-right: 8px;
}
.padding-8 {
padding: 8px;
}
.padding-t-8 {
padding-top: 8px;
}
.padding-b-8 {
padding-bottom: 8px;
}
.padding-y-8 {
padding-top: 8px;
padding-bottom: 8px;
}
.padding-b-16 {
padding-bottom: 16px;
}
.padding-t-16 {
padding-top: 16px;
}
.padding-y-16 {
padding-top: 16px;
padding-bottom: 16px;
}
.padding-x-16 {
padding-left: 16px;
padding-right: 16px;
}
.padding-16 {
padding: 16px;
}
.margin-l-4 {
margin-left: 4px;
}
.margin-l-8 {
margin-left: 8px;
}
.margin-l-16 {
margin-left: 16px;
}
.margin-x-4 {
margin-left: 4px;
margin-right: 4px;
}
.margin-r-4 {
margin-right: 4px;
}
.margin-r-8 {
margin-right: 8px;
}
.margin-l-12 {
margin-left: 12px;
}
.margin-y-8 {
margin-top: 8px;
margin-bottom: 8px;
}
.margin-t-8 {
margin-top: 8px;
}
.margin-b-8 {
margin-bottom: 8px;
}
.margin-t-16 {
margin-top: 16px;
}
.margin-b-16 {
margin-bottom: 16px;
}
.margin-b-24 {
margin-bottom: 24px;
}
.display-block {
display: block;
}
.opacity-0 {
opacity: 0;
}
.opacity-06 {
opacity: .6;
}
.opacity-hover-1:hover {
opacity: 1;
transition: opacity .2s;
}
*:hover > .parent-hover-opacity-09 {
opacity: .9;
transition: opacity .2s;
}
.blur-2 {
filter: blur(2px);
}
.blur-hover-0:hover {
filter: blur(0);
}
/* responsive rules */
.width-100 {
width: 100%;
}
.width-50 {
width: 50%;
}
.width-13px {
width: 13px;
}
@media (max-width: 1220px) {
.width-tablet-100 {
width: 100%;
}
}
@media (max-width: 512px) {
.width-mobile-100 {
width: 100%;
}
}

View file

@ -1,335 +0,0 @@
<?php
// Bootstrap
require_once __DIR__ . '/../config/bootstrap.php';
// Define response
$response = (object)
[
'success' => true,
'message' => _('Internal server error'),
'html' => (object)
[
'title' => sprintf(_('Oops - %s'), WEBSITE_NAME),
'h1' => false,
'link' => (object) [],
]
];
// Yggdrasil connections only
if (!Valid::host($_SERVER['REMOTE_ADDR']))
{
$response->success = false;
$response->message = _('Yggdrasil connection required for this action');
}
// Init session
else if (!$userId = $db->initUserId($_SERVER['REMOTE_ADDR'], USER_DEFAULT_APPROVED, time()))
{
$response->success = false;
$response->message = _('Could not init user session');
}
// Magnet exists
else if (!$magnet = $db->getMagnet(isset($_GET['magnetId']) && $_GET['magnetId'] > 0 ? (int) $_GET['magnetId'] : 0))
{
$response->success = false;
$response->message = _('Requested magnet not found');
}
// Access allowed
else if (!($_SERVER['REMOTE_ADDR'] == $db->getUser($magnet->userId)->address || in_array($_SERVER['REMOTE_ADDR'], MODERATOR_IP_LIST) || ($magnet->public && $magnet->approved))) {
$response->success = false;
$response->message = _('Magnet not available for this action');
}
// Get user
else if (!$user = $db->getUser($userId))
{
$response->success = false;
$response->message = _('Could not init user info');
}
// On first visit, redirect user to the welcome page with access level question
else if (is_null($user->public))
{
header(
sprintf('Location: %s/welcome.php', WEBSITE_URL)
);
}
// Request valid
else
{
// Register magnet download
if ($magnetDownloadId = $db->addMagnetDownload($magnet->magnetId, $user->userId, time()))
{
// Push event to other nodes
if (API_EXPORT_ENABLED &&
API_EXPORT_PUSH_ENABLED &&
API_EXPORT_USERS_ENABLED &&
API_EXPORT_MAGNETS_ENABLED &&
API_EXPORT_MAGNET_DOWNLOADS_ENABLED)
{
if (!$memoryApiExportPush = $memory->get('api.export.push'))
{
$memoryApiExportPush = [];
}
$memoryApiExportPush[] = (object)
[
'time' => time(),
'userId' => $user->userId,
'magnetId' => $magnet->magnetId,
'magnetDownloadId' => $magnetDownloadId
];
$memory->set('api.export.push', $memoryApiExportPush, 3600);
}
}
// Build magnet link
$link = (object)
[
'magnet' => [],
'direct' => [],
];
/// Exact Topic
$xt = [];
foreach ($db->findMagnetToInfoHashByMagnetId($magnet->magnetId) as $result)
{
if ($infoHash = $db->getInfoHash($result->infoHashId))
{
switch ($infoHash->version)
{
case 1:
$xt[] = sprintf('xt=urn:btih:%s', $infoHash->value);
break;
case 2:
$xt[] = sprintf('xt=urn:btmh:1220%s', $infoHash->value);
break;
}
}
}
$link->magnet[] = sprintf('magnet:?%s', implode('&', $xt));
/// Display Name
$link->magnet[] = sprintf('dn=%s', urlencode($magnet->dn));
// Keyword Topic
$kt = [];
foreach ($db->findKeywordTopicByMagnetId($magnet->magnetId) as $result)
{
$kt[] = urlencode($db->getKeywordTopic($result->keywordTopicId)->value);
}
$link->magnet[] = sprintf('kt=%s', implode('+', $kt));
/// Address Tracker
foreach ($db->findAddressTrackerByMagnetId($magnet->magnetId) as $result)
{
$addressTracker = $db->getAddressTracker($result->addressTrackerId);
$scheme = $db->getScheme($addressTracker->schemeId);
$host = $db->getHost($addressTracker->hostId);
$port = $db->getPort($addressTracker->portId);
$uri = $db->getUri($addressTracker->uriId);
// Yggdrasil host only
if (!Valid::host($host->value))
{
continue;
}
$link->magnet[] = sprintf('tr=%s', urlencode($port->value ? sprintf('%s://%s:%s%s', $scheme->value,
$host->value,
$port->value,
$uri->value) : sprintf('%s://%s%s', $scheme->value,
$host->value,
$uri->value)));
}
// Append trackers.json
foreach (json_decode(file_get_contents(__DIR__ . '/../config/trackers.json')) as $tracker)
{
$link->magnet[] = sprintf('tr=%s', urlencode($tracker->announce));
}
/// Acceptable Source
foreach ($db->findAcceptableSourceByMagnetId($magnet->magnetId) as $result)
{
$acceptableSource = $db->getAcceptableSource($result->acceptableSourceId);
$scheme = $db->getScheme($acceptableSource->schemeId);
$host = $db->getHost($acceptableSource->hostId);
$port = $db->getPort($acceptableSource->portId);
$uri = $db->getUri($acceptableSource->uriId);
// Yggdrasil host only
if (!Valid::host($host->value))
{
continue;
}
$link->magnet[] = sprintf('as=%s', urlencode($port->value ? sprintf('%s://%s:%s%s', $scheme->value,
$host->value,
$port->value,
$uri->value) : sprintf('%s://%s%s', $scheme->value,
$host->value,
$uri->value)));
$link->direct[] = $port->value ? sprintf('%s://%s:%s%s', $scheme->value,
$host->value,
$port->value,
$uri->value) : sprintf('%s://%s%s', $scheme->value,
$host->value,
$uri->value);
}
/// Exact Source
foreach ($db->findExactSourceByMagnetId($magnet->magnetId) as $result)
{
$eXactSource = $db->getExactSource($result->eXactSourceId);
$scheme = $db->getScheme($eXactSource->schemeId);
$host = $db->getHost($eXactSource->hostId);
$port = $db->getPort($eXactSource->portId);
$uri = $db->getUri($eXactSource->uriId);
// Yggdrasil host only
if (!Valid::host($host->value))
{
continue;
}
$link->magnet[] = sprintf('xs=%s', urlencode($port->value ? sprintf('%s://%s:%s%s', $scheme->value,
$host->value,
$port->value,
$uri->value) : sprintf('%s://%s%s', $scheme->value,
$host->value,
$uri->value)));
}
// Return html
$response->html->title = sprintf(
_('%s - Download - %s'),
htmlentities($magnet->title),
WEBSITE_NAME
);
$response->html->h1 = htmlentities($magnet->title);
// @TODO implement .bittorrent, separated v1/v2 magnet links
$response->html->link->magnet = implode('&', array_unique($link->magnet));
$response->html->link->direct = $link->direct;
}
?>
<!DOCTYPE html>
<html lang="en-US">
<head>
<link rel="stylesheet" type="text/css" href="<?php echo WEBSITE_URL ?>/assets/theme/default/css/common.css?<?php echo WEBSITE_CSS_VERSION ?>" />
<link rel="stylesheet" type="text/css" href="<?php echo WEBSITE_URL ?>/assets/theme/default/css/framework.css?<?php echo WEBSITE_CSS_VERSION ?>" />
<title>
<?php echo $response->html->title ?>
</title>
<meta name="robots" content="noindex,nofollow"/>
<meta name="author" content="YGGtracker" />
<meta charset="UTF-8" />
</head>
<body>
<header>
<div class="container">
<div class="row margin-t-8 text-center">
<a class="logo" href="<?php echo WEBSITE_URL ?>"><?php echo str_replace('YGG', '<span>YGG</span>', WEBSITE_NAME) ?></a>
<form class="margin-t-8" name="search" method="get" action="<?php echo WEBSITE_URL ?>/index.php">
<input type="text" name="query" value="" placeholder="<?php echo _('search or submit magnet link') ?>" />
<input type="submit" value="<?php echo _('submit') ?>" />
</form>
</div>
</div>
</header>
<main>
<div class="container">
<div class="row">
<div class="column width-100">
<div class="padding-16 margin-y-8 border-radius-3 background-color-night">
<?php if ($response->success) { ?>
<div class="text-center">
<h1 class="display-block margin-b-16 font-size-16"><?php echo $response->html->h1 ?></h1>
<div class="margin-b-16 text-color-night">
<?php echo _('* make sure BitTorrent client listen Yggdrasil interface!') ?>
</div>
<a class="padding-x-4" href="<?php echo $response->html->link->magnet ?>" title="<?php echo _('Magnet') ?>">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-magnet" viewBox="0 0 16 16">
<path d="M8 1a7 7 0 0 0-7 7v3h4V8a3 3 0 0 1 6 0v3h4V8a7 7 0 0 0-7-7Zm7 11h-4v3h4v-3ZM5 12H1v3h4v-3ZM0 8a8 8 0 1 1 16 0v8h-6V8a2 2 0 1 0-4 0v8H0V8Z"/>
</svg>
</a>
<?php foreach ($response->html->link->direct as $direct) { ?>
<a class="padding-x-4" href="<?php echo $direct ?>" title="<?php echo _('Direct') ?>">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-database" viewBox="0 0 16 16">
<path d="M4.318 2.687C5.234 2.271 6.536 2 8 2s2.766.27 3.682.687C12.644 3.125 13 3.627 13 4c0 .374-.356.875-1.318 1.313C10.766 5.729 9.464 6 8 6s-2.766-.27-3.682-.687C3.356 4.875 3 4.373 3 4c0-.374.356-.875 1.318-1.313ZM13 5.698V7c0 .374-.356.875-1.318 1.313C10.766 8.729 9.464 9 8 9s-2.766-.27-3.682-.687C3.356 7.875 3 7.373 3 7V5.698c.271.202.58.378.904.525C4.978 6.711 6.427 7 8 7s3.022-.289 4.096-.777A4.92 4.92 0 0 0 13 5.698ZM14 4c0-1.007-.875-1.755-1.904-2.223C11.022 1.289 9.573 1 8 1s-3.022.289-4.096.777C2.875 2.245 2 2.993 2 4v9c0 1.007.875 1.755 1.904 2.223C4.978 15.71 6.427 16 8 16s3.022-.289 4.096-.777C13.125 14.755 14 14.007 14 13V4Zm-1 4.698V10c0 .374-.356.875-1.318 1.313C10.766 11.729 9.464 12 8 12s-2.766-.27-3.682-.687C3.356 10.875 3 10.373 3 10V8.698c.271.202.58.378.904.525C4.978 9.71 6.427 10 8 10s3.022-.289 4.096-.777A4.92 4.92 0 0 0 13 8.698Zm0 3V13c0 .374-.356.875-1.318 1.313C10.766 14.729 9.464 15 8 15s-2.766-.27-3.682-.687C3.356 13.875 3 13.373 3 13v-1.302c.271.202.58.378.904.525C4.978 12.71 6.427 13 8 13s3.022-.289 4.096-.777c.324-.147.633-.323.904-.525Z"/>
</svg>
</a>
<?php } ?>
</div>
<?php } else { ?>
<div class="text-center">
<?php echo $response->message ?>
</div>
<?php } ?>
</div>
</div>
</div>
<?php if (!empty($_SERVER['HTTP_REFERER']) && false !== strpos($_SERVER['HTTP_REFERER'], WEBSITE_URL)) { ?>
<div class="row">
<div class="column width-100 text-right">
<a class="button margin-l-8"
rel="nofollow"
href="<?php echo $_SERVER['HTTP_REFERER'] ?>">
<?php echo _('back') ?>
</a>
</div>
</div>
<?php } ?>
</div>
</main>
<footer>
<div class="container">
<div class="row">
<div class="column width-100 text-center margin-y-8">
<?php foreach (json_decode(file_get_contents(__DIR__ . '/../config/trackers.json')) as $i => $tracker) { ?>
<?php if (!empty($tracker->announce) && !empty($tracker->stats)) { ?>
<a href="<?php echo $tracker->announce ?>"><?php echo sprintf('Tracker %s', $i + 1) ?></a>
/
<a href="<?php echo $tracker->stats ?>"><?php echo _('Stats') ?></a>
|
<?php } ?>
<?php } ?>
<a href="<?php echo WEBSITE_URL ?>/faq.php"><?php echo _('F.A.Q') ?></a>
|
<a href="<?php echo WEBSITE_URL ?>/node.php"><?php echo _('Node') ?></a>
|
<a rel="nofollow" href="<?php echo WEBSITE_URL ?>/index.php?rss"><?php echo _('RSS') ?></a>
<?php if (API_EXPORT_ENABLED) { ?>
|
<a rel="nofollow" href="<?php echo WEBSITE_URL ?>/api/manifest.json"><?php echo _('API') ?></a>
<?php } ?>
|
<a href="https://github.com/YGGverse/YGGtracker"><?php echo _('GitHub') ?></a>
</div>
</div>
</div>
</footer>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show more