Sync branch with master, fix conflict
This commit is contained in:
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -20,13 +20,8 @@ Steps to reproduce the behavior:
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
You can enable the screenshot using: `BrowserSettings(do_screenshot=True)`
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. Windows]
|
||||
- Browser [e.g. chrome, firefox]
|
||||
- Python version [e.g. 3.x]
|
||||
|
||||
**Additional context**
|
||||
|
||||
26
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
26
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# Description
|
||||
|
||||
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
|
||||
|
||||
Fixes # (issue)
|
||||
|
||||
## Type of change
|
||||
|
||||
Please delete options that are not relevant.
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
|
||||
# How Has This Been Tested?
|
||||
|
||||
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration.
|
||||
|
||||
# Checklist:
|
||||
|
||||
- [ ] My code follows the style guidelines of this project
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have commented on my code, particularly in hard-to-understand areas
|
||||
- [ ] I have made corresponding changes to the documentation (README.md)
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] Any dependent changes have been updated in requirements.txt
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -137,6 +137,9 @@ dmypy.json
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
.idea/
|
||||
|
||||
# Custom files
|
||||
run.py
|
||||
chromedriver*
|
||||
|
||||
76
CODE_OF_CONDUCT.md
Normal file
76
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at alex.tkd.alex@gmail.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
110
CONTRIBUTING.md
Normal file
110
CONTRIBUTING.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Contributing to this repository
|
||||
|
||||
## Getting started
|
||||
|
||||
Before you begin:
|
||||
- Have you read the [code of conduct](CODE_OF_CONDUCT.md)?
|
||||
- Check out the [existing issues](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues) & see if there is already an opened issued
|
||||
|
||||
### Ready to make a change? Fork the repo
|
||||
|
||||
Fork using GitHub Desktop:
|
||||
|
||||
- [Getting started with GitHub Desktop](https://docs.github.com/en/desktop/installing-and-configuring-github-desktop/getting-started-with-github-desktop) will guide you through setting up Desktop.
|
||||
- Once Desktop is set up, you can use it to [fork the repo](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/cloning-and-forking-repositories-from-github-desktop)!
|
||||
|
||||
Fork using the command line:
|
||||
|
||||
- [Fork the repo](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#fork-an-example-repository) so that you can make your changes without affecting the original project until you're ready to merge them.
|
||||
|
||||
Fork with [GitHub Codespaces](https://github.com/features/codespaces):
|
||||
|
||||
- [Fork, edit, and preview](https://docs.github.com/en/free-pro-team@latest/github/developing-online-with-codespaces/creating-a-codespace) using [GitHub Codespaces](https://github.com/features/codespaces) without having to install and run the project locally.
|
||||
|
||||
### Open a pull request
|
||||
When you're done making changes, and you'd like to propose them for review, use the [pull request template](#pull-request-template) to open your PR (pull request).
|
||||
|
||||
### Submit your PR & get it reviewed
|
||||
- Once you submit your PR, other users from the community will review it with you. The first thing you're going to want to do is a [self review](#self-review).
|
||||
- After that, we may have questions. Check back on your PR to keep up with the conversation.
|
||||
- Did you have an issue, like a merge conflict? Check out our [git tutorial](https://lab.github.com/githubtraining/managing-merge-conflicts) on resolving merge conflicts and other issues.
|
||||
|
||||
### Your PR is merged!
|
||||
Congratulations! The whole GitHub community thanks you. :sparkles:
|
||||
|
||||
Once your PR is merged, you will be proudly listed as a contributor in the [contributor chart](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/graphs/contributors).
|
||||
|
||||
### Keep contributing as you use GitHub Docs
|
||||
|
||||
Now that you're a part of the GitHub Docs community, you can keep participating in many ways.
|
||||
|
||||
**Learn more about contributing:**
|
||||
|
||||
- [Types of contributions :memo:](#types-of-contributions-memo)
|
||||
- [:beetle: Issues](#beetle-issues)
|
||||
- [:hammer_and_wrench: Pull requests](#hammer_and_wrench-pull-requests)
|
||||
- [Starting with an issue](#starting-with-an-issue)
|
||||
- [Labels](#labels)
|
||||
- [Opening a pull request](#opening-a-pull-request)
|
||||
- [Reviewing](#reviewing)
|
||||
- [Self review](#self-review)
|
||||
- [Pull request template](#pull-request-template)
|
||||
- [Python Styleguide](#python-styleguide)
|
||||
- [Suggested changes](#suggested-changes)
|
||||
|
||||
## Types of contributions :memo:
|
||||
You can contribute to the Twitch-Channel-Points-Miner-v2 in several ways. Bug reporting, pull request, propose new features, fork, donate, and much more :muscle: .
|
||||
|
||||
### :beetle: Issues
|
||||
[Issues](https://docs.github.com/en/github/managing-your-work-on-github/about-issues) are used to report a bug, propose new features, or ask for help. When you open an issue, please use the appropriate template and label.
|
||||
|
||||
### :hammer_and_wrench: Pull requests
|
||||
A [pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) is a way to suggest changes in our repository.
|
||||
|
||||
When we merge those changes, they should be deployed to the live site within 24 hours. :earth_africa: To learn more about opening a pull request in this repo, see [Opening a pull request](#opening-a-pull-request) below.
|
||||
|
||||
## Starting with an issue
|
||||
You can browse existing issues to find something that needs help!
|
||||
|
||||
### Labels
|
||||
Labels can help you find an issue you'd like to help with.
|
||||
- The `bug` label is used when something isn't working
|
||||
- The `documentation` label is used when you suggest improvements or additions to documentation (README.md update)
|
||||
- The `duplicate` label is used when this issue or pull request already exists
|
||||
- The `enhancement` label is used when you ask for / or propose a new feature or request
|
||||
- The `help wanted` is used when labeling when you need help with something
|
||||
- The `improvements` label is used when you would suggest improvements on already existing features
|
||||
- The `invalid` label is used for a non-valid issue
|
||||
- The `question` label, is used for further information is requested
|
||||
- The `wontfix` label is used if we will not work on it
|
||||
|
||||
## Opening a pull request
|
||||
You can use the GitHub user interface :pencil2: for some small changes, like fixing a typo or updating a readme. You can also fork the repo and then clone it locally to view changes and run your tests on your machine.
|
||||
|
||||
### Self review
|
||||
You should always review your own PR first.
|
||||
|
||||
For content changes, make sure that you:
|
||||
- [ ] Confirm that the changes address every part of the content design plan from your issue (if there are differences, explain them).
|
||||
- [ ] Review the content for technical accuracy.
|
||||
- [ ] Review the entire pull request using the checklist present in the template.
|
||||
- [ ] Copy-edit the changes for grammar, spelling, and adherence to the style guide.
|
||||
- [ ] Check new or updated Liquid statements to confirm that versioning is correct.
|
||||
- [ ] Check that all of your changes render correctly in staging. Remember, that lists and tables can be tricky.
|
||||
- [ ] If there are any failing checks in your PR, troubleshoot them until they're all passing.
|
||||
|
||||
### Pull request template
|
||||
When you open a pull request, you must fill out the "Ready for review" template before we can review your PR. This template helps reviewers understand your changes and the purpose of your pull request.
|
||||
|
||||
### Python Styleguide
|
||||
All Python code is formatted with [Black](https://github.com/psf/black) using the default settings. Your code will not be accepted if it is not blackened.
|
||||
You can use the pre-commit hook.
|
||||
```
|
||||
pip install pre-commit
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
### Suggested changes
|
||||
We may ask for changes to be made before a PR can be merged, either using [suggested changes](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/incorporating-feedback-in-your-pull-request) or pull request comments. You can apply suggested changes directly through the UI. You can make any other changes in your fork, then commit them to your branch.
|
||||
|
||||
As you update your PR and apply changes, mark each conversation as [resolved](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request#resolving-conversations).
|
||||
289
README.md
289
README.md
@@ -1,30 +1,16 @@
|
||||
# Twitch Channel Points Miner - v2
|
||||
|
||||

|
||||

|
||||
<p align="center">
|
||||
<a href="https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/blob/master/LICENSE">
|
||||
<img alt="License" src="https://img.shields.io/github/license/Tkd-Alex/Twitch-Channel-Points-Miner-v2" />
|
||||
</a>
|
||||
<a href="https://www.python.org/download/releases/3.0/">
|
||||
<img alt="Python3" src="https://img.shields.io/badge/built%20with-Python3-red.svg?style=flat" />
|
||||
</a>
|
||||
<a href="https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/pulls">
|
||||
<img alt="PRsWelcome" src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" />
|
||||
</a>
|
||||
<a href="https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/stargazers">
|
||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/Tkd-Alex/Twitch-Channel-Points-Miner-v2" />
|
||||
</a>
|
||||
<a href="https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues?q=is%3Aissue+is%3Aclosed">
|
||||
<img alt="GitHub closed issues" src="https://img.shields.io/github/issues-closed/Tkd-Alex/Twitch-Channel-Points-Miner-v2">
|
||||
</a>
|
||||
<a href="https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2">
|
||||
<img alt="GitHub last commit" src="https://img.shields.io/github/last-commit/Tkd-Alex/Twitch-Channel-Points-Miner-v2" />
|
||||
</a>
|
||||
<a href="https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/blob/master/LICENSE"><img alt="License" src="https://img.shields.io/github/license/Tkd-Alex/Twitch-Channel-Points-Miner-v2"></a>
|
||||
<a href="https://www.python.org/downloads/release/python-360/"><img alt="Python3" src="https://img.shields.io/badge/built%20for-Python≥3.6-red.svg?style=flat"></a>
|
||||
<a href="https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/pulls"><img alt="PRsWelcome" src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat"></a>
|
||||
<a href="https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/stargazers"><img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/Tkd-Alex/Twitch-Channel-Points-Miner-v2"></a>
|
||||
<a href="https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues?q=is%3Aissue+is%3Aclosed"><img alt="GitHub closed issues" src="https://img.shields.io/github/issues-closed/Tkd-Alex/Twitch-Channel-Points-Miner-v2"></a>
|
||||
<a href="https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2"><img alt="GitHub last commit" src="https://img.shields.io/github/last-commit/Tkd-Alex/Twitch-Channel-Points-Miner-v2"></a>
|
||||
</p>
|
||||
|
||||
**Credits**
|
||||
- Main idea: https://github.com/gottagofaster236/Twitch-Channel-Points-Miner
|
||||
- Bet system (Selenium): https://github.com/ClementRoyer/TwitchAutoCollect-AutoBet
|
||||
- ~~Bet system (Selenium): https://github.com/ClementRoyer/TwitchAutoCollect-AutoBet~~
|
||||
|
||||
> A simple script that will watch a stream for you and earn the channel points.
|
||||
|
||||
@@ -32,18 +18,50 @@
|
||||
|
||||
Read more about channels point [here](https://help.twitch.tv/s/article/channel-points-guide)
|
||||
|
||||
## Main difference from the original repository:
|
||||
# README Contents
|
||||
1. 🤝 [Community](#community)
|
||||
2. 🚀 [Main differences from the original repository](#main-differences-from-the-original-repository)
|
||||
3. 🧾 [Logs feature](#logs-feature)
|
||||
- [Full logs](#full-logs)
|
||||
- [Less logs](#less-logs)
|
||||
- [Final report](#final-report)
|
||||
4. 🧐 [How to use](#how-to-use)
|
||||
- [Limits](#limits)
|
||||
5. 🔧 [Settings](#settings)
|
||||
- [LoggerSettings](#loggersettings)
|
||||
- [StreamerSettings](#streamersettings)
|
||||
- [BetSettings](#betsettings)
|
||||
- [Bet strategy](#bet-strategy)
|
||||
- [FilterCondition](#filtercondition)
|
||||
- [Example](#example)
|
||||
6. 🍪 [Migrating from old repository (the original one)](#migrating-from-old-repository-the-original-one)
|
||||
7. 🪟 [Windows](#windows)
|
||||
8. ⚠️ [Disclaimer](#disclaimer)
|
||||
|
||||
|
||||
## Community
|
||||
If you have any type of issue, you need help, or you just want to suggest a new feature, please open a GitHub Issue. Don't write me on [Instagram](https://www.instagram.com/tkd_alex/), [Telegram](https://t.me/TkdAlex), [Discord](https://discordapp.com/users/641397388132483121), [Twitter](https://twitter.com/TkdAxel) (but you can follow me 😆) or somewhere else. If you don't have an account on this platform, you can create it. It's free. I do not want to be rude, but if you have a problem, maybe another user can also have the same problem, and your issue can help the community. Same for the new feature, your idea can help other users, and It's beautiful to discuss between us.
|
||||
|
||||
If you want to help on this project, please leave a star 🌟 and share it with your friends! 😎
|
||||
|
||||
A coffee is always a gesture of LOVE ❤️
|
||||
|
||||
<a href="https://www.buymeacoffee.com/tkdalex" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/lato-yellow.png" alt="Buy Me A Coffee" height="41" width="174"></a>
|
||||
|
||||
If you have any issues or you want to contribute, you are welcome! But please before read the [CONTRIBUTING.md](/CONTRIBUTING.md)
|
||||
|
||||
## Main differences from the original repository:
|
||||
|
||||
- Improve the logging
|
||||
- Final report with all the datas
|
||||
- Final report with all the data
|
||||
- Rewrite the entire code using classe instead of module with global variables
|
||||
- Automatic download the followers list and use as input
|
||||
- Automatic download the follower's list and use it as input
|
||||
- Better 'Watch Streak' strategy in priority system [#11](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/11)
|
||||
- Auto claim game drops from Twitch inventory [#21](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/21) Read more about game drops [here](https://help.twitch.tv/s/article/mission-based-drops)
|
||||
- Place the bet / make prediction and won or lose (good luck) your channel points! **(CURRENTLY IN BETA)**
|
||||
|
||||
For the bet system the script use Selenium. Could be usefull understand how to MakePrediction usign a [POST] request. I've also write a [poc](/TwitchChannelPointsMiner/classes/Twitch.py#L160) but I don't know how to calculate/create the transactionID. Any helps are welcome
|
||||
- Place the bet / make a prediction and win or lose (🍀) your channel points!
|
||||
No browser needed. [#41](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/41) ([@lay295](https://github.com/lay295))
|
||||
|
||||
## Logs feature
|
||||
### Full logs
|
||||
```
|
||||
%d/%m/%y %H:%M:%S - INFO - [run]: 💣 Start session: '9eb934b0-1684-4a62-b3e2-ba097bd67d35'
|
||||
@@ -59,10 +77,8 @@ For the bet system the script use Selenium. Could be usefull understand how to M
|
||||
%d/%m/%y %H:%M:%S - INFO - [__enable_custom_bet_value]: 🔧 Enable input of custom value for EventPrediction(event_id=xxxx-xxxx-xxxx-xxxx, title=Please star this repo)
|
||||
%d/%m/%y %H:%M:%S - INFO - [on_message]: ⏰ Place the bet after: 89.99s for: EventPrediction(event_id=xxxx-xxxx-xxxx-xxxx-15c61914ef69, title=Please star this repo)
|
||||
%d/%m/%y %H:%M:%S - INFO - [on_message]: 🚀 +12 → Streamer(username=streamer-username, channel_id=0000000, channel_points=61377) - Reason: WATCH.
|
||||
%d/%m/%y %H:%M:%S - INFO - [place_bet]: 🔧 Going to complete bet for EventPrediction(event_id=xxxx-xxxx-xxxx-xxxx, title=Please star this repo) owned by Streamer(username=streamer-username, channel_id=0000000, channel_points=61365)
|
||||
%d/%m/%y %H:%M:%S - INFO - [place_bet]: 🔧 Decision: YES (PINK), Points: 156k, Users: 46 (61.33%), Odds: 1.57 (63.69%)
|
||||
%d/%m/%y %H:%M:%S - INFO - [place_bet]: 🔧 Going to write: 4296 channel points on input B
|
||||
%d/%m/%y %H:%M:%S - INFO - [place_bet]: 🔧 Going to place the bet for EventPrediction(event_id=xxxx-xxxx-xxxx-xxxx, title=Please star this repo)
|
||||
%d/%m/%y %H:%M:%S - INFO - [make_predictions]: 🍀 Going to complete bet for EventPrediction(event_id=xxxx-xxxx-xxxx-xxxx-15c61914ef69, title=Please star this repo) owned by Streamer(username=streamer-username, channel_id=0000000, channel_points=61377)
|
||||
%d/%m/%y %H:%M:%S - INFO - [make_predictions]: 🍀 Place 5k channel points on: SI (BLUE), Points: 848k, Users: 190 (70.63%), Odds: 1.24 (80.65%)
|
||||
%d/%m/%y %H:%M:%S - INFO - [on_message]: 🚀 +6675 → Streamer(username=streamer-username, channel_id=0000000, channel_points=64206) - Reason: PREDICTION.
|
||||
%d/%m/%y %H:%M:%S - INFO - [on_message]: 📊 EventPrediction(event_id=xxxx-xxxx-xxxx-xxxx, title=Please star this repo) - Result: WIN, Points won: 6675
|
||||
%d/%m/%y %H:%M:%S - INFO - [on_message]: 🚀 +12 → Streamer(username=streamer-username, channel_id=0000000, channel_points=64218) - Reason: WATCH.
|
||||
@@ -92,10 +108,8 @@ For the bet system the script use Selenium. Could be usefull understand how to M
|
||||
%d/%m %H:%M:%S - 🔧 Enable input of custom value for EventPrediction: Please star this repo
|
||||
%d/%m %H:%M:%S - ⏰ Place the bet after: 89.99s EventPrediction: Please star this repo
|
||||
%d/%m %H:%M:%S - 🚀 +12 → streamer-username (xxx points) - Reason: WATCH.
|
||||
%d/%m %H:%M:%S - 🔧 Going to complete bet for EventPrediction: Please star this repo owned by streamer-username (xxx points)
|
||||
%d/%m %H:%M:%S - 🔧 Decision: YES (PINK), Points: 156k, Users: 46 (61.33%), Odds: 1.57 (63.69%)
|
||||
%d/%m %H:%M:%S - 🔧 Going to write: 4296 channel points on input B
|
||||
%d/%m %H:%M:%S - 🔧 Going to place the bet for EventPrediction: Please star this repo
|
||||
%d/%m %H:%M:%S - 🍀 Going to complete bet for EventPrediction: Please star this repo owned by streamer-username (xxx points)
|
||||
%d/%m %H:%M:%S - 🍀 Place 5k channel points on: SI (BLUE), Points: 848k, Users: 190 (70.63%), Odds: 1.24 (80.65%)
|
||||
%d/%m %H:%M:%S - 🚀 +6675 → streamer-username (xxx points) - Reason: PREDICTION.
|
||||
%d/%m %H:%M:%S - 📊 EventPrediction: Please star this repo - Result: WIN, Points won: 6675
|
||||
%d/%m %H:%M:%S - 🚀 +12 → streamer-username (xxx points) - Reason: WATCH.
|
||||
@@ -135,23 +149,24 @@ For the bet system the script use Selenium. Could be usefull understand how to M
|
||||
Result: {'type': 'LOSE', 'won': 0}
|
||||
|
||||
%d/%m/%y %H:%M:%S - 🤖 Streamer(username=streamer-username, channel_id=0000000, channel_points=67247), Total points gained (after farming - before farming): -7838
|
||||
%d/%m/%y %H:%M:%S - 💰 WATCH(35 times, 350 gained), CLAIM(11 times, 550 gained), PREDICTION(1 times, 6531 gained)
|
||||
%d/%m/%y %H:%M:%S - 🤖 Streamer(username=streamer-username1, channel_id=0000000, channel_points=4240), Total points gained (after farming - before farming): 0
|
||||
%d/%m/%y %H:%M:%S - 💰 CLAIM(11 times, 550 gained), PREDICTION(1 times, 6531 gained), WATCH(35 times, 350 gained)
|
||||
%d/%m/%y %H:%M:%S - 🤖 Streamer(username=streamer-username2, channel_id=0000000, channel_points=61365), Total points gained (after farming - before farming): 977
|
||||
%d/%m/%y %H:%M:%S - 💰 WATCH(11 times, 132 gained), REFUND(1 times, 605 gained), CLAIM(4 times, 240 gained)
|
||||
%d/%m/%y %H:%M:%S - 🤖 Streamer(username=streamer-username3, channel_id=0000000, channel_points=6815), Total points gained (after farming - before farming): 0
|
||||
%d/%m/%y %H:%M:%S - 🤖 Streamer(username=streamer-username4, channel_id=0000000, channel_points=16386), Total points gained (after farming - before farming): 0
|
||||
%d/%m/%y %H:%M:%S - 💰 CLAIM(4 times, 240 gained), REFUND(1 times, 605 gained), WATCH(11 times, 132 gained)
|
||||
%d/%m/%y %H:%M:%S - 🤖 Streamer(username=streamer-username5, channel_id=0000000, channel_points=25960), Total points gained (after farming - before farming): 1680
|
||||
%d/%m/%y %H:%M:%S - 💰 WATCH(53 times, 530 gained), CLAIM(17 times, 850 gained)
|
||||
%d/%m/%y %H:%M:%S - 💰 CLAIM(17 times, 850 gained), WATCH(53 times, 530 gained)
|
||||
%d/%m/%y %H:%M:%S - 🤖 Streamer(username=streamer-username6, channel_id=0000000, channel_points=9430), Total points gained (after farming - before farming): 1120
|
||||
%d/%m/%y %H:%M:%S - 💰 WATCH(42 times, 420 gained), WATCH_STREAK(1 times, 450 gained), CLAIM(14 times, 700 gained)
|
||||
%d/%m/%y %H:%M:%S - 🤖 Streamer(username=streamer-username7, channel_id=0000000, channel_points=2380), Total points gained (after farming - before farming): 0
|
||||
%d/%m/%y %H:%M:%S - 🤖 Streamer(username=streamer-username8, channel_id=0000000, channel_points=10230), Total points gained (after farming - before farming): 0
|
||||
%d/%m/%y %H:%M:%S - 💰 CLAIM(14 times, 700 gained), WATCH(42 times, 420 gained), WATCH_STREAK(1 times, 450 gained)
|
||||
```
|
||||
|
||||
## How to use:
|
||||
1. Clone this repository `git clone https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2`
|
||||
2. Install all the requirements `pip install -r requirements.txt`
|
||||
2. Install all the requirements `pip install -r requirements.txt` . If you have problems with requirements make sure to have at least Python3.6. You could also try to create a virtualenv and then install all the requirements
|
||||
```sh
|
||||
pip install virtualenv
|
||||
virtualenv -p python3 venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
3. Create your `run.py` file start from [example.py](/example.py)
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -159,54 +174,61 @@ For the bet system the script use Selenium. Could be usefull understand how to M
|
||||
import logging
|
||||
from TwitchChannelPointsMiner import TwitchChannelPointsMiner
|
||||
from TwitchChannelPointsMiner.logger import LoggerSettings
|
||||
from TwitchChannelPointsMiner.classes.entities.Bet import Strategy, BetSettings
|
||||
from TwitchChannelPointsMiner.classes.Settings import Priority
|
||||
from TwitchChannelPointsMiner.classes.entities.Bet import Strategy, BetSettings, Condition, OutcomeKeys, FilterCondition
|
||||
from TwitchChannelPointsMiner.classes.entities.Streamer import Streamer, StreamerSettings
|
||||
from TwitchChannelPointsMiner.classes.TwitchBrowser import Browser, BrowserSettings
|
||||
|
||||
twitch_miner = TwitchChannelPointsMiner(
|
||||
username="your-twitch-username",
|
||||
claim_drops_startup=False, # If you want to auto claim all drops from Twitch inventory on startup
|
||||
password="write-your-secure-psw", # If no password will be provided the script will ask interactively
|
||||
claim_drops_startup=False, # If you want to auto claim all drops from Twitch inventory on startup
|
||||
priority=[ # Custom priority in this case for example:
|
||||
Priority.STREAK, # - we want first of all to catch all watch streak from all streamers
|
||||
Priority.DROPS, # - when we don't have anymore watch streak to catch wait until all drops are collected over the streamers
|
||||
Priority.ORDER # - when we have all of drops claimed and no watch-streak avaialable use the order priority (POINTS_ASCENDING, POINTS_DESCEDING)
|
||||
],
|
||||
logger_settings=LoggerSettings(
|
||||
save=True, # If you want to save logs in file (suggested)
|
||||
console_level=logging.INFO, # Level of logs - use logging.DEBUG for more info)
|
||||
file_level=logging.DEBUG, # Level of logs - If you think the log file it's too big use logging.INFO
|
||||
emoji=True, # On Windows we have a problem to print emoji. Set to false if you have a problem
|
||||
less=False # If you think that the logs are too much verborse set this to True
|
||||
),
|
||||
browser_settings=BrowserSettings(
|
||||
browser=Browser.FIREFOX, # Choose if you want to use Chrome or Firefox as browser
|
||||
show=False, # Show the browser during bet else headless mode
|
||||
do_screenshot=False, # Do screenshot during the bet
|
||||
save=True, # If you want to save logs in a file (suggested)
|
||||
console_level=logging.INFO, # Level of logs - use logging.DEBUG for more info)
|
||||
file_level=logging.DEBUG, # Level of logs - If you think the log file it's too big, use logging.INFO
|
||||
emoji=True, # On Windows, we have a problem printing emoji. Set to false if you have a problem
|
||||
less=False # If you think that the logs are too verbose, set this to True
|
||||
),
|
||||
streamer_settings=StreamerSettings(
|
||||
make_predictions=True, # If you want to Bet / Make prediction
|
||||
follow_raid=True, # Follow raid to obtain more points
|
||||
claim_drops=True, # We can't filter rewards base on stream. Set to False for skip viewing counter increase and you will never obtain a drop reward from this script. Issue #21
|
||||
watch_streak=True, # If a streamer go online change the priotiry of streamers array and catch the watch screak. Issue #11
|
||||
make_predictions=True, # If you want to Bet / Make prediction
|
||||
follow_raid=True, # Follow raid to obtain more points
|
||||
claim_drops=True, # We can't filter rewards base on stream. Set to False for skip viewing counter increase and you will never obtain a drop reward from this script. Issue #21
|
||||
watch_streak=True, # If a streamer go online change the priotiry of streamers array and catch the watch screak. Issue #11
|
||||
bet=BetSettings(
|
||||
strategy=Strategy.SMART, # Choose you strategy!
|
||||
percentage=5, # Place the x% of your channel points
|
||||
percentage_gap=20, # Gap difference between outcomesA and outcomesB (for SMART stragegy)
|
||||
max_points=50000, # If the x percentage of your channel points is gt bet_max_points set this value
|
||||
strategy=Strategy.SMART, # Choose you strategy!
|
||||
percentage=5, # Place the x% of your channel points
|
||||
percentage_gap=20, # Gap difference between outcomesA and outcomesB (for SMART stragegy)
|
||||
max_points=50000, # If the x percentage of your channel points is gt bet_max_points set this value
|
||||
stealth_mode=True, # If the calculated amount of channel points is GT the highest bet, place the highest value minus 1-2 points #33
|
||||
filter_condition=FilterCondition(
|
||||
by=OutcomeKeys.TOTAL_USERS, # Where apply the filter. Allowed [PERCENTAGE_USERS, ODDS_PERCENTAGE, ODDS, TOP_POINTS, TOTAL_USERS, TOTAL_POINTS]
|
||||
where=Condition.LTE, # 'by' must be [GT, LT, GTE, LTE] than value
|
||||
value=800
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# You can customize the settings for each streamer. If not settings was provided the script will use the streamer_settings from TwitchChannelPointsMiner.
|
||||
# If no streamer_settings provided in TwitchChannelPointsMiner the script will use default settings.
|
||||
# You can customize the settings for each streamer. If not settings were provided, the script would use the streamer_settings from TwitchChannelPointsMiner.
|
||||
# If no streamer_settings are provided in TwitchChannelPointsMiner the script will use default settings.
|
||||
# The streamers array can be a String -> username or Streamer instance.
|
||||
|
||||
# The settings priority are: settings in mine function, settings in TwitchChannelPointsMiner instance, default settings.
|
||||
# For example if in the mine function you don't provide any value for 'make_prediction' but you have set it on TwitchChannelPointsMiner instance the script will take the value from here.
|
||||
# For example, if in the mine function you don't provide any value for 'make_prediction' but you have set it on TwitchChannelPointsMiner instance, the script will take the value from here.
|
||||
# If you haven't set any value even in the instance the default one will be used
|
||||
|
||||
twitch_miner.mine(
|
||||
[
|
||||
Streamer("streamer-username01", settings=StreamerSettings(make_predictions=True , follow_raid=False , claim_drops=True , watch_streak=True , bet=BetSettings(strategy=Strategy.SMART , percentage=5 , percentage_gap=20 , max_points=234 ) )),
|
||||
Streamer("streamer-username02", settings=StreamerSettings(make_predictions=False , follow_raid=True , claim_drops=False , bet=BetSettings(strategy=Strategy.PERCENTAGE , percentage=5 , percentage_gap=20 , max_points=1234 ) )),
|
||||
Streamer("streamer-username03", settings=StreamerSettings(make_predictions=True , follow_raid=False , watch_streak=True , bet=BetSettings(strategy=Strategy.SMART , percentage=5 , percentage_gap=30 , max_points=50000 ) )),
|
||||
Streamer("streamer-username04", settings=StreamerSettings(make_predictions=False , follow_raid=True , watch_streak=True )),
|
||||
Streamer("streamer-username05", settings=StreamerSettings(make_predictions=True , follow_raid=True , claim_drops=True , watch_streak=True , bet=BetSettings(strategy=Strategy.HIGH_ODDS , percentage=7 , percentage_gap=20 , max_points=90 ) )),
|
||||
Streamer("streamer-username01", settings=StreamerSettings(make_predictions=True , follow_raid=False , claim_drops=True , watch_streak=True , bet=BetSettings(strategy=Strategy.SMART , percentage=5 , stealth_mode=True, percentage_gap=20 , max_points=234 , filter_condition=FilterCondition(by=OutcomeKeys.TOTAL_USERS, where=Condition.LTE, value=800 ) ) )),
|
||||
Streamer("streamer-username02", settings=StreamerSettings(make_predictions=False , follow_raid=True , claim_drops=False , bet=BetSettings(strategy=Strategy.PERCENTAGE , percentage=5 , stealth_mode=False, percentage_gap=20 , max_points=1234 , filter_condition=FilterCondition(by=OutcomeKeys.TOTAL_POINTS, where=Condition.GTE, value=250 ) ) )),
|
||||
Streamer("streamer-username03", settings=StreamerSettings(make_predictions=True , follow_raid=False , watch_streak=True , bet=BetSettings(strategy=Strategy.SMART , percentage=5 , stealth_mode=False, percentage_gap=30 , max_points=50000 , filter_condition=FilterCondition(by=OutcomeKeys.ODDS, where=Condition.LT, value=300 ) ) )),
|
||||
Streamer("streamer-username04", settings=StreamerSettings(make_predictions=False , follow_raid=True , watch_streak=True )),
|
||||
Streamer("streamer-username05", settings=StreamerSettings(make_predictions=True , follow_raid=True , claim_drops=True , watch_streak=True , bet=BetSettings(strategy=Strategy.HIGH_ODDS , percentage=7 , stealth_mode=True, percentage_gap=20 , max_points=90 , filter_condition=FilterCondition(by=OutcomeKeys.PERCENTAGE_USERS, where=Condition.GTE, value=300 ) ) )),
|
||||
Streamer("streamer-username06"),
|
||||
Streamer("streamer-username07"),
|
||||
Streamer("streamer-username08"),
|
||||
@@ -225,21 +247,63 @@ twitch_miner.mine(["streamer1", "streamer2"]) # Array of strea
|
||||
twitch_miner.mine(followers=True) # Automatic use the followers list OR
|
||||
twitch_miner.mine(["streamer1", "streamer2"], followers=True) # Mixed
|
||||
```
|
||||
If you follow so many streamers on Twitch, but you don't want to mine points for all of them, you can blacklist the users with `blacklist` keyword. [#94](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/94)
|
||||
```python
|
||||
from TwitchChannelPointsMiner import TwitchChannelPointsMiner
|
||||
twitch_miner = TwitchChannelPointsMiner("your-twitch-username")
|
||||
twitch_miner.mine(followers=True, blacklist=["user1", "user2"]) # Automatic use the followers list OR
|
||||
```
|
||||
4. Start mining! `python run.py`
|
||||
|
||||
### Limits
|
||||
> Twitch has a limit - you can't watch more than 2 channels at one time. We take the first two streamers from the list as they have the highest priority.
|
||||
|
||||
Make sure to write the streamers array in order of priority from left to right. If you use `followers=True` Twitch return the streamers order by followed_at. So your last follow have the highest priority.
|
||||
Make sure to write the streamers array in order of priority from left to right. If you use `followers=True` Twitch return the streamers order by followed_at. So your last follow has the highest priority.
|
||||
|
||||
If the browser are currently betting or wait for more data It's impossible to interact with another event prediction from another streamer.
|
||||
## Settings
|
||||
Most of the settings are self-explained and are commented in example.
|
||||
You can watch only two streamers per time. With `priority` settings you can select which streamers watch by use priority. You can use an array of priority or single item. I suggest to use at least one priority from `ORDER`, `POINTS_ASCENDING`, `POINTS_DESCEDING` because for example If you set only `STREAK` after catch all watch streak the script will stop to watch streamers.
|
||||
Available values are the following:
|
||||
- `STREAK` - Catch the watch streak from all streamers
|
||||
- `DROPS` - Claim all drops from streamers with drops tags enabled
|
||||
- `ORDER` - Following the order of the list
|
||||
- `POINTS_ASCENDING` - On top the streamers with the lowest points
|
||||
- `POINTS_DESCEDING` - On top the streamers with the highest points
|
||||
|
||||
### Bet strategy
|
||||
You can combine all priority but keep in mind that use `ORDER` and `POINTS_ASCENDING` in the same settings doesn't make sense.
|
||||
|
||||
### LoggerSettings
|
||||
| Key | Type | Default | Description |
|
||||
|----------------- |----------------- |-------------------------------- |---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `save` | bool | True | If you want to save logs in file (suggested) |
|
||||
| `less` | bool | False | Reduce the logging format and message verbosity [#10](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/10) |
|
||||
| `console_level` | level | logging.INFO | Level of logs in terminal - Use logging.DEBUG for more helpful messages. |
|
||||
| `file_level` | level | logging.DEBUG | Level of logs in file save - If you think the log file it's too big, use logging.INFO |
|
||||
| `emoji` | bool | For Windows is False else True | On Windows, we have a problem printing emoji. Set to false if you have a problem |
|
||||
### StreamerSettings
|
||||
| Key | Type | Default | Description |
|
||||
|-------------------- |------------- |-------------------------------- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `make_predictions` | bool | True | Choose if you want to make predictions / bet or not |
|
||||
| `follow_raid` | bool | True | Choose if you want to follow raid +250 points |
|
||||
| `claim_drops` | bool | True | If this value is True, the script will increase the watch-time for the current game. With this, you are able to claim the drops from Twitch Inventory [#21](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/21) |
|
||||
| `watch_streak` | bool | True | Choose if you want to change a priority for these streamers and try to catch the Watch Streak event [#11](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/11) |
|
||||
| `bet` | BetSettings | | Rules to follow for the bet |
|
||||
### BetSettings
|
||||
| Key | Type | Default | Description |
|
||||
|-------------------- |----------------- |--------- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `strategy` | Strategy | SMART | Choose your strategy! See above for more info |
|
||||
| `percentage` | int | 5 | Place the x% of your channel points |
|
||||
| `percentage_gap` | int | 20 | Gap difference between outcomesA and outcomesB (for SMART stragegy) |
|
||||
| `max_points` | int | 50000 | If the x percentage of your channel points is GT bet_max_points set this value |
|
||||
| `stealth_mode` | bool | False | If the calculated amount of channel points is GT the highest bet, place the highest value minus 1-2 points [#33](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/33) |
|
||||
| `filter_condition` | FilterCondition | None | Based on this filter the script will skip some bet [#29](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/29) |
|
||||
|
||||
#### Bet strategy
|
||||
|
||||
- **MOST_VOTED**: Select the option most voted based on users count
|
||||
- **HIGH_ODDS**: Select the option with the highest odds
|
||||
- **PERCENTAGE**: Select the option with the highest percentage based on odds (It's the same that show Twitch) - Should be the same of select LOWEST_ODDS
|
||||
- **SMART**: If the majority in percent chose an option then follow the other users, otherwise choose the option with the highest odds
|
||||
- **PERCENTAGE**: Select the option with the highest percentage based on odds (It's the same that show Twitch) - Should be the same as select LOWEST_ODDS
|
||||
- **SMART**: If the majority in percent chose an option, then follow the other users, otherwise choose the option with the highest odds
|
||||
|
||||

|
||||
|
||||
@@ -248,11 +312,38 @@ Here a concrete example:
|
||||
- **MOST_VOTED**: 21 Users have select **'over 7.5'**, instead of 9 'under 7.5'
|
||||
- **HIGH_ODDS**: The highest odd is 2.27 on **'over 7.5'** vs 1.79 on 'under 7.5'
|
||||
- **PERCENTAGE**: The highest percentage is 56% for **'under 7.5'**
|
||||
- **SMART**: Calculate the percentage based on the users. The percentage are: 'over 7.5': 70% and 'under 7.5': 30%. If the difference between the two percatage are highter thant `percentage_gap` select the highest percentage, else the highest odds.
|
||||
In this case if percentage_gap = 20 ; 70-30 = 40 > percentage_gap, so the bot will select 'over 7.5'
|
||||
- **SMART**: Calculate the percentage based on the users. The percentages are: 'over 7.5': 70% and 'under 7.5': 30%. If the difference between the two percentages are higher than `percentage_gap` select the highest percentage, else the highest odds.
|
||||
|
||||
## Migrating from old repository (the original one):
|
||||
If you already have a `twitch-cookies.pkl` and you don't want to login again please create a `cookies/` folder in the current directory and then copy the .pkl file with a new name `your-twitch-username.pkl`
|
||||
In this case if percentage_gap = 20 ; 70-30 = 40 > percentage_gap, so the bot will select 'over 7.5'
|
||||
### FilterCondition
|
||||
| Key | Type | Default | Description |
|
||||
|------------- |------------- |--------- |---------------------------------------------------------------------------------- |
|
||||
| `by` | OutcomeKeys | None | Key to apply the filter |
|
||||
| `where` | Condition | None | Condition that should match for place bet |
|
||||
| `value` | number | None | Value to compare |
|
||||
|
||||
Allowed values for `by` are:
|
||||
- `PERCENTAGE_USERS` (no sum) [Would never want a sum as it'd always be 100%]
|
||||
- `ODDS_PERCENTAGE` (no sum) [Doesn't make sense to sum odds]
|
||||
- `ODDS` (no sum) [Doesn't make sense to sum odds]
|
||||
- `DECISION_USERS` (no sum)
|
||||
- `DECISION_POINTS` (no sum)
|
||||
- `TOP_POINTS` (no sum) [Doesn't make sense to the top points of both sides]
|
||||
- `TOTAL_USERS` (sum)
|
||||
- `TOTAL_POINTS` (sum)
|
||||
|
||||
Allowed values for `where` are: `GT, LT, GTE, LTE`
|
||||
|
||||
#### Example
|
||||
- If you want to place the bet ONLY if the total of users participants in the bet is greater than 200
|
||||
`FilterCondition(by=OutcomeKeys.TOTAL_USERS, where=Condition.GT, value=200)`
|
||||
- If you want to place the bet ONLY if the winning odd of your decision is greater than or equal to 1.3
|
||||
`FilterCondition(by=OutcomeKeys.ODDS, where=Condition.GTE, value=1.3)`
|
||||
- If you want to place the bet ONLY if the highest bet is lower than 2000
|
||||
`FilterCondition(by=OutcomeKeys.TOP_POINTS, where=Condition.LT, value=2000)`
|
||||
|
||||
## Migrating from an old repository (the original one):
|
||||
If you already have a `twitch-cookies.pkl` and you don't want to login again, please create a `cookies/` folder in the current directory and then copy the .pkl file with a new name `your-twitch-username.pkl`
|
||||
```
|
||||
.
|
||||
+-- run.py
|
||||
@@ -264,32 +355,12 @@ If you already have a `twitch-cookies.pkl` and you don't want to login again ple
|
||||
Other users have find multiple problems on Windows my suggestion are:
|
||||
- Stop use Windows :stuck_out_tongue_closed_eyes:
|
||||
- Suppress the emoji in logs with `logger_settings=LoggerSettings(emoji=False)`
|
||||
- Download the geckodriver from here: https://github.com/mozilla/geckodriver/releases/ and extract in the same folder of this project. For other issue with geckodriver just googling: https://stackoverflow.com/questions/40208051/selenium-using-python-geckodriver-executable-needs-to-be-in-path
|
||||
|
||||
Other usefully infos can be founded here: https://github.com/gottagofaster236/Twitch-Channel-Points-Miner/issues/31
|
||||
Other useful info can be founded here:
|
||||
- https://github.com/gottagofaster236/Twitch-Channel-Points-Miner/issues/31
|
||||
- https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/55
|
||||
|
||||
## Use Chrome instead Firefox
|
||||
If you prefer Chrome instead Firefox please download the WebDriver matching with your Chrome version and OS from this link: https://chromedriver.chromium.org/downloads.
|
||||
Extract the archivie, copy the chromedriver file in this project folder.
|
||||
Edit your run.py file and the browser_settings should something like this:
|
||||
```python
|
||||
browser_settings=BrowserSettings(
|
||||
browser=Browser.CHROME,
|
||||
driver_path="/path/of/your/chromedriver" # If no path was provided the script will try to search automatically
|
||||
),
|
||||
```
|
||||
|
||||
## Issue / Debug
|
||||
When you open a new issue please use the correct template.
|
||||
Please provide at least the following information/files:
|
||||
- Browser (if you have the prediction feature enabled)
|
||||
- Operation System
|
||||
- Python Version
|
||||
- logs/ `LoggerSettings(file_level=logging.DEBUG)`
|
||||
- htmls/ `BrowserSettings(save_html=True)`
|
||||
- screenshots/ `BrowserSettings(do_screenshot=True)`
|
||||
|
||||
Make sure also to have the latest commit.
|
||||
You can also follow this [video tutorial](https://www.youtube.com/watch?v=0VkM7NOZkuA).
|
||||
|
||||
## Disclaimer
|
||||
This project comes with no gurantee or warranty. You are responsible for whatever happens from using this project. It is possible to get soft or hard banned by using this project if you are not careful. This is a personal project and is in no way affiliated with Twitch.
|
||||
This project comes with no guarantee or warranty. You are responsible for whatever happens from using this project. It is possible to get soft or hard banned by using this project if you are not careful. This is a personal project and is in no way affiliated with Twitch.
|
||||
|
||||
@@ -19,40 +19,58 @@ from TwitchChannelPointsMiner.classes.entities.Streamer import (
|
||||
StreamerSettings,
|
||||
)
|
||||
from TwitchChannelPointsMiner.classes.Exceptions import StreamerDoesNotExistException
|
||||
from TwitchChannelPointsMiner.classes.Settings import Settings
|
||||
from TwitchChannelPointsMiner.classes.Settings import Priority, Settings
|
||||
from TwitchChannelPointsMiner.classes.Twitch import Twitch
|
||||
from TwitchChannelPointsMiner.classes.TwitchBrowser import (
|
||||
BrowserSettings,
|
||||
TwitchBrowser,
|
||||
)
|
||||
from TwitchChannelPointsMiner.classes.WebSocketsPool import WebSocketsPool
|
||||
from TwitchChannelPointsMiner.logger import LoggerSettings, configure_loggers
|
||||
from TwitchChannelPointsMiner.utils import (
|
||||
_millify,
|
||||
at_least_one_value_in_settings_is,
|
||||
get_user_agent,
|
||||
internet_connection_available,
|
||||
set_default_settings,
|
||||
)
|
||||
|
||||
# Suppress warning for urllib3.connectionpool (selenium close connection)
|
||||
# Suppress Selenium and Flask (werkzeug) logs
|
||||
logging.getLogger("urllib3").setLevel(logging.ERROR)
|
||||
logging.getLogger("selenium").setLevel(logging.ERROR)
|
||||
# Suppress:
|
||||
# - chardet.charsetprober - [feed]
|
||||
# - chardet.charsetprober - [get_confidence]
|
||||
# - requests - [Starting new HTTPS connection (1)]
|
||||
# - Flask (werkzeug) logs
|
||||
logging.getLogger("chardet.charsetprober").setLevel(logging.ERROR)
|
||||
logging.getLogger("requests").setLevel(logging.ERROR)
|
||||
logging.getLogger("werkzeug").setLevel(logging.ERROR)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TwitchChannelPointsMiner(object):
|
||||
class TwitchChannelPointsMiner:
|
||||
__slots__ = [
|
||||
"username",
|
||||
"twitch",
|
||||
"claim_drops_startup",
|
||||
"priority",
|
||||
"streamers",
|
||||
"events_predictions",
|
||||
"minute_watcher_thread",
|
||||
"sync_campaigns_thread",
|
||||
"ws_pool",
|
||||
"session_id",
|
||||
"running",
|
||||
"start_datetime",
|
||||
"original_streamers",
|
||||
"logs_file",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
username: str,
|
||||
password: str = None,
|
||||
claim_drops_startup: bool = False,
|
||||
analytics: bool = True,
|
||||
# Settings for logging and selenium as you can see.
|
||||
priority: list = [Priority.STREAK, Priority.DROPS, Priority.ORDER],
|
||||
# This settings will be global shared trought Settings class
|
||||
logger_settings: LoggerSettings = LoggerSettings(),
|
||||
browser_settings: BrowserSettings = BrowserSettings(),
|
||||
# Default values for all streamers
|
||||
streamer_settings: StreamerSettings = StreamerSettings(),
|
||||
):
|
||||
@@ -61,23 +79,24 @@ class TwitchChannelPointsMiner(object):
|
||||
|
||||
self.username = username
|
||||
|
||||
# Set as globally config
|
||||
# Set as global config
|
||||
Settings.logger = logger_settings
|
||||
Settings.browser = browser_settings
|
||||
|
||||
# Init as default all the missing values
|
||||
streamer_settings.default()
|
||||
streamer_settings.bet.default()
|
||||
Settings.streamer_settings = streamer_settings
|
||||
|
||||
user_agent = get_user_agent(browser_settings.browser)
|
||||
self.twitch = Twitch(self.username, user_agent)
|
||||
user_agent = get_user_agent("FIREFOX")
|
||||
self.twitch = Twitch(self.username, user_agent, password)
|
||||
|
||||
self.twitch_browser = None
|
||||
self.claim_drops_startup = claim_drops_startup
|
||||
self.priority = priority if isinstance(priority, list) else [priority]
|
||||
|
||||
self.streamers = []
|
||||
self.events_predictions = {}
|
||||
self.minute_watcher_thread = None
|
||||
self.sync_campaigns_thread = None
|
||||
self.ws_pool = None
|
||||
|
||||
self.session_id = str(uuid.uuid4())
|
||||
@@ -94,10 +113,10 @@ class TwitchChannelPointsMiner(object):
|
||||
for sign in [signal.SIGINT, signal.SIGSEGV, signal.SIGTERM]:
|
||||
signal.signal(sign, self.end)
|
||||
|
||||
def mine(self, streamers: list = [], followers=False):
|
||||
self.run(streamers, followers)
|
||||
def mine(self, streamers: list = [], blacklist: list = [], followers=False):
|
||||
self.run(streamers=streamers, blacklist=blacklist, followers=followers)
|
||||
|
||||
def run(self, streamers: list = [], followers=False):
|
||||
def run(self, streamers: list = [], blacklist: list = [], followers=False):
|
||||
if self.running:
|
||||
logger.error("You can't start multiple sessions of this instance!")
|
||||
else:
|
||||
@@ -121,8 +140,9 @@ class TwitchChannelPointsMiner(object):
|
||||
if isinstance(streamer, Streamer)
|
||||
else streamer.lower().strip()
|
||||
)
|
||||
streamers_name.append(username)
|
||||
streamers_dict[username] = streamer
|
||||
if username not in blacklist:
|
||||
streamers_name.append(username)
|
||||
streamers_dict[username] = streamer
|
||||
|
||||
if followers is True:
|
||||
followers_array = self.twitch.get_followers()
|
||||
@@ -131,7 +151,7 @@ class TwitchChannelPointsMiner(object):
|
||||
extra={"emoji": ":clipboard:"},
|
||||
)
|
||||
for username in followers_array:
|
||||
if username not in streamers_dict:
|
||||
if username not in streamers_dict and username not in blacklist:
|
||||
streamers_dict[username] = username.lower().strip()
|
||||
else:
|
||||
followers_array = []
|
||||
@@ -147,12 +167,11 @@ class TwitchChannelPointsMiner(object):
|
||||
for username in streamers_name:
|
||||
time.sleep(random.uniform(0.3, 0.7))
|
||||
try:
|
||||
|
||||
if isinstance(streamers_dict[username], Streamer) is True:
|
||||
streamer = streamers_dict[username]
|
||||
else:
|
||||
streamer = Streamer(username)
|
||||
|
||||
streamer = (
|
||||
streamers_dict[username]
|
||||
if isinstance(streamers_dict[username], Streamer) is True
|
||||
else Streamer(username)
|
||||
)
|
||||
streamer.channel_id = self.twitch.get_channel_id(username)
|
||||
streamer.settings = set_default_settings(
|
||||
streamer.settings, Settings.streamer_settings
|
||||
@@ -160,7 +179,6 @@ class TwitchChannelPointsMiner(object):
|
||||
streamer.settings.bet = set_default_settings(
|
||||
streamer.settings.bet, Settings.streamer_settings.bet
|
||||
)
|
||||
|
||||
self.streamers.append(streamer)
|
||||
except StreamerDoesNotExistException:
|
||||
logger.info(
|
||||
@@ -170,7 +188,7 @@ class TwitchChannelPointsMiner(object):
|
||||
|
||||
# Populate the streamers with default values.
|
||||
# 1. Load channel points and auto-claim bonus
|
||||
# 2. Check if streamers is online
|
||||
# 2. Check if streamers are online
|
||||
# 3. Check if the user is a Streamer. In thi case you can't do prediction
|
||||
for streamer in self.streamers:
|
||||
time.sleep(random.uniform(0.3, 0.7))
|
||||
@@ -188,29 +206,30 @@ class TwitchChannelPointsMiner(object):
|
||||
make_predictions = at_least_one_value_in_settings_is(
|
||||
self.streamers, "make_predictions", True
|
||||
)
|
||||
# We need a browser to make predictions / bet
|
||||
if make_predictions is True:
|
||||
self.twitch_browser = TwitchBrowser(
|
||||
self.twitch.twitch_login.get_auth_token(),
|
||||
self.session_id,
|
||||
settings=Settings.browser,
|
||||
|
||||
# If we have at least one streamer with settings = claim_drops True
|
||||
# Spawn a thread for sync inventory and dashboard
|
||||
if (
|
||||
at_least_one_value_in_settings_is(self.streamers, "claim_drops", True)
|
||||
is True
|
||||
):
|
||||
self.sync_campaigns_thread = threading.Thread(
|
||||
target=self.twitch.sync_campaigns,
|
||||
args=(self.streamers,),
|
||||
)
|
||||
self.twitch_browser.init()
|
||||
self.sync_campaigns_thread.name = "Sync campaigns/inventory"
|
||||
self.sync_campaigns_thread.start()
|
||||
time.sleep(30)
|
||||
|
||||
self.minute_watcher_thread = threading.Thread(
|
||||
target=self.twitch.send_minute_watched_events,
|
||||
args=(
|
||||
self.streamers,
|
||||
at_least_one_value_in_settings_is(
|
||||
self.streamers, "watch_streak", True
|
||||
),
|
||||
),
|
||||
args=(self.streamers, self.priority),
|
||||
)
|
||||
self.minute_watcher_thread.name = "Minute watcher"
|
||||
self.minute_watcher_thread.start()
|
||||
|
||||
self.ws_pool = WebSocketsPool(
|
||||
twitch=self.twitch,
|
||||
browser=self.twitch_browser,
|
||||
streamers=self.streamers,
|
||||
events_predictions=self.events_predictions,
|
||||
)
|
||||
@@ -223,19 +242,6 @@ class TwitchChannelPointsMiner(object):
|
||||
)
|
||||
)
|
||||
|
||||
# If we have at least one streamer with settings = claim_drops True
|
||||
# Going to subscribe to user-drop-events. Get update for drop-progress
|
||||
claim_drops = at_least_one_value_in_settings_is(
|
||||
self.streamers, "claim_drops", True
|
||||
)
|
||||
if claim_drops is True:
|
||||
self.ws_pool.submit(
|
||||
PubsubTopic(
|
||||
"user-drop-events",
|
||||
user_id=self.twitch.twitch_login.get_user_id(),
|
||||
)
|
||||
)
|
||||
|
||||
# Going to subscribe to predictions-user-v1. Get update when we place a new prediction (confirm)
|
||||
if make_predictions is True:
|
||||
self.ws_pool.submit(
|
||||
@@ -261,22 +267,29 @@ class TwitchChannelPointsMiner(object):
|
||||
while self.running:
|
||||
time.sleep(random.uniform(20, 60))
|
||||
# Do an external control for WebSocket. Check if the thread is running
|
||||
if self.ws_pool.ws.elapsed_last_ping() > 5:
|
||||
logger.info(
|
||||
"The last ping was sent more than 5 minutes ago. Reconnecting to the WebSocket..."
|
||||
)
|
||||
WebSocketsPool.handle_websocket_reconnection(self.ws_pool.ws)
|
||||
# Check if is not None because maybe we have already created a new connection on array+1 and now index is None
|
||||
for index in range(0, len(self.ws_pool.ws)):
|
||||
if (
|
||||
self.ws_pool.ws[index].is_reconneting is False
|
||||
and self.ws_pool.ws[index].elapsed_last_ping() > 10
|
||||
and internet_connection_available() is True
|
||||
):
|
||||
logger.info(
|
||||
f"#{index} - The last PING was sent more than 10 minutes ago. Reconnecting to the WebSocket..."
|
||||
)
|
||||
WebSocketsPool.handle_reconnection(self.ws_pool.ws[index])
|
||||
|
||||
def end(self, signum, frame):
|
||||
logger.info("CTRL+C Detected! Please wait just a moments!")
|
||||
|
||||
if self.twitch_browser is not None:
|
||||
self.twitch_browser.browser.quit()
|
||||
logger.info("CTRL+C Detected! Please wait just a moment!")
|
||||
|
||||
self.running = self.twitch.running = False
|
||||
self.ws_pool.end()
|
||||
|
||||
self.minute_watcher_thread.join()
|
||||
if self.minute_watcher_thread is not None:
|
||||
self.minute_watcher_thread.join()
|
||||
|
||||
if self.sync_campaigns_thread is not None:
|
||||
self.sync_campaigns_thread.join()
|
||||
|
||||
# Check if all the mutex are unlocked.
|
||||
# Prevent breaks of .json file
|
||||
@@ -286,7 +299,6 @@ class TwitchChannelPointsMiner(object):
|
||||
streamer.mutex.release()
|
||||
|
||||
self.__print_report()
|
||||
time.sleep(3.5) # Do sleep for ending browser and threads
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
@@ -304,29 +316,41 @@ class TwitchChannelPointsMiner(object):
|
||||
extra={"emoji": ":hourglass:"},
|
||||
)
|
||||
|
||||
for event_id in self.events_predictions:
|
||||
if (
|
||||
self.events_predictions[event_id].bet_confirmed is True
|
||||
and self.events_predictions[event_id].streamer.settings.make_predictions
|
||||
is True
|
||||
):
|
||||
logger.info(
|
||||
f"{self.events_predictions[event_id].streamer.settings.bet}",
|
||||
extra={"emoji": ":bar_chart:"},
|
||||
)
|
||||
logger.info(
|
||||
f"{self.events_predictions[event_id].print_recap()}",
|
||||
extra={"emoji": ":bar_chart:"},
|
||||
)
|
||||
print("")
|
||||
if self.events_predictions != {}:
|
||||
print("")
|
||||
for event_id in self.events_predictions:
|
||||
event = self.events_predictions[event_id]
|
||||
if (
|
||||
event.bet_confirmed is True
|
||||
and event.streamer.settings.make_predictions is True
|
||||
):
|
||||
logger.info(
|
||||
f"{event.streamer.settings.bet}",
|
||||
extra={"emoji": ":wrench:"},
|
||||
)
|
||||
if event.streamer.settings.bet.filter_condition is not None:
|
||||
logger.info(
|
||||
f"{event.streamer.settings.bet.filter_condition}",
|
||||
extra={"emoji": ":pushpin:"},
|
||||
)
|
||||
logger.info(
|
||||
f"{event.print_recap()}",
|
||||
extra={"emoji": ":bar_chart:"},
|
||||
)
|
||||
|
||||
print("")
|
||||
for streamer_index in range(0, len(self.streamers)):
|
||||
logger.info(
|
||||
f"{repr(self.streamers[streamer_index])}, Total Points Gained (after farming - before farming): {_millify(self.streamers[streamer_index].channel_points - self.original_streamers[streamer_index])}",
|
||||
extra={"emoji": ":robot:"},
|
||||
)
|
||||
if self.streamers[streamer_index].history != {}:
|
||||
logger.info(
|
||||
f"{self.streamers[streamer_index].print_history()}",
|
||||
extra={"emoji": ":moneybag:"},
|
||||
gained = (
|
||||
self.streamers[streamer_index].channel_points
|
||||
- self.original_streamers[streamer_index].channel_points
|
||||
)
|
||||
logger.info(
|
||||
f"{repr(self.streamers[streamer_index])}, Total Points Gained (after farming - before farming): {_millify(gained)}",
|
||||
extra={"emoji": ":robot:"},
|
||||
)
|
||||
if self.streamers[streamer_index].history != {}:
|
||||
logger.info(
|
||||
f"{self.streamers[streamer_index].print_history()}",
|
||||
extra={"emoji": ":moneybag:"},
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
__version__ = "2.5.0"
|
||||
__version__ = "2.7.1"
|
||||
from .TwitchChannelPointsMiner import TwitchChannelPointsMiner
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -8,7 +8,3 @@ class StreamerIsOfflineException(Exception):
|
||||
|
||||
class WrongCookiesException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TimeBasedDropNotFound(Exception):
|
||||
pass
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
from enum import Enum, auto
|
||||
|
||||
|
||||
class Priority(Enum):
|
||||
ORDER = auto()
|
||||
STREAK = auto()
|
||||
DROPS = auto()
|
||||
POINTS_ASCENDING = auto()
|
||||
POINTS_DESCEDING = auto()
|
||||
|
||||
|
||||
# Empty object shared between class
|
||||
class Settings(object):
|
||||
pass
|
||||
__slots__ = ["logger", "streamer_settings"]
|
||||
|
||||
@@ -11,28 +11,35 @@ import random
|
||||
import re
|
||||
import time
|
||||
from pathlib import Path
|
||||
from secrets import token_hex
|
||||
|
||||
import requests
|
||||
|
||||
from TwitchChannelPointsMiner.classes.entities.Campaign import Campaign
|
||||
from TwitchChannelPointsMiner.classes.entities.Drop import Drop
|
||||
from TwitchChannelPointsMiner.classes.Exceptions import (
|
||||
StreamerDoesNotExistException,
|
||||
StreamerIsOfflineException,
|
||||
TimeBasedDropNotFound,
|
||||
)
|
||||
from TwitchChannelPointsMiner.classes.Settings import Settings
|
||||
from TwitchChannelPointsMiner.classes.Settings import Priority, Settings
|
||||
from TwitchChannelPointsMiner.classes.TwitchLogin import TwitchLogin
|
||||
from TwitchChannelPointsMiner.constants.twitch import API, CLIENT_ID, GQLOperations
|
||||
from TwitchChannelPointsMiner.constants import API, CLIENT_ID, GQLOperations
|
||||
from TwitchChannelPointsMiner.utils import _millify, internet_connection_available
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Twitch:
|
||||
def __init__(self, username, user_agent):
|
||||
class Twitch(object):
|
||||
__slots__ = ["cookies_file", "user_agent", "twitch_login", "running"]
|
||||
|
||||
def __init__(self, username, user_agent, password=None):
|
||||
cookies_path = os.path.join(Path().absolute(), "cookies")
|
||||
Path(cookies_path).mkdir(parents=True, exist_ok=True)
|
||||
self.cookies_file = os.path.join(cookies_path, f"{username}.pkl")
|
||||
self.user_agent = user_agent
|
||||
self.twitch_login = TwitchLogin(CLIENT_ID, username, self.user_agent)
|
||||
self.twitch_login = TwitchLogin(
|
||||
CLIENT_ID, username, self.user_agent, password=password
|
||||
)
|
||||
self.running = True
|
||||
|
||||
def login(self):
|
||||
@@ -43,79 +50,75 @@ class Twitch:
|
||||
self.twitch_login.load_cookies(self.cookies_file)
|
||||
self.twitch_login.set_token(self.twitch_login.get_auth_token())
|
||||
|
||||
# === STREAMER / STREAM / INFO === #
|
||||
def update_stream(self, streamer):
|
||||
if streamer.stream.update_required() is True:
|
||||
stream_info = self.get_stream_info(streamer)
|
||||
streamer.stream.update(
|
||||
broadcast_id=stream_info["stream"]["id"],
|
||||
title=stream_info["broadcastSettings"]["title"],
|
||||
game=stream_info["broadcastSettings"]["game"],
|
||||
tags=stream_info["stream"]["tags"],
|
||||
viewers_count=stream_info["stream"]["viewersCount"],
|
||||
)
|
||||
if stream_info is not None:
|
||||
streamer.stream.update(
|
||||
broadcast_id=stream_info["stream"]["id"],
|
||||
title=stream_info["broadcastSettings"]["title"],
|
||||
game=stream_info["broadcastSettings"]["game"],
|
||||
tags=stream_info["stream"]["tags"],
|
||||
viewers_count=stream_info["stream"]["viewersCount"],
|
||||
)
|
||||
|
||||
event_properties = {
|
||||
"channel_id": streamer.channel_id,
|
||||
"broadcast_id": streamer.stream.broadcast_id,
|
||||
"player": "site",
|
||||
"user_id": self.twitch_login.get_user_id(),
|
||||
}
|
||||
event_properties = {
|
||||
"channel_id": streamer.channel_id,
|
||||
"broadcast_id": streamer.stream.broadcast_id,
|
||||
"player": "site",
|
||||
"user_id": self.twitch_login.get_user_id(),
|
||||
}
|
||||
|
||||
if (
|
||||
streamer.stream.game_name() is not None
|
||||
and streamer.settings.claim_drops is True
|
||||
):
|
||||
event_properties["game"] = streamer.stream.game_name()
|
||||
if (
|
||||
streamer.stream.game_name() is not None
|
||||
and streamer.settings.claim_drops is True
|
||||
):
|
||||
event_properties["game"] = streamer.stream.game_name()
|
||||
# Update also the campaigns_ids so we are sure to tracking the correct campaign
|
||||
streamer.stream.campaigns_ids = (
|
||||
self.__get_campaign_ids_from_streamer(streamer)
|
||||
)
|
||||
|
||||
streamer.stream.payload = [
|
||||
{"event": "minute-watched", "properties": event_properties}
|
||||
]
|
||||
streamer.stream.payload = [
|
||||
{"event": "minute-watched", "properties": event_properties}
|
||||
]
|
||||
|
||||
def get_spade_url(self, streamer):
|
||||
headers = {"User-Agent": self.user_agent}
|
||||
main_page_request = requests.get(streamer.streamer_url, headers=headers)
|
||||
response = main_page_request.text
|
||||
settings_url = re.search(
|
||||
"(https://static.twitchcdn.net/config/settings.*?js)", response
|
||||
).group(1)
|
||||
try:
|
||||
headers = {"User-Agent": self.user_agent}
|
||||
main_page_request = requests.get(streamer.streamer_url, headers=headers)
|
||||
response = main_page_request.text
|
||||
regex_settings = "(https://static.twitchcdn.net/config/settings.*?js)"
|
||||
settings_url = re.search(regex_settings, response).group(1)
|
||||
|
||||
settings_request = requests.get(settings_url, headers=headers)
|
||||
response = settings_request.text
|
||||
streamer.stream.spade_url = re.search('"spade_url":"(.*?)"', response).group(1)
|
||||
|
||||
def post_gql_request(self, json_data):
|
||||
response = requests.post(
|
||||
GQLOperations.url,
|
||||
json=json_data,
|
||||
headers={
|
||||
"Authorization": f"OAuth {self.twitch_login.get_auth_token()}",
|
||||
"Client-Id": CLIENT_ID,
|
||||
"User-Agent": self.user_agent,
|
||||
},
|
||||
)
|
||||
logger.debug(
|
||||
f"Data: {json_data}, Status code: {response.status_code}, Content: {response.json()}"
|
||||
)
|
||||
return response.json()
|
||||
settings_request = requests.get(settings_url, headers=headers)
|
||||
response = settings_request.text
|
||||
regex_spade = '"spade_url":"(.*?)"'
|
||||
streamer.stream.spade_url = re.search(regex_spade, response).group(1)
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"Something went wrong during extraction of 'spade_url': {e}")
|
||||
|
||||
def get_broadcast_id(self, streamer):
|
||||
json_data = copy.deepcopy(GQLOperations.WithIsStreamLiveQuery)
|
||||
json_data["variables"] = {"id": streamer.channel_id}
|
||||
response = self.post_gql_request(json_data)
|
||||
stream = response["data"]["user"]["stream"]
|
||||
if stream is not None:
|
||||
return stream["id"]
|
||||
else:
|
||||
raise StreamerIsOfflineException
|
||||
if response != {}:
|
||||
stream = response["data"]["user"]["stream"]
|
||||
if stream is not None:
|
||||
return stream["id"]
|
||||
else:
|
||||
raise StreamerIsOfflineException
|
||||
|
||||
def get_stream_info(self, streamer):
|
||||
json_data = copy.deepcopy(GQLOperations.VideoPlayerStreamInfoOverlayChannel)
|
||||
json_data["variables"] = {"channel": streamer.username}
|
||||
response = self.post_gql_request(json_data)
|
||||
if response["data"]["user"]["stream"] is None:
|
||||
raise StreamerIsOfflineException
|
||||
else:
|
||||
return response["data"]["user"]
|
||||
if response != {}:
|
||||
if response["data"]["user"]["stream"] is None:
|
||||
raise StreamerIsOfflineException
|
||||
else:
|
||||
return response["data"]["user"]
|
||||
|
||||
def check_streamer_online(self, streamer):
|
||||
if time.time() < streamer.offline_at + 60:
|
||||
@@ -135,166 +138,16 @@ class Twitch:
|
||||
except StreamerIsOfflineException:
|
||||
streamer.set_offline()
|
||||
|
||||
def claim_bonus(self, streamer, claim_id):
|
||||
if Settings.logger.less is False:
|
||||
logger.info(
|
||||
f"Claiming the bonus for {streamer}!", extra={"emoji": ":gift:"}
|
||||
)
|
||||
|
||||
json_data = copy.deepcopy(GQLOperations.ClaimCommunityPoints)
|
||||
json_data["variables"] = {
|
||||
"input": {"channelID": streamer.channel_id, "claimID": claim_id}
|
||||
}
|
||||
self.post_gql_request(json_data)
|
||||
|
||||
def claim_drop(self, drop_instance_id, streamer=None):
|
||||
if streamer is not None:
|
||||
logger.info(
|
||||
f"Claiming the drop for {streamer}!", extra={"emoji": ":package:"}
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"Startup claim drop {drop_instance_id}", extra={"emoji": ":package:"}
|
||||
)
|
||||
|
||||
json_data = copy.deepcopy(GQLOperations.DropsPage_ClaimDropRewards)
|
||||
json_data["variables"] = {"input": {"dropInstanceID": drop_instance_id}}
|
||||
self.post_gql_request(json_data)
|
||||
|
||||
def search_drop_in_inventory(self, streamer, drop_id):
|
||||
inventory = self.__get_inventory()
|
||||
for campaign in inventory["dropCampaignsInProgress"]:
|
||||
for drop in campaign["timeBasedDrops"]:
|
||||
if drop["id"] == drop_id:
|
||||
return drop["self"]
|
||||
raise TimeBasedDropNotFound
|
||||
|
||||
def claim_all_drops_from_inventory(self):
|
||||
inventory = self.__get_inventory()
|
||||
for campaign in inventory["dropCampaignsInProgress"]:
|
||||
for drop in campaign["timeBasedDrops"]:
|
||||
if drop["self"]["dropInstanceID"] is not None:
|
||||
self.claim_drop(drop["self"]["dropInstanceID"])
|
||||
time.sleep(random.uniform(10, 30))
|
||||
|
||||
def __get_inventory(self):
|
||||
response = self.post_gql_request(GQLOperations.Inventory)
|
||||
return response["data"]["currentUser"]["inventory"]
|
||||
|
||||
# Load the amount of current points for a channel, check if a bonus is available
|
||||
def load_channel_points_context(self, streamer):
|
||||
json_data = copy.deepcopy(GQLOperations.ChannelPointsContext)
|
||||
json_data["variables"] = {"channelLogin": streamer.username}
|
||||
|
||||
response = self.post_gql_request(json_data)
|
||||
if response["data"]["community"] is None:
|
||||
raise StreamerDoesNotExistException
|
||||
channel = response["data"]["community"]["channel"]
|
||||
community_points = channel["self"]["communityPoints"]
|
||||
streamer.channel_points = community_points["balance"]
|
||||
|
||||
if community_points["availableClaim"] is not None:
|
||||
self.claim_bonus(streamer, community_points["availableClaim"]["id"])
|
||||
|
||||
def make_predictions(self, event):
|
||||
decision = event.bet.calculate(event.streamer.channel_points)
|
||||
json_data = copy.deepcopy(GQLOperations.MakePrediction)
|
||||
json_data["variables"] = {
|
||||
"input": {
|
||||
"eventID": event.event_id,
|
||||
"outcomeID": decision["id"],
|
||||
"points": decision["amount"],
|
||||
"transactionID": "412118d3********79ac856", # How we can calculate this?
|
||||
}
|
||||
}
|
||||
return self.post_gql_request(json_data)
|
||||
|
||||
def send_minute_watched_events(self, streamers, watch_streak=False, chunk_size=3):
|
||||
while self.running:
|
||||
streamers_index = [
|
||||
i
|
||||
for i in range(0, len(streamers))
|
||||
if streamers[i].is_online
|
||||
and (
|
||||
streamers[i].online_at == 0
|
||||
or (time.time() - streamers[i].online_at) > 30
|
||||
)
|
||||
]
|
||||
|
||||
"""
|
||||
Check if we need need to change priority based on watch streak
|
||||
Viewers receive points for returning for x consecutive streams.
|
||||
Each stream must be at least 10 minutes long and it must have been at least 30 minutes since the last stream ended.
|
||||
|
||||
Watch at least 6m for get the +10
|
||||
"""
|
||||
streamers_watching = []
|
||||
if watch_streak is True:
|
||||
for index in streamers_index:
|
||||
if (
|
||||
streamers[index].settings.watch_streak is True
|
||||
and streamers[index].stream.watch_streak_missing is True
|
||||
and (
|
||||
streamers[index].offline_at == 0
|
||||
or ((time.time() - streamers[index].offline_at) // 60) > 30
|
||||
)
|
||||
and streamers[index].stream.minute_watched < 7
|
||||
):
|
||||
logger.debug(
|
||||
f"Switch priority: {streamers[index]}, WatchStreak missing is {streamers[index].stream.watch_streak_missing} and minute_watched: {round(streamers[index].stream.minute_watched, 2)}"
|
||||
)
|
||||
streamers_watching.append(index)
|
||||
if len(streamers_watching) == 2:
|
||||
break
|
||||
|
||||
if streamers_watching == []:
|
||||
streamers_watching = streamers_index
|
||||
else:
|
||||
while len(streamers_watching) < 2 and len(streamers_index) > 1:
|
||||
another_streamer_index = streamers_index.pop(0)
|
||||
if another_streamer_index not in streamers_watching:
|
||||
streamers_watching.append(another_streamer_index)
|
||||
|
||||
"""
|
||||
Twitch has a limit - you can't watch more than 2 channels at one time.
|
||||
We take the first two streamers from the list as they have the highest priority (based on order or WatchStreak).
|
||||
"""
|
||||
streamers_watching = streamers_watching[:2]
|
||||
|
||||
for index in streamers_watching:
|
||||
next_iteration = time.time() + 60 / len(streamers_watching)
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
streamers[index].stream.spade_url,
|
||||
data=streamers[index].stream.encode_payload(),
|
||||
headers={"User-Agent": self.user_agent},
|
||||
)
|
||||
logger.debug(
|
||||
f"Send minute watched request for {streamers[index]} - Status code: {response.status_code}"
|
||||
)
|
||||
if response.status_code == 204:
|
||||
streamers[index].stream.update_minute_watched()
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
logger.error(f"Error while trying to watch a minute: {e}")
|
||||
|
||||
# Create chunk of sleep of speed-up the break loop after CTRL+C
|
||||
sleep_time = max(next_iteration - time.time(), 0) / chunk_size
|
||||
for i in range(0, chunk_size):
|
||||
time.sleep(sleep_time)
|
||||
if self.running is False:
|
||||
break
|
||||
|
||||
if streamers_watching == []:
|
||||
time.sleep(60)
|
||||
|
||||
def get_channel_id(self, streamer_username):
|
||||
json_response = self.__do_helix_request(f"/users?login={streamer_username}")
|
||||
data = json_response["data"]
|
||||
if len(data) >= 1:
|
||||
return data[0]["id"]
|
||||
else:
|
||||
if "data" not in json_response:
|
||||
raise StreamerDoesNotExistException
|
||||
else:
|
||||
data = json_response["data"]
|
||||
if len(data) >= 1:
|
||||
return data[0]["id"]
|
||||
else:
|
||||
raise StreamerDoesNotExistException
|
||||
|
||||
def get_followers(self, first=100):
|
||||
followers = []
|
||||
@@ -306,7 +159,7 @@ class Twitch:
|
||||
|
||||
json_response = self.__do_helix_request(query)
|
||||
pagination = json_response["pagination"]
|
||||
followers += [fw["to_name"].lower() for fw in json_response["data"]]
|
||||
followers += [fw["to_login"].lower() for fw in json_response["data"]]
|
||||
time.sleep(random.uniform(0.3, 0.7))
|
||||
|
||||
if pagination == {}:
|
||||
@@ -314,14 +167,6 @@ class Twitch:
|
||||
|
||||
return followers
|
||||
|
||||
def __do_helix_request(self, query, response_as_json=True):
|
||||
url = f"{API}/helix/{query.strip('/')}"
|
||||
response = self.twitch_login.session.get(url)
|
||||
logger.debug(
|
||||
f"Query: {query}, Status code: {response.status_code}, Content: {response.json()}"
|
||||
)
|
||||
return response.json() if response_as_json is True else response
|
||||
|
||||
def update_raid(self, streamer, raid):
|
||||
if streamer.raid != raid:
|
||||
streamer.raid = raid
|
||||
@@ -342,3 +187,395 @@ class Twitch:
|
||||
streamer.viewer_is_mod = response["data"]["user"]["self"]["isModerator"]
|
||||
except (ValueError, KeyError):
|
||||
streamer.viewer_is_mod = False
|
||||
|
||||
# === 'GLOBALS' METHODS === #
|
||||
# Create chunk of sleep of speed-up the break loop after CTRL+C
|
||||
def __chuncked_sleep(self, seconds, chunk_size=3):
|
||||
sleep_time = max(seconds, 0) / chunk_size
|
||||
for i in range(0, chunk_size):
|
||||
time.sleep(sleep_time)
|
||||
if self.running is False:
|
||||
break
|
||||
|
||||
def __check_connection_handler(self, chunk_size):
|
||||
# The success rate It's very hight usually. Why we have failed?
|
||||
# Check internet connection ...
|
||||
while internet_connection_available() is False:
|
||||
random_sleep = random.randint(1, 3)
|
||||
logger.warning(
|
||||
f"No internet connection available! Retry after {random_sleep}m"
|
||||
)
|
||||
self.__chuncked_sleep(random_sleep * 60, chunk_size=chunk_size)
|
||||
|
||||
def __do_helix_request(self, query, response_as_json=True):
|
||||
url = f"{API}/helix/{query.strip('/')}"
|
||||
response = self.twitch_login.session.get(url)
|
||||
logger.debug(
|
||||
f"Query: {query}, Status code: {response.status_code}, Content: {response.json()}"
|
||||
)
|
||||
return response.json() if response_as_json is True else response
|
||||
|
||||
def post_gql_request(self, json_data):
|
||||
try:
|
||||
response = requests.post(
|
||||
GQLOperations.url,
|
||||
json=json_data,
|
||||
headers={
|
||||
"Authorization": f"OAuth {self.twitch_login.get_auth_token()}",
|
||||
"Client-Id": CLIENT_ID,
|
||||
"User-Agent": self.user_agent,
|
||||
},
|
||||
)
|
||||
logger.debug(
|
||||
f"Data: {json_data}, Status code: {response.status_code}, Content: {response.text}"
|
||||
)
|
||||
return response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(
|
||||
f"Error with GQLOperations ({json_data['operationName']}): {e}"
|
||||
)
|
||||
return {}
|
||||
|
||||
def send_minute_watched_events(self, streamers, priority, chunk_size=3):
|
||||
while self.running:
|
||||
try:
|
||||
streamers_index = [
|
||||
i
|
||||
for i in range(0, len(streamers))
|
||||
if streamers[i].is_online is True
|
||||
and (
|
||||
streamers[i].online_at == 0
|
||||
or (time.time() - streamers[i].online_at) > 30
|
||||
)
|
||||
]
|
||||
|
||||
for index in streamers_index:
|
||||
if (streamers[index].stream.update_elapsed() / 60) > 10:
|
||||
# Why this user It's currently online but the last updated was more than 10minutes ago?
|
||||
# Please perform a manually update and check if the user it's online
|
||||
self.check_streamer_online(streamers[index])
|
||||
|
||||
streamers_watching = []
|
||||
for prior in priority:
|
||||
if prior == Priority.ORDER and len(streamers_watching) < 2:
|
||||
# Get the first 2 items, they are already in order
|
||||
streamers_watching += streamers_index[:2]
|
||||
|
||||
elif (
|
||||
prior in [Priority.POINTS_ASCENDING, Priority.POINTS_DESCEDING]
|
||||
and len(streamers_watching) < 2
|
||||
):
|
||||
items = [
|
||||
{"points": streamers[index].channel_points, "index": index}
|
||||
for index in streamers_index
|
||||
]
|
||||
items = sorted(
|
||||
items,
|
||||
key=lambda x: x["points"],
|
||||
reverse=(
|
||||
True if prior == Priority.POINTS_DESCEDING else False
|
||||
),
|
||||
)
|
||||
streamers_watching += [item["index"] for item in items][:2]
|
||||
|
||||
elif prior == Priority.STREAK and len(streamers_watching) < 2:
|
||||
"""
|
||||
Check if we need need to change priority based on watch streak
|
||||
Viewers receive points for returning for x consecutive streams.
|
||||
Each stream must be at least 10 minutes long and it must have been at least 30 minutes since the last stream ended.
|
||||
Watch at least 6m for get the +10
|
||||
"""
|
||||
for index in streamers_index:
|
||||
if (
|
||||
streamers[index].settings.watch_streak is True
|
||||
and streamers[index].stream.watch_streak_missing is True
|
||||
and (
|
||||
streamers[index].offline_at == 0
|
||||
or (
|
||||
(time.time() - streamers[index].offline_at)
|
||||
// 60
|
||||
)
|
||||
> 30
|
||||
)
|
||||
and streamers[index].stream.minute_watched < 7
|
||||
):
|
||||
streamers_watching.append(index)
|
||||
if len(streamers_watching) == 2:
|
||||
break
|
||||
|
||||
elif prior == Priority.DROPS and len(streamers_watching) < 2:
|
||||
for index in streamers_index:
|
||||
if streamers[index].drops_condition() is True:
|
||||
streamers_watching.append(index)
|
||||
if len(streamers_watching) == 2:
|
||||
break
|
||||
|
||||
"""
|
||||
Twitch has a limit - you can't watch more than 2 channels at one time.
|
||||
We take the first two streamers from the list as they have the highest priority (based on order or WatchStreak).
|
||||
"""
|
||||
streamers_watching = streamers_watching[:2]
|
||||
|
||||
for index in streamers_watching:
|
||||
next_iteration = time.time() + 60 / len(streamers_watching)
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
streamers[index].stream.spade_url,
|
||||
data=streamers[index].stream.encode_payload(),
|
||||
headers={"User-Agent": self.user_agent},
|
||||
timeout=60,
|
||||
)
|
||||
logger.debug(
|
||||
f"Send minute watched request for {streamers[index]} - Status code: {response.status_code}"
|
||||
)
|
||||
if response.status_code == 204:
|
||||
streamers[index].stream.update_minute_watched()
|
||||
|
||||
"""
|
||||
Remember, you can only earn progress towards a time-based Drop on one participating channel at a time. [ ! ! ! ]
|
||||
You can also check your progress towards Drops within a campaign anytime by viewing the Drops Inventory.
|
||||
For time-based Drops, if you are unable to claim the Drop in time, you will be able to claim it from the inventory page until the Drops campaign ends.
|
||||
"""
|
||||
|
||||
for campaign in streamers[index].stream.campaigns:
|
||||
for drop in campaign.drops:
|
||||
# We could add .has_preconditions_met condition inside is_printable
|
||||
if (
|
||||
drop.has_preconditions_met is not False
|
||||
and drop.is_printable is True
|
||||
):
|
||||
# print("=" * 125)
|
||||
logger.info(
|
||||
f"{streamers[index]} is streaming {streamers[index].stream}"
|
||||
)
|
||||
logger.info(f"Campaign: {campaign}")
|
||||
logger.info(f"Drop: {drop}")
|
||||
logger.info(f"{drop.progress_bar()}")
|
||||
# print("=" * 125)
|
||||
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
logger.error(f"Error while trying to send minute watched: {e}")
|
||||
self.__check_connection_handler(chunk_size)
|
||||
except requests.exceptions.Timeout as e:
|
||||
logger.error(f"Error while trying to send minute watched: {e}")
|
||||
|
||||
self.__chuncked_sleep(
|
||||
next_iteration - time.time(), chunk_size=chunk_size
|
||||
)
|
||||
|
||||
if streamers_watching == []:
|
||||
self.__chuncked_sleep(60, chunk_size=chunk_size)
|
||||
except Exception:
|
||||
logger.error("Exception raised in send minute watched", exc_info=True)
|
||||
|
||||
# === CHANNEL POINTS / PREDICTION === #
|
||||
# Load the amount of current points for a channel, check if a bonus is available
|
||||
def load_channel_points_context(self, streamer):
|
||||
json_data = copy.deepcopy(GQLOperations.ChannelPointsContext)
|
||||
json_data["variables"] = {"channelLogin": streamer.username}
|
||||
|
||||
response = self.post_gql_request(json_data)
|
||||
if response != {}:
|
||||
if response["data"]["community"] is None:
|
||||
raise StreamerDoesNotExistException
|
||||
channel = response["data"]["community"]["channel"]
|
||||
community_points = channel["self"]["communityPoints"]
|
||||
streamer.channel_points = community_points["balance"]
|
||||
|
||||
if community_points["availableClaim"] is not None:
|
||||
self.claim_bonus(streamer, community_points["availableClaim"]["id"])
|
||||
|
||||
def make_predictions(self, event):
|
||||
decision = event.bet.calculate(event.streamer.channel_points)
|
||||
selector_index = 0 if decision["choice"] == "A" else 1
|
||||
|
||||
logger.info(
|
||||
f"Going to complete bet for {event}",
|
||||
extra={"emoji": ":four_leaf_clover:"},
|
||||
)
|
||||
if event.status == "ACTIVE":
|
||||
skip, compared_value = event.bet.skip()
|
||||
if skip is True:
|
||||
logger.info(
|
||||
f"Skip betting for the event {event}", extra={"emoji": ":pushpin:"}
|
||||
)
|
||||
logger.info(
|
||||
f"Skip settings {event.bet.settings.filter_condition}, current value is: {compared_value}",
|
||||
extra={"emoji": ":pushpin:"},
|
||||
)
|
||||
else:
|
||||
if decision["amount"] >= 10:
|
||||
logger.info(
|
||||
f"Place {_millify(decision['amount'])} channel points on: {event.bet.get_outcome(selector_index)}",
|
||||
extra={"emoji": ":four_leaf_clover:"},
|
||||
)
|
||||
|
||||
json_data = copy.deepcopy(GQLOperations.MakePrediction)
|
||||
json_data["variables"] = {
|
||||
"input": {
|
||||
"eventID": event.event_id,
|
||||
"outcomeID": decision["id"],
|
||||
"points": decision["amount"],
|
||||
"transactionID": token_hex(16),
|
||||
}
|
||||
}
|
||||
return self.post_gql_request(json_data)
|
||||
else:
|
||||
logger.info(
|
||||
f"Oh no! The event is not active anymore! Current status: {event.status}",
|
||||
extra={"emoji": ":disappointed_relieved:"},
|
||||
)
|
||||
|
||||
def claim_bonus(self, streamer, claim_id):
|
||||
if Settings.logger.less is False:
|
||||
logger.info(
|
||||
f"Claiming the bonus for {streamer}!", extra={"emoji": ":gift:"}
|
||||
)
|
||||
|
||||
json_data = copy.deepcopy(GQLOperations.ClaimCommunityPoints)
|
||||
json_data["variables"] = {
|
||||
"input": {"channelID": streamer.channel_id, "claimID": claim_id}
|
||||
}
|
||||
self.post_gql_request(json_data)
|
||||
|
||||
# === CAMPAIGNS / DROPS / INVENTORY === #
|
||||
def __get_campaign_ids_from_streamer(self, streamer):
|
||||
json_data = copy.deepcopy(GQLOperations.DropsHighlightService_AvailableDrops)
|
||||
json_data["variables"] = {"channelID": streamer.channel_id}
|
||||
response = self.post_gql_request(json_data)
|
||||
try:
|
||||
return (
|
||||
[]
|
||||
if response["data"]["channel"]["viewerDropCampaigns"] is None
|
||||
else [
|
||||
item["id"]
|
||||
for item in response["data"]["channel"]["viewerDropCampaigns"]
|
||||
]
|
||||
)
|
||||
except (ValueError, KeyError):
|
||||
return []
|
||||
|
||||
def __get_inventory(self):
|
||||
response = self.post_gql_request(GQLOperations.Inventory)
|
||||
try:
|
||||
return (
|
||||
response["data"]["currentUser"]["inventory"] if response != {} else {}
|
||||
)
|
||||
except (ValueError, KeyError, TypeError):
|
||||
return {}
|
||||
|
||||
def __get_drops_dashboard(self, status=None):
|
||||
response = self.post_gql_request(GQLOperations.ViewerDropsDashboard)
|
||||
campaigns = response["data"]["currentUser"]["dropCampaigns"]
|
||||
if status is not None:
|
||||
campaigns = list(filter(lambda x: x["status"] == status.upper(), campaigns))
|
||||
return campaigns
|
||||
|
||||
def __get_campaigns_details(self, campaigns):
|
||||
json_data = []
|
||||
for campaign in campaigns:
|
||||
json_data.append(copy.deepcopy(GQLOperations.DropCampaignDetails))
|
||||
json_data[-1]["variables"] = {
|
||||
"dropID": campaign["id"],
|
||||
"channelLogin": f"{self.twitch_login.get_user_id()}",
|
||||
}
|
||||
|
||||
response = self.post_gql_request(json_data)
|
||||
return list(map(lambda x: x["data"]["user"]["dropCampaign"], response))
|
||||
|
||||
def __sync_campaigns(self, campaigns):
|
||||
# We need the inventory only for get the real updated value/progress
|
||||
# Get data from inventory and sync current status with streamers.campaigns
|
||||
inventory = self.__get_inventory()
|
||||
if inventory not in [None, {}] and inventory["dropCampaignsInProgress"] not in [
|
||||
None,
|
||||
{},
|
||||
]:
|
||||
# Iterate all campaigns from dashboard (only active, with working drops)
|
||||
# In this array we have also the campaigns never started from us (not in nventory)
|
||||
for i in range(len(campaigns)):
|
||||
campaigns[i].clear_drops() # Remove all the claimed drops
|
||||
# Iterate all campaigns currently in progress from out inventory
|
||||
for progress in inventory["dropCampaignsInProgress"]:
|
||||
if progress["id"] == campaigns[i].id:
|
||||
campaigns[i].in_inventory = True
|
||||
campaigns[i].sync_drops(
|
||||
progress["timeBasedDrops"], self.claim_drop
|
||||
)
|
||||
campaigns[i].clear_drops() # Remove all the claimed drops
|
||||
break
|
||||
return campaigns
|
||||
|
||||
def claim_drop(self, drop):
|
||||
logger.info(f"Claim {drop}", extra={"emoji": ":package:"})
|
||||
|
||||
json_data = copy.deepcopy(GQLOperations.DropsPage_ClaimDropRewards)
|
||||
json_data["variables"] = {"input": {"dropInstanceID": drop.drop_instance_id}}
|
||||
response = self.post_gql_request(json_data)
|
||||
try:
|
||||
return response["data"]["claimDropRewards"]["status"] == "ELIGIBLE_FOR_ALL"
|
||||
except (ValueError, KeyError):
|
||||
return False
|
||||
|
||||
def claim_all_drops_from_inventory(self):
|
||||
inventory = self.__get_inventory()
|
||||
if inventory not in [None, {}]:
|
||||
if inventory["dropCampaignsInProgress"] not in [None, {}]:
|
||||
for campaign in inventory["dropCampaignsInProgress"]:
|
||||
for drop_dict in campaign["timeBasedDrops"]:
|
||||
drop = Drop(drop_dict)
|
||||
drop.update(drop_dict["self"])
|
||||
if drop.is_claimable is True:
|
||||
drop.is_claimed = self.claim_drop(drop)
|
||||
time.sleep(random.uniform(5, 10))
|
||||
|
||||
def sync_campaigns(self, streamers, chunk_size=3):
|
||||
campaigns_update = 0
|
||||
while self.running:
|
||||
try:
|
||||
# Get update from dashboard each 60minutes
|
||||
if (
|
||||
campaigns_update == 0
|
||||
or ((time.time() - campaigns_update) / 60) > 60
|
||||
):
|
||||
campaigns_update = time.time()
|
||||
# Get full details from current ACTIVE campaigns
|
||||
# Use dashboard so we can explore new drops not currently active in our Inventory
|
||||
campaigns_details = self.__get_campaigns_details(
|
||||
self.__get_drops_dashboard(status="ACTIVE")
|
||||
)
|
||||
campaigns = []
|
||||
|
||||
# Going to clear array and structure. Remove all the timeBasedDrops expired or not started yet
|
||||
for index in range(0, len(campaigns_details)):
|
||||
campaign = Campaign(campaigns_details[index])
|
||||
if campaign.dt_match is True:
|
||||
# Remove all the drops already claimed or with dt not matching
|
||||
campaign.clear_drops()
|
||||
if campaign.drops != []:
|
||||
campaigns.append(campaign)
|
||||
|
||||
# Divide et impera :)
|
||||
campaigns = self.__sync_campaigns(campaigns)
|
||||
|
||||
# Check if user It's currently streaming the same game present in campaigns_details
|
||||
for i in range(0, len(streamers)):
|
||||
if streamers[i].drops_condition() is True:
|
||||
# yes! The streamer[i] have the drops_tags enabled and we It's currently stream a game with campaign active!
|
||||
# With 'campaigns_ids' we are also sure that this streamer have the campaign active.
|
||||
# yes! The streamer[index] have the drops_tags enabled and we It's currently stream a game with campaign active!
|
||||
streamers[i].stream.campaigns = list(
|
||||
filter(
|
||||
lambda x: x.drops != []
|
||||
and x.game == streamers[i].stream.game
|
||||
and x.id in streamers[i].stream.campaigns_ids,
|
||||
campaigns,
|
||||
)
|
||||
)
|
||||
|
||||
except (ValueError, KeyError, requests.exceptions.ConnectionError) as e:
|
||||
logger.error(f"Error while syncing inventory: {e}")
|
||||
self.__check_connection_handler(chunk_size)
|
||||
|
||||
self.__chuncked_sleep(60, chunk_size=chunk_size)
|
||||
|
||||
@@ -1,453 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import random
|
||||
import time
|
||||
from enum import Enum, auto
|
||||
from pathlib import Path
|
||||
|
||||
from selenium import webdriver
|
||||
from selenium.common.exceptions import JavascriptException, TimeoutException
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support import expected_conditions
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
|
||||
from TwitchChannelPointsMiner.classes.entities.EventPrediction import EventPrediction
|
||||
from TwitchChannelPointsMiner.constants.browser import Javascript, Selectors
|
||||
from TwitchChannelPointsMiner.constants.twitch import URL
|
||||
from TwitchChannelPointsMiner.utils import _millify, bet_condition, get_user_agent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Browser(Enum):
|
||||
CHROME = auto()
|
||||
FIREFOX = auto()
|
||||
|
||||
|
||||
class BrowserSettings:
|
||||
def __init__(
|
||||
self,
|
||||
timeout: float = 10.0,
|
||||
implicitly_wait: int = 5,
|
||||
max_attempts: int = 3,
|
||||
do_screenshot: bool = False, # Options for debug
|
||||
save_html: bool = False, # Options for debug
|
||||
show: bool = True,
|
||||
browser: Browser = Browser.FIREFOX,
|
||||
driver_path: str = None,
|
||||
):
|
||||
self.timeout = timeout
|
||||
self.implicitly_wait = implicitly_wait
|
||||
self.max_attempts = max_attempts
|
||||
self.do_screenshot = do_screenshot
|
||||
self.save_html = save_html
|
||||
self.show = show
|
||||
self.browser = browser
|
||||
self.driver_path = (
|
||||
driver_path
|
||||
if driver_path is not None
|
||||
else os.path.join(
|
||||
Path().absolute(),
|
||||
(
|
||||
("chromedriver" if browser == Browser.CHROME else "geckodriver")
|
||||
+ (".exe" if platform.system() == "Windows" else "")
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class TwitchBrowser:
|
||||
def __init__(
|
||||
self,
|
||||
auth_token: str,
|
||||
session_id: str,
|
||||
settings: BrowserSettings,
|
||||
):
|
||||
self.auth_token = auth_token
|
||||
self.session_id = session_id
|
||||
self.settings = settings
|
||||
|
||||
self.currently_is_betting = False
|
||||
self.browser = None
|
||||
|
||||
def init(self):
|
||||
if self.settings.browser == Browser.FIREFOX:
|
||||
self.__init_firefox()
|
||||
elif self.settings.browser == Browser.CHROME:
|
||||
self.__init_chrome()
|
||||
|
||||
if self.browser is not None:
|
||||
self.browser.set_window_size(450, 800)
|
||||
self.browser.implicitly_wait(self.settings.implicitly_wait)
|
||||
|
||||
self.__init_twitch()
|
||||
|
||||
def __init_twitch(self):
|
||||
logger.debug(
|
||||
"Init Twitch page - Cookies - LocalStorage items",
|
||||
extra={"emoji": ":wrench:"},
|
||||
)
|
||||
cookie = {
|
||||
"domain": ".twitch.tv",
|
||||
"hostOnly": False,
|
||||
"httpOnly": False,
|
||||
"name": "auth-token",
|
||||
"path": "/",
|
||||
"SameSite": "no_restriction",
|
||||
"secure": True,
|
||||
"session": False,
|
||||
"storeId": "0",
|
||||
"id": 1,
|
||||
"value": self.auth_token,
|
||||
}
|
||||
self.browser.get(URL)
|
||||
self.browser.add_cookie(cookie)
|
||||
time.sleep(random.uniform(2.5, 3.5))
|
||||
|
||||
self.__click_when_exist(
|
||||
Selectors.cookiePolicy, By.CSS_SELECTOR, suppress_error=True
|
||||
)
|
||||
time.sleep(random.uniform(0.5, 1.5))
|
||||
|
||||
# Edit value in localStorage for dark theme, point consent etc.
|
||||
self.__execute_script(Javascript.localStorage)
|
||||
time.sleep(random.uniform(0.5, 1.5))
|
||||
self.__blank()
|
||||
|
||||
def __blank(self):
|
||||
self.browser.get("about:blank")
|
||||
|
||||
def __execute_script(self, javascript_code, suppress_error=False):
|
||||
try:
|
||||
self.browser.execute_script(javascript_code)
|
||||
return True
|
||||
except JavascriptException:
|
||||
if suppress_error is False:
|
||||
logger.warning(f"Failed to execute: {javascript_code}")
|
||||
return False
|
||||
|
||||
# Private method __ - We can instantiate webdriver only with init_browser
|
||||
def __init_chrome(self):
|
||||
logger.debug("Init Chrome browser", extra={"emoji": ":wrench:"})
|
||||
options = webdriver.ChromeOptions()
|
||||
if not self.settings.show:
|
||||
options.add_argument("headless")
|
||||
|
||||
options.add_argument("mute-audio")
|
||||
options.add_argument("disable-dev-shm-usage")
|
||||
options.add_argument("disable-accelerated-2d-canvas")
|
||||
options.add_argument("no-first-run")
|
||||
options.add_argument("no-zygote")
|
||||
options.add_argument("disable-gpu")
|
||||
options.add_argument("no-sandbox")
|
||||
options.add_argument("disable-setuid-sandbox")
|
||||
options.add_argument("disable-infobars")
|
||||
options.add_argument(f"user-agent={get_user_agent(self.settings.browser)}")
|
||||
|
||||
options.add_experimental_option(
|
||||
"prefs", {"profile.managed_default_content_settings.images": 2}
|
||||
)
|
||||
options.add_experimental_option("useAutomationExtension", False)
|
||||
options.add_experimental_option(
|
||||
"excludeSwitches", ["enable-automation", "enable-logging"]
|
||||
)
|
||||
|
||||
if os.path.isfile(self.settings.driver_path) is True:
|
||||
self.browser = webdriver.Chrome(self.settings.driver_path, options=options)
|
||||
else:
|
||||
logger.warning(
|
||||
f"The path {self.settings.driver_path} is not valid. Using default path...",
|
||||
extra={"emoji": ":wrench:"},
|
||||
)
|
||||
self.browser = webdriver.Chrome(options=options)
|
||||
|
||||
# Private method __ - We can instantiate webdriver only with init_browser
|
||||
def __init_firefox(self):
|
||||
logger.debug("Init Firefox browser", extra={"emoji": ":wrench:"})
|
||||
options = webdriver.FirefoxOptions()
|
||||
if not self.settings.show:
|
||||
options.headless = True
|
||||
|
||||
fp = webdriver.FirefoxProfile()
|
||||
fp.set_preference("permissions.default.image", 2)
|
||||
fp.set_preference("permissions.default.stylesheet", 2)
|
||||
fp.set_preference("dom.ipc.plugins.enabled.libflashplayer.so", "false")
|
||||
fp.set_preference("media.volume_scale", "0.0")
|
||||
fp.set_preference("browser.startup.homepage", "about:blank")
|
||||
fp.set_preference("startup.homepage_welcome_url", "about:blank")
|
||||
fp.set_preference("startup.homepage_welcome_url.additional", "about:blank")
|
||||
fp.set_preference(
|
||||
"general.useragent.override",
|
||||
get_user_agent(self.settings.browser),
|
||||
)
|
||||
|
||||
if os.path.isfile(self.settings.driver_path) is True:
|
||||
self.browser = webdriver.Firefox(
|
||||
executable_path=self.settings.driver_path,
|
||||
options=options,
|
||||
firefox_profile=fp,
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"The path {self.settings.driver_path} is not valid. Using default path...",
|
||||
extra={"emoji": ":wrench:"},
|
||||
)
|
||||
self.browser = webdriver.Firefox(options=options, firefox_profile=fp)
|
||||
|
||||
def __debug(self, event, method):
|
||||
if self.settings.do_screenshot:
|
||||
self.screenshot(f"{event.event_id}___{method}")
|
||||
if self.settings.save_html:
|
||||
self.save_html(f"{event.event_id}___{method}")
|
||||
|
||||
def save_html(self, fname):
|
||||
htmls_path = os.path.join(Path().absolute(), "htmls")
|
||||
Path(htmls_path).mkdir(parents=True, exist_ok=True)
|
||||
Path(os.path.join(htmls_path, self.session_id)).mkdir(
|
||||
parents=True, exist_ok=True
|
||||
)
|
||||
|
||||
fname = f"{fname}.html" if fname.endswith(".html") is False else fname
|
||||
fname = fname.replace(".html", f".{time.time()}.html")
|
||||
fname = os.path.join(htmls_path, self.session_id, fname)
|
||||
|
||||
# Little delay ...
|
||||
time.sleep(0.2)
|
||||
with open(fname, "w", encoding="utf-8") as writer:
|
||||
writer.write(self.browser.page_source)
|
||||
|
||||
def screenshot(self, fname):
|
||||
screenshots_path = os.path.join(Path().absolute(), "screenshots")
|
||||
Path(screenshots_path).mkdir(parents=True, exist_ok=True)
|
||||
Path(os.path.join(screenshots_path, self.session_id)).mkdir(
|
||||
parents=True, exist_ok=True
|
||||
)
|
||||
|
||||
fname = f"{fname}.png" if fname.endswith(".png") is False else fname
|
||||
fname = fname.replace(".png", f".{time.time()}.png")
|
||||
fname = os.path.join(screenshots_path, self.session_id, fname)
|
||||
# Little pause prevent effect/css animations in browser delayed
|
||||
time.sleep(0.1)
|
||||
self.browser.save_screenshot(fname)
|
||||
|
||||
def __click_when_exist(
|
||||
self, selector, by: By = By.CSS_SELECTOR, suppress_error=False, timeout=None
|
||||
) -> bool:
|
||||
timeout = self.settings.timeout if timeout is None else timeout
|
||||
try:
|
||||
element = WebDriverWait(self.browser, timeout).until(
|
||||
expected_conditions.element_to_be_clickable((by, selector))
|
||||
)
|
||||
ActionChains(self.browser).move_to_element(element).click().perform()
|
||||
return True
|
||||
except Exception:
|
||||
if suppress_error is False:
|
||||
logger.error(f"Exception raised with: {selector}", exc_info=True)
|
||||
return False
|
||||
|
||||
def __send_text(
|
||||
self, selector, text, by: By = By.CSS_SELECTOR, suppress_error=False
|
||||
) -> bool:
|
||||
try:
|
||||
element = WebDriverWait(self.browser, self.settings.timeout).until(
|
||||
expected_conditions.element_to_be_clickable((by, selector))
|
||||
)
|
||||
ActionChains(self.browser).move_to_element(element).click().send_keys(
|
||||
text
|
||||
).perform()
|
||||
return True
|
||||
except Exception:
|
||||
if suppress_error is False:
|
||||
logger.error(f"Exception raised with: {selector}", exc_info=True)
|
||||
return False
|
||||
|
||||
def start_bet(self, event: EventPrediction):
|
||||
start_time = time.time()
|
||||
if bet_condition(self, event, logger) is True:
|
||||
for attempt in range(0, self.settings.max_attempts):
|
||||
logger.info(
|
||||
f"Starting betting for {event} owned by {event.streamer}",
|
||||
extra={"emoji": ":wrench:"},
|
||||
)
|
||||
self.browser.get(event.streamer.chat_url)
|
||||
time.sleep(random.uniform(3, 5))
|
||||
self.__click_when_exist(
|
||||
Selectors.cookiePolicy,
|
||||
By.CSS_SELECTOR,
|
||||
suppress_error=True,
|
||||
timeout=1.5,
|
||||
)
|
||||
|
||||
# Hide the chat ... Don't ask me why
|
||||
self.__execute_script(Javascript.clearStyleChat, suppress_error=True)
|
||||
|
||||
if self.__bet_chains_methods(event) is True:
|
||||
return self.currently_is_betting, time.time() - start_time
|
||||
logger.error(
|
||||
f"Attempt {attempt+1} failed!", extra={"emoji": ":wrench:"}
|
||||
)
|
||||
self.__blank() # If we fail return to blank page
|
||||
return False, time.time() - start_time
|
||||
|
||||
def __bet_chains_methods(self, event) -> bool:
|
||||
if self.__open_coins_menu(event) is True:
|
||||
if self.__click_on_bet(event) is True:
|
||||
if self.__enable_custom_bet_value(event) is True:
|
||||
return True
|
||||
return False
|
||||
|
||||
def place_bet(self, event: EventPrediction):
|
||||
logger.info(
|
||||
f"Going to complete bet for {event} owned by {event.streamer}",
|
||||
extra={"emoji": ":wrench:"},
|
||||
)
|
||||
if event.status == "ACTIVE":
|
||||
if event.box_fillable and self.currently_is_betting:
|
||||
|
||||
div_bet_is_open = False
|
||||
self.__debug(event, "place_bet")
|
||||
try:
|
||||
WebDriverWait(self.browser, 1).until(
|
||||
expected_conditions.visibility_of_element_located(
|
||||
(By.XPATH, Selectors.betMainDivXP)
|
||||
)
|
||||
)
|
||||
div_bet_is_open = True
|
||||
except TimeoutException:
|
||||
logger.info(
|
||||
"The bet div was not found, maybe It was closed. Attempting to open again... Hopefully in time!",
|
||||
extra={"emoji": ":wrench:"},
|
||||
)
|
||||
div_bet_is_open = self.__bet_chains_methods(event)
|
||||
if div_bet_is_open is True:
|
||||
logger.info(
|
||||
"Success! Bet div is now open, we can complete the bet!",
|
||||
extra={"emoji": ":wrench:"},
|
||||
)
|
||||
|
||||
if div_bet_is_open is True:
|
||||
decision = event.bet.calculate(event.streamer.channel_points)
|
||||
if decision["choice"] is not None:
|
||||
selector_index = 1 if decision["choice"] == "A" else 2
|
||||
logger.info(
|
||||
f"Decision: {event.bet.get_outcome(selector_index - 1)}",
|
||||
extra={"emoji": ":wrench:"},
|
||||
)
|
||||
|
||||
try:
|
||||
logger.info(
|
||||
f"Going to write: {_millify(decision['amount'])} channel points on input {decision['choice']}",
|
||||
extra={"emoji": ":wrench:"},
|
||||
)
|
||||
if (
|
||||
self.__send_text_on_bet(
|
||||
event, selector_index, decision["amount"]
|
||||
)
|
||||
is True
|
||||
):
|
||||
logger.info(
|
||||
f"Going to place the bet for {event}",
|
||||
extra={"emoji": ":wrench:"},
|
||||
)
|
||||
if self.__click_on_vote(event, selector_index) is True:
|
||||
event.bet_placed = True
|
||||
time.sleep(random.uniform(5, 10))
|
||||
except Exception:
|
||||
logger.error("Exception raised", exc_info=True)
|
||||
else:
|
||||
logger.info(
|
||||
"Sorry, unable to complete the bet. The bet div is still closed!"
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"Sorry, unable to complete the bet. Event box fillable: {event.box_fillable}, the browser is betting: {self.currently_is_betting}"
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"Oh no! The event is not active anymore! Current status: {event.status}",
|
||||
extra={"emoji": ":disappointed_relieved:"},
|
||||
)
|
||||
|
||||
self.browser.get("about:blank")
|
||||
self.currently_is_betting = False
|
||||
|
||||
def __open_coins_menu(self, event: EventPrediction) -> bool:
|
||||
logger.info(f"Opening coins menu for {event}", extra={"emoji": ":wrench:"})
|
||||
status = self.__click_when_exist(Selectors.coinsMenuXP, By.XPATH)
|
||||
if status is False:
|
||||
status = self.__execute_script(Javascript.coinsMenu)
|
||||
|
||||
if status is True:
|
||||
time.sleep(random.uniform(0.01, 0.1))
|
||||
self.__debug(event, "open_coins_menu")
|
||||
return True
|
||||
return False
|
||||
|
||||
def __click_on_bet(self, event, maximize_div=True) -> bool:
|
||||
logger.info(f"Clicking on the bet for {event}", extra={"emoji": ":wrench:"})
|
||||
if self.__click_when_exist(Selectors.betTitle, By.CSS_SELECTOR) is True:
|
||||
time.sleep(random.uniform(0.01, 0.1))
|
||||
if maximize_div is True:
|
||||
# Edit the css for make the window full-screen in browser. Another useless change
|
||||
self.__execute_script(Javascript.maximizeBetWindow, suppress_error=True)
|
||||
self.__debug(event, "click_on_bet")
|
||||
return True
|
||||
return False
|
||||
|
||||
def __enable_custom_bet_value(self, event, scroll_down=True) -> bool:
|
||||
logger.info(
|
||||
f"Enable input of custom value for {event}",
|
||||
extra={"emoji": ":wrench:"},
|
||||
)
|
||||
|
||||
if scroll_down is True:
|
||||
time.sleep(random.uniform(0.01, 0.1))
|
||||
if self.__execute_script(Javascript.scrollDownBetWindow) is False:
|
||||
logger.error("Unable to scroll down in the bet window!")
|
||||
|
||||
status = self.__click_when_exist(Selectors.betCustomVote, By.CSS_SELECTOR)
|
||||
if status is False:
|
||||
status = self.__execute_script(Javascript.betCustomVote)
|
||||
|
||||
if status is True:
|
||||
time.sleep(random.uniform(0.01, 0.1))
|
||||
self.__debug(event, "enable_custom_bet_value")
|
||||
event.box_fillable = True
|
||||
self.currently_is_betting = True
|
||||
return True
|
||||
else:
|
||||
logger.info(
|
||||
"Something went wrong unable to continue with betting - Fillable box not available!"
|
||||
)
|
||||
return False
|
||||
|
||||
def __send_text_on_bet(self, event, selector_index, text) -> bool:
|
||||
self.__debug(event, "before__send_text")
|
||||
status = self.__send_text(
|
||||
f"{Selectors.betVoteInputXP}[{selector_index}]", text, By.XPATH
|
||||
)
|
||||
if status is False:
|
||||
status = self.__execute_script(
|
||||
Javascript.betVoteInput.format(int(selector_index) - 1, int(text))
|
||||
)
|
||||
|
||||
if status is True:
|
||||
self.__debug(event, "send_text")
|
||||
return True
|
||||
return False
|
||||
|
||||
def __click_on_vote(self, event, selector_index) -> bool:
|
||||
status = self.__click_when_exist(
|
||||
f"{Selectors.betVoteButtonXP}[{selector_index}]", By.XPATH
|
||||
)
|
||||
if status is False:
|
||||
status = self.__execute_script(
|
||||
Javascript.betVoteButton.format(int(selector_index) - 1)
|
||||
)
|
||||
|
||||
if status is True:
|
||||
self.__debug(event, "click_on_vote")
|
||||
return True
|
||||
return False
|
||||
@@ -15,8 +15,21 @@ from TwitchChannelPointsMiner.classes.Exceptions import WrongCookiesException
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TwitchLogin:
|
||||
def __init__(self, client_id, username, user_agent):
|
||||
class TwitchLogin(object):
|
||||
__self__ = [
|
||||
"client_id",
|
||||
"token",
|
||||
"login_check_result",
|
||||
"session",
|
||||
"session",
|
||||
"username",
|
||||
"password",
|
||||
"user_id",
|
||||
"email",
|
||||
"cookies",
|
||||
]
|
||||
|
||||
def __init__(self, client_id, username, user_agent, password=None):
|
||||
self.client_id = client_id
|
||||
self.token = None
|
||||
self.login_check_result = False
|
||||
@@ -25,6 +38,7 @@ class TwitchLogin:
|
||||
{"Client-ID": self.client_id, "User-Agent": user_agent}
|
||||
)
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.user_id = None
|
||||
self.email = None
|
||||
|
||||
@@ -43,7 +57,11 @@ class TwitchLogin:
|
||||
|
||||
while True:
|
||||
# self.username = input('Enter Twitch username: ')
|
||||
password = getpass.getpass(f"Enter Twitch password for {self.username}: ")
|
||||
password = (
|
||||
getpass.getpass(f"Enter Twitch password for {self.username}: ")
|
||||
if self.password is None
|
||||
else self.password
|
||||
)
|
||||
|
||||
post_data["username"] = self.username
|
||||
post_data["password"] = password
|
||||
@@ -117,9 +135,8 @@ class TwitchLogin:
|
||||
self.session.headers.update({"Authorization": f"Bearer {self.token}"})
|
||||
|
||||
def send_login_request(self, json_data):
|
||||
r = self.session.post("https://passport.twitch.tv/login", json=json_data)
|
||||
j = r.json()
|
||||
return j
|
||||
response = self.session.post("https://passport.twitch.tv/login", json=json_data)
|
||||
return response.json()
|
||||
|
||||
def login_flow_backup(self):
|
||||
"""Backup OAuth login flow in case manual captcha solving is required"""
|
||||
@@ -149,8 +166,10 @@ class TwitchLogin:
|
||||
if self.token is None:
|
||||
return False
|
||||
|
||||
r = self.session.get(f"https://api.twitch.tv/helix/users?login={self.username}")
|
||||
response = r.json()
|
||||
response = self.session.get(
|
||||
f"https://api.twitch.tv/helix/users?login={self.username}"
|
||||
)
|
||||
response = response.json()
|
||||
if "data" in response:
|
||||
self.login_check_result = True
|
||||
self.user_id = response["data"][0]["id"]
|
||||
|
||||
@@ -2,7 +2,7 @@ import json
|
||||
import logging
|
||||
import time
|
||||
|
||||
from websocket import WebSocketApp
|
||||
from websocket import WebSocketApp, WebSocketConnectionClosedException
|
||||
|
||||
from TwitchChannelPointsMiner.utils import create_nonce
|
||||
|
||||
@@ -10,6 +10,35 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TwitchWebSocket(WebSocketApp):
|
||||
def __init__(self, index, parent_pool, *args, **kw):
|
||||
super().__init__(*args, **kw)
|
||||
self.index = index
|
||||
|
||||
self.parent_pool = parent_pool
|
||||
self.is_closed = False
|
||||
self.is_opened = False
|
||||
|
||||
self.is_reconneting = False
|
||||
self.forced_close = False
|
||||
|
||||
# Custom attribute
|
||||
self.topics = []
|
||||
self.pending_topics = []
|
||||
|
||||
self.twitch = parent_pool.twitch
|
||||
self.streamers = parent_pool.streamers
|
||||
self.events_predictions = parent_pool.events_predictions
|
||||
|
||||
self.last_message_timestamp = None
|
||||
self.last_message_type_channel = None
|
||||
|
||||
self.last_pong = time.time()
|
||||
self.last_ping = time.time()
|
||||
|
||||
# def close(self):
|
||||
# self.forced_close = True
|
||||
# super().close()
|
||||
|
||||
def listen(self, topic, auth_token=None):
|
||||
data = {"topics": [str(topic)]}
|
||||
if topic.is_user_topic() and auth_token is not None:
|
||||
@@ -23,31 +52,12 @@ class TwitchWebSocket(WebSocketApp):
|
||||
self.last_ping = time.time()
|
||||
|
||||
def send(self, request):
|
||||
request_str = json.dumps(request, separators=(",", ":"))
|
||||
logger.debug(f"Send: {request_str}")
|
||||
super().send(request_str)
|
||||
|
||||
def reset(self, parent_pool):
|
||||
self.parent_pool = parent_pool
|
||||
self.keep_running = True
|
||||
self.is_closed = False
|
||||
self.is_opened = False
|
||||
self.is_reconneting = False
|
||||
|
||||
# Custom attribute
|
||||
self.topics = []
|
||||
self.pending_topics = []
|
||||
|
||||
self.twitch = parent_pool.twitch
|
||||
self.browser = parent_pool.browser
|
||||
self.streamers = parent_pool.streamers
|
||||
self.events_predictions = parent_pool.events_predictions
|
||||
|
||||
self.last_message_timestamp = None
|
||||
self.last_message_type_channel = None
|
||||
|
||||
self.last_pong = time.time()
|
||||
self.last_ping = time.time()
|
||||
try:
|
||||
request_str = json.dumps(request, separators=(",", ":"))
|
||||
logger.debug(f"#{self.index} - Send: {request_str}")
|
||||
super().send(request_str)
|
||||
except WebSocketConnectionClosedException:
|
||||
self.is_closed = True
|
||||
|
||||
def elapsed_last_pong(self):
|
||||
return (time.time() - self.last_pong) // 60
|
||||
|
||||
@@ -9,102 +9,152 @@ from dateutil import parser
|
||||
from TwitchChannelPointsMiner.classes.entities.EventPrediction import EventPrediction
|
||||
from TwitchChannelPointsMiner.classes.entities.Message import Message
|
||||
from TwitchChannelPointsMiner.classes.entities.Raid import Raid
|
||||
from TwitchChannelPointsMiner.classes.Exceptions import TimeBasedDropNotFound
|
||||
|
||||
# from TwitchChannelPointsMiner.classes.Exceptions import TimeBasedDropNotFound
|
||||
from TwitchChannelPointsMiner.classes.TwitchWebSocket import TwitchWebSocket
|
||||
from TwitchChannelPointsMiner.constants.twitch import WEBSOCKET
|
||||
from TwitchChannelPointsMiner.constants import WEBSOCKET
|
||||
from TwitchChannelPointsMiner.utils import (
|
||||
_millify,
|
||||
bet_condition,
|
||||
calculate_start_after,
|
||||
get_streamer_index,
|
||||
internet_connection_available,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WebSocketsPool:
|
||||
def __init__(self, twitch, browser, streamers, events_predictions):
|
||||
self.ws = None
|
||||
__slots__ = ["ws", "twitch", "streamers", "events_predictions"]
|
||||
|
||||
def __init__(self, twitch, streamers, events_predictions):
|
||||
self.ws = []
|
||||
self.twitch = twitch
|
||||
self.browser = browser
|
||||
self.streamers = streamers
|
||||
self.events_predictions = events_predictions
|
||||
|
||||
"""
|
||||
API Limits
|
||||
- Clients can listen on up to 50 topics per connection. Trying to listen on more topics will result in an error message.
|
||||
- Clients can listen to up to 50 topics per connection. Trying to listen to more topics will result in an error message.
|
||||
- We recommend that a single client IP address establishes no more than 10 simultaneous connections.
|
||||
The two limits above are likely to be relaxed for approved third-party applications, as we start to better understand third-party requirements.
|
||||
"""
|
||||
|
||||
def submit(self, topic):
|
||||
if self.ws is None or len(self.ws.topics) >= 50:
|
||||
self.create_new_websocket()
|
||||
# Check if we need to create a new WebSocket instance
|
||||
if self.ws == [] or len(self.ws[-1].topics) >= 50:
|
||||
self.ws.append(self.__new(len(self.ws)))
|
||||
self.__start(-1)
|
||||
|
||||
self.ws.topics.append(topic)
|
||||
self.__submit(-1, topic)
|
||||
|
||||
if not self.ws.is_opened:
|
||||
self.ws.pending_topics.append(topic)
|
||||
def __submit(self, index, topic):
|
||||
# Topic in topics should never happen. Anyway prevent any types of duplicates
|
||||
if topic not in self.ws[index].topics:
|
||||
self.ws[index].topics.append(topic)
|
||||
|
||||
if self.ws[index].is_opened is False:
|
||||
self.ws[index].pending_topics.append(topic)
|
||||
else:
|
||||
self.ws.listen(topic, self.twitch.twitch_login.get_auth_token())
|
||||
self.ws[index].listen(topic, self.twitch.twitch_login.get_auth_token())
|
||||
|
||||
def create_new_websocket(self):
|
||||
self.ws = TwitchWebSocket(
|
||||
WEBSOCKET,
|
||||
def __new(self, index):
|
||||
return TwitchWebSocket(
|
||||
index=index,
|
||||
parent_pool=self,
|
||||
url=WEBSOCKET,
|
||||
on_message=WebSocketsPool.on_message,
|
||||
on_open=WebSocketsPool.on_open,
|
||||
on_close=WebSocketsPool.handle_websocket_reconnection,
|
||||
on_error=WebSocketsPool.on_error,
|
||||
on_close=WebSocketsPool.on_close
|
||||
# on_close=WebSocketsPool.handle_reconnection, # Do nothing.
|
||||
)
|
||||
self.ws.reset(self)
|
||||
|
||||
self.thread_ws = threading.Thread(target=lambda: self.ws.run_forever())
|
||||
self.thread_ws.daemon = True
|
||||
self.thread_ws.start()
|
||||
def __start(self, index):
|
||||
thread_ws = threading.Thread(target=lambda: self.ws[index].run_forever())
|
||||
thread_ws.daemon = True
|
||||
thread_ws.name = f"WebSocket #{self.ws[index].index}"
|
||||
thread_ws.start()
|
||||
|
||||
def end(self):
|
||||
self.ws.keep_running = False
|
||||
self.ws.close()
|
||||
for index in range(0, len(self.ws)):
|
||||
self.ws[index].forced_close = True
|
||||
self.ws[index].close()
|
||||
|
||||
@staticmethod
|
||||
def on_open(ws):
|
||||
def run():
|
||||
ws.is_opened = True
|
||||
ws.ping()
|
||||
|
||||
for topic in ws.pending_topics:
|
||||
ws.listen(topic, ws.twitch.twitch_login.get_auth_token())
|
||||
|
||||
while not ws.is_closed:
|
||||
ws.ping()
|
||||
time.sleep(random.uniform(25, 30))
|
||||
while ws.is_closed is False:
|
||||
# Else: the ws is currently in reconnecting phase, you can't do ping or other operation.
|
||||
# Probably this ws will be closed very soon with ws.is_closed = True
|
||||
if ws.is_reconneting is False:
|
||||
ws.ping() # We need ping for keep the connection alive
|
||||
time.sleep(random.uniform(25, 30))
|
||||
|
||||
if ws.elapsed_last_pong() > 15 and ws.is_reconneting is False:
|
||||
logger.info(
|
||||
"The last pong was received more than 15 minutes ago. Reconnect the WebSocket"
|
||||
)
|
||||
ws.keep_running = True
|
||||
ws.is_reconneting = True
|
||||
WebSocketsPool.handle_websocket_reconnection(ws)
|
||||
if ws.elapsed_last_pong() > 5:
|
||||
logger.info(
|
||||
f"#{ws.index} - The last PONG was received more than 5 minutes ago"
|
||||
)
|
||||
WebSocketsPool.handle_reconnection(ws)
|
||||
|
||||
thread_ws = threading.Thread(target=run)
|
||||
thread_ws.daemon = True
|
||||
thread_ws.start()
|
||||
|
||||
@staticmethod
|
||||
def handle_websocket_reconnection(ws):
|
||||
ws.is_closed = True
|
||||
if ws.keep_running is True:
|
||||
logger.info("Reconnecting to Twitch PubSub server in 60 seconds")
|
||||
time.sleep(60)
|
||||
def on_error(ws, error):
|
||||
# Connection lost | [WinError 10054] An existing connection was forcibly closed by the remote host
|
||||
# Connection already closed | Connection is already closed (raise WebSocketConnectionClosedException)
|
||||
logger.error(f"#{ws.index} - WebSocket error: {error}")
|
||||
|
||||
@staticmethod
|
||||
def on_close(ws):
|
||||
logger.info(f"#{ws.index} - WebSocket closed")
|
||||
# On close please reconnect automatically
|
||||
WebSocketsPool.handle_reconnection(ws)
|
||||
|
||||
@staticmethod
|
||||
def handle_reconnection(ws):
|
||||
# Close the current WebSocket.
|
||||
ws.is_closed = True
|
||||
ws.keep_running = False
|
||||
# Reconnect only if ws.forced_close is False (replace the keep_running)
|
||||
|
||||
# Set the current socket as reconnecting status
|
||||
# So the external ping check will be locked
|
||||
ws.is_reconneting = True
|
||||
|
||||
if ws.forced_close is False:
|
||||
logger.info(
|
||||
f"#{ws.index} - Reconnecting to Twitch PubSub server in ~60 seconds"
|
||||
)
|
||||
time.sleep(30)
|
||||
|
||||
while internet_connection_available() is False:
|
||||
random_sleep = random.randint(1, 3)
|
||||
logger.warning(
|
||||
f"#{ws.index} - No internet connection available! Retry after {random_sleep}m"
|
||||
)
|
||||
time.sleep(random_sleep * 60)
|
||||
|
||||
# Why not create a new ws on the same array index? Let's try.
|
||||
self = ws.parent_pool
|
||||
if self.ws == ws:
|
||||
self.ws = None
|
||||
self.ws[ws.index] = self.__new(ws.index) # Create a new connection.
|
||||
# self.ws[ws.index].topics = ws.topics
|
||||
|
||||
self.__start(ws.index) # Start a new thread.
|
||||
time.sleep(30)
|
||||
|
||||
for topic in ws.topics:
|
||||
self.submit(topic)
|
||||
self.__submit(ws.index, topic)
|
||||
|
||||
@staticmethod
|
||||
def on_message(ws, message):
|
||||
logger.debug(f"Received: {message.strip()}")
|
||||
logger.debug(f"#{ws.index} - Received: {message.strip()}")
|
||||
response = json.loads(message)
|
||||
|
||||
if response["type"] == "MESSAGE":
|
||||
@@ -189,9 +239,8 @@ class WebSocketsPool:
|
||||
prediction_window_seconds = float(
|
||||
event_dict["prediction_window_seconds"]
|
||||
)
|
||||
prediction_window_seconds -= (
|
||||
25 if prediction_window_seconds <= 180 else 60
|
||||
)
|
||||
# Reduce prediction window by 3/6s - Collect more accurate data for decision
|
||||
prediction_window_seconds -= random.uniform(3, 6)
|
||||
event = EventPrediction(
|
||||
ws.streamers[streamer_index],
|
||||
event_id,
|
||||
@@ -204,30 +253,20 @@ class WebSocketsPool:
|
||||
if (
|
||||
ws.streamers[streamer_index].is_online
|
||||
and event.closing_bet_after(current_tmsp) > 0
|
||||
and bet_condition(
|
||||
ws.browser,
|
||||
event,
|
||||
logger,
|
||||
)
|
||||
is True
|
||||
):
|
||||
ws.events_predictions[event_id] = event
|
||||
(
|
||||
start_bet_status,
|
||||
execution_time,
|
||||
) = ws.browser.start_bet(
|
||||
ws.events_predictions[event_id]
|
||||
)
|
||||
if start_bet_status is True:
|
||||
# place_bet_thread = threading.Timer(event.closing_bet_after(current_tmsp), ws.twitch.make_predictions, (ws.events_predictions[event_id],))
|
||||
start_after = calculate_start_after(
|
||||
event.closing_bet_after(current_tmsp),
|
||||
execution_time,
|
||||
if event.streamer.viewer_is_mod is True:
|
||||
logger.info(
|
||||
f"Sorry, you are moderator of {event.streamer}, so you can't bet!"
|
||||
)
|
||||
else:
|
||||
ws.events_predictions[event_id] = event
|
||||
start_after = event.closing_bet_after(
|
||||
current_tmsp
|
||||
)
|
||||
|
||||
place_bet_thread = threading.Timer(
|
||||
start_after,
|
||||
ws.browser.place_bet,
|
||||
ws.twitch.make_predictions,
|
||||
(ws.events_predictions[event_id],),
|
||||
)
|
||||
place_bet_thread.daemon = True
|
||||
@@ -237,8 +276,6 @@ class WebSocketsPool:
|
||||
f"Place the bet after: {start_after}s for: {ws.events_predictions[event_id]}",
|
||||
extra={"emoji": ":alarm_clock:"},
|
||||
)
|
||||
else:
|
||||
del ws.events_predictions[event_id]
|
||||
|
||||
elif (
|
||||
message.type == "event-updated"
|
||||
@@ -257,62 +294,68 @@ class WebSocketsPool:
|
||||
elif message.topic == "predictions-user-v1":
|
||||
event_id = message.data["prediction"]["event_id"]
|
||||
if event_id in ws.events_predictions:
|
||||
if message.type == "prediction-result":
|
||||
event_prediction = ws.events_predictions[event_id]
|
||||
if (
|
||||
message.type == "prediction-result"
|
||||
and event_prediction.bet_confirmed
|
||||
):
|
||||
event_result = message.data["prediction"]["result"]
|
||||
bet = ws.events_predictions[event_id].bet
|
||||
poinst_placed = bet.decision["amount"]
|
||||
points_prefix = (
|
||||
"+" if event_result["points_won"] else "-"
|
||||
)
|
||||
result_type = event_result["type"]
|
||||
points_placed = event_prediction.bet.decision["amount"]
|
||||
points_won = (
|
||||
event_result["points_won"]
|
||||
if event_result["points_won"]
|
||||
else poinst_placed
|
||||
or result_type == "REFUND"
|
||||
else 0
|
||||
)
|
||||
points_gained = (
|
||||
points_won - points_placed
|
||||
if result_type != "REFUND"
|
||||
else 0
|
||||
)
|
||||
points_prefix = "+" if points_gained >= 0 else ""
|
||||
action = (
|
||||
"Lost"
|
||||
if result_type == "LOSE"
|
||||
else (
|
||||
"Refunded"
|
||||
if result_type == "REFUND"
|
||||
else "Gained"
|
||||
)
|
||||
)
|
||||
logger.info(
|
||||
f"{ws.events_predictions[event_id]} - Result: {event_result['type']}, Points won: {points_prefix}{_millify(points_won)}",
|
||||
f"{ws.events_predictions[event_id]} - Result: {result_type}, {action}: {points_prefix}{_millify(points_gained)}",
|
||||
extra={"emoji": ":bar_chart:"},
|
||||
)
|
||||
ws.events_predictions[event_id].final_result = {
|
||||
"type": event_result["type"],
|
||||
"won": f"{points_prefix}{_millify(points_won)}",
|
||||
"points_won": points_won,
|
||||
"gained": points_gained,
|
||||
}
|
||||
# ws.events_predictions[event_id].streamer
|
||||
ws.streamers[streamer_index].update_history(
|
||||
"PREDICTION", points_gained
|
||||
)
|
||||
|
||||
# Remove duplicate history records from previous message sent in community-points-user-v1
|
||||
if result_type == "REFUND":
|
||||
ws.streamers[streamer_index].update_history(
|
||||
"REFUND",
|
||||
-points_placed,
|
||||
counter=-1,
|
||||
)
|
||||
elif result_type == "WIN":
|
||||
ws.streamers[streamer_index].update_history(
|
||||
"PREDICTION",
|
||||
-points_won,
|
||||
counter=-1,
|
||||
)
|
||||
|
||||
ws.streamers[streamer_index].persistent_points(
|
||||
event_result["type"],
|
||||
f"{points_prefix}{points_won} - {event_result['type']}: {ws.events_predictions[event_id].title}",
|
||||
f"{points_prefix}{points_won} - {result_type}: {ws.events_predictions[event_id].title}",
|
||||
)
|
||||
elif message.type == "prediction-made":
|
||||
ws.events_predictions[event_id].bet_confirmed = True
|
||||
|
||||
elif message.topic == "user-drop-events":
|
||||
if message.type == "drop-progress":
|
||||
current = message.data["current_progress_min"]
|
||||
required = message.data["required_progress_min"]
|
||||
if current >= required:
|
||||
try:
|
||||
drop = ws.twitch.search_drop_in_inventory(
|
||||
ws.streamers[streamer_index],
|
||||
message.data["drop_id"],
|
||||
)
|
||||
if drop["dropInstanceID"] is not None:
|
||||
ws.twitch.claim_drop(
|
||||
drop["dropInstanceID"],
|
||||
ws.streamers[streamer_index],
|
||||
)
|
||||
except TimeBasedDropNotFound:
|
||||
logger.error(
|
||||
f"Unable to find {message.data['drop_id']} in your inventory"
|
||||
)
|
||||
else:
|
||||
# Skip 0% and 100% ...
|
||||
percentage_state = int((current / required) * 100)
|
||||
if percentage_state != 0 and percentage_state % 25 == 0:
|
||||
logger.info(
|
||||
f"Drop event {percentage_state}% for {ws.streamers[streamer_index]}!",
|
||||
extra={"emoji": ":package:"},
|
||||
)
|
||||
|
||||
event_prediction.bet_confirmed = True
|
||||
except Exception:
|
||||
logger.error(
|
||||
f"Exception raised for topic: {message.topic} and message: {message}",
|
||||
@@ -323,9 +366,8 @@ class WebSocketsPool:
|
||||
raise RuntimeError(f"Error while trying to listen for a topic: {response}")
|
||||
|
||||
elif response["type"] == "RECONNECT":
|
||||
logger.info(f"Reconnection required and keep running is: {ws.keep_running}")
|
||||
ws.is_reconneting = True
|
||||
WebSocketsPool.handle_websocket_reconnection(ws)
|
||||
logger.info(f"#{ws.index} - Reconnection required")
|
||||
WebSocketsPool.handle_reconnection(ws)
|
||||
|
||||
elif response["type"] == "PONG":
|
||||
ws.last_pong = time.time()
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import copy
|
||||
import logging
|
||||
from enum import Enum, auto
|
||||
from random import uniform
|
||||
|
||||
from millify import millify
|
||||
|
||||
from TwitchChannelPointsMiner.utils import float_round
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from TwitchChannelPointsMiner.utils import char_decision_as_index, float_round
|
||||
|
||||
|
||||
class Strategy(Enum):
|
||||
@@ -15,31 +13,90 @@ class Strategy(Enum):
|
||||
PERCENTAGE = auto()
|
||||
SMART = auto()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Condition(Enum):
|
||||
GT = auto()
|
||||
LT = auto()
|
||||
GTE = auto()
|
||||
LTE = auto()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class OutcomeKeys(object):
|
||||
# Real key on Bet dict ['']
|
||||
PERCENTAGE_USERS = "percentage_users"
|
||||
ODDS_PERCENTAGE = "odds_percentage"
|
||||
ODDS = "odds"
|
||||
TOP_POINTS = "top_points"
|
||||
# Real key on Bet dict [''] - Sum()
|
||||
TOTAL_USERS = "total_users"
|
||||
TOTAL_POINTS = "total_points"
|
||||
# This key does not exist
|
||||
DECISION_USERS = "decision_users"
|
||||
DECISION_POINTS = "decision_points"
|
||||
|
||||
|
||||
class FilterCondition(object):
|
||||
__slots__ = [
|
||||
"by",
|
||||
"where",
|
||||
"value",
|
||||
]
|
||||
|
||||
def __init__(self, by=None, where=None, value=None, decision=None):
|
||||
self.by = by
|
||||
self.where = where
|
||||
self.value = value
|
||||
|
||||
def __repr__(self):
|
||||
return f"FilterCondition(by={self.by.upper()}, where={self.where}, value={self.value})"
|
||||
|
||||
|
||||
class BetSettings(object):
|
||||
__slots__ = [
|
||||
"strategy",
|
||||
"percentage",
|
||||
"percentage_gap",
|
||||
"max_points",
|
||||
"stealth_mode",
|
||||
"filter_condition",
|
||||
]
|
||||
|
||||
class BetSettings:
|
||||
def __init__(
|
||||
self,
|
||||
strategy: Strategy = None,
|
||||
percentage: int = None,
|
||||
percentage_gap: int = None,
|
||||
max_points: int = None,
|
||||
stealth_mode: bool = None,
|
||||
filter_condition: FilterCondition = None,
|
||||
):
|
||||
self.strategy = strategy
|
||||
self.percentage = percentage
|
||||
self.percentage_gap = percentage_gap
|
||||
self.max_points = max_points
|
||||
self.stealth_mode = stealth_mode
|
||||
self.filter_condition = filter_condition
|
||||
|
||||
def default(self):
|
||||
self.strategy = self.strategy if not None else Strategy.SMART
|
||||
self.percentage = self.percentage if not None else 5
|
||||
self.percentage_gap = self.percentage_gap if not None else 2
|
||||
self.percentage_gap = self.percentage_gap if not None else 20
|
||||
self.max_points = self.max_points if not None else 50000
|
||||
self.stealth_mode = self.stealth_mode if not None else False
|
||||
|
||||
def __repr__(self):
|
||||
return f"BetSettings(Strategy={self.strategy}, Percentage={self.percentage}, PercentageGap={self.percentage_gap}, MaxPoints={self.max_points})"
|
||||
return f"BetSettings(strategy={self.strategy}, percentage={self.percentage}, percentage_gap={self.percentage_gap}, max_points={self.max_points}, stealth_mode={self.stealth_mode})"
|
||||
|
||||
|
||||
class Bet:
|
||||
class Bet(object):
|
||||
__slots__ = ["outcomes", "decision", "total_users", "total_points", "settings"]
|
||||
|
||||
def __init__(self, outcomes: list, settings: BetSettings):
|
||||
self.outcomes = outcomes
|
||||
self.__clear_outcomes()
|
||||
@@ -49,92 +106,158 @@ class Bet:
|
||||
self.settings = settings
|
||||
|
||||
def update_outcomes(self, outcomes):
|
||||
self.outcomes[0]["total_users"] = int(outcomes[0]["total_users"])
|
||||
self.outcomes[1]["total_users"] = int(outcomes[1]["total_users"])
|
||||
self.outcomes[0]["total_points"] = int(outcomes[0]["total_points"])
|
||||
self.outcomes[1]["total_points"] = int(outcomes[1]["total_points"])
|
||||
for index in range(0, len(self.outcomes)):
|
||||
self.outcomes[index][OutcomeKeys.TOTAL_USERS] = int(
|
||||
outcomes[index][OutcomeKeys.TOTAL_USERS]
|
||||
)
|
||||
self.outcomes[index][OutcomeKeys.TOTAL_POINTS] = int(
|
||||
outcomes[index][OutcomeKeys.TOTAL_POINTS]
|
||||
)
|
||||
if outcomes[index]["top_predictors"] != []:
|
||||
# Sort by points placed by other users
|
||||
outcomes[index]["top_predictors"] = sorted(
|
||||
outcomes[index]["top_predictors"],
|
||||
key=lambda x: x["points"],
|
||||
reverse=True,
|
||||
)
|
||||
# Get the first elements (most placed)
|
||||
top_points = outcomes[index]["top_predictors"][0]["points"]
|
||||
self.outcomes[index][OutcomeKeys.TOP_POINTS] = top_points
|
||||
|
||||
self.total_users = (
|
||||
self.outcomes[0]["total_users"] + self.outcomes[1]["total_users"]
|
||||
self.outcomes[0][OutcomeKeys.TOTAL_USERS]
|
||||
+ self.outcomes[1][OutcomeKeys.TOTAL_USERS]
|
||||
)
|
||||
self.total_points = (
|
||||
self.outcomes[0]["total_points"] + self.outcomes[1]["total_points"]
|
||||
self.outcomes[0][OutcomeKeys.TOTAL_POINTS]
|
||||
+ self.outcomes[1][OutcomeKeys.TOTAL_POINTS]
|
||||
)
|
||||
|
||||
if (
|
||||
self.total_users > 0
|
||||
and self.outcomes[0]["total_points"] > 0
|
||||
and self.outcomes[1]["total_points"] > 0
|
||||
and self.outcomes[0][OutcomeKeys.TOTAL_POINTS] > 0
|
||||
and self.outcomes[1][OutcomeKeys.TOTAL_POINTS] > 0
|
||||
):
|
||||
for index in range(0, len(self.outcomes)):
|
||||
self.outcomes[index]["percentage_users"] = float_round(
|
||||
(100 * self.outcomes[index]["total_users"]) / self.total_users
|
||||
self.outcomes[index][OutcomeKeys.PERCENTAGE_USERS] = float_round(
|
||||
(100 * self.outcomes[index][OutcomeKeys.TOTAL_USERS])
|
||||
/ self.total_users
|
||||
)
|
||||
self.outcomes[index]["odds"] = float_round(
|
||||
self.total_points / self.outcomes[index]["total_points"]
|
||||
self.outcomes[index][OutcomeKeys.ODDS] = float_round(
|
||||
self.total_points / self.outcomes[index][OutcomeKeys.TOTAL_POINTS]
|
||||
)
|
||||
self.outcomes[index]["odds_percentage"] = float_round(
|
||||
100 / self.outcomes[index]["odds"]
|
||||
self.outcomes[index][OutcomeKeys.ODDS_PERCENTAGE] = float_round(
|
||||
100 / self.outcomes[index][OutcomeKeys.ODDS]
|
||||
)
|
||||
|
||||
self.__clear_outcomes()
|
||||
|
||||
def __repr__(self):
|
||||
return f"Bet(TotalUsers={millify(self.total_users)}, TotalPoints={millify(self.total_points)}), Decision={self.decision})\n\t\tOutcome0({self.get_outcome(0)})\n\t\tOutcome1({self.get_outcome(1)})"
|
||||
return f"Bet(total_users={millify(self.total_users)}, total_points={millify(self.total_points)}), decision={self.decision})\n\t\tOutcome0({self.get_outcome(0)})\n\t\tOutcome1({self.get_outcome(1)})"
|
||||
|
||||
def get_outcome(self, index):
|
||||
outcome = self.outcomes[index]
|
||||
return f"{outcome['title']} ({outcome['color']}), Points: {millify(outcome['total_points'])}, Users: {millify(outcome['total_users'])} ({outcome['percentage_users']}%), Odds: {outcome['odds']} ({outcome['odds_percentage']}%)"
|
||||
return f"{outcome['title']} ({outcome['color']}), Points: {millify(outcome[OutcomeKeys.TOTAL_POINTS])}, Users: {millify(outcome[OutcomeKeys.TOTAL_USERS])} ({outcome[OutcomeKeys.PERCENTAGE_USERS]}%), Odds: {outcome[OutcomeKeys.ODDS]} ({outcome[OutcomeKeys.ODDS_PERCENTAGE]}%)"
|
||||
|
||||
def __clear_outcomes(self):
|
||||
for index in range(0, len(self.outcomes)):
|
||||
keys = copy.deepcopy(list(self.outcomes[index].keys()))
|
||||
for key in keys:
|
||||
if key not in [
|
||||
"total_users",
|
||||
"total_points",
|
||||
"percentage_users",
|
||||
"odds",
|
||||
"odds_percentage",
|
||||
OutcomeKeys.TOTAL_USERS,
|
||||
OutcomeKeys.TOTAL_POINTS,
|
||||
OutcomeKeys.TOP_POINTS,
|
||||
OutcomeKeys.PERCENTAGE_USERS,
|
||||
OutcomeKeys.ODDS,
|
||||
OutcomeKeys.ODDS_PERCENTAGE,
|
||||
"title",
|
||||
"color",
|
||||
"id",
|
||||
]:
|
||||
del self.outcomes[index][key]
|
||||
for key in ["percentage_users", "odds", "odds_percentage"]:
|
||||
for key in [
|
||||
OutcomeKeys.PERCENTAGE_USERS,
|
||||
OutcomeKeys.ODDS,
|
||||
OutcomeKeys.ODDS_PERCENTAGE,
|
||||
OutcomeKeys.TOP_POINTS,
|
||||
]:
|
||||
if key not in self.outcomes[index]:
|
||||
self.outcomes[index][key] = 0
|
||||
|
||||
def __return_choice(self, key) -> str:
|
||||
return "A" if self.outcomes[0][key] > self.outcomes[1][key] else "B"
|
||||
|
||||
def skip(self) -> bool:
|
||||
if self.settings.filter_condition is not None:
|
||||
# key == by , condition == where
|
||||
key = self.settings.filter_condition.by
|
||||
condition = self.settings.filter_condition.where
|
||||
value = self.settings.filter_condition.value
|
||||
|
||||
fixed_key = (
|
||||
key
|
||||
if key not in [OutcomeKeys.DECISION_USERS, OutcomeKeys.DECISION_POINTS]
|
||||
else key.replace("decision", "total")
|
||||
)
|
||||
if key in [OutcomeKeys.TOTAL_USERS, OutcomeKeys.TOTAL_POINTS]:
|
||||
compared_value = (
|
||||
self.outcomes[0][fixed_key] + self.outcomes[1][fixed_key]
|
||||
)
|
||||
else:
|
||||
outcome_index = char_decision_as_index(self.decision["choice"])
|
||||
compared_value = self.outcomes[outcome_index][fixed_key]
|
||||
|
||||
# Check if condition is satisfied
|
||||
if condition == Condition.GT:
|
||||
if compared_value > value:
|
||||
return False, compared_value
|
||||
elif condition == Condition.LT:
|
||||
if compared_value < value:
|
||||
return False, compared_value
|
||||
elif condition == Condition.GTE:
|
||||
if compared_value >= value:
|
||||
return False, compared_value
|
||||
elif condition == Condition.LTE:
|
||||
if compared_value <= value:
|
||||
return False, compared_value
|
||||
return True, compared_value # Else skip the bet
|
||||
else:
|
||||
return False, 0 # Default don't skip the bet
|
||||
|
||||
def calculate(self, balance: int) -> dict:
|
||||
self.decision = {"choice": None, "amount": 0, "id": None}
|
||||
if self.settings.strategy == Strategy.MOST_VOTED:
|
||||
self.decision["choice"] = self.__return_choice("total_users")
|
||||
self.decision["choice"] = self.__return_choice(OutcomeKeys.TOTAL_USERS)
|
||||
elif self.settings.strategy == Strategy.HIGH_ODDS:
|
||||
self.decision["choice"] = self.__return_choice("odds")
|
||||
self.decision["choice"] = self.__return_choice(OutcomeKeys.ODDS)
|
||||
elif self.settings.strategy == Strategy.PERCENTAGE:
|
||||
self.decision["choice"] = self.__return_choice("odds_percentage")
|
||||
self.decision["choice"] = self.__return_choice(OutcomeKeys.ODDS_PERCENTAGE)
|
||||
elif self.settings.strategy == Strategy.SMART:
|
||||
difference = abs(
|
||||
self.outcomes[0]["percentage_users"]
|
||||
- self.outcomes[1]["percentage_users"]
|
||||
self.outcomes[0][OutcomeKeys.PERCENTAGE_USERS]
|
||||
- self.outcomes[1][OutcomeKeys.PERCENTAGE_USERS]
|
||||
)
|
||||
self.decision["choice"] = (
|
||||
self.__return_choice("odds")
|
||||
self.__return_choice(OutcomeKeys.ODDS)
|
||||
if difference < self.settings.percentage_gap
|
||||
else self.__return_choice("total_users")
|
||||
else self.__return_choice(OutcomeKeys.TOTAL_USERS)
|
||||
)
|
||||
|
||||
if self.decision["choice"] is not None:
|
||||
self.decision["id"] = (
|
||||
self.outcomes[0]["id"]
|
||||
if self.decision["choice"] == "A"
|
||||
else self.outcomes[1]["id"]
|
||||
)
|
||||
index = char_decision_as_index(self.decision["choice"])
|
||||
self.decision["id"] = self.outcomes[index]["id"]
|
||||
self.decision["amount"] = min(
|
||||
int(balance * (self.settings.percentage / 100)),
|
||||
self.settings.max_points,
|
||||
)
|
||||
if (
|
||||
self.settings.stealth_mode is True
|
||||
and self.decision["amount"]
|
||||
>= self.outcomes[index][OutcomeKeys.TOP_POINTS]
|
||||
):
|
||||
reduce_amount = uniform(1, 5)
|
||||
self.decision["amount"] = (
|
||||
self.outcomes[index][OutcomeKeys.TOP_POINTS] - reduce_amount
|
||||
)
|
||||
self.decision["amount"] = int(self.decision["amount"])
|
||||
return self.decision
|
||||
|
||||
74
TwitchChannelPointsMiner/classes/entities/Campaign.py
Normal file
74
TwitchChannelPointsMiner/classes/entities/Campaign.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from datetime import datetime
|
||||
|
||||
from TwitchChannelPointsMiner.classes.entities.Drop import Drop
|
||||
from TwitchChannelPointsMiner.classes.Settings import Settings
|
||||
|
||||
|
||||
class Campaign(object):
|
||||
__slots__ = [
|
||||
"id",
|
||||
"game",
|
||||
"name",
|
||||
"status",
|
||||
"in_inventory",
|
||||
"end_at",
|
||||
"start_at",
|
||||
"dt_match",
|
||||
"drops",
|
||||
"channels",
|
||||
]
|
||||
|
||||
def __init__(self, dict):
|
||||
self.id = dict["id"]
|
||||
self.game = dict["game"]
|
||||
self.name = dict["name"]
|
||||
self.status = dict["status"]
|
||||
self.channels = (
|
||||
[]
|
||||
if dict["allow"]["channels"] is None
|
||||
else list(map(lambda x: x["id"], dict["allow"]["channels"]))
|
||||
)
|
||||
self.in_inventory = False
|
||||
|
||||
self.end_at = datetime.strptime(dict["endAt"], "%Y-%m-%dT%H:%M:%SZ")
|
||||
self.start_at = datetime.strptime(dict["startAt"], "%Y-%m-%dT%H:%M:%SZ")
|
||||
self.dt_match = self.start_at < datetime.now() < self.end_at
|
||||
|
||||
self.drops = list(map(lambda x: Drop(x), dict["timeBasedDrops"]))
|
||||
|
||||
def __repr__(self):
|
||||
return f"Campaign(id={self.id}, name={self.name}, game={self.game}, in_inventory={self.in_inventory})"
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"{self.name}, Game: {self.game['displayName']} - Drops: {len(self.drops)} pcs. - In inventory: {self.in_inventory}"
|
||||
if Settings.logger.less
|
||||
else self.__repr__()
|
||||
)
|
||||
|
||||
def clear_drops(self):
|
||||
self.drops = list(
|
||||
filter(lambda x: x.dt_match is True and x.is_claimed is False, self.drops)
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return self.id == other.id
|
||||
else:
|
||||
return False
|
||||
|
||||
def sync_drops(self, drops, callback):
|
||||
# Iterate all the drops from inventory
|
||||
for drop in drops:
|
||||
# Iterate all the drops from out campaigns array
|
||||
# After id match update with:
|
||||
# [currentMinutesWatched, hasPreconditionsMet, dropInstanceID, isClaimed]
|
||||
for i in range(len(self.drops)):
|
||||
current_id = self.drops[i].id
|
||||
if drop["id"] == current_id:
|
||||
self.drops[i].update(drop["self"])
|
||||
# If after update we all conditions are meet we can claim the drop
|
||||
if self.drops[i].is_claimable is True:
|
||||
claimed = callback(self.drops[i])
|
||||
self.drops[i].is_claimed = claimed
|
||||
break
|
||||
103
TwitchChannelPointsMiner/classes/entities/Drop.py
Normal file
103
TwitchChannelPointsMiner/classes/entities/Drop.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from datetime import datetime
|
||||
|
||||
from TwitchChannelPointsMiner.classes.Settings import Settings
|
||||
from TwitchChannelPointsMiner.utils import percentage
|
||||
|
||||
|
||||
class Drop(object):
|
||||
__slots__ = [
|
||||
"id",
|
||||
"name",
|
||||
"benefit",
|
||||
"minutes_required",
|
||||
"has_preconditions_met",
|
||||
"current_minutes_watched",
|
||||
"drop_instance_id",
|
||||
"is_claimed",
|
||||
"is_claimable",
|
||||
"percentage_progress",
|
||||
"end_at",
|
||||
"start_at",
|
||||
"dt_match",
|
||||
"is_printable",
|
||||
]
|
||||
|
||||
def __init__(self, dict):
|
||||
self.id = dict["id"]
|
||||
self.name = dict["name"]
|
||||
self.benefit = ", ".join(
|
||||
list(set([bf["benefit"]["name"] for bf in dict["benefitEdges"]]))
|
||||
)
|
||||
self.minutes_required = dict["requiredMinutesWatched"]
|
||||
|
||||
self.has_preconditions_met = None # [True, False], None we don't know
|
||||
self.current_minutes_watched = 0
|
||||
self.drop_instance_id = None
|
||||
self.is_claimed = False
|
||||
self.is_claimable = False
|
||||
self.is_printable = False
|
||||
self.percentage_progress = 0
|
||||
|
||||
self.end_at = datetime.strptime(dict["endAt"], "%Y-%m-%dT%H:%M:%SZ")
|
||||
self.start_at = datetime.strptime(dict["startAt"], "%Y-%m-%dT%H:%M:%SZ")
|
||||
self.dt_match = self.start_at < datetime.now() < self.end_at
|
||||
|
||||
def update(
|
||||
self,
|
||||
progress,
|
||||
):
|
||||
self.has_preconditions_met = progress["hasPreconditionsMet"]
|
||||
|
||||
updated_percentage = percentage(
|
||||
progress["currentMinutesWatched"], self.minutes_required
|
||||
)
|
||||
quarter = round((updated_percentage / 25), 4).is_integer()
|
||||
self.is_printable = (
|
||||
# The new currentMinutesWatched are GT than previous
|
||||
progress["currentMinutesWatched"] > self.current_minutes_watched
|
||||
and (
|
||||
# The drop is printable when we have a new updated values and:
|
||||
# - also the percentage It's different and quarter is True (self.current_minutes_watched != 0 for skip boostrap phase)
|
||||
# - or we have watched 1 and the previous value is 0 - We are collecting a new drop :)
|
||||
(
|
||||
updated_percentage > self.percentage_progress
|
||||
and quarter is True
|
||||
and self.current_minutes_watched != 0
|
||||
)
|
||||
or (
|
||||
progress["currentMinutesWatched"] == 1
|
||||
and self.current_minutes_watched == 0
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
self.current_minutes_watched = progress["currentMinutesWatched"]
|
||||
self.drop_instance_id = progress["dropInstanceID"]
|
||||
self.is_claimed = progress["isClaimed"]
|
||||
self.is_claimable = (
|
||||
self.is_claimed is False and self.drop_instance_id is not None
|
||||
)
|
||||
self.percentage_progress = updated_percentage
|
||||
|
||||
def __repr__(self):
|
||||
return f"Drop(id={self.id}, name={self.name}, benefit={self.benefit}, minutes_required={self.minutes_required}, has_preconditions_met={self.has_preconditions_met}, current_minutes_watched={self.current_minutes_watched}, percentage_progress={self.percentage_progress}%, drop_instance_id={self.drop_instance_id}, is_claimed={self.is_claimed})"
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"{self.name} ({self.benefit}) {self.current_minutes_watched}/{self.minutes_required} ({self.percentage_progress}%)"
|
||||
if Settings.logger.less
|
||||
else self.__repr__()
|
||||
)
|
||||
|
||||
def progress_bar(self):
|
||||
progress = self.percentage_progress // 2
|
||||
remaining = (100 - self.percentage_progress) // 2
|
||||
if remaining + progress < 50:
|
||||
remaining += 50 - (remaining + progress)
|
||||
return f"|{('█' * progress)}{(' ' * remaining)}|\t{self.percentage_progress}% [{self.current_minutes_watched}/{self.minutes_required}]"
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return self.id == other.id
|
||||
else:
|
||||
return False
|
||||
@@ -4,7 +4,21 @@ from TwitchChannelPointsMiner.classes.Settings import Settings
|
||||
from TwitchChannelPointsMiner.utils import float_round
|
||||
|
||||
|
||||
class EventPrediction:
|
||||
class EventPrediction(object):
|
||||
__slots__ = [
|
||||
"streamer",
|
||||
"event_id",
|
||||
"title",
|
||||
"created_at",
|
||||
"prediction_window_seconds",
|
||||
"status",
|
||||
"final_result",
|
||||
"box_fillable",
|
||||
"bet_confirmed",
|
||||
"bet_placed",
|
||||
"bet",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
streamer: Streamer,
|
||||
@@ -30,11 +44,11 @@ class EventPrediction:
|
||||
self.bet = Bet(outcomes, streamer.settings.bet)
|
||||
|
||||
def __repr__(self):
|
||||
return f"EventPrediction(event_id={self.event_id}, title={self.title})"
|
||||
return f"EventPrediction(event_id={self.event_id}, streamer={self.streamer}, title={self.title})"
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"EventPrediction: {self.title}"
|
||||
f"EventPrediction: {self.streamer} - {self.title}"
|
||||
if Settings.logger.less
|
||||
else self.__repr__()
|
||||
)
|
||||
@@ -46,4 +60,4 @@ class EventPrediction:
|
||||
return float_round(self.prediction_window_seconds - self.elapsed(timestamp))
|
||||
|
||||
def print_recap(self) -> str:
|
||||
return f"{self}\n\t\t{self.streamer}\n\t\t{self.bet}\n\t\tResult: {self.final_result}"
|
||||
return f"{self}\n\t\t{self.bet}\n\t\tResult: {self.final_result}"
|
||||
|
||||
@@ -3,7 +3,18 @@ import json
|
||||
from TwitchChannelPointsMiner.utils import server_time
|
||||
|
||||
|
||||
class Message:
|
||||
class Message(object):
|
||||
__slots__ = [
|
||||
"topic",
|
||||
"topic_user",
|
||||
"message",
|
||||
"type",
|
||||
"data",
|
||||
"timestamp",
|
||||
"channel_id",
|
||||
"identifier",
|
||||
]
|
||||
|
||||
def __init__(self, data):
|
||||
self.topic, self.topic_user = data["topic"].split(".")
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class PubsubTopic:
|
||||
class PubsubTopic(object):
|
||||
__slots__ = ["topic", "user_id", "streamer"]
|
||||
|
||||
def __init__(self, topic, user_id=None, streamer=None):
|
||||
self.topic = topic
|
||||
self.user_id = user_id
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class Raid:
|
||||
class Raid(object):
|
||||
__slots__ = ["raid_id", "target_login"]
|
||||
|
||||
def __init__(self, raid_id, target_login):
|
||||
self.raid_id = raid_id
|
||||
self.target_login = target_login
|
||||
|
||||
@@ -4,19 +4,40 @@ import time
|
||||
from base64 import b64encode
|
||||
|
||||
from TwitchChannelPointsMiner.classes.Settings import Settings
|
||||
from TwitchChannelPointsMiner.constants.twitch import DROP_ID
|
||||
from TwitchChannelPointsMiner.constants import DROP_ID
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Stream:
|
||||
class Stream(object):
|
||||
__slots__ = [
|
||||
"broadcast_id",
|
||||
"title",
|
||||
"game",
|
||||
"tags",
|
||||
"drops_tags",
|
||||
"campaigns",
|
||||
"campaigns_ids",
|
||||
"viewers_count",
|
||||
"spade_url",
|
||||
"payload",
|
||||
"watch_streak_missing",
|
||||
"minute_watched",
|
||||
"__last_update",
|
||||
"__minute_watched_timestamp",
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self.broadcast_id = None
|
||||
|
||||
self.title = None
|
||||
self.game = {}
|
||||
self.tags = []
|
||||
self.drops_enabled = False
|
||||
|
||||
self.drops_tags = False
|
||||
self.campaigns = []
|
||||
self.campaigns_ids = []
|
||||
|
||||
self.viewers_count = 0
|
||||
self.__last_update = 0
|
||||
|
||||
@@ -36,7 +57,7 @@ class Stream:
|
||||
self.tags = tags
|
||||
self.viewers_count = viewers_count
|
||||
|
||||
self.drops_enabled = (
|
||||
self.drops_tags = (
|
||||
DROP_ID in [tag["id"] for tag in self.tags] and self.game != {}
|
||||
)
|
||||
self.__last_update = time.time()
|
||||
@@ -63,7 +84,10 @@ class Stream:
|
||||
return None if self.game in [{}, None] else self.game["name"]
|
||||
|
||||
def update_required(self):
|
||||
return self.__last_update == 0 or (time.time() - self.__last_update) >= 120
|
||||
return self.__last_update == 0 or self.update_elapsed() >= 120
|
||||
|
||||
def update_elapsed(self):
|
||||
return 0 if self.__last_update == 0 else (time.time() - self.__last_update)
|
||||
|
||||
def init_watch_streak(self):
|
||||
self.watch_streak_missing = True
|
||||
|
||||
@@ -7,13 +7,21 @@ from threading import Lock
|
||||
from TwitchChannelPointsMiner.classes.entities.Bet import BetSettings
|
||||
from TwitchChannelPointsMiner.classes.entities.Stream import Stream
|
||||
from TwitchChannelPointsMiner.classes.Settings import Settings
|
||||
from TwitchChannelPointsMiner.constants.twitch import URL
|
||||
from TwitchChannelPointsMiner.constants import URL
|
||||
from TwitchChannelPointsMiner.utils import _millify
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StreamerSettings(object):
|
||||
__slots__ = [
|
||||
"make_predictions",
|
||||
"follow_raid",
|
||||
"claim_drops",
|
||||
"watch_streak",
|
||||
"bet",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
make_predictions: bool = None,
|
||||
@@ -36,13 +44,30 @@ class StreamerSettings(object):
|
||||
self.bet = BetSettings()
|
||||
|
||||
def __repr__(self):
|
||||
return f"BetSettings(MakePredictions={self.make_predictions}, FollowRaid={self.follow_raid}, ClaimDrops={self.claim_drops}, WatchStreak={self.watch_streak}, Bet={self.bet})"
|
||||
return f"BetSettings(make_predictions={self.make_predictions}, follow_raid={self.follow_raid}, claim_drops={self.claim_drops}, watch_streak={self.watch_streak}, bet={self.bet})"
|
||||
|
||||
|
||||
class Streamer(object):
|
||||
__slots__ = [
|
||||
"username",
|
||||
"channel_id",
|
||||
"settings",
|
||||
"is_online",
|
||||
"stream_up",
|
||||
"online_at",
|
||||
"offline_at",
|
||||
"channel_points",
|
||||
"minute_watched_requests",
|
||||
"viewer_is_mod",
|
||||
"stream",
|
||||
"raid",
|
||||
"history",
|
||||
"streamer_url",
|
||||
]
|
||||
|
||||
def __init__(self, username, settings=None):
|
||||
self.username = username.lower().strip()
|
||||
self.channel_id = 0
|
||||
self.username: str = username.lower().strip()
|
||||
self.channel_id: str = ""
|
||||
self.settings = settings
|
||||
self.is_online = False
|
||||
self.stream_up = 0
|
||||
@@ -58,7 +83,6 @@ class Streamer(object):
|
||||
self.history = {}
|
||||
|
||||
self.streamer_url = f"{URL}/{self.username}"
|
||||
self.chat_url = f"{URL}/popout/{self.username}/chat?popout="
|
||||
|
||||
self.mutex = Lock()
|
||||
|
||||
@@ -91,14 +115,15 @@ class Streamer(object):
|
||||
return ", ".join(
|
||||
[
|
||||
f"{key}({self.history[key]['counter']} times, {_millify(self.history[key]['amount'])} gained)"
|
||||
for key in self.history
|
||||
for key in sorted(self.history)
|
||||
if self.history[key]["counter"] != 0
|
||||
]
|
||||
)
|
||||
|
||||
def update_history(self, reason_code, earned):
|
||||
def update_history(self, reason_code, earned, counter=1):
|
||||
if reason_code not in self.history:
|
||||
self.history[reason_code] = {"counter": 0, "amount": 0}
|
||||
self.history[reason_code]["counter"] += 1
|
||||
self.history[reason_code]["counter"] += counter
|
||||
self.history[reason_code]["amount"] += earned
|
||||
|
||||
if reason_code == "WATCH_STREAK":
|
||||
@@ -107,6 +132,15 @@ class Streamer(object):
|
||||
def stream_up_elapsed(self):
|
||||
return self.stream_up == 0 or ((time.time() - self.stream_up) > 120)
|
||||
|
||||
def drops_condition(self):
|
||||
return (
|
||||
self.settings.claim_drops is True
|
||||
and self.is_online is True
|
||||
and self.stream.drops_tags is True
|
||||
and self.stream.campaigns_ids != []
|
||||
)
|
||||
|
||||
# === ANALYTICS === #
|
||||
def persistent_points(self, event_type, event_text):
|
||||
event_type = event_type.upper()
|
||||
if event_type in ["WATCH_STREAK", "WIN", "LOSE"]:
|
||||
|
||||
@@ -5,6 +5,17 @@ WEBSOCKET = "wss://pubsub-edge.twitch.tv/v1"
|
||||
CLIENT_ID = "kimne78kx3ncx6brgo4mv6wki5h1ko"
|
||||
DROP_ID = "c2542d6d-cd10-4532-919b-3d19f30a768b"
|
||||
|
||||
USER_AGENTS = {
|
||||
"Windows": {
|
||||
"CHROME": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.104 Safari/537.36",
|
||||
"FIREFOX": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0",
|
||||
},
|
||||
"Linux": {
|
||||
"CHROME": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36",
|
||||
"FIREFOX": "Mozilla/5.0 (X11; Linux x86_64; rv:85.0) Gecko/20100101 Firefox/85.0",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class GQLOperations:
|
||||
url = "https://gql.twitch.tv/gql"
|
||||
@@ -90,3 +101,31 @@ class GQLOperations:
|
||||
}
|
||||
},
|
||||
}
|
||||
ViewerDropsDashboard = {
|
||||
"operationName": "ViewerDropsDashboard",
|
||||
"variables": {},
|
||||
"extensions": {
|
||||
"persistedQuery": {
|
||||
"version": 1,
|
||||
"sha256Hash": "c4d61d7b71d03b324914d3cf8ca0bc23fe25dacf54120cc954321b9704a3f4e2",
|
||||
}
|
||||
},
|
||||
}
|
||||
DropCampaignDetails = {
|
||||
"operationName": "DropCampaignDetails",
|
||||
"extensions": {
|
||||
"persistedQuery": {
|
||||
"version": 1,
|
||||
"sha256Hash": "7da6078b1bfa2f0a4dd061cb47bdcd1ffddf31cccadd966ec192e4cd06666e2b",
|
||||
}
|
||||
},
|
||||
}
|
||||
DropsHighlightService_AvailableDrops = {
|
||||
"operationName": "DropsHighlightService_AvailableDrops",
|
||||
"extensions": {
|
||||
"persistedQuery": {
|
||||
"version": 1,
|
||||
"sha256Hash": "b19ee96a0e79e3f8281c4108bc4c7b3f232266db6f96fd04a339ab393673a075",
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
USER_AGENTS = {
|
||||
"Windows": {
|
||||
"CHROME": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.104 Safari/537.36",
|
||||
"FIREFOX": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0",
|
||||
},
|
||||
"Linux": {
|
||||
"CHROME": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36",
|
||||
"FIREFOX": "Mozilla/5.0 (X11; Linux x86_64; rv:85.0) Gecko/20100101 Firefox/85.0",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class Selectors:
|
||||
# XPath Selector and Javascript helpers
|
||||
cookiePolicy = 'button[data-a-target="consent-banner-accept"]'
|
||||
coinsMenuXP = '//div[@data-test-selector="community-points-summary"]//button'
|
||||
betTitle = '[data-test-selector="predictions-list-item__title"]'
|
||||
betCustomVote = "button[data-test-selector='prediction-checkout-active-footer__input-type-toggle']"
|
||||
betMainDivXP = "//div[@id='channel-points-reward-center-body']//div[contains(@class,'custom-prediction-button')]"
|
||||
betVoteInputXP = f"({betMainDivXP}//input)"
|
||||
betVoteButtonXP = f"({betMainDivXP}//button)"
|
||||
|
||||
|
||||
class Javascript:
|
||||
# Helpers for selenium. I think is very useless
|
||||
coinsMenu = 'document.querySelector("[data-test-selector=\'community-points-summary\']").getElementsByTagName("button")[0].click();'
|
||||
betVoteInput = 'document.getElementById("channel-points-reward-center-body").getElementsByTagName("input")[{}].value = {};'
|
||||
betVoteButton = 'document.getElementById("channel-points-reward-center-body").getElementsByTagName("button")[{}].click();'
|
||||
betCustomVote = f'document.querySelector("{Selectors.betCustomVote}").click();'
|
||||
|
||||
# Some Javascript code that should help the script
|
||||
localStorage = """
|
||||
window.localStorage.setItem("volume", 0);
|
||||
window.localStorage.setItem("channelPointsOnboardingDismissed", true);
|
||||
window.localStorage.setItem("twilight.theme", 1);
|
||||
window.localStorage.setItem("mature", true);
|
||||
window.localStorage.setItem("rebrand-notice-dismissed", true);
|
||||
window.localStorage.setItem("emoteAnimationsEnabled", false);
|
||||
window.localStorage.setItem("chatPauseSetting", "ALTKEY");
|
||||
"""
|
||||
clearStyleChat = """
|
||||
var item = document.querySelector('[data-test-selector="chat-scrollable-area__message-container"]');
|
||||
if (item) {
|
||||
var parent = item.closest("div.simplebar-scroll-content");
|
||||
if(parent) parent.hidden = true;
|
||||
}
|
||||
var header = document.querySelector('[data-test-selector="channel-leaderboard-container"]');
|
||||
if(header) header.hidden = true;
|
||||
"""
|
||||
maximizeBetWindow = """
|
||||
var absolute = document.querySelector('[aria-describedby="channel-points-reward-center-body"]').closest("div.tw-absolute")
|
||||
if(absolute) absolute.classList.remove("tw-absolute")
|
||||
|
||||
document.getElementsByClassName("reward-center__content")[0].style.width = "44rem";
|
||||
document.getElementsByClassName("reward-center__content")[0].style.height = "55rem";
|
||||
|
||||
document.querySelector('[aria-describedby="channel-points-reward-center-body"]').style["max-height"] = "55rem";
|
||||
|
||||
document.getElementsByClassName("reward-center-body")[0].style["max-width"] = "44rem";
|
||||
// document.getElementsByClassName("reward-center-body")[0].style["min-height"] = "55rem";
|
||||
"""
|
||||
scrollDownBetWindow = """
|
||||
var scrollable = document.getElementById("channel-points-reward-center-body").closest("div.simplebar-scroll-content");
|
||||
scrollable.scrollTop = scrollable.scrollHeight;
|
||||
"""
|
||||
@@ -1,12 +1,14 @@
|
||||
import platform
|
||||
import re
|
||||
import socket
|
||||
import time
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timezone
|
||||
from random import randrange
|
||||
|
||||
from millify import millify
|
||||
|
||||
from TwitchChannelPointsMiner.constants.browser import USER_AGENTS
|
||||
from TwitchChannelPointsMiner.constants import USER_AGENTS
|
||||
|
||||
|
||||
def _millify(input, precision=2):
|
||||
@@ -35,10 +37,6 @@ def server_time(message_data):
|
||||
)
|
||||
|
||||
|
||||
def calculate_start_after(closing_bet_after, execution_time):
|
||||
return round(max(1, closing_bet_after - execution_time), 2)
|
||||
|
||||
|
||||
# https://en.wikipedia.org/wiki/Cryptographic_nonce
|
||||
def create_nonce(length=30) -> str:
|
||||
nonce = ""
|
||||
@@ -54,28 +52,9 @@ def create_nonce(length=30) -> str:
|
||||
return nonce
|
||||
|
||||
|
||||
def bet_condition(twitch_browser, event, logger) -> bool:
|
||||
if twitch_browser.currently_is_betting is True:
|
||||
logger.info(
|
||||
f"Sorry, unable to start {event}, the browser is currently betting on another event!"
|
||||
)
|
||||
return False
|
||||
elif twitch_browser.browser.current_url != "about:blank":
|
||||
logger.info(
|
||||
"Sorry, but the browser is not currently on 'about:blank' screen. Unable to start bet!"
|
||||
)
|
||||
return False
|
||||
elif event.streamer.viewer_is_mod is True:
|
||||
logger.info(f"Sorry, you are moderator of {event.streamer}, so you can't bet!")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_user_agent(browser) -> str:
|
||||
def get_user_agent(browser: str) -> str:
|
||||
try:
|
||||
return USER_AGENTS[platform.system()][
|
||||
browser.name if type(browser) != str else browser
|
||||
]
|
||||
return USER_AGENTS[platform.system()][browser]
|
||||
except KeyError:
|
||||
return USER_AGENTS["Linux"]["FIREFOX"]
|
||||
|
||||
@@ -87,10 +66,12 @@ def remove_emoji(string: str) -> str:
|
||||
"\U0001F300-\U0001F5FF" # symbols & pictographs
|
||||
"\U0001F680-\U0001F6FF" # transport & map symbols
|
||||
"\U0001F1E0-\U0001F1FF" # flags (iOS)
|
||||
"\U00002500-\U00002BEF" # chinese char
|
||||
"\U00002500-\U00002587" # chinese char
|
||||
"\U00002589-\U00002BEF" # I need Unicode Character “█” (U+2588)
|
||||
"\U00002702-\U000027B0"
|
||||
"\U00002702-\U000027B0"
|
||||
"\U000024C2-\U0001F251"
|
||||
"\U000024C2-\U00002587"
|
||||
"\U00002589-\U0001F251"
|
||||
"\U0001f926-\U0001f937"
|
||||
"\U00010000-\U0010ffff"
|
||||
"\u2640-\u2642"
|
||||
@@ -121,18 +102,22 @@ def remove_emoji(string: str) -> str:
|
||||
return emoji_pattern.sub(r"", string)
|
||||
|
||||
|
||||
def at_least_one_value_in_settings_is(array, attr_name, condition=True):
|
||||
return [
|
||||
itme for itme in array if getattr(itme.settings, attr_name) == condition
|
||||
] != []
|
||||
def at_least_one_value_in_settings_is(items, attr, value=True):
|
||||
for item in items:
|
||||
if getattr(item.settings, attr) == value:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def copy_values_if_none(settings, defaults):
|
||||
values = [
|
||||
name
|
||||
for name in dir(settings)
|
||||
if name.startswith("__") is False and callable(getattr(settings, name)) is False
|
||||
]
|
||||
values = list(
|
||||
filter(
|
||||
lambda x: x.startswith("__") is False
|
||||
and callable(getattr(settings, x)) is False,
|
||||
dir(settings),
|
||||
)
|
||||
)
|
||||
|
||||
for value in values:
|
||||
if getattr(settings, value) is None:
|
||||
setattr(settings, value, getattr(defaults, value))
|
||||
@@ -141,10 +126,27 @@ def copy_values_if_none(settings, defaults):
|
||||
|
||||
def set_default_settings(settings, defaults):
|
||||
# If no settings was provided use the default settings ...
|
||||
if settings is None:
|
||||
settings = defaults
|
||||
else:
|
||||
# If settings was provided but maybe are only partial set
|
||||
# Get the default values from Settings.streamer_settings
|
||||
settings = copy_values_if_none(settings, defaults)
|
||||
return settings
|
||||
# If settings was provided but maybe are only partial set
|
||||
# Get the default values from Settings.streamer_settings
|
||||
return (
|
||||
deepcopy(defaults)
|
||||
if settings is None
|
||||
else copy_values_if_none(settings, defaults)
|
||||
)
|
||||
|
||||
|
||||
def char_decision_as_index(char):
|
||||
return 0 if char == "A" else 1
|
||||
|
||||
|
||||
def internet_connection_available(host="8.8.8.8", port=53, timeout=3):
|
||||
try:
|
||||
socket.setdefaulttimeout(timeout)
|
||||
socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port))
|
||||
return True
|
||||
except socket.error:
|
||||
return False
|
||||
|
||||
|
||||
def percentage(a, b):
|
||||
return 0 if a == 0 else int((a / b) * 100)
|
||||
|
||||
65
example.py
65
example.py
@@ -3,54 +3,61 @@
|
||||
import logging
|
||||
from TwitchChannelPointsMiner import TwitchChannelPointsMiner
|
||||
from TwitchChannelPointsMiner.logger import LoggerSettings
|
||||
from TwitchChannelPointsMiner.classes.entities.Bet import Strategy, BetSettings
|
||||
from TwitchChannelPointsMiner.classes.Settings import Priority
|
||||
from TwitchChannelPointsMiner.classes.entities.Bet import Strategy, BetSettings, Condition, OutcomeKeys, FilterCondition
|
||||
from TwitchChannelPointsMiner.classes.entities.Streamer import Streamer, StreamerSettings
|
||||
from TwitchChannelPointsMiner.classes.TwitchBrowser import Browser, BrowserSettings
|
||||
|
||||
twitch_miner = TwitchChannelPointsMiner(
|
||||
username="your-twitch-username",
|
||||
claim_drops_startup=False, # If you want to auto claim all drops from Twitch inventory on startup
|
||||
password="write-your-secure-psw", # If no password will be provided the script will ask interactively
|
||||
claim_drops_startup=False, # If you want to auto claim all drops from Twitch inventory on startup
|
||||
priority=[ # Custom priority in this case for example:
|
||||
Priority.STREAK, # - we want first of all to catch all watch streak from all streamers
|
||||
Priority.DROPS, # - when we don't have anymore watch streak to catch wait until all drops are collected over the streamers
|
||||
Priority.ORDER # - when we have all of drops claimed and no watch-streak avaialable use the order priority (POINTS_ASCENDING, POINTS_DESCEDING)
|
||||
],
|
||||
logger_settings=LoggerSettings(
|
||||
save=True, # If you want to save logs in file (suggested)
|
||||
console_level=logging.INFO, # Level of logs - use logging.DEBUG for more info)
|
||||
file_level=logging.DEBUG, # Level of logs - If you think the log file it's too big use logging.INFO
|
||||
emoji=True, # On Windows we have a problem to print emoji. Set to false if you have a problem
|
||||
less=False # If you think that the logs are too much verborse set this to True
|
||||
),
|
||||
browser_settings=BrowserSettings(
|
||||
browser=Browser.FIREFOX, # Choose if you want to use Chrome or Firefox as browser
|
||||
show=False, # Show the browser during bet else headless mode
|
||||
do_screenshot=False, # Do screenshot during the bet
|
||||
save=True, # If you want to save logs in a file (suggested)
|
||||
console_level=logging.INFO, # Level of logs - use logging.DEBUG for more info)
|
||||
file_level=logging.DEBUG, # Level of logs - If you think the log file it's too big, use logging.INFO
|
||||
emoji=True, # On Windows, we have a problem printing emoji. Set to false if you have a problem
|
||||
less=False # If you think that the logs are too verbose, set this to True
|
||||
),
|
||||
streamer_settings=StreamerSettings(
|
||||
make_predictions=True, # If you want to Bet / Make prediction
|
||||
follow_raid=True, # Follow raid to obtain more points
|
||||
claim_drops=True, # We can't filter rewards base on stream. Set to False for skip viewing counter increase and you will never obtain a drop reward from this script. Issue #21
|
||||
watch_streak=True, # If a streamer go online change the priotiry of streamers array and catch the watch screak. Issue #11
|
||||
make_predictions=True, # If you want to Bet / Make prediction
|
||||
follow_raid=True, # Follow raid to obtain more points
|
||||
claim_drops=True, # We can't filter rewards base on stream. Set to False for skip viewing counter increase and you will never obtain a drop reward from this script. Issue #21
|
||||
watch_streak=True, # If a streamer go online change the priotiry of streamers array and catch the watch screak. Issue #11
|
||||
bet=BetSettings(
|
||||
strategy=Strategy.SMART, # Choose you strategy!
|
||||
percentage=5, # Place the x% of your channel points
|
||||
percentage_gap=20, # Gap difference between outcomesA and outcomesB (for SMART stragegy)
|
||||
max_points=50000, # If the x percentage of your channel points is gt bet_max_points set this value
|
||||
strategy=Strategy.SMART, # Choose you strategy!
|
||||
percentage=5, # Place the x% of your channel points
|
||||
percentage_gap=20, # Gap difference between outcomesA and outcomesB (for SMART stragegy)
|
||||
max_points=50000, # If the x percentage of your channel points is gt bet_max_points set this value
|
||||
stealth_mode=True, # If the calculated amount of channel points is GT the highest bet, place the highest value minus 1-2 points #33
|
||||
filter_condition=FilterCondition(
|
||||
by=OutcomeKeys.TOTAL_USERS, # Where apply the filter. Allowed [PERCENTAGE_USERS, ODDS_PERCENTAGE, ODDS, TOP_POINTS, TOTAL_USERS, TOTAL_POINTS]
|
||||
where=Condition.LTE, # 'by' must be [GT, LT, GTE, LTE] than value
|
||||
value=800
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# You can customize the settings for each streamer. If not settings was provided the script will use the streamer_settings from TwitchChannelPointsMiner.
|
||||
# If no streamer_settings provided in TwitchChannelPointsMiner the script will use default settings.
|
||||
# You can customize the settings for each streamer. If not settings were provided, the script would use the streamer_settings from TwitchChannelPointsMiner.
|
||||
# If no streamer_settings are provided in TwitchChannelPointsMiner the script will use default settings.
|
||||
# The streamers array can be a String -> username or Streamer instance.
|
||||
|
||||
# The settings priority are: settings in mine function, settings in TwitchChannelPointsMiner instance, default settings.
|
||||
# For example if in the mine function you don't provide any value for 'make_prediction' but you have set it on TwitchChannelPointsMiner instance the script will take the value from here.
|
||||
# For example, if in the mine function you don't provide any value for 'make_prediction' but you have set it on TwitchChannelPointsMiner instance, the script will take the value from here.
|
||||
# If you haven't set any value even in the instance the default one will be used
|
||||
|
||||
twitch_miner.mine(
|
||||
[
|
||||
Streamer("streamer-username01", settings=StreamerSettings(make_predictions=True , follow_raid=False , claim_drops=True , watch_streak=True , bet=BetSettings(strategy=Strategy.SMART , percentage=5 , percentage_gap=20 , max_points=234 ) )),
|
||||
Streamer("streamer-username02", settings=StreamerSettings(make_predictions=False , follow_raid=True , claim_drops=False , bet=BetSettings(strategy=Strategy.PERCENTAGE , percentage=5 , percentage_gap=20 , max_points=1234 ) )),
|
||||
Streamer("streamer-username03", settings=StreamerSettings(make_predictions=True , follow_raid=False , watch_streak=True , bet=BetSettings(strategy=Strategy.SMART , percentage=5 , percentage_gap=30 , max_points=50000 ) )),
|
||||
Streamer("streamer-username04", settings=StreamerSettings(make_predictions=False , follow_raid=True , watch_streak=True )),
|
||||
Streamer("streamer-username05", settings=StreamerSettings(make_predictions=True , follow_raid=True , claim_drops=True , watch_streak=True , bet=BetSettings(strategy=Strategy.HIGH_ODDS , percentage=7 , percentage_gap=20 , max_points=90 ) )),
|
||||
Streamer("streamer-username01", settings=StreamerSettings(make_predictions=True , follow_raid=False , claim_drops=True , watch_streak=True , bet=BetSettings(strategy=Strategy.SMART , percentage=5 , stealth_mode=True, percentage_gap=20 , max_points=234 , filter_condition=FilterCondition(by=OutcomeKeys.TOTAL_USERS, where=Condition.LTE, value=800 ) ) )),
|
||||
Streamer("streamer-username02", settings=StreamerSettings(make_predictions=False , follow_raid=True , claim_drops=False , bet=BetSettings(strategy=Strategy.PERCENTAGE , percentage=5 , stealth_mode=False, percentage_gap=20 , max_points=1234 , filter_condition=FilterCondition(by=OutcomeKeys.TOTAL_POINTS, where=Condition.GTE, value=250 ) ) )),
|
||||
Streamer("streamer-username03", settings=StreamerSettings(make_predictions=True , follow_raid=False , watch_streak=True , bet=BetSettings(strategy=Strategy.SMART , percentage=5 , stealth_mode=False, percentage_gap=30 , max_points=50000 , filter_condition=FilterCondition(by=OutcomeKeys.ODDS, where=Condition.LT, value=300 ) ) )),
|
||||
Streamer("streamer-username04", settings=StreamerSettings(make_predictions=False , follow_raid=True , watch_streak=True )),
|
||||
Streamer("streamer-username05", settings=StreamerSettings(make_predictions=True , follow_raid=True , claim_drops=True , watch_streak=True , bet=BetSettings(strategy=Strategy.HIGH_ODDS , percentage=7 , stealth_mode=True, percentage_gap=20 , max_points=90 , filter_condition=FilterCondition(by=OutcomeKeys.PERCENTAGE_USERS, where=Condition.GTE, value=300 ) ) )),
|
||||
Streamer("streamer-username06"),
|
||||
Streamer("streamer-username07"),
|
||||
Streamer("streamer-username08"),
|
||||
|
||||
@@ -2,7 +2,6 @@ requests
|
||||
websocket-client
|
||||
browser_cookie3
|
||||
pillow
|
||||
selenium
|
||||
python-dateutil
|
||||
emoji
|
||||
millify
|
||||
|
||||
4
setup.py
4
setup.py
@@ -30,8 +30,6 @@ setuptools.setup(
|
||||
long_description_content_type="text/markdown",
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.4",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
@@ -45,5 +43,5 @@ setuptools.setup(
|
||||
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
||||
"Natural Language :: English",
|
||||
],
|
||||
python_requires=">=3, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
|
||||
python_requires=">=3, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user