mirror of
https://git.phreedom.club/localhost_frssoft/FMN_bot.git
synced 2025-04-18 02:56:31 +02:00
Compare commits
48 Commits
Author | SHA1 | Date | |
---|---|---|---|
d84d86e2dd | |||
b3932f06f0 | |||
a6f2e89297 | |||
a053df3a8a | |||
53a26221ef | |||
e3b0e104ce | |||
e415e92d49 | |||
31a9d3d28c | |||
ba6fce2b56 | |||
9b1fc812d4 | |||
e696eaa375 | |||
973e25adfa | |||
1ecaddfaff | |||
7a78aa6244 | |||
07b469ccfc | |||
ee9e42e092 | |||
3ad6b279d7 | |||
7930d5d996 | |||
1558ab6a2d | |||
63f94ed574 | |||
c96056f9a4 | |||
e86d9829b0 | |||
81c13c3c96 | |||
6414cd863b | |||
176c25f200 | |||
8f07ed7f70 | |||
00f2dc33fb | |||
00b80493f7 | |||
b58f39bfe1 | |||
0bdb95ee30 | |||
885bfb5a53 | |||
b166016df8 | |||
3eebf7266c | |||
cf6e0a6d7a | |||
045ccccb44 | |||
a8bedff4ed | |||
f7431a72f1 | |||
eaf57df418 | |||
47f20fe4e8 | |||
d4da5358af | |||
aca05b4ffa | |||
3ec64dc934 | |||
725df056ed | |||
ab7a5cfa52 | |||
e4a5534edd | |||
de6c3eb8fa | |||
40f88d4e7e | |||
f6b7d41df5 |
25
.gitea/workflows/gitea-actions.yaml
Normal file
25
.gitea/workflows/gitea-actions.yaml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
name: Run tests
|
||||||
|
run-name: ${{ gitea.actor }} is testing out test
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Run-Test:
|
||||||
|
runs-on: ubuntu-act-latest
|
||||||
|
steps:
|
||||||
|
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
|
||||||
|
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
|
||||||
|
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
|
||||||
|
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
||||||
|
- name: List files in the repository
|
||||||
|
run: |
|
||||||
|
ls ${{ gitea.workspace }}
|
||||||
|
- run: python3 -m venv venv
|
||||||
|
- run: . venv/bin/activate
|
||||||
|
- run: pip install -r requirements.txt
|
||||||
|
- run: pip install time_machine
|
||||||
|
- run: cp config.py.example config.py
|
||||||
|
- run: python test.py
|
||||||
|
- run: echo "🍏 This job's status is ${{ job.status }}."
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -4,4 +4,5 @@
|
||||||
.app_sessions
|
.app_sessions
|
||||||
*.log
|
*.log
|
||||||
*.sqlite
|
*.sqlite
|
||||||
|
*.json
|
||||||
|
config.py
|
||||||
|
|
110
COPYING
Normal file
110
COPYING
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
Creative Commons Legal Code CC0 1.0 Universal Official translations of this
|
||||||
|
legal tool are available
|
||||||
|
|
||||||
|
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL
|
||||||
|
SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT
|
||||||
|
RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS"
|
||||||
|
BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS
|
||||||
|
DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS
|
||||||
|
LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE
|
||||||
|
INFORMATION OR WORKS PROVIDED HEREUNDER.
|
||||||
|
|
||||||
|
Statement of Purpose
|
||||||
|
|
||||||
|
The laws of most jurisdictions throughout the world automatically confer
|
||||||
|
exclusive Copyright and Related Rights (defined below) upon the creator and
|
||||||
|
subsequent owner(s) (each and all, an "owner") of an original work of
|
||||||
|
authorship and/or a database (each, a "Work").
|
||||||
|
|
||||||
|
Certain owners wish to permanently relinquish those rights to a Work for the
|
||||||
|
purpose of contributing to a commons of creative, cultural and scientific works
|
||||||
|
("Commons") that the public can reliably and without fear of later claims of
|
||||||
|
infringement build upon, modify, incorporate in other works, reuse and
|
||||||
|
redistribute as freely as possible in any form whatsoever and for any purposes,
|
||||||
|
including without limitation commercial purposes. These owners may contribute
|
||||||
|
to the Commons to promote the ideal of a free culture and the further
|
||||||
|
production of creative, cultural and scientific works, or to gain reputation or
|
||||||
|
greater distribution for their Work in part through the use and efforts of
|
||||||
|
others.
|
||||||
|
|
||||||
|
For these and/or other purposes and motivations, and without any expectation of
|
||||||
|
additional consideration or compensation, the person associating CC0 with a
|
||||||
|
Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
|
||||||
|
and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and
|
||||||
|
publicly distribute the Work under its terms, with knowledge of his or her
|
||||||
|
Copyright and Related Rights in the Work and the meaning and intended legal
|
||||||
|
effect of CC0 on those rights.
|
||||||
|
|
||||||
|
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||||
|
protected by copyright and related or neighboring rights ("Copyright and
|
||||||
|
Related Rights"). Copyright and Related Rights include, but are not limited to,
|
||||||
|
the following:
|
||||||
|
|
||||||
|
the right to reproduce, adapt, distribute, perform, display, communicate,
|
||||||
|
and translate a Work; moral rights retained by the original author(s)
|
||||||
|
and/or performer(s); publicity and privacy rights pertaining to a person's
|
||||||
|
image or likeness depicted in a Work; rights protecting against unfair
|
||||||
|
competition in regards to a Work, subject to the limitations in paragraph
|
||||||
|
4(a), below; rights protecting the extraction, dissemination, use and reuse
|
||||||
|
of data in a Work; database rights (such as those arising under Directive
|
||||||
|
96/9/EC of the European Parliament and of the Council of 11 March 1996 on
|
||||||
|
the legal protection of databases, and under any national implementation
|
||||||
|
thereof, including any amended or successor version of such directive); and
|
||||||
|
other similar, equivalent or corresponding rights throughout the world
|
||||||
|
based on applicable law or treaty, and any national implementations
|
||||||
|
thereof.
|
||||||
|
|
||||||
|
2. Waiver. To the greatest extent permitted by, but not in contravention of,
|
||||||
|
applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
|
||||||
|
unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
|
||||||
|
and Related Rights and associated claims and causes of action, whether now
|
||||||
|
known or unknown (including existing as well as future claims and causes of
|
||||||
|
action), in the Work (i) in all territories worldwide, (ii) for the maximum
|
||||||
|
duration provided by applicable law or treaty (including future time
|
||||||
|
extensions), (iii) in any current or future medium and for any number of
|
||||||
|
copies, and (iv) for any purpose whatsoever, including without limitation
|
||||||
|
commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
|
||||||
|
the Waiver for the benefit of each member of the public at large and to the
|
||||||
|
detriment of Affirmer's heirs and successors, fully intending that such Waiver
|
||||||
|
shall not be subject to revocation, rescission, cancellation, termination, or
|
||||||
|
any other legal or equitable action to disrupt the quiet enjoyment of the Work
|
||||||
|
by the public as contemplated by Affirmer's express Statement of Purpose.
|
||||||
|
|
||||||
|
3. Public License Fallback. Should any part of the Waiver for any reason be
|
||||||
|
judged legally invalid or ineffective under applicable law, then the Waiver
|
||||||
|
shall be preserved to the maximum extent permitted taking into account
|
||||||
|
Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
|
||||||
|
is so judged Affirmer hereby grants to each affected person a royalty-free, non
|
||||||
|
transferable, non sublicensable, non exclusive, irrevocable and unconditional
|
||||||
|
license to exercise Affirmer's Copyright and Related Rights in the Work (i) in
|
||||||
|
all territories worldwide, (ii) for the maximum duration provided by applicable
|
||||||
|
law or treaty (including future time extensions), (iii) in any current or
|
||||||
|
future medium and for any number of copies, and (iv) for any purpose
|
||||||
|
whatsoever, including without limitation commercial, advertising or promotional
|
||||||
|
purposes (the "License"). The License shall be deemed effective as of the date
|
||||||
|
CC0 was applied by Affirmer to the Work. Should any part of the License for any
|
||||||
|
reason be judged legally invalid or ineffective under applicable law, such
|
||||||
|
partial invalidity or ineffectiveness shall not invalidate the remainder of the
|
||||||
|
License, and in such case Affirmer hereby affirms that he or she will not (i)
|
||||||
|
exercise any of his or her remaining Copyright and Related Rights in the Work
|
||||||
|
or (ii) assert any associated claims and causes of action with respect to the
|
||||||
|
Work, in either case contrary to Affirmer's express Statement of Purpose.
|
||||||
|
|
||||||
|
4. Limitations and Disclaimers.
|
||||||
|
|
||||||
|
No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||||
|
surrendered, licensed or otherwise affected by this document. Affirmer
|
||||||
|
offers the Work as-is and makes no representations or warranties of any
|
||||||
|
kind concerning the Work, express, implied, statutory or otherwise,
|
||||||
|
including without limitation warranties of title, merchantability, fitness
|
||||||
|
for a particular purpose, non infringement, or the absence of latent or
|
||||||
|
other defects, accuracy, or the present or absence of errors, whether or
|
||||||
|
not discoverable, all to the greatest extent permissible under applicable
|
||||||
|
law. Affirmer disclaims responsibility for clearing rights of other
|
||||||
|
persons that may apply to the Work or any use thereof, including without
|
||||||
|
limitation any person's Copyright and Related Rights in the Work. Further,
|
||||||
|
Affirmer disclaims responsibility for obtaining any necessary consents,
|
||||||
|
permissions or other rights required for any use of the Work. Affirmer
|
||||||
|
understands and acknowledges that Creative Commons is not a party to this
|
||||||
|
document and has no duty or obligation with respect to this CC0 or use of
|
||||||
|
the Work.
|
5
MIRRORS
Normal file
5
MIRRORS
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# Avalaible mirrors
|
||||||
|
https://inex.dev/localhost_frssoft/FMN_bot.git
|
||||||
|
https://git.macaw.me/localhost_frssoft/FMN_bot.git
|
||||||
|
https://code.criminallycute.fi/localhost_frssoft/FMN_bot.git
|
||||||
|
https://git.poridge.club/localhost_frssoft/FMN_bot.git
|
18
README.md
18
README.md
|
@ -4,8 +4,10 @@
|
||||||
Special for [XXIVProduction](https://xxivproduction.video/) and [drq@mastodon.ml](https://mastodon.ml/@drq).
|
Special for [XXIVProduction](https://xxivproduction.video/) and [drq@mastodon.ml](https://mastodon.ml/@drq).
|
||||||
|
|
||||||
## Первичная инициализация
|
## Первичная инициализация
|
||||||
* Установка зависимостей python3
|
* Установка зависимостей python3 и виртуального окружения
|
||||||
```
|
```
|
||||||
|
python3 -m venv venv
|
||||||
|
. venv/bin/activate
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -33,7 +35,10 @@ browser=links ./auth_helper.sh
|
||||||
Следуйте указаниям скрипта, залогиньтесь в ваш аккаунт и скопируйте код-ключ с браузера, закройте и вставьте в появившееся поле ввода.
|
Следуйте указаниям скрипта, залогиньтесь в ваш аккаунт и скопируйте код-ключ с браузера, закройте и вставьте в появившееся поле ввода.
|
||||||
|
|
||||||
* Настройка бота
|
* Настройка бота
|
||||||
В файле config.py описан каждый параметр, который можно менять
|
В файле config.py.example описан каждый параметр, который можно менять сделайте его копию в каталог с ботом убрав расширение .example
|
||||||
|
```
|
||||||
|
cp config.py.example config.py
|
||||||
|
```
|
||||||
|
|
||||||
## Запуск
|
## Запуск
|
||||||
```
|
```
|
||||||
|
@ -41,15 +46,16 @@ python3 fmn_bot.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## Использование
|
## Использование
|
||||||
Просто упомяните бота (упоминающий должен быть прописан в администраторах бота см. config.py), в воскресенье/понедельник, когда Fediverse Movie Night окончен. Бот инициализирует сборщика предложений и будет собирать фильмы, которые будут поступать в виде ссылок на imdb.com (также теперь есть поддержка фронтедов [libremdb](https://github.com/zyachel/libremdb)) и kinopoisk.ru в том треде, где его упомянули. Сбор будет завершен во вторник 16:00 (по-умолчанию) MSK+3. После этого всех кто не успел предложить фильм бот будет уведомлять, что сбор завершен и даст ссылку на голосовалку. По завершению голосовалки (суббота 16:00), будет вычисляться фильм-победитель на FMN, если у нескольких фильмов одинаковые голоса, то будет создан tie breaker. Победивший фильм будет записан сразу как "просмотренный", чтобы не добавлять его на следующий FMN повторно. Далее бот будет ждать очередного упоминания.
|
Просто упомяните бота (упоминающий должен быть прописан в администраторах бота см. config.py), в воскресенье/понедельник, когда Fediverse Movie Night окончен (В новой версии, теперь встроена функция автоматического срабатывания в полночь, если упоминания не было). Бот инициализирует сборщика предложений и будет собирать фильмы, которые будут поступать в виде ссылок на imdb.com (также теперь есть поддержка фронтедов [libremdb](https://github.com/zyachel/libremdb)) и kinopoisk.ru в том треде, где его упомянули. Сбор будет завершен во вторник 16:00 (по-умолчанию) MSK+3. После этого всех кто не успел предложить фильм бот будет уведомлять, что сбор завершен и даст ссылку на голосовалку. По завершению голосовалки (суббота 16:00), будет вычисляться фильм-победитель на FMN, если у нескольких фильмов одинаковые голоса, то будет создан tie breaker. Победивший фильм будет записан сразу как "просмотренный", чтобы не добавлять его на следующий FMN повторно. Далее бот будет ждать очередного упоминания.
|
||||||
|
|
||||||
Note: Рекомендуется использовать ссылки на imdb.com, так как локальная база IMDB надёжнее, чем сетевой сторонний API Кинопоиска.
|
Note: Рекомендуется использовать ссылки на imdb.com, так как локальная база IMDB надёжнее, чем сетевой сторонний API Кинопоиска.
|
||||||
Note2: Список доступных для приёма инстансов libremdb обновляется вручную и может не соотвествовать официальному.
|
Note2: Список доступных для приёма инстансов libremdb обновляется вручную и может не соотвествовать официальному.
|
||||||
|
|
||||||
## Список поддерживаемых инстансов libremdb:
|
## Список поддерживаемых инстансов libremdb:
|
||||||
* https://libremdb.herokuapp.com
|
* https://libremdb.leemoon.network/ (Спасибо [Саре](https://lamp.leemoon.network/@sarahquartz) в рамках self-host проекта [Leemoon Network 🍋](https://leemoon.network))
|
||||||
* https://libremdb.pussthecat.org
|
* https://libremdb.herokuapp.com/
|
||||||
* https://libremdbeu.herokuapp.com
|
* https://libremdb.pussthecat.org/
|
||||||
|
* https://libremdbeu.herokuapp.com/
|
||||||
* https://lmdb.tokhmi.xyz/
|
* https://lmdb.tokhmi.xyz/
|
||||||
* https://libremdb.esmailelbob.xyz/
|
* https://libremdb.esmailelbob.xyz/
|
||||||
* http://libremdb.lqs5fjmajyp7rvp4qvyubwofzi6d4imua7vs237rkc4m5qogitqwrgyd.onion/
|
* http://libremdb.lqs5fjmajyp7rvp4qvyubwofzi6d4imua7vs237rkc4m5qogitqwrgyd.onion/
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
admins_bot = ('drq@mastodon.ml',) # Адреса админов бота, которые могут упомянуть бота для инициализации FMN
|
admins_bot = ('drq@mastodon.ml',) # Адреса админов бота, которые могут упомянуть бота для инициализации FMN
|
||||||
# Example: ('admin_user', 'another_admin_user2@example.example') or ('admin_user',)
|
# Example: ('admin_user', 'another_admin_user2@example.example') or ('admin_user',)
|
||||||
bot_acct = 'fmn' # Ник бота на инстансе
|
bot_acct = 'fmn' # Ник бота на инстансе
|
||||||
instance = 'expired.mentality.rip' # Инстанс, где будет запущен бот
|
instance = 'pleroma.dark-alexandr.net' # Инстанс, где будет запущен бот
|
||||||
|
# Ссылка на live stream FMN (API)
|
||||||
|
peertube_stream_url = 'https://xxivproduction.video/api/v1/videos/1FZeVVVzWBFShaxQVkYiXd'
|
||||||
|
|
||||||
|
|
||||||
# Лимиты
|
# Лимиты
|
||||||
limit_movies_per_user = 2 # Ограничение количества фильмов на одного пользователя
|
limit_movies_per_user = 2 # Ограничение количества фильмов на одного пользователя
|
||||||
|
@ -13,5 +16,5 @@ max_fail_limit = 4 # Игнорировать предложения польз
|
||||||
hour_poll_posting = 16 # Час в который будет создан пост с голосовалкой (и завершение сбора)
|
hour_poll_posting = 16 # Час в который будет создан пост с голосовалкой (и завершение сбора)
|
||||||
fmn_next_watching_hour = 21 # Час начала киносеанса
|
fmn_next_watching_hour = 21 # Час начала киносеанса
|
||||||
|
|
||||||
logger_default_level = 'DEBUG' # Уровень логгирования TRACE, DEBUG, INFO, SUCCESS, WARNING, ERROR, CRITICAL
|
logger_default_level = 'INFO' # Уровень логгирования TRACE, DEBUG, INFO, SUCCESS, WARNING, ERROR, CRITICAL
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
from src import listener_context, listener_mention, imdb_datasets_worker
|
from src import listener_context, listener_mention
|
||||||
from config import logger_default_level
|
from config import logger_default_level
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
logger.remove()
|
logger.remove()
|
||||||
logger.add(sink=sys.stderr, level=logger_default_level)
|
logger.add(sink=sys.stderr, level=logger_default_level)
|
||||||
logger.add(sink='suggestions.log', level='INFO', filter='src.listener_context')
|
logger.add(sink='suggestions.log', level='INFO', filter='src.listener_context')
|
||||||
|
|
||||||
listener_mention.run_scan_notif() # Слушаем упоминания в фоне
|
listener_mention.run_scan_notif() # Слушаем упоминания в фоне
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
listener_context.scan_context_thread() # Слушаем тред на новые предложения фильмов
|
listener_context.scan_context_thread() # Слушаем тред на новые предложения фильмов
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
BIN
src/FMN.png
BIN
src/FMN.png
Binary file not shown.
Before Width: | Height: | Size: 788 KiB |
1
src/FMN.webp
Symbolic link
1
src/FMN.webp
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
FMN_prev.webp
|
BIN
src/FMN_new_year.webp
Normal file
BIN
src/FMN_new_year.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 154 KiB |
BIN
src/FMN_prev.webp
Normal file
BIN
src/FMN_prev.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 98 KiB |
|
@ -1,6 +1,5 @@
|
||||||
from config import instance
|
from config import instance
|
||||||
import time
|
import time
|
||||||
import json
|
|
||||||
import requests
|
import requests
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
@ -9,9 +8,13 @@ instance_point = f"https://{instance}/api/v1"
|
||||||
with open(".auth", mode='rt') as auth:
|
with open(".auth", mode='rt') as auth:
|
||||||
tkn = auth.read().replace('\n', '')
|
tkn = auth.read().replace('\n', '')
|
||||||
|
|
||||||
headers= {
|
|
||||||
"Authorization": "Bearer " + tkn
|
s = requests.Session()
|
||||||
}
|
s.headers.update({
|
||||||
|
"Authorization": "Bearer " + tkn,
|
||||||
|
"Accept-encoding": 'gzip'
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def get_notifications():
|
def get_notifications():
|
||||||
params = {
|
params = {
|
||||||
|
@ -21,7 +24,7 @@ def get_notifications():
|
||||||
success = 0
|
success = 0
|
||||||
while success == 0:
|
while success == 0:
|
||||||
try:
|
try:
|
||||||
r = requests.get(instance_point + "/notifications", json=params, headers=headers)
|
r = s.get(instance_point + "/notifications", json=params)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
success = 1
|
success = 1
|
||||||
return r.json()
|
return r.json()
|
||||||
|
@ -31,12 +34,11 @@ def get_notifications():
|
||||||
logger.info('Retrying get notificatios...')
|
logger.info('Retrying get notificatios...')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def mark_as_read_notification(id_notification):
|
def mark_as_read_notification(id_notification):
|
||||||
success = 0
|
success = 0
|
||||||
while success == 0:
|
while success == 0:
|
||||||
try:
|
try:
|
||||||
r = requests.post(instance_point + f"/notifications/{id_notification}/dismiss", headers=headers)
|
r = s.post(instance_point + f"/notifications/{id_notification}/dismiss")
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
success = 1
|
success = 1
|
||||||
return r.json()
|
return r.json()
|
||||||
|
@ -47,24 +49,28 @@ def mark_as_read_notification(id_notification):
|
||||||
|
|
||||||
|
|
||||||
def get_status_context(status_id):
|
def get_status_context(status_id):
|
||||||
|
retry = 0
|
||||||
success = 0
|
success = 0
|
||||||
while success == 0:
|
while success == 0:
|
||||||
try:
|
try:
|
||||||
r = requests.get(instance_point + f"/statuses/{status_id}/context", headers=headers)
|
r = s.get(instance_point + f"/statuses/{status_id}/context", timeout=30)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
success = 1
|
success = 1
|
||||||
return r.json()
|
return r.json()
|
||||||
except:
|
except Exception as E:
|
||||||
logger.exception(f'Ошибка получения контекста треда {status_id}')
|
logger.exception(f'Ошибка получения контекста треда {status_id}')
|
||||||
time.sleep(30)
|
time.sleep(30)
|
||||||
logger.info('Повторный запрос треда...')
|
logger.info('Повторный запрос треда...')
|
||||||
|
retry += 1
|
||||||
|
if retry > 5:
|
||||||
|
raise IOError(f'Фетчинг треда поломан! {E}')
|
||||||
|
|
||||||
|
|
||||||
def get_status(status_id):
|
def get_status(status_id):
|
||||||
success = 0
|
success = 0
|
||||||
while success == 0:
|
while success == 0:
|
||||||
try:
|
try:
|
||||||
r = requests.get(instance_point + f"/statuses/{status_id}", headers=headers)
|
r = s.get(instance_point + f"/statuses/{status_id}")
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
success = 1
|
success = 1
|
||||||
return r.json()
|
return r.json()
|
||||||
|
@ -74,8 +80,7 @@ def get_status(status_id):
|
||||||
logger.info(f'Retrying get status {status_id}')
|
logger.info(f'Retrying get status {status_id}')
|
||||||
|
|
||||||
|
|
||||||
|
def post_status(text, reply_to_status_id=None, poll_options=None, poll_expires=345600, attachments=None, visibility='unlisted'):
|
||||||
def post_status(text, reply_to_status_id=None, poll_options=None, poll_expires=345600, attachments=None):
|
|
||||||
poll = None
|
poll = None
|
||||||
if poll_options is not None:
|
if poll_options is not None:
|
||||||
poll = {
|
poll = {
|
||||||
|
@ -86,7 +91,7 @@ def post_status(text, reply_to_status_id=None, poll_options=None, poll_expires=3
|
||||||
params = {
|
params = {
|
||||||
"status": text,
|
"status": text,
|
||||||
"in_reply_to_id": reply_to_status_id,
|
"in_reply_to_id": reply_to_status_id,
|
||||||
"visibility": "unlisted",
|
"visibility": visibility,
|
||||||
"content_type": "text/plain",
|
"content_type": "text/plain",
|
||||||
"language": "ru",
|
"language": "ru",
|
||||||
"poll": poll
|
"poll": poll
|
||||||
|
@ -96,7 +101,7 @@ def post_status(text, reply_to_status_id=None, poll_options=None, poll_expires=3
|
||||||
success = 0
|
success = 0
|
||||||
while success == 0:
|
while success == 0:
|
||||||
try:
|
try:
|
||||||
r = requests.post(instance_point + "/statuses", json=params, headers=headers)
|
r = s.post(instance_point + "/statuses", json=params)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
success = 1
|
success = 1
|
||||||
return r.json()
|
return r.json()
|
||||||
|
@ -112,19 +117,9 @@ def upload_attachment(file_path):
|
||||||
params = {
|
params = {
|
||||||
"description": "Fediverse Movie Night\nВоскресенье, 21:00\nLIVE ON XXIV Production",
|
"description": "Fediverse Movie Night\nВоскресенье, 21:00\nLIVE ON XXIV Production",
|
||||||
}
|
}
|
||||||
success = 0
|
r = s.post(instance_point + "/media", params, files=file, timeout=30)
|
||||||
while success == 0:
|
r.raise_for_status()
|
||||||
try:
|
return r.json()['id']
|
||||||
r = requests.post(instance_point + "/media", params, files=file, headers=headers)
|
|
||||||
r.raise_for_status()
|
|
||||||
success = 1
|
|
||||||
return r.json()['id']
|
|
||||||
except:
|
|
||||||
logger.exception(f'Error uploading {file_path} attachment')
|
|
||||||
time.sleep(5)
|
|
||||||
logger.info(f'Retrying upload {file_path}...')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def mute_user(acct_id=str, acct=str, duration=None):
|
def mute_user(acct_id=str, acct=str, duration=None):
|
||||||
|
@ -134,7 +129,7 @@ def mute_user(acct_id=str, acct=str, duration=None):
|
||||||
success = 0
|
success = 0
|
||||||
while success == 0:
|
while success == 0:
|
||||||
try:
|
try:
|
||||||
r = requests.post(instance_point + '/accounts' + f"/{acct_id}/mute", params, headers=headers)
|
r = s.post(instance_point + '/accounts' + f"/{acct_id}/mute", params)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
logger.info(f'Пользователь {acct} был заглушен на {duration} secs')
|
logger.info(f'Пользователь {acct} был заглушен на {duration} secs')
|
||||||
success = 1
|
success = 1
|
||||||
|
@ -142,4 +137,3 @@ def mute_user(acct_id=str, acct=str, duration=None):
|
||||||
logger.exception(f'Ошибка глушения {acct}')
|
logger.exception(f'Ошибка глушения {acct}')
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
logger.info(f'Повторное глушение {acct}...')
|
logger.info(f'Повторное глушение {acct}...')
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,9 @@ c.execute(
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
def force_commit():
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
def mark_as_watched_movie(original_name=None, ru_name=None, year=None):
|
def mark_as_watched_movie(original_name=None, ru_name=None, year=None):
|
||||||
try:
|
try:
|
||||||
|
@ -36,6 +39,11 @@ def get_already_watched(original_name=None, ru_name=None, year=None):
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_count_all_watched_movies():
|
||||||
|
return len(c.execute('SELECT * FROM watched_movies').fetchall())
|
||||||
|
|
||||||
|
|
||||||
def get_already_suggested(acct, original_name, ru_name, year):
|
def get_already_suggested(acct, original_name, ru_name, year):
|
||||||
already_suggested = c.execute('''SELECT * FROM poll
|
already_suggested = c.execute('''SELECT * FROM poll
|
||||||
WHERE (original_name = (?) OR ru_name = (?)) COLLATE NOCASE
|
WHERE (original_name = (?) OR ru_name = (?)) COLLATE NOCASE
|
||||||
|
@ -105,4 +113,3 @@ def reset_poll():
|
||||||
'''Сброс содержимого предложки-опроса'''
|
'''Сброс содержимого предложки-опроса'''
|
||||||
c.execute("DELETE FROM poll")
|
c.execute("DELETE FROM poll")
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,24 @@
|
||||||
from src.fedi_api import get_status, post_status, upload_attachment
|
from src.fedi_api import get_status, post_status, upload_attachment
|
||||||
from src.fmn_states_db import add_state, get_state, clear_all_states
|
from src.fmn_states_db import states_stor, write_states
|
||||||
from src.fmn_database import get_movies_for_poll, write_votes, read_votes, mark_as_watched_movie, get_already_watched, rewrite_db, reset_poll
|
from src.fmn_database import get_movies_for_poll, write_votes, read_votes, mark_as_watched_movie, get_already_watched, rewrite_db, reset_poll, get_count_all_watched_movies, force_commit
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
||||||
text_create_poll = '''Друзья, голосование за следующий Fediverse Movie Night объявляю открытым!
|
def text_create_poll():
|
||||||
|
force_commit()
|
||||||
|
count_poll = get_count_all_watched_movies()
|
||||||
|
text_poll = f'''Друзья, {count_poll} голосование за следующий Fediverse Movie Night объявляю открытым!
|
||||||
|
|
||||||
Ставки сделаны, ставок больше нет
|
Ставки сделаны, ставок больше нет
|
||||||
'''.replace('\t', '')
|
'''.replace('\t', '')
|
||||||
|
return text_poll
|
||||||
|
|
||||||
|
|
||||||
def create_poll_movies(text=text_create_poll, poll_expires=345600):
|
@logger.catch
|
||||||
|
def create_poll_movies(text=text_create_poll(), poll_expires=345600):
|
||||||
|
logger.debug('Creating poll')
|
||||||
formated_poll_options = []
|
formated_poll_options = []
|
||||||
raw_poll = get_movies_for_poll()
|
raw_poll = get_movies_for_poll()
|
||||||
for i in raw_poll:
|
for i in raw_poll:
|
||||||
|
@ -21,6 +27,7 @@ def create_poll_movies(text=text_create_poll, poll_expires=345600):
|
||||||
ru_name = i[2]
|
ru_name = i[2]
|
||||||
year = i[3]
|
year = i[3]
|
||||||
poll_option_string = f"{ru_name} / {orig_name}, {year} ({acct})"
|
poll_option_string = f"{ru_name} / {orig_name}, {year} ({acct})"
|
||||||
|
logger.debug(f"Adding option in poll: {poll_option_string}")
|
||||||
if ru_name is None:
|
if ru_name is None:
|
||||||
poll_option_string = f"{orig_name}, {year} ({acct})"
|
poll_option_string = f"{orig_name}, {year} ({acct})"
|
||||||
if orig_name is None:
|
if orig_name is None:
|
||||||
|
@ -28,16 +35,26 @@ def create_poll_movies(text=text_create_poll, poll_expires=345600):
|
||||||
if len(poll_option_string) >= 200:
|
if len(poll_option_string) >= 200:
|
||||||
poll_option_string = poll_option_string[0:199] # Обрезка на 200 символов.
|
poll_option_string = poll_option_string[0:199] # Обрезка на 200 символов.
|
||||||
formated_poll_options.append(poll_option_string)
|
formated_poll_options.append(poll_option_string)
|
||||||
|
|
||||||
|
attaches = []
|
||||||
|
try:
|
||||||
|
attaches = [upload_attachment('src/FMN.webp')]
|
||||||
|
except Exception as E:
|
||||||
|
logger.error(f"attachements can't do upload: {E}")
|
||||||
|
|
||||||
poll_status_id = post_status(text, None, formated_poll_options,
|
poll_status_id = post_status(text, None, formated_poll_options,
|
||||||
poll_expires=poll_expires, attachments=[upload_attachment('src/FMN.png')])
|
poll_expires=poll_expires, attachments=attaches)
|
||||||
logger.success('Голосовалка создана')
|
logger.success('Голосовалка создана')
|
||||||
add_state('poll_expires_at', int(time.time()) + poll_expires)
|
states_stor.states['poll_expires_at'] = int(time.time()) + poll_expires
|
||||||
add_state('poll_status_id', poll_status_id['id'])
|
states_stor.states['poll_status_id'] = poll_status_id['id']
|
||||||
|
write_states(states_stor.states)
|
||||||
return poll_status_id
|
return poll_status_id
|
||||||
|
|
||||||
|
|
||||||
|
@logger.catch
|
||||||
def get_winner_movie(poll_status_id=str):
|
def get_winner_movie(poll_status_id=str):
|
||||||
'''Отмечаем победивший фильм на голосовании как просмотренный или постим tie breaker'''
|
'''Отмечаем победивший фильм на голосовании как просмотренный или постим tie breaker'''
|
||||||
|
states = states_stor.states
|
||||||
votes_counters = []
|
votes_counters = []
|
||||||
status_with_poll = get_status(poll_status_id)
|
status_with_poll = get_status(poll_status_id)
|
||||||
poll = status_with_poll['poll']
|
poll = status_with_poll['poll']
|
||||||
|
@ -45,7 +62,7 @@ def get_winner_movie(poll_status_id=str):
|
||||||
for option in poll['options']:
|
for option in poll['options']:
|
||||||
votes_count = option['votes_count']
|
votes_count = option['votes_count']
|
||||||
votes_counters.append(votes_count)
|
votes_counters.append(votes_count)
|
||||||
|
|
||||||
write_votes(votes_counters)
|
write_votes(votes_counters)
|
||||||
voted_movies = read_votes()
|
voted_movies = read_votes()
|
||||||
max_vote = voted_movies[0][4]
|
max_vote = voted_movies[0][4]
|
||||||
|
@ -57,7 +74,7 @@ def get_winner_movie(poll_status_id=str):
|
||||||
if len(winned_movies) > 1:
|
if len(winned_movies) > 1:
|
||||||
logger.warning('Будет создан tie breaker')
|
logger.warning('Будет создан tie breaker')
|
||||||
rewrite_db(winned_movies)
|
rewrite_db(winned_movies)
|
||||||
if get_state('tie_breaker'):
|
if states.get('tie_breaker'):
|
||||||
create_tie_breaker(2)
|
create_tie_breaker(2)
|
||||||
else:
|
else:
|
||||||
create_tie_breaker()
|
create_tie_breaker()
|
||||||
|
@ -71,20 +88,23 @@ def get_winner_movie(poll_status_id=str):
|
||||||
win_variant = f"{orig_name}, {year}"
|
win_variant = f"{orig_name}, {year}"
|
||||||
if orig_name is None:
|
if orig_name is None:
|
||||||
win_variant = f"{ru_name}, {year}"
|
win_variant = f"{ru_name}, {year}"
|
||||||
text_winned = f"Голосование завершилось! Победил вариант предложенный @{acct_suggested}:\n{win_variant}"
|
expired_poll_count = get_count_all_watched_movies() - 1
|
||||||
|
text_winned = f"{expired_poll_count} голосование завершилось! Победил вариант предложенный @{acct_suggested}:\n{win_variant}"
|
||||||
|
|
||||||
logger.success("Победил " + str(movie))
|
logger.success("Победил " + str(movie))
|
||||||
post_status(text_winned, attachments=[upload_attachment('src/FMN.png')])
|
post_status(text_winned, attachments=[upload_attachment('src/FMN.webp')])
|
||||||
clear_all_states()
|
states_stor.states = {}
|
||||||
|
write_states()
|
||||||
reset_poll()
|
reset_poll()
|
||||||
|
|
||||||
|
|
||||||
|
@logger.catch
|
||||||
def create_tie_breaker(count_tie=1):
|
def create_tie_breaker(count_tie=1):
|
||||||
'''Создание tie breaker'''
|
'''Создание tie breaker'''
|
||||||
if count_tie == 1:
|
if count_tie == 1:
|
||||||
add_state('tie_breaker', 1)
|
states_stor.states['tie_breaker'] = 1
|
||||||
|
write_states(states_stor.states)
|
||||||
poll_expires = 8*60*60
|
poll_expires = 8*60*60
|
||||||
else:
|
else:
|
||||||
poll_expires = 4*60*60
|
poll_expires = 4*60*60
|
||||||
tie_poll = create_poll_movies("TIE BREAKER!!!\n\nВыбираем из победителей!", poll_expires)
|
tie_poll = create_poll_movies("TIE BREAKER!!!\n\nВыбираем из победителей!", poll_expires)
|
||||||
|
|
||||||
|
|
|
@ -1,41 +1,33 @@
|
||||||
import sqlite3
|
import json
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
states_file = 'fmn_states.json'
|
||||||
conn = sqlite3.connect("fmn_states.sqlite", check_same_thread=False)
|
|
||||||
c = conn.cursor()
|
|
||||||
|
|
||||||
c.execute(
|
|
||||||
'''CREATE TABLE IF NOT EXISTS states(key VARCHAR (500) UNIQUE DEFAULT NULL, value VARCHAR (500) UNIQUE DEFAULT NULL)''')
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def add_state(key, value):
|
class states_stor:
|
||||||
'''Создание стейта, если существует - будет заменен'''
|
states = None
|
||||||
logger.debug(f'Adding {key}: {value}')
|
|
||||||
c.execute("INSERT OR REPLACE INTO states(key, value) VALUES (?, ?)", (key, value))
|
|
||||||
conn.commit()
|
|
||||||
logger.warning(f'Добавлен стейт {key}: {value}')
|
|
||||||
|
|
||||||
|
|
||||||
def get_state(key):
|
@logger.catch
|
||||||
'''Получение стейта по ключу'''
|
def read_states():
|
||||||
logger.trace(f'Запрошен стейт {key}')
|
try:
|
||||||
value = c.execute("SELECT value FROM states WHERE key = (?)", (key,)).fetchone()
|
with open(states_file, 'rt') as f:
|
||||||
if value:
|
current_states = json.loads(f.read())
|
||||||
return value[0]
|
except:
|
||||||
|
logger.warning('Стейты не найдены, создание плейсхолдера')
|
||||||
|
write_states()
|
||||||
|
current_states = {}
|
||||||
|
return current_states
|
||||||
|
|
||||||
|
|
||||||
def remove_state(key):
|
@logger.catch
|
||||||
'''Удалить стейт по ключу'''
|
def write_states(new_states={}):
|
||||||
c.execute("DELETE FROM states WHERE key = (?)", (key,))
|
with open(states_file, 'wt') as f:
|
||||||
conn.commit()
|
f.write(json.dumps(new_states, indent=4))
|
||||||
logger.warning(f'Удален стейт {key}')
|
if new_states == {}:
|
||||||
|
logger.info('states empty wrote')
|
||||||
|
return new_states
|
||||||
|
|
||||||
|
|
||||||
def clear_all_states():
|
if not states_stor.states:
|
||||||
c.execute("DELETE FROM states")
|
states_stor.states = read_states()
|
||||||
conn.commit()
|
|
||||||
logger.warning(f'Все стейты удалены')
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import gzip
|
import gzip
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
conn = sqlite3.connect("imdb_titles.sqlite")
|
conn = sqlite3.connect("imdb_titles.sqlite")
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
|
@ -26,15 +27,20 @@ def convert_tsv_to_db(title_basics_tsv):
|
||||||
original_name = line[3]
|
original_name = line[3]
|
||||||
ru_name = None
|
ru_name = None
|
||||||
year = line[5]
|
year = line[5]
|
||||||
|
genres = line[-1].strip().split(',')
|
||||||
if year.startswith(r"\N"):
|
if year.startswith(r"\N"):
|
||||||
year = None
|
year = None
|
||||||
else:
|
else:
|
||||||
year = int(year)
|
year = int(year)
|
||||||
|
|
||||||
if tt_type not in ("movie", "video"):
|
if tt_type not in ("movie", "tvMovie", "video"):
|
||||||
original_name = None
|
original_name = None
|
||||||
year = None
|
year = None
|
||||||
|
if "Documentary" in genres:
|
||||||
|
logger.debug(f'Документальный {original_name} отсеян')
|
||||||
|
original_name = None
|
||||||
|
year = None
|
||||||
|
tt_type = "doc"
|
||||||
write_dataset.append((tt_id, tt_type, original_name, ru_name, year))
|
write_dataset.append((tt_id, tt_type, original_name, ru_name, year))
|
||||||
counter += 1
|
counter += 1
|
||||||
if counter >= chunk:
|
if counter >= chunk:
|
||||||
|
@ -42,9 +48,9 @@ def convert_tsv_to_db(title_basics_tsv):
|
||||||
write_dataset = []
|
write_dataset = []
|
||||||
counter = 0
|
counter = 0
|
||||||
progress_counter += chunk
|
progress_counter += chunk
|
||||||
print(f'Обработано: {progress_counter}')
|
logger.info(f'Обработано: {progress_counter}')
|
||||||
except Exception as E:
|
except:
|
||||||
print(E)
|
logger.exception('Err')
|
||||||
pass
|
pass
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
@ -61,16 +67,16 @@ def extract_ru_locale_from_tsv(title_akas_tsv):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
tt_id = int(line[0].split("tt")[1])
|
tt_id = int(line[0].split("tt")[1])
|
||||||
tt_type = c.execute(f"SELECT type FROM titles WHERE tt_id={tt_id}").fetchone()[0]
|
tt_type = c.execute("SELECT type FROM titles WHERE tt_id = (?)", (tt_id, )).fetchone()[0]
|
||||||
if tt_type not in ("movie", "video"):
|
if tt_type not in ("movie", "tvMovie", "video"):
|
||||||
continue
|
continue
|
||||||
ru_name = line[2]
|
ru_name = line[2]
|
||||||
ru_name_writer.append((ru_name, tt_id))
|
ru_name_writer.append((ru_name, tt_id))
|
||||||
counter += 1
|
counter += 1
|
||||||
print(f'Обработано ru_name: {counter}')
|
logger.info(f'Обработано ru_name: {counter}')
|
||||||
|
|
||||||
except Exception as E:
|
except:
|
||||||
print(E)
|
logger.exception('Err')
|
||||||
pass
|
pass
|
||||||
|
|
||||||
c.executemany("UPDATE titles SET ru_name = ? WHERE tt_id = ?", ru_name_writer)
|
c.executemany("UPDATE titles SET ru_name = ? WHERE tt_id = ?", ru_name_writer)
|
||||||
|
|
|
@ -3,13 +3,10 @@ from src.fedi_api import get_status_context, get_status, post_status, mute_user
|
||||||
from src.kinopoisk_api import get_kinopoisk_movie_to_imdb
|
from src.kinopoisk_api import get_kinopoisk_movie_to_imdb
|
||||||
from src.imdb_datasets_worker import get_title_by_id
|
from src.imdb_datasets_worker import get_title_by_id
|
||||||
from src.fmn_database import add_movie_to_poll, get_already_watched, get_suggested_movies_count
|
from src.fmn_database import add_movie_to_poll, get_already_watched, get_suggested_movies_count
|
||||||
from src.fmn_states_db import get_state, add_state
|
from src.fmn_states_db import states_stor
|
||||||
from src.fmn_poll import create_poll_movies, get_winner_movie
|
from src.fmn_poll import create_poll_movies, get_winner_movie
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
|
||||||
from dateutil.parser import parse as dateutilparse
|
|
||||||
from dateutil.relativedelta import relativedelta, TU
|
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
@ -23,38 +20,48 @@ def parse_links(text=str):
|
||||||
|
|
||||||
|
|
||||||
def parse_links_imdb(text=str):
|
def parse_links_imdb(text=str):
|
||||||
regex = r"imdb\.com/|libremdb\.pussthecat\.org/|libremdb\.esmailelbob\.xyz/|libremdb\.herokuapp\.com/|libremdbeu\.herokuapp\.com/|lmdb\.tokhmi\.xyz/|libremdb\.lqs5fjmajyp7rvp4qvyubwofzi6d4imua7vs237rkc4m5qogitqwrgyd\.onion/"
|
regex = r"imdb\.com/|libremdb\.leemoon\.network/|libremdb\.pussthecat\.org/|libremdb\.esmailelbob\.xyz/|libremdb\.herokuapp\.com/|libremdbeu\.herokuapp\.com/|lmdb\.tokhmi\.xyz/|libremdb\.lqs5fjmajyp7rvp4qvyubwofzi6d4imua7vs237rkc4m5qogitqwrgyd\.onion/"
|
||||||
if re.search(regex, text.lower(), flags=re.MULTILINE):
|
if re.search(regex, text.lower(), flags=re.MULTILINE):
|
||||||
imdb_ids = re.findall(r"tt(\d{1,})", text.lower())
|
imdb_ids = re.findall(r"tt(\d{1,})", text.lower())
|
||||||
if imdb_ids != []:
|
if imdb_ids != []:
|
||||||
return imdb_ids[:limit_movies_per_user]
|
return imdb_ids[:limit_movies_per_user]
|
||||||
|
|
||||||
|
|
||||||
def scan_context_thread():
|
def scan_context_thread():
|
||||||
fail_limit = Counter()
|
fail_limit = Counter()
|
||||||
while True:
|
while True:
|
||||||
status_id = get_state('last_thread_id')
|
states = states_stor.states
|
||||||
poll_created = get_state('poll_status_id')
|
status_id = states.get('last_thread_id')
|
||||||
stop_thread_scan = get_state('stop_thread_scan')
|
poll_created = states.get('poll_status_id')
|
||||||
|
stop_thread_scan = states.get('stop_thread_scan')
|
||||||
time_now = int(time.time())
|
time_now = int(time.time())
|
||||||
|
reserve_time = False
|
||||||
while status_id is None or stop_thread_scan is None:
|
while status_id is None or stop_thread_scan is None:
|
||||||
|
states = states_stor.states
|
||||||
fail_limit = Counter()
|
fail_limit = Counter()
|
||||||
status_id = get_state('last_thread_id')
|
status_id = states.get('last_thread_id')
|
||||||
stop_thread_scan = get_state('stop_thread_scan')
|
stop_thread_scan = states.get('stop_thread_scan')
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
if time_now >= int(stop_thread_scan):
|
if time_now >= int(stop_thread_scan):
|
||||||
|
reserve_time = True
|
||||||
logger.debug('Сбор завершён, сканирование треда на опоздавших')
|
logger.debug('Сбор завершён, сканирование треда на опоздавших')
|
||||||
if poll_created is None:
|
if poll_created is None:
|
||||||
create_poll_movies()
|
create_poll_movies()
|
||||||
poll_created = get_state('poll_status_id')
|
poll_created = states_stor.states.get('poll_status_id')
|
||||||
else:
|
else:
|
||||||
if time_now >= int(get_state('poll_expires_at')):
|
if time_now >= int(states.get('poll_expires_at')):
|
||||||
get_winner_movie(poll_created)
|
get_winner_movie(poll_created)
|
||||||
else:
|
else:
|
||||||
endings = int(stop_thread_scan) - time_now
|
endings = int(stop_thread_scan) - time_now
|
||||||
logger.debug(f'Осталось до закрытия сбора: {endings}')
|
logger.debug(f'Осталось до закрытия сбора: {endings}')
|
||||||
|
if reserve_time: # Reduce instance load
|
||||||
|
time.sleep(30)
|
||||||
|
get_thread_time = time.time()
|
||||||
descendants = get_status_context(status_id)['descendants']
|
descendants = get_status_context(status_id)['descendants']
|
||||||
|
get_thread_time2 = time.time()
|
||||||
|
get_thread_delta = get_thread_time2 - get_thread_time
|
||||||
|
logger.debug(f'Get thread time: {get_thread_delta}')
|
||||||
replyed = []
|
replyed = []
|
||||||
for status in descendants:
|
for status in descendants:
|
||||||
if status['account']['acct'] == bot_acct:
|
if status['account']['acct'] == bot_acct:
|
||||||
|
@ -62,6 +69,10 @@ def scan_context_thread():
|
||||||
|
|
||||||
for status in descendants:
|
for status in descendants:
|
||||||
id_st = status['id']
|
id_st = status['id']
|
||||||
|
visibility = status['visibility']
|
||||||
|
if visibility == 'direct':
|
||||||
|
# Игнорируем личку
|
||||||
|
continue
|
||||||
in_reply_acct = status['in_reply_to_account_id']
|
in_reply_acct = status['in_reply_to_account_id']
|
||||||
in_reply_id = status['in_reply_to_id']
|
in_reply_id = status['in_reply_to_id']
|
||||||
muted = status['muted']
|
muted = status['muted']
|
||||||
|
@ -69,14 +80,14 @@ def scan_context_thread():
|
||||||
acct_id = status['account']['id']
|
acct_id = status['account']['id']
|
||||||
content = status['pleroma']['content']['text/plain']
|
content = status['pleroma']['content']['text/plain']
|
||||||
|
|
||||||
if id_st in replyed: # Игнорировать уже отвеченное
|
if id_st in replyed: # Игнорировать уже отвеченное
|
||||||
continue
|
continue
|
||||||
if muted is True:
|
if muted is True:
|
||||||
continue
|
continue
|
||||||
if fail_limit[acct] >= max_fail_limit: # Игнорировать пользователя если он превысил fail limit
|
if fail_limit[acct] >= max_fail_limit: # Игнорировать пользователя если он превысил fail limit
|
||||||
mute_user(acct_id, acct, int(get_state('max_mute_time')) - time_now)
|
mute_user(acct_id, acct, int(states.get('max_mute_time')) - time_now)
|
||||||
logger.warning(f'{acct} игнорируется - превышение fail limit')
|
logger.warning(f'{acct} игнорируется - превышение fail limit')
|
||||||
break # Нужно обновить тред, чтобы muted на заглушенном стал True
|
break # Нужно обновить тред, чтобы muted на заглушенном стал True
|
||||||
|
|
||||||
parsed_result = parse_links(content)
|
parsed_result = parse_links(content)
|
||||||
parsed_result_imdb = parse_links_imdb(content)
|
parsed_result_imdb = parse_links_imdb(content)
|
||||||
|
@ -89,29 +100,40 @@ def scan_context_thread():
|
||||||
logger.info(f'{acct} был уведомлен о завершенной голосовалке')
|
logger.info(f'{acct} был уведомлен о завершенной голосовалке')
|
||||||
fail_limit[acct] += 1
|
fail_limit[acct] += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
index_type = 1
|
index_type = 1
|
||||||
index_name = 2
|
index_name = 2
|
||||||
index_ru_name = 3
|
index_ru_name = 3
|
||||||
index_year = 4
|
index_year = 4
|
||||||
|
|
||||||
|
message_writer = []
|
||||||
|
success = False
|
||||||
|
if parsed_result and parsed_result_imdb:
|
||||||
|
post_status('❌ Не смешивайте IMDB и кинопоиск в одном посте, пожалуйста.', id_st)
|
||||||
|
fail_limit[acct] += 1
|
||||||
|
continue
|
||||||
if parsed_result is not None:
|
if parsed_result is not None:
|
||||||
print(parsed_result)
|
print(parsed_result)
|
||||||
suggested_movies = get_kinopoisk_movie_to_imdb(parsed_result)
|
suggested_movies = get_kinopoisk_movie_to_imdb(parsed_result)
|
||||||
|
message_writer.append('⚠️ внимание при использовании Кинопоиска стабильность может быть понижена')
|
||||||
if suggested_movies is None:
|
if suggested_movies is None:
|
||||||
post_status('❌ Не удалось выполнить запрос: возможно некорректный тип фильма, попробуйте использовать imdb.com', id_st)
|
post_status('❌ Не удалось выполнить запрос: возможно некорректный тип фильма, попробуйте использовать https://libremdb.leemoon.network', id_st)
|
||||||
fail_limit[acct] += 1
|
fail_limit[acct] += 1
|
||||||
continue
|
continue
|
||||||
elif parsed_result_imdb is not None:
|
elif parsed_result_imdb is not None:
|
||||||
suggested_movies = get_title_by_id(parsed_result_imdb)
|
suggested_movies = get_title_by_id(parsed_result_imdb)
|
||||||
|
|
||||||
message_writer = []
|
if suggested_movies is None:
|
||||||
success = False
|
post_status('❌ Фильм(ы) не найден в базе данных IMDB, пожалуйста обратитесь к администратору, чтобы обновить базу. Примечание: IMDB выкладывает новые изменения не сразу.', id_st)
|
||||||
|
fail_limit[acct] += 1
|
||||||
|
continue
|
||||||
|
|
||||||
for movie in suggested_movies:
|
for movie in suggested_movies:
|
||||||
logger.debug(str(movie))
|
logger.debug(str(movie))
|
||||||
if movie[index_type] == "404":
|
if movie[index_type] == "404":
|
||||||
message_writer.append("❌ Не найдено.")
|
message_writer.append("❌ Не найдено.")
|
||||||
fail_limit[acct] += 1
|
fail_limit[acct] += 1
|
||||||
elif movie[index_type] not in ("movie", "video"):
|
elif movie[index_type] not in ("movie", "tvMovie", "video"):
|
||||||
type_of_title = movie[index_type]
|
type_of_title = movie[index_type]
|
||||||
message_writer.append(f"❌ Не принято:\n- Нам не подходят: сериалы, короткометражные и документальные фильмы")
|
message_writer.append(f"❌ Не принято:\n- Нам не подходят: сериалы, короткометражные и документальные фильмы")
|
||||||
logger.info(f'Предложение {acct} отклонено: не подходящий тип фильма: {type_of_title}')
|
logger.info(f'Предложение {acct} отклонено: не подходящий тип фильма: {type_of_title}')
|
||||||
|
@ -122,7 +144,7 @@ def scan_context_thread():
|
||||||
name_ru = movie[index_ru_name]
|
name_ru = movie[index_ru_name]
|
||||||
year = movie[index_year]
|
year = movie[index_year]
|
||||||
movie_string = f"{name_ru} / {name}, {year}"
|
movie_string = f"{name_ru} / {name}, {year}"
|
||||||
|
|
||||||
if name is None:
|
if name is None:
|
||||||
movie_string = f"{name_ru}, {year}"
|
movie_string = f"{name_ru}, {year}"
|
||||||
if name_ru is None:
|
if name_ru is None:
|
||||||
|
@ -136,8 +158,8 @@ def scan_context_thread():
|
||||||
logger.warning(f'Предложение {acct} было отклонено: количество уже предложенных фильмов превышает\равно {limit_all_movies_poll}')
|
logger.warning(f'Предложение {acct} было отклонено: количество уже предложенных фильмов превышает\равно {limit_all_movies_poll}')
|
||||||
fail_limit[acct] += 1
|
fail_limit[acct] += 1
|
||||||
break
|
break
|
||||||
|
|
||||||
if get_already_watched(name, name_ru, year) == True:
|
if get_already_watched(name, name_ru, year) is True:
|
||||||
message_writer.append(f"ℹ️ Этот фильм уже был на FMN: {movie_string}")
|
message_writer.append(f"ℹ️ Этот фильм уже был на FMN: {movie_string}")
|
||||||
logger.info(f'Попытка предложить уже просмотренный фильм: {acct} {name} {name_ru} {year}')
|
logger.info(f'Попытка предложить уже просмотренный фильм: {acct} {name} {name_ru} {year}')
|
||||||
fail_limit[acct] += 1
|
fail_limit[acct] += 1
|
||||||
|
@ -153,7 +175,7 @@ def scan_context_thread():
|
||||||
logger.info(f'Предложение от {acct} было отлонено - фильм в опросе существует')
|
logger.info(f'Предложение от {acct} было отлонено - фильм в опросе существует')
|
||||||
fail_limit[acct] += 1
|
fail_limit[acct] += 1
|
||||||
else:
|
else:
|
||||||
message_writer.append("❌ Вы не можете добавить больше 2х фильмов")
|
message_writer.append(f"❌ Вы не можете добавить больше {limit_movies_per_user}x фильмов")
|
||||||
logger.info(f'Предложение от {acct} было отлонено - лимит на пользователя')
|
logger.info(f'Предложение от {acct} было отлонено - лимит на пользователя')
|
||||||
fail_limit[acct] += 1
|
fail_limit[acct] += 1
|
||||||
if message_writer != []:
|
if message_writer != []:
|
||||||
|
@ -163,5 +185,3 @@ def scan_context_thread():
|
||||||
post_status('\n'.join(message_writer) + message, id_st)
|
post_status('\n'.join(message_writer) + message, id_st)
|
||||||
|
|
||||||
time.sleep(30)
|
time.sleep(30)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,61 +1,128 @@
|
||||||
from src.fedi_api import get_notifications, mark_as_read_notification, post_status, upload_attachment
|
from src.fedi_api import get_notifications, mark_as_read_notification, post_status, upload_attachment
|
||||||
from src.fmn_states_db import add_state, get_state
|
from src.fmn_states_db import write_states, states_stor
|
||||||
from config import admins_bot, limit_movies_per_user, limit_all_movies_poll, hour_poll_posting, fmn_next_watching_hour
|
from src.sheduler import check_stop_thread_scan
|
||||||
|
from config import admins_bot, limit_movies_per_user, limit_all_movies_poll, hour_poll_posting, fmn_next_watching_hour, peertube_stream_url
|
||||||
|
|
||||||
import threading, time
|
import threading
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from dateutil.parser import parse as dateutilparse
|
from dateutil.parser import parse as dateutilparse
|
||||||
from dateutil.relativedelta import relativedelta, TU, SU
|
from dateutil.relativedelta import relativedelta, TU, SU
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
|
@logger.catch
|
||||||
|
def get_peertube_stream_name():
|
||||||
|
try:
|
||||||
|
return requests.get(peertube_stream_url).json()['name']
|
||||||
|
except Exception as E:
|
||||||
|
return f"[не удалось получить название {E}]"
|
||||||
|
|
||||||
|
|
||||||
|
@logger.catch
|
||||||
def get_control_mention():
|
def get_control_mention():
|
||||||
while True:
|
while True:
|
||||||
|
states = states_stor.states
|
||||||
time.sleep(30)
|
time.sleep(30)
|
||||||
time_now = datetime.now()
|
time_now = datetime.now()
|
||||||
now_week = time_now.weekday()
|
now_week = time_now.weekday()
|
||||||
now_hour = time_now.hour
|
now_hour = time_now.hour
|
||||||
if now_week not in (0, 6):
|
if now_week not in (0, 6):
|
||||||
continue
|
continue
|
||||||
if now_week == 6 and now_hour < fmn_next_watching_hour: # Предотвращение работы в холстую до начала сеанса
|
if now_week == 6 and now_hour < fmn_next_watching_hour:
|
||||||
|
# Предотвращение работы в холстую до начала сеанса
|
||||||
continue
|
continue
|
||||||
post_exists = get_state('last_thread_id')
|
post_exists = states.get('last_thread_id')
|
||||||
if post_exists:
|
if post_exists:
|
||||||
continue
|
continue
|
||||||
logger.debug('Wait for from admin mention...')
|
logger.debug('Wait for from admin mention...')
|
||||||
notif = get_notifications()
|
notif = get_notifications()
|
||||||
for i in notif:
|
for i in notif:
|
||||||
if i['type'] != "mention":
|
if i['type'] not in ("mention"):
|
||||||
continue
|
continue
|
||||||
seen = i['pleroma']['is_seen']
|
seen = i['pleroma']['is_seen']
|
||||||
acct_mention = i['account']['acct']
|
acct_mention = i['account']['acct']
|
||||||
reply_to_id = i['status']['in_reply_to_id']
|
reply_to_id = i['status']['in_reply_to_id']
|
||||||
if acct_mention in admins_bot and seen == False and reply_to_id == None and now_week in (0, 6):
|
if acct_mention in admins_bot and seen is False and reply_to_id is None and now_week in (0, 6):
|
||||||
logger.success(f'Найдено упоминание от {acct_mention}')
|
logger.success(f'Найдено упоминание от {acct_mention}')
|
||||||
st_id = i['status']['id']
|
st_id = i['status']['id']
|
||||||
st_date = i['status']['created_at']
|
st_date = i['status']['created_at']
|
||||||
thread_created_at = dateutilparse(st_date)
|
thread_created_at = dateutilparse(st_date)
|
||||||
|
|
||||||
delta = relativedelta(hour=hour_poll_posting, minute=0, second=0, weekday=TU(1))
|
delta = relativedelta(
|
||||||
|
hour=hour_poll_posting, minute=0, second=0, weekday=TU(1))
|
||||||
stop_thread_scan = thread_created_at + delta
|
stop_thread_scan = thread_created_at + delta
|
||||||
movies_accept_time = stop_thread_scan.strftime('%H:%M %d.%m.%Y по Москве')
|
# if check_stop_thread_scan(stop_thread_scan) == stop_thread_scan: # broken because pytz
|
||||||
stop_thread_scan = time.mktime(time.struct_time(stop_thread_scan.timetuple()))
|
# logger.success("проверка на корректность даты пройдена")
|
||||||
|
# else:
|
||||||
if now_week == 6: # Фикс стыков двух недель. Если вс, то расчитываем на следующую неделю
|
# logger.warning("проверка на корректность даты не пройдена")
|
||||||
|
# stop_thread_scan = check_stop_thread_scan(stop_thread_scan)
|
||||||
|
movies_accept_time = stop_thread_scan.strftime(
|
||||||
|
'%H:%M %d.%m.%Y по Москве')
|
||||||
|
stop_thread_scan = time.mktime(
|
||||||
|
time.struct_time(stop_thread_scan.timetuple()))
|
||||||
|
|
||||||
|
if now_week == 6: # Фикс стыков двух недель. Если вс, то расчитываем на следующую неделю
|
||||||
next_week = 2
|
next_week = 2
|
||||||
else:
|
else:
|
||||||
next_week = 1
|
next_week = 1
|
||||||
next_movie_watching_delta = relativedelta(hour=fmn_next_watching_hour, minute=0, second=0, weekday=SU(next_week))
|
next_movie_watching_delta = relativedelta(
|
||||||
|
hour=fmn_next_watching_hour, minute=0, second=0, weekday=SU(next_week))
|
||||||
next_movie_watching = time_now + next_movie_watching_delta
|
next_movie_watching = time_now + next_movie_watching_delta
|
||||||
max_mute_time = time.mktime(time.struct_time(next_movie_watching.timetuple())) # Глушение до следующего сеанса FMN.
|
# Глушение до следующего сеанса FMN.
|
||||||
|
max_mute_time = time.mktime(
|
||||||
|
time.struct_time(next_movie_watching.timetuple()))
|
||||||
next_movie_watching = next_movie_watching.strftime('%d.%m.%Y')
|
next_movie_watching = next_movie_watching.strftime('%d.%m.%Y')
|
||||||
post_status(start_collect_movies_text(movies_accept_time, next_movie_watching), st_id, attachments=[upload_attachment('src/FMN.png')])
|
post_status(start_collect_movies_text(movies_accept_time, next_movie_watching),
|
||||||
|
st_id, attachments=[upload_attachment('src/FMN.webp')])
|
||||||
|
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
mark_as_read_notification(i['id'])
|
mark_as_read_notification(i['id'])
|
||||||
add_state('max_mute_time', int(max_mute_time))
|
|
||||||
add_state('stop_thread_scan', int(stop_thread_scan))
|
states_stor.states['max_mute_time'] = int(max_mute_time)
|
||||||
add_state('last_thread_id', st_id)
|
states_stor.states['stop_thread_scan'] = int(stop_thread_scan)
|
||||||
|
states_stor.states['last_thread_id'] = st_id
|
||||||
|
write_states(states_stor.states)
|
||||||
|
break
|
||||||
|
if now_hour == 0:
|
||||||
|
logger.warning('Автоматический триггер в полночи сработал')
|
||||||
|
thread_created_at = time_now
|
||||||
|
|
||||||
|
delta = relativedelta(
|
||||||
|
hour=hour_poll_posting, minute=0, second=0, weekday=TU(1))
|
||||||
|
stop_thread_scan = thread_created_at + delta
|
||||||
|
if check_stop_thread_scan(stop_thread_scan) == stop_thread_scan:
|
||||||
|
logger.success("проверка на корректность даты пройдена")
|
||||||
|
else:
|
||||||
|
logger.warning("проверка на корректность даты не пройдена")
|
||||||
|
# stop_thread_scan = check_stop_thread_scan(stop_thread_scan)
|
||||||
|
movies_accept_time = stop_thread_scan.strftime(
|
||||||
|
'%H:%M %d.%m.%Y по Москве')
|
||||||
|
stop_thread_scan = time.mktime(
|
||||||
|
time.struct_time(stop_thread_scan.timetuple()))
|
||||||
|
|
||||||
|
if now_week == 6: # Фикс стыков двух недель. Если вс, то расчитываем на следующую неделю
|
||||||
|
next_week = 2
|
||||||
|
else:
|
||||||
|
next_week = 1
|
||||||
|
next_movie_watching_delta = relativedelta(
|
||||||
|
hour=fmn_next_watching_hour, minute=0, second=0, weekday=SU(next_week))
|
||||||
|
next_movie_watching = time_now + next_movie_watching_delta
|
||||||
|
# Глушение до следующего сеанса FMN.
|
||||||
|
max_mute_time = time.mktime(
|
||||||
|
time.struct_time(next_movie_watching.timetuple()))
|
||||||
|
next_movie_watching = next_movie_watching.strftime('%d.%m.%Y')
|
||||||
|
watched_movie_name = get_peertube_stream_name()
|
||||||
|
st_id = post_status("Спасибо что посмотрели " + watched_movie_name + "\n\n" + start_collect_movies_text(movies_accept_time, next_movie_watching) +
|
||||||
|
'\n\n@rf@mastodon.ml', reply_to_status_id=None, attachments=[upload_attachment('src/FMN.webp')], visibility="public")['id']
|
||||||
|
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
states_stor.states['max_mute_time'] = int(max_mute_time)
|
||||||
|
states_stor.states['stop_thread_scan'] = int(stop_thread_scan)
|
||||||
|
states_stor.states['last_thread_id'] = st_id
|
||||||
|
write_states(states_stor.states)
|
||||||
break
|
break
|
||||||
time.sleep(30)
|
time.sleep(30)
|
||||||
|
|
||||||
|
@ -66,19 +133,20 @@ def start_collect_movies_text(movies_accept_time=str, next_movie_watching=str):
|
||||||
|
|
||||||
Напоминаем правила:
|
Напоминаем правила:
|
||||||
- Мы принимаем на просмотр полнометражные художественные фильмы;
|
- Мы принимаем на просмотр полнометражные художественные фильмы;
|
||||||
- Прием варианта осуществляется путем публикации ссылки на этот фильм на IMDB (libremdb) или Кинопоиске в этом треде;
|
- Прием варианта осуществляется путем публикации ссылки на этот фильм на IMDB https://libremdb.leemoon.network/ или Кинопоиске в этом треде;
|
||||||
- Нам не подходят: сериалы, короткометражные и документальные фильмы;
|
- Нам не подходят: сериалы, короткометражные и документальные фильмы;
|
||||||
- Максимальное количество вариантов, предложенных одним человеком не должно превышать {limit_movies_per_user};
|
- Максимальное количество вариантов, предложенных одним человеком не должно превышать {limit_movies_per_user};
|
||||||
- Всего может быть собрано до {limit_all_movies_poll} фильмов;
|
- Всего может быть собрано до {limit_all_movies_poll} фильмов;
|
||||||
- Заявки принимаются до крайнего срока, после чего будет объявлено голосование по собранным вариантам.
|
- Заявки принимаются до крайнего срока, после чего будет объявлено голосование по собранным вариантам.
|
||||||
|
|
||||||
Крайний срок подачи заявки - {movies_accept_time}.
|
Крайний срок подачи заявки - {movies_accept_time}.
|
||||||
|
Рекомендуем посетить список, чтобы случайно не предложить уже просмотренный фильм: https://pub.phreedom.club/~localhost/fmn_watched.gmi
|
||||||
|
|
||||||
Желаем удачи.
|
Желаем удачи.
|
||||||
'''.replace('\t', '')
|
'''.replace('\t', '')
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
def run_scan_notif():
|
def run_scan_notif():
|
||||||
scan_notif = threading.Thread(target=get_control_mention, daemon=True)
|
scan_notif = threading.Thread(target=get_control_mention, daemon=True)
|
||||||
scan_notif.start()
|
scan_notif.start()
|
||||||
|
|
||||||
|
|
23
src/sheduler.py
Normal file
23
src/sheduler.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import calendar
|
||||||
|
import datetime
|
||||||
|
from config import hour_poll_posting
|
||||||
|
|
||||||
|
|
||||||
|
def check_stop_thread_scan(suggested_date):
|
||||||
|
suggested_month = suggested_date.month
|
||||||
|
suggested_year = suggested_date.year
|
||||||
|
tuesdays = []
|
||||||
|
for i in calendar.Calendar().itermonthdays4(suggested_year, suggested_month):
|
||||||
|
if i[3] == 1 and datetime.datetime(year=i[0], month=i[1], day=i[2]) > suggested_date:
|
||||||
|
tuesdays.append(datetime.datetime(
|
||||||
|
year=i[0], month=i[1], day=i[2], hour=hour_poll_posting))
|
||||||
|
if tuesdays == []:
|
||||||
|
shift_for_next_week = suggested_date + datetime.timedelta(days=1)
|
||||||
|
suggested_month = shift_for_next_week.month
|
||||||
|
suggested_year = shift_for_next_week.year
|
||||||
|
for i in calendar.Calendar().itermonthdays4(suggested_year, suggested_month):
|
||||||
|
if i[3] == 1 and datetime.datetime(year=i[0], month=i[1], day=i[2]) > suggested_date:
|
||||||
|
tuesdays.append(datetime.datetime(
|
||||||
|
year=i[0], month=i[1], day=i[2], hour=hour_poll_posting))
|
||||||
|
|
||||||
|
return tuesdays[0] # near tuesday
|
30
test.py
Normal file
30
test.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import unittest
|
||||||
|
import datetime
|
||||||
|
import time_machine
|
||||||
|
from src import sheduler
|
||||||
|
|
||||||
|
|
||||||
|
class TestDate(unittest.TestCase):
|
||||||
|
def test_date_tue(self):
|
||||||
|
"""
|
||||||
|
test near tue
|
||||||
|
тестирует корректность расчета даты на след. вторник
|
||||||
|
условия: расчет начат от воскресенья или понедельника (случайно)
|
||||||
|
"""
|
||||||
|
import calendar
|
||||||
|
import random
|
||||||
|
for month in range(1, 13):
|
||||||
|
sundays = [i for i in calendar.Calendar().itermonthdays4(year=2024, month=month) if i[3] == 6]
|
||||||
|
for sunday in sundays:
|
||||||
|
fake_date = datetime.datetime(year=sunday[0], month=sunday[1], day=sunday[2], hour=random.randint(21, 23)) + datetime.timedelta(days=random.randint(0, 1))
|
||||||
|
with time_machine.travel(fake_date):
|
||||||
|
try:
|
||||||
|
result = sheduler.check_stop_thread_scan(fake_date)
|
||||||
|
print("current fake date: " + fake_date.strftime("%Y.%m.%d %H:%M") + f" near tue: {result}")
|
||||||
|
except Exception as E:
|
||||||
|
print("fail fake date: " + fake_date.strftime("%Y.%m.%d %H:%M") + f", err: {E}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
Loading…
Reference in New Issue
Block a user