arizona-forum-api-async 1.0__py3-none-any.whl → 1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arizona-forum-api-async
3
- Version: 1.0
3
+ Version: 1.1
4
4
  Summary: Асинхронная Python библиотека для взаимодействия с форумом Arizona RP (forum.arizona-rp.com) без необходимости получения API ключа.
5
5
  Home-page: https://github.com/fakelag28/Arizona-Forum-API-Async
6
6
  Author: fakelag28
@@ -10,7 +10,6 @@ Classifier: License :: OSI Approved :: MIT License
10
10
  Classifier: Operating System :: OS Independent
11
11
  Requires-Python: >=3.6
12
12
  Description-Content-Type: text/markdown
13
- License-File: LICENSE
14
13
  Requires-Dist: aiohttp
15
14
  Requires-Dist: aiohttp-socks
16
15
  Requires-Dist: beautifulsoup4
@@ -22,7 +21,6 @@ Dynamic: classifier
22
21
  Dynamic: description
23
22
  Dynamic: description-content-type
24
23
  Dynamic: home-page
25
- Dynamic: license-file
26
24
  Dynamic: requires-dist
27
25
  Dynamic: requires-python
28
26
  Dynamic: summary
@@ -31,7 +29,6 @@ Dynamic: summary
31
29
 
32
30
  [![PyPI version](https://img.shields.io/pypi/v/arizona-forum-api-async.svg)](https://pypi.org/project/arizona-forum-api-async/)
33
31
  [![Python Versions](https://img.shields.io/pypi/pyversions/arizona-forum-api-async.svg)](https://pypi.org/project/arizona-forum-api-async/)
34
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
35
32
  [![Downloads](https://static.pepy.tech/badge/arizona-forum-api-async)](https://pepy.tech/project/arizona-forum-api-async)
36
33
 
37
34
  **Асинхронная Python библиотека для взаимодействия с форумом Arizona RP (forum.arizona-rp.com) без необходимости получения API ключа.**
@@ -80,12 +77,6 @@ pip install arizona-forum-api-async
80
77
 
81
78
  ## Документация и примеры
82
79
 
83
- * **[Wiki (Расширенная документация синхронной версии)](https://github.com/fakelag28/Arizona-Forum-API-Async/wiki/%D0%9E%D1%81%D0%BD%D0%BE%D0%B2%D0%BD%D0%B0%D1%8F-%D0%B4%D0%BE%D0%BA%D1%83%D0%BC%D0%B5%D0%BD%D1%82%D0%B0%D1%86%D0%B8%D1%8F):** Подробная документация для другой [расширенной синхронной версии](https://github.com/fakelag28/Arizona-Forum-API-Extended/).
80
+ * **[Wiki (Расширенная документация синхронной версии)](https://github.com/fakelag28/Arizona-Forum-API-Extended/wiki/Основная-документация):** Подробная документация для другой [расширенной синхронной версии](https://github.com/fakelag28/Arizona-Forum-API-Extended/).
84
81
  * **[Документация оригинальной библиотеки](https://tastybread123.github.io/Arizona-API/arz_api.html):** Документация для оригинальной синхронной библиотеки.
85
82
  * **[Папка с примерами](https://github.com/fakelag28/Arizona-Forum-API-Async/tree/main/examples):** Практические примеры, демонстрирующие различные возможности библиотеки.
86
-
87
- ---
88
-
89
- ## Лицензия
90
-
91
- Этот проект лицензирован под **MIT License**.
@@ -1,17 +1,16 @@
1
- arizona_forum_api_async-1.0.dist-info/licenses/LICENSE,sha256=lvvIkvjnIhkBIYnSGe3_UIt3uPDEWcthURuMP8NBvLs,1085
2
1
  arizona_forum_async/__init__.py,sha256=LJfbSfw1rC8SKOl5AON1KYPnuKA8CamlgEaNRbMybI4,68
3
- arizona_forum_async/api.py,sha256=62sulCo6PsEkEAnUd9qhrpCMr1FZP8KXijXPIbg-6wU,73358
2
+ arizona_forum_async/api.py,sha256=DJiTVLAW47xqy6R3wl_h-UxsCwKxf5d0n2Kwj70Rc4g,94559
4
3
  arizona_forum_async/consts.py,sha256=AlYiIL9Z5t_HvZfaPKfQc72yg6cbl_VHvEs3GYIo3JU,527
5
4
  arizona_forum_async/exceptions.py,sha256=mcWTrRgl1m5ljMYK2-WlNXWnnKSzI5K7fd_EYxJ6-eo,563
6
5
  arizona_forum_async/bypass_antibot/__init__.py,sha256=8FaH5DlQ4acb-sjj-CUYmLsm1Y2zrB2zPVWayybokGo,21
7
- arizona_forum_async/bypass_antibot/script.py,sha256=oSP_SFwpuSsrfJiTwT2aDBozaBbW-FLE6raolnbbyKk,35306
6
+ arizona_forum_async/bypass_antibot/script.py,sha256=vlIkzhWHJOL5hX8YY5hdcI7q9TReNsZJYSEiecq7H0U,35300
8
7
  arizona_forum_async/models/__init__.py,sha256=Oz9cUlLgRC2Wbqd79bE9ffiHjxRIwE_Zguf4t9dDDyE,112
9
8
  arizona_forum_async/models/category_object.py,sha256=6quTtKSOyROK87MKCmrMyBxiMJv65weB0JAxYg95E10,4832
10
9
  arizona_forum_async/models/member_object.py,sha256=8DpJ-IbQkMnEQZAD9hWGRJT4rXECGjKHLiCijL3ja4Y,4882
11
10
  arizona_forum_async/models/other.py,sha256=hC9Hg749RRSULUpZ_l_cT5VwpsrG1MOFGZ8MyP3nuCc,536
12
11
  arizona_forum_async/models/post_object.py,sha256=eJ3YrO1QbYtpCcErV1jfRfGFuXNmpXYwaIZT0h5sjVM,6097
13
12
  arizona_forum_async/models/thread_object.py,sha256=pCrv1yRY01EYEMEbQES6TUeR5yfJGPE0oaS17nTsmxI,5737
14
- arizona_forum_api_async-1.0.dist-info/METADATA,sha256=Q75ET_m8AvMbBNznNcH0a4gKkmj88uTpruqgR-nOCgQ,5471
15
- arizona_forum_api_async-1.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
16
- arizona_forum_api_async-1.0.dist-info/top_level.txt,sha256=a9GRkw-bNV0GUAHWG7S7n5lGQvBuNu6dfXWF5WqEw6A,20
17
- arizona_forum_api_async-1.0.dist-info/RECORD,,
13
+ arizona_forum_api_async-1.1.dist-info/METADATA,sha256=t4tV5DSxHwH-3jCjdolGDcP1EDIS94zp36Q_eJHST5Y,5135
14
+ arizona_forum_api_async-1.1.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
15
+ arizona_forum_api_async-1.1.dist-info/top_level.txt,sha256=a9GRkw-bNV0GUAHWG7S7n5lGQvBuNu6dfXWF5WqEw6A,20
16
+ arizona_forum_api_async-1.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (79.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -3,7 +3,9 @@ from bs4 import BeautifulSoup
3
3
  from re import compile, findall
4
4
  import re
5
5
  from html import unescape
6
- from typing import List, Dict, Optional, Union
6
+ from typing import List, Dict, Optional, Union, Tuple
7
+ from collections import defaultdict
8
+ import datetime
7
9
 
8
10
  from arizona_forum_async.consts import MAIN_URL, ROLE_COLOR
9
11
  from arizona_forum_async.bypass_antibot import bypass_async
@@ -22,7 +24,6 @@ class ArizonaAPI:
22
24
  self.cookie_str = "; ".join([f"{k}={v}" for k, v in cookie.items()])
23
25
  self._session: aiohttp.ClientSession = None
24
26
  self._token: str = None
25
-
26
27
 
27
28
  async def connect(self, do_bypass: bool = True):
28
29
  """Асинхронный метод для создания сессии, получения токена и обхода анти-бота."""
@@ -139,7 +140,6 @@ class ArizonaAPI:
139
140
  print(f"Неожиданная ошибка при получении категории {category_id}: {e}")
140
141
  return None
141
142
 
142
-
143
143
  async def get_member(self, user_id: int) -> 'Member | None':
144
144
  if not self._session or self._session.closed:
145
145
  raise Exception("Сессия не активна. Вызовите connect() сначала.")
@@ -465,7 +465,7 @@ class ArizonaAPI:
465
465
  return None
466
466
 
467
467
 
468
- # ---------------================ МЕТОДЫ ОБЪЕКТОВ ====================--------------------
468
+ # ---------------================ МЕТОДЫ ОБЪЕКТОВ ====================--------------------
469
469
 
470
470
  # CATEGORY
471
471
  async def create_thread(self, category_id: int, title: str, message_html: str, discussion_type: str = 'discussion', watch_thread: bool = True) -> aiohttp.ClientResponse:
@@ -562,7 +562,6 @@ class ArizonaAPI:
562
562
  print(f"Неожиданная ошибка при получении тем из категории {category_id} (страница {page}): {e}")
563
563
  return None
564
564
 
565
-
566
565
  async def get_thread_category_detail(self, category_id: int, page: int = 1) -> Optional[List[Dict]]:
567
566
  if not self._session or self._session.closed:
568
567
  raise Exception("Сессия не активна. Вызовите connect() сначала.")
@@ -648,7 +647,6 @@ class ArizonaAPI:
648
647
  print(f"Неожиданная ошибка при получении расширенных тем из категории {category_id} (страница {page}): {e}")
649
648
  return None
650
649
 
651
-
652
650
  async def get_parent_category_of_category(self, category_id: int) -> Optional[Category]:
653
651
  if not self._session or self._session.closed:
654
652
  raise Exception("Сессия не активна. Вызовите connect() сначала.")
@@ -682,7 +680,6 @@ class ArizonaAPI:
682
680
  print(f"Неожиданная ошибка при получении родительской категории для {category_id}: {e}")
683
681
  return None
684
682
 
685
-
686
683
  async def get_categories(self, category_id: int) -> Optional[List[int]]:
687
684
  if not self._session or self._session.closed:
688
685
  raise Exception("Сессия не активна. Вызовите connect() сначала.")
@@ -714,7 +711,6 @@ class ArizonaAPI:
714
711
  print(f"Неожиданная ошибка при получении дочерних категорий из {category_id}: {e}")
715
712
  return None
716
713
 
717
-
718
714
  # MEMBER
719
715
  async def follow_member(self, member_id: int) -> aiohttp.ClientResponse:
720
716
  if member_id == self.current_member.id:
@@ -731,7 +727,6 @@ class ArizonaAPI:
731
727
  print(f"Ошибка сети при подписке/отписке от пользователя {member_id}: {e}")
732
728
  raise e
733
729
 
734
-
735
730
  async def ignore_member(self, member_id: int) -> aiohttp.ClientResponse:
736
731
  if member_id == self.current_member.id:
737
732
  raise ThisIsYouError(member_id)
@@ -1021,7 +1016,6 @@ class ArizonaAPI:
1021
1016
  print(f"Ошибка сети при редактировании темы {thread_id} (пост {thread_post_id}): {e}")
1022
1017
  raise e
1023
1018
 
1024
-
1025
1019
  async def edit_thread_info(self, thread_id: int, title: str, prefix_id: Optional[int] = None, sticky: bool = True, opened: bool = True) -> aiohttp.ClientResponse:
1026
1020
  if not self._session or self._session.closed:
1027
1021
  raise Exception("Сессия не активна. Вызовите connect() сначала.")
@@ -1368,6 +1362,54 @@ class ArizonaAPI:
1368
1362
  print(f"Неожиданная ошибка при поиске тем '{query}': {e}")
1369
1363
  return []
1370
1364
 
1365
+ async def search_members(self, nickname: str) -> list:
1366
+ """Поиск пользователей по нику
1367
+
1368
+ Attributes:
1369
+ nickname (str): Никнейм или его часть для поиска
1370
+
1371
+ Returns:
1372
+ Список словарей с информацией о найденных пользователях
1373
+ """
1374
+ if not self._session or self._session.closed:
1375
+ raise Exception("Сессия не активна. Вызовите connect() сначала.")
1376
+
1377
+ try:
1378
+ token = await self.token
1379
+ url = f"{MAIN_URL}/index.php?members/find&q={nickname}&_xfRequestUri=%2Fsearch%2F&_xfWithData=1&_xfToken={token}&_xfResponseType=json"
1380
+
1381
+ async with self._session.get(url) as response:
1382
+ response.raise_for_status()
1383
+ data = await response.json()
1384
+
1385
+ results = []
1386
+ if data.get('results'):
1387
+ for user in data['results']:
1388
+ try:
1389
+ user_id_match = re.search(r'data-user-id="(\d+)"', user.get('username_color', ''))
1390
+ user_id = int(user_id_match.group(1)) if user_id_match else None
1391
+ avatar_match = re.search(r'src="([^"]+)"', user.get('iconHtml', ''))
1392
+ avatar = avatar_match.group(1) if avatar_match else None
1393
+
1394
+ user_data = {
1395
+ 'user_id': user_id,
1396
+ 'username': user.get('id'),
1397
+ 'avatar': avatar,
1398
+ 'profile_url': f"{MAIN_URL}{user['username_color'].split('href=\"')[1].split('\"')[0]}" if user.get('username_color') else None
1399
+ }
1400
+ results.append(user_data)
1401
+ except (ValueError, KeyError, AttributeError) as e:
1402
+ print(f"Ошибка обработки данных пользователя: {e}")
1403
+ continue
1404
+
1405
+ return results
1406
+ except aiohttp.ClientError as e:
1407
+ print(f"Ошибка сети при поиске пользователей по нику '{nickname}': {e}")
1408
+ return []
1409
+ except Exception as e:
1410
+ print(f"Неожиданная ошибка при поиске пользователей '{nickname}': {e}")
1411
+ return []
1412
+
1371
1413
  async def mark_notifications_read(self, alert_ids: list[int]) -> aiohttp.ClientResponse:
1372
1414
  """Пометить уведомления как прочитанные"""
1373
1415
  if not self._session or self._session.closed:
@@ -1446,4 +1488,359 @@ class ArizonaAPI:
1446
1488
  return ''
1447
1489
  except Exception as e:
1448
1490
  print(f"Неожиданная ошибка при конвертации BBCode для поста {post_id}: {e}")
1449
- return ''
1491
+ return ''
1492
+
1493
+ async def get_category_statistics_threads(self, category_id: int, duration: str = 'week') -> Optional[Dict]:
1494
+ """
1495
+ Собирает статистику по темам в указанной категории за определенный период.
1496
+ Останавливает просмотр страниц, как только на странице не будет найдено тем, созданных после начала периода.
1497
+
1498
+ Args:
1499
+ category_id (int): ID категории форума.
1500
+ duration (str): Период для статистики ('day', 'week', 'month'). По умолчанию 'week'.
1501
+
1502
+ Returns:
1503
+ Optional[Dict]: Словарь со статистикой или None в случае ошибки.
1504
+ Структура словаря:
1505
+ {
1506
+ 'category_title': str,
1507
+ 'category_id': int,
1508
+ 'period': str,
1509
+ 'start_timestamp': int,
1510
+ 'end_timestamp': int,
1511
+ 'total_threads_in_category': int, # Общее кол-во тем, обработанных до остановки
1512
+ 'on_review': int, # Открытые и не закрепленные
1513
+ 'pinned': int,
1514
+ 'unpinned': int, # Все не закрепленные (включая 'on_review')
1515
+ 'closed_in_period': int, # Закрытые именно в этот период
1516
+ 'currently_open': int, # Текущее кол-во открытых тем (среди обработанных)
1517
+ 'currently_closed': int, # Текущее кол-во закрытых тем (среди обработанных)
1518
+ 'average_closing_time': str, # Среднее время закрытия в ЧЧ:ММ:СС
1519
+ 'average_closing_time_seconds': float,
1520
+ 'closer_stats': List[Dict], # Список закрывших с кол-вом и процентом
1521
+ 'total_pages_in_category': int, # Общее кол-во страниц в категории
1522
+ 'processed_pages': int # Кол-во фактически обработанных страниц
1523
+ }
1524
+ """
1525
+ if not self._session or self._session.closed:
1526
+ print("Ошибка: Сессия не активна. Вызовите connect() сначала.")
1527
+ return None
1528
+
1529
+ now = datetime.datetime.now(datetime.timezone.utc)
1530
+ if duration == 'day':
1531
+ delta = datetime.timedelta(days=1)
1532
+ period_str = "день"
1533
+ elif duration == 'week':
1534
+ delta = datetime.timedelta(weeks=1)
1535
+ period_str = "неделю"
1536
+ elif duration == 'month':
1537
+ delta = datetime.timedelta(days=30)
1538
+ period_str = "месяц"
1539
+ elif duration == 'year':
1540
+ delta = datetime.timedelta(days=365)
1541
+ period_str = "год"
1542
+ else:
1543
+ print(f"Ошибка: Неверное значение duration '{duration}'. Используйте 'day', 'week' или 'month'.")
1544
+ return None
1545
+
1546
+ start_timestamp = int((now - delta).timestamp())
1547
+
1548
+ category_info = await self.get_category(category_id)
1549
+ if not category_info:
1550
+ print(f"Не удалось получить информацию о категории {category_id}")
1551
+ return None
1552
+
1553
+ total_pages_in_category = category_info.pages_count
1554
+ category_title = category_info.title
1555
+
1556
+ pinned_count = 0
1557
+ unpinned_count = 0
1558
+ currently_closed_count = 0
1559
+ currently_open_count = 0
1560
+ on_review_count = 0
1561
+
1562
+ closed_in_period_count = 0
1563
+ total_closing_duration_seconds = 0
1564
+ closers_stats = defaultdict(int)
1565
+
1566
+ all_threads_processed = 0
1567
+ processed_pages_count = 0
1568
+
1569
+ for page in range(1, total_pages_in_category + 1):
1570
+ processed_pages_count = page
1571
+ page_contains_recent_threads = False
1572
+
1573
+ try:
1574
+ page_threads = await self.get_thread_category_detail(category_id, page)
1575
+
1576
+ if page_threads is None:
1577
+ print(f"Предупреждение: Не удалось получить или обработать темы со страницы {page} категории {category_id} (возможно, ошибка парсинга). Пропускаем страницу.")
1578
+ continue
1579
+
1580
+ except AttributeError as e:
1581
+ print(f"Ошибка атрибута при обработке страницы {page} категории {category_id}: {e}. Пропускаем страницу.")
1582
+ continue
1583
+ except Exception as e:
1584
+ print(f"Неожиданная ошибка при получении тем из категории {category_id} (страница {page}): {e}. Пропускаем страницу.")
1585
+ continue
1586
+
1587
+ if not page_threads:
1588
+ print(f"Страница {page} категории {category_id} пуста или не содержит тем.")
1589
+ continue
1590
+
1591
+ all_threads_processed += len(page_threads)
1592
+
1593
+ for thread_data in page_threads:
1594
+ if thread_data.get('is_pinned'):
1595
+ pinned_count += 1
1596
+ else:
1597
+ unpinned_count += 1
1598
+
1599
+ if thread_data.get('is_closed'):
1600
+ currently_closed_count += 1
1601
+ else:
1602
+ currently_open_count += 1
1603
+ if not thread_data.get('is_pinned'):
1604
+ on_review_count += 1
1605
+
1606
+ last_message_date = thread_data.get('last_message_date')
1607
+ closer_username = thread_data.get('username_last_message')
1608
+ created_date = thread_data.get('created_date')
1609
+
1610
+ if thread_data.get('is_closed') and last_message_date and closer_username and created_date:
1611
+ if last_message_date >= start_timestamp:
1612
+ closed_in_period_count += 1
1613
+ closing_time_seconds = last_message_date - created_date
1614
+ if closing_time_seconds >= 0:
1615
+ total_closing_duration_seconds += closing_time_seconds
1616
+ closers_stats[closer_username] += 1
1617
+
1618
+ if created_date and created_date >= start_timestamp:
1619
+ page_contains_recent_threads = True
1620
+
1621
+ if not page_contains_recent_threads:
1622
+ print(f"Остановка на странице {page}: не найдено тем, созданных после {datetime.datetime.fromtimestamp(start_timestamp, tz=datetime.timezone.utc).strftime('%Y-%m-%d %H:%M:%S')}.")
1623
+ break
1624
+
1625
+ average_closing_time_str = "N/A"
1626
+ avg_seconds_float = 0.0
1627
+ if closed_in_period_count > 0:
1628
+ avg_seconds = total_closing_duration_seconds / closed_in_period_count
1629
+ avg_seconds_float = avg_seconds
1630
+ avg_td = datetime.timedelta(seconds=int(avg_seconds))
1631
+
1632
+ total_seconds_int = avg_td.days * 86400 + avg_td.seconds
1633
+ hours, remainder = divmod(total_seconds_int, 3600)
1634
+ minutes, seconds = divmod(remainder, 60)
1635
+ average_closing_time_str = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
1636
+
1637
+
1638
+ sorted_closers = sorted(closers_stats.items(), key=lambda item: item[1], reverse=True)
1639
+ formatted_closers = []
1640
+ total_closed_by_tracked = sum(closers_stats.values())
1641
+
1642
+ for username, count in sorted_closers:
1643
+ percentage = (count / total_closed_by_tracked * 100) if total_closed_by_tracked > 0 else 0
1644
+ formatted_closers.append({
1645
+ 'username': username,
1646
+ 'count': count,
1647
+ 'percentage': round(percentage, 2)
1648
+ })
1649
+
1650
+ result = {
1651
+ 'category_title': category_title,
1652
+ 'category_id': category_id,
1653
+ 'period': period_str,
1654
+ 'start_timestamp': start_timestamp,
1655
+ 'end_timestamp': int(now.timestamp()),
1656
+ 'total_threads_in_category': all_threads_processed,
1657
+ 'on_review': on_review_count,
1658
+ 'pinned': pinned_count,
1659
+ 'unpinned': unpinned_count,
1660
+ 'closed_in_period': closed_in_period_count,
1661
+ 'currently_open': currently_open_count,
1662
+ 'currently_closed': currently_closed_count,
1663
+ 'average_closing_time': average_closing_time_str,
1664
+ 'average_closing_time_seconds': avg_seconds_float,
1665
+ 'closer_stats': formatted_closers,
1666
+ 'total_pages_in_category': total_pages_in_category,
1667
+ 'processed_pages': processed_pages_count
1668
+ }
1669
+
1670
+ return result
1671
+
1672
+ async def get_category_statistics_posts(self, category_id: int, duration: str = 'week') -> Optional[Dict]:
1673
+ """
1674
+ Собирает статистику по постам в темах указанной категории за определенный период.
1675
+
1676
+ Args:
1677
+ category_id (int): ID категории форума.
1678
+ duration (str): Период для статистики ('day', 'week', 'month', 'year'). По умолчанию 'week'.
1679
+
1680
+ Returns:
1681
+ Optional[Dict]: Словарь со статистикой по постам или None в случае ошибки.
1682
+ Структура словаря:
1683
+ {
1684
+ 'category_title': str,
1685
+ 'category_id': int,
1686
+ 'period': str, # Описание периода ('за день', 'за неделю'...)
1687
+ 'start_timestamp': int, # Начало периода (Unix time)
1688
+ 'end_timestamp': int, # Конец периода (Unix time)
1689
+ 'total_threads_checked': int, # Кол-во тем, чьи посты проверялись
1690
+ 'total_posts_in_period': int, # Общее кол-во постов за период
1691
+ 'posts_by_user': List[Dict], # Список пользователей с кол-вом постов и %
1692
+ # [{'username': str, 'count': int, 'percentage': float}]
1693
+ 'total_category_pages': int, # Общее кол-во страниц в категории
1694
+ 'processed_category_pages': int, # Кол-во обработанных страниц категории
1695
+ }
1696
+ """
1697
+ if not self._session or self._session.closed:
1698
+ print("Ошибка: Сессия не активна. Вызовите connect() сначала.")
1699
+ return None
1700
+
1701
+ now = datetime.datetime.now(datetime.timezone.utc)
1702
+ if duration == 'day':
1703
+ delta = datetime.timedelta(days=1)
1704
+ period_str = "за день"
1705
+ elif duration == 'week':
1706
+ delta = datetime.timedelta(weeks=1)
1707
+ period_str = "за неделю"
1708
+ elif duration == 'month':
1709
+ delta = datetime.timedelta(days=30)
1710
+ period_str = "за месяц"
1711
+ elif duration == 'year':
1712
+ delta = datetime.timedelta(days=365)
1713
+ period_str = "за год"
1714
+ else:
1715
+ print(f"Ошибка: Неверное значение duration '{duration}'. Используйте 'day', 'week', 'month' или 'year'.")
1716
+ return None
1717
+
1718
+ start_timestamp = int((now - delta).timestamp())
1719
+ end_timestamp = int(now.timestamp())
1720
+
1721
+ category_info = await self.get_category(category_id)
1722
+ if not category_info:
1723
+ print(f"Не удалось получить информацию о категории {category_id}")
1724
+ return None
1725
+
1726
+ total_category_pages = category_info.pages_count
1727
+ category_title = category_info.title
1728
+
1729
+ posts_by_user = defaultdict(int)
1730
+ total_posts_in_period = 0
1731
+ total_threads_checked = 0
1732
+ processed_category_pages = 0
1733
+
1734
+ for cat_page_num in range(1, total_category_pages + 1):
1735
+ processed_category_pages = cat_page_num
1736
+
1737
+ try:
1738
+ threads_on_page = await self.get_thread_category_detail(category_id, cat_page_num)
1739
+
1740
+ if threads_on_page is None:
1741
+ print(f"Предупреждение: Не удалось получить темы со страницы {cat_page_num} категории {category_id}. Пропуск страницы.")
1742
+ continue
1743
+ if not threads_on_page:
1744
+ continue
1745
+
1746
+ except Exception as e:
1747
+ print(f"Неожиданная ошибка при получении тем из категории {category_id} (страница {cat_page_num}): {e}. Пропуск страницы.")
1748
+ continue
1749
+
1750
+ for thread_data in threads_on_page:
1751
+ thread_id = thread_data.get('thread_id')
1752
+ last_message_date = thread_data.get('last_message_date')
1753
+
1754
+ if not thread_id:
1755
+ print(f"Предупреждение: Пропуск темы без ID на стр. {cat_page_num} категории {category_id}.")
1756
+ continue
1757
+
1758
+ if last_message_date and last_message_date < start_timestamp:
1759
+ continue
1760
+
1761
+ total_threads_checked += 1
1762
+
1763
+ try:
1764
+ thread_details = await self.get_thread(thread_id)
1765
+ if not thread_details:
1766
+ print(f"Предупреждение: Не удалось получить детали темы {thread_id}. Пропуск темы.")
1767
+ continue
1768
+ thread_pages_count = thread_details.pages_count
1769
+ except Exception as e:
1770
+ print(f"Ошибка при получении деталей темы {thread_id}: {e}. Пропуск темы.")
1771
+ continue
1772
+
1773
+ stop_processing_this_thread = False
1774
+
1775
+ for thread_page_num in range(thread_pages_count, 0, -1):
1776
+ if stop_processing_this_thread:
1777
+ break
1778
+
1779
+ page_url = f"{MAIN_URL}/threads/{thread_id}/page-{thread_page_num}"
1780
+ try:
1781
+ async with self._session.get(page_url) as response:
1782
+ if response.status == 404:
1783
+ print(f"Предупреждение: Страница {thread_page_num} темы {thread_id} не найдена (404).")
1784
+ continue
1785
+ response.raise_for_status()
1786
+ page_html = await response.text()
1787
+ page_soup = BeautifulSoup(page_html, 'lxml')
1788
+
1789
+ posts_on_page = page_soup.find_all('article', class_=re.compile(r'\bmessage--post\b'))
1790
+ if not posts_on_page:
1791
+ continue
1792
+
1793
+ page_had_relevant_posts = False
1794
+
1795
+ for post_article in posts_on_page:
1796
+ post_author_name = "Неизвестный автор"
1797
+ post_author_tag = post_article.find('a', class_='username', attrs={'data-user-id': True})
1798
+ if post_author_tag:
1799
+ post_author_name = post_author_tag.text.strip()
1800
+
1801
+ post_timestamp = 0
1802
+ post_time_tag = post_article.find('time', class_='u-dt', attrs={'data-time': True})
1803
+ if post_time_tag and post_time_tag.get('data-time','').isdigit():
1804
+ post_timestamp = int(post_time_tag['data-time'])
1805
+ else:
1806
+ continue
1807
+
1808
+ if post_timestamp >= start_timestamp:
1809
+ total_posts_in_period += 1
1810
+ posts_by_user[post_author_name] += 1
1811
+ page_had_relevant_posts = True
1812
+ else:
1813
+ stop_processing_this_thread = True
1814
+ break
1815
+
1816
+ except aiohttp.ClientError as e:
1817
+ print(f"Ошибка сети при получении страницы {thread_page_num} темы {thread_id}: {e}")
1818
+ continue
1819
+ except Exception as e:
1820
+ print(f"Неожиданная ошибка при обработке страницы {thread_page_num} темы {thread_id}: {e}")
1821
+ continue
1822
+
1823
+ sorted_users = sorted(posts_by_user.items(), key=lambda item: item[1], reverse=True)
1824
+ formatted_users = []
1825
+ for username, count in sorted_users:
1826
+ percentage = (count / total_posts_in_period * 100) if total_posts_in_period > 0 else 0
1827
+ formatted_users.append({
1828
+ 'username': username,
1829
+ 'count': count,
1830
+ 'percentage': round(percentage, 2)
1831
+ })
1832
+
1833
+ result = {
1834
+ 'category_title': category_title,
1835
+ 'category_id': category_id,
1836
+ 'period': period_str,
1837
+ 'start_timestamp': start_timestamp,
1838
+ 'end_timestamp': end_timestamp,
1839
+ 'total_threads_checked': total_threads_checked,
1840
+ 'total_posts_in_period': total_posts_in_period,
1841
+ 'posts_by_user': formatted_users,
1842
+ 'total_category_pages': total_category_pages,
1843
+ 'processed_category_pages': processed_category_pages,
1844
+ }
1845
+
1846
+ return result
@@ -810,7 +810,6 @@ _0xfab6 = [
810
810
 
811
811
  user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36 OPR/86.0.4363.64"
812
812
 
813
-
814
813
  def to_numbers(value):
815
814
  return dukpy.evaljs('''
816
815
  var _0x9ee6x3 = [];
@@ -869,7 +868,6 @@ async def bypass_async(agent=user_agent, proxy=""):
869
868
  a, b, c = to_numbers(found[0]), to_numbers(found[1]), to_numbers(found[2])
870
869
  return _0xfab6[11] + to_hex([slow_aes([c, a, b]), _0xfab6]), session.headers.get("user-agent")
871
870
 
872
-
873
871
  def main():
874
872
  code = bypass()
875
873
  cookies = "name=value; name=value; name=value; " # Из браузера копируем авторизованные куки без куки react lab arz
@@ -878,6 +876,5 @@ def main():
878
876
  username = re.compile("<span class=\"p-navgroup-linkText username--.*\">(.*)</span>").findall(r.text)
879
877
  print(username)
880
878
 
881
-
882
879
  if __name__ == '__main__':
883
880
  main()
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 Mikhail
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.