Merge branch 'master' into docker-ci
This commit is contained in:
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -10,7 +10,7 @@ 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)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected)
|
||||
|
||||
# How Has This Been Tested?
|
||||
|
||||
@@ -19,7 +19,7 @@ Please describe the tests that you ran to verify your changes. Provide instructi
|
||||
# Checklist:
|
||||
|
||||
- [ ] My code follows the style guidelines of this project
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have performed a self-review of my 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
|
||||
|
||||
2
.github/stale.yml
vendored
2
.github/stale.yml
vendored
@@ -1,5 +1,5 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 60
|
||||
daysUntilStale: 150
|
||||
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -148,4 +148,5 @@ chromedriver*
|
||||
cookies/*
|
||||
logs/*
|
||||
screenshots/*
|
||||
htmls/*
|
||||
htmls/*
|
||||
analytics/*
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
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
|
||||
- Check out the [existing issues](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues) & see if there is already an opened issue.
|
||||
|
||||
### Ready to make a change? Fork the repo
|
||||
|
||||
@@ -72,14 +72,14 @@ Labels can help you find an issue you'd like to help with.
|
||||
- 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 `help wanted` is used 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 `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.
|
||||
You can use the GitHub user interface :pencil2: for minor 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.
|
||||
|
||||
206
README.md
206
README.md
@@ -1,4 +1,4 @@
|
||||

|
||||

|
||||
<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/downloads/release/python-360/"><img alt="Python3" src="https://img.shields.io/badge/built%20for-Python≥3.6-red.svg?style=flat"></a>
|
||||
@@ -18,6 +18,14 @@
|
||||
|
||||
Read more about channels point [here](https://help.twitch.tv/s/article/channel-points-guide)
|
||||
|
||||
## 📢 Help wanted
|
||||
Currently, we have a lot of PRs requests opened, but the time to test and improve It's less and less. If you want to help the community and the project, please test the following PRs and give us feedback:
|
||||
- [Features/improvements to analytics #131](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/pull/131)
|
||||
- [Add SMART_HIGH_ODDS strategy #172](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/pull/172)
|
||||
- [Replace join_chat boolean with chat: ChatPresence #253](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/pull/253)
|
||||
- [Receive updates to Telegram #254](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/pull/254)
|
||||
- [Add stats #318](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/pull/318)
|
||||
|
||||
# README Contents
|
||||
1. 🤝 [Community](#community)
|
||||
2. 🚀 [Main differences from the original repository](#main-differences-from-the-original-repository)
|
||||
@@ -27,6 +35,7 @@ Read more about channels point [here](https://help.twitch.tv/s/article/channel-p
|
||||
- [Final report](#final-report)
|
||||
4. 🧐 [How to use](#how-to-use)
|
||||
- [Cloning](#by-cloning-the-repository)
|
||||
- [pip](#pip)
|
||||
- [Docker](#docker)
|
||||
- [Limits](#limits)
|
||||
5. 🔧 [Settings](#settings)
|
||||
@@ -36,21 +45,23 @@ Read more about channels point [here](https://help.twitch.tv/s/article/channel-p
|
||||
- [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)
|
||||
6. 📈 [Analytics](#analytics)
|
||||
7. 🍪 [Migrating from an old repository (the original one)](#migrating-from-an-old-repository-the-original-one)
|
||||
8. 🪟 [Windows](#windows)
|
||||
9. 📱 [Termux](#termux)
|
||||
10. ⚠️ [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 have any type of issue, need help, or 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 ❤️
|
||||
If you want to offer me a coffee, I would be grateful ❤️
|
||||
|
||||
<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)
|
||||
If you have any issues or you want to contribute, you are welcome! But please before read the [CONTRIBUTING.md](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/blob/master/CONTRIBUTING.md) file.
|
||||
|
||||
## Main differences from the original repository:
|
||||
|
||||
@@ -62,6 +73,8 @@ If you have any issues or you want to contribute, you are welcome! But please be
|
||||
- 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 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))
|
||||
- Analytics chart that show the progress of your points with various annotations [#96](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/96)
|
||||
- Join IRC Chat for increase watch-time and get StreamElements points [#47](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/47)
|
||||
|
||||
## Logs feature
|
||||
### Full logs
|
||||
@@ -161,18 +174,7 @@ No browser needed. [#41](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner
|
||||
```
|
||||
|
||||
## How to use:
|
||||
|
||||
### By cloning the repository
|
||||
|
||||
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` . 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)
|
||||
First of all please create a run.py file. You can just copy [example.py](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/blob/master/example.py) and modify it according to your needs.
|
||||
```python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
@@ -181,17 +183,17 @@ from colorama import Fore
|
||||
from TwitchChannelPointsMiner import TwitchChannelPointsMiner
|
||||
from TwitchChannelPointsMiner.logger import LoggerSettings, ColorPalette
|
||||
from TwitchChannelPointsMiner.classes.Settings import Priority
|
||||
from TwitchChannelPointsMiner.classes.entities.Bet import Strategy, BetSettings, Condition, OutcomeKeys, FilterCondition
|
||||
from TwitchChannelPointsMiner.classes.entities.Bet import Strategy, BetSettings, Condition, OutcomeKeys, FilterCondition, DelayMode
|
||||
from TwitchChannelPointsMiner.classes.entities.Streamer import Streamer, StreamerSettings
|
||||
|
||||
twitch_miner = TwitchChannelPointsMiner(
|
||||
username="your-twitch-username",
|
||||
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
|
||||
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 the 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)
|
||||
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 the drops claimed and no watch-streak available, use the order priority (POINTS_ASCENDING, POINTS_DESCEDING)
|
||||
],
|
||||
logger_settings=LoggerSettings(
|
||||
save=True, # If you want to save logs in a file (suggested)
|
||||
@@ -201,7 +203,7 @@ twitch_miner = TwitchChannelPointsMiner(
|
||||
less=False, # If you think that the logs are too verbose, set this to True
|
||||
colored=True, # If you want to print colored text
|
||||
color_palette=ColorPalette( # You can also create a custom palette color (for the common message).
|
||||
STREAMER_online="GREEN", # Don't worry about lower/upper case the script will be parse all the values.
|
||||
STREAMER_online="GREEN", # Don't worry about lower/upper case. The script will parse all the values.
|
||||
streamer_offline="red", # Read more in README.md
|
||||
BET_wiN=Fore.MAGENTA # Color allowed are: [BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, RESET].
|
||||
)
|
||||
@@ -211,15 +213,19 @@ twitch_miner = TwitchChannelPointsMiner(
|
||||
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
|
||||
join_chat=True, # Join irc chat to increase watch-time
|
||||
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
|
||||
stealth_mode=True, # If the calculated amount of channel points is GT the highest bet, place the highest value minus 1-2 points #33
|
||||
stealth_mode=True, # If the calculated amount of channel points is GT the highest bet, place the highest value minus 1-2 points Issue #33
|
||||
delay_mode=DelayMode.FROM_END, # When placing a bet, we will wait until `delay` seconds before the end of the timer
|
||||
delay=6,
|
||||
minimum_points=2000, # Place the bet only if we have at least 20k points. Issue #113
|
||||
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
|
||||
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
|
||||
)
|
||||
)
|
||||
@@ -259,13 +265,29 @@ 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)
|
||||
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 the `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
|
||||
twitch_miner.mine(followers=True, blacklist=["user1", "user2"]) # Blacklist example
|
||||
```
|
||||
4. Start mining! `python run.py`
|
||||
|
||||
### By cloning the repository
|
||||
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` . 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
|
||||
```
|
||||
|
||||
Start mining! `python run.py` 🥳
|
||||
|
||||
### pip
|
||||
Install the package via pip, you will find a stable version - maybe a different version from the master branch.
|
||||
- `pip install Twitch-Channel-Points-Miner-v2`
|
||||
- Exceute the run.py file `python run.py` 🥳
|
||||
|
||||
### Docker
|
||||
|
||||
@@ -300,16 +322,17 @@ services:
|
||||
```
|
||||
|
||||
### 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.
|
||||
> Twitch has a limit - you can't watch more than two 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 has the highest priority.
|
||||
|
||||
## 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.
|
||||
Most of the settings are self-explained and are commented on in the 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 using 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
|
||||
- `SUBSCRIBED` - Prioritize streamers you're subscribed to (higher subscription tiers are mined first)
|
||||
- `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
|
||||
@@ -325,6 +348,7 @@ You can combine all priority but keep in mind that use `ORDER` and `POINTS_ASCEN
|
||||
| `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 |
|
||||
| `colored` | bool | True | If you want to print colored text [#45](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/45) [#82](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/82) |
|
||||
| `auto_clear` | bool | True | Create a file rotation handler with interval = 1D and backupCount = 7 [#215](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/215) |
|
||||
| `color_palette` | ColorPalette | All messages are Fore.RESET except WIN and LOSE bet (GREEN and RED) | Create your custom color palette. Read more above. |
|
||||
|
||||
#### Color Palette
|
||||
@@ -348,17 +372,18 @@ The script was developed to handle all the human error, lower-case upper case an
|
||||
```python
|
||||
from colorama import Fore
|
||||
ColorPalette(
|
||||
"STREAMER_ONLINE" = Fore.GREEN,
|
||||
"STREAMER_OFFLINE" = Fore.RED,
|
||||
"GAIN_FOR_RAID" = Fore.YELLOW,
|
||||
"GAIN_FOR_CLAIM" = Fore.YELLOW,
|
||||
"GAIN_FOR_WATCH" = Fore.YELLOW,
|
||||
"BET_WIN" = Fore.GREEN,
|
||||
"BET_LOSE" = Fore.RED,
|
||||
"BET_REFUND" = Fore.RESET,
|
||||
"BET_FILTERS" = Fore.MAGENTA,
|
||||
"BET_GENERAL" = Fore.BLUE,
|
||||
"BET_FAILED" = Fore.RED,
|
||||
STREAMER_ONLINE = Fore.GREEN,
|
||||
STREAMER_OFFLINE = Fore.RED,
|
||||
GAIN_FOR_RAID = Fore.YELLOW,
|
||||
GAIN_FOR_CLAIM = Fore.YELLOW,
|
||||
GAIN_FOR_WATCH = Fore.YELLOW,
|
||||
GAIN_FOR_WATCH_STREAK = Fore.YELLOW,
|
||||
BET_WIN = Fore.GREEN,
|
||||
BET_LOSE = Fore.RED,
|
||||
BET_REFUND = Fore.RESET,
|
||||
BET_FILTERS = Fore.MAGENTA,
|
||||
BET_GENERAL = Fore.BLUE,
|
||||
BET_FAILED = Fore.RED,
|
||||
)
|
||||
```
|
||||
|
||||
@@ -367,34 +392,36 @@ ColorPalette(
|
||||
|-------------------- |------------- |-------------------------------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `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) |
|
||||
| `claim_drops` | bool | True | If this value is True, the script will increase the watch-time for the current game. With this, you can 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) |
|
||||
| `join_chat` | bool | True | Join IRC-Chat to appear online in chat and attempt to get StreamElements channel points and increase view-time [#47](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/47) |
|
||||
| `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) |
|
||||
| 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) |
|
||||
| `delay_mode` | DelayMode | FROM_END | Define how is calculating the waiting time before placing a bet |
|
||||
| `delay` | float | 6 | Value to be used to calculate bet delay depending on `delay_mode` value |
|
||||
|
||||
#### 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 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
|
||||
- **SMART**: If the majority in percent chose an option, then follow the other users, otherwise select the option with the highest odds
|
||||
|
||||

|
||||

|
||||
|
||||
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 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.
|
||||
- **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 is higher than `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'
|
||||
### FilterCondition
|
||||
@@ -424,8 +451,39 @@ Allowed values for `where` are: `GT, LT, GTE, LTE`
|
||||
- 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)`
|
||||
|
||||
### DelayMode
|
||||
|
||||
- **FROM_START**: Will wait `delay` seconds from when the bet was opened
|
||||
- **FROM_END**: Will until there is `delay` seconds left to place the bet
|
||||
- **PERCENTAGE**: Will place the bet when `delay` percent of the set timer is elapsed
|
||||
|
||||
Here's a concrete example. Let's suppose we have a bet that is opened with a timer of 10 minutes:
|
||||
|
||||
- **FROM_START** with `delay=20`: The bet will be placed 20s after the bet is opened
|
||||
- **FROM_END** with `delay=20`: The bet will be placed 20s before the end of the bet (so 9mins 40s after the bet is opened)
|
||||
- **PERCENTAGE** with `delay=0.2`: The bet will be placed when the timer went down by 20% (so 2mins after the bet is opened)
|
||||
|
||||
## Analytics
|
||||
We have recently introduced a little frontend where you can show with a chart you points trend. The script will spawn a Flask web-server on your machine where you can select binding address and port.
|
||||
The chart provides some annotation to handle the prediction and watch strike events. Usually annotation are used to notice big increase / decrease of points. If you want to can disable annotations.
|
||||
On each (x, y) points Its present a tooltip that show points, date time and reason of points gained / lost. This web page was just a funny idea, and it is not intended to use for a professional usage.
|
||||
If you want you can toggle the dark theme with the dedicated checkbox.
|
||||
|
||||
| Light theme | Dark theme |
|
||||
| ----------- | ---------- |
|
||||
|  |  |
|
||||
|
||||
For use this feature just call the `analytics` method before start mining. Read more at: [#96](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/96)
|
||||
The chart will be autofreshed each `refresh` minutes. If you want to connect from one to second machine that have that webpanel you have to use `0.0.0.0` instead of `127.0.0.1`.
|
||||
```python
|
||||
from TwitchChannelPointsMiner import TwitchChannelPointsMiner
|
||||
twitch_miner = TwitchChannelPointsMiner("your-twitch-username")
|
||||
twitch_miner.analytics(host="127.0.0.1", port=5000, refresh=5) # Analytics web-server
|
||||
twitch_miner.mine(followers=True, blacklist=["user1", "user2"])
|
||||
```
|
||||
|
||||
## 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`
|
||||
If you already have a `twitch-cookies.pkl` and you don't want to log in 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
|
||||
@@ -444,5 +502,35 @@ Other useful info can be founded here:
|
||||
|
||||
You can also follow this [video tutorial](https://www.youtube.com/watch?v=0VkM7NOZkuA).
|
||||
|
||||
## Termux
|
||||
Install the requirements
|
||||
```
|
||||
pkg install python git rust libjpeg-turbo libcrypt ndk-sysroot clang zlib`
|
||||
LDFLAGS="-L${PREFIX}/lib/" CFLAGS="-I${PREFIX}/include/" pip install --upgrade wheel pillow
|
||||
```
|
||||
|
||||
Clone this repository
|
||||
`git clone https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2`
|
||||
|
||||
(2 way):
|
||||
Download sources from GitHub and put it into your Termux storage
|
||||
|
||||
Now you can enter the directory with our miner, type this command:
|
||||
`cd Twitch-Channel-Points-Miner-v2`
|
||||
|
||||
Configure your miner on your preferences by typing
|
||||
`nano example.py`
|
||||
|
||||
When you have configured it now we can rename it (optional):
|
||||
`mv example.py run.py`
|
||||
|
||||
We have to also install dependences required to run miner:
|
||||
`pip install -r requirements.txt`
|
||||
|
||||
Now when we did everything we can run miner:
|
||||
`python run.py`
|
||||
|
||||
Read more at [#92](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/92) [#76](https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/issues/76)
|
||||
|
||||
## Disclaimer
|
||||
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.
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from TwitchChannelPointsMiner.classes.AnalyticsServer import AnalyticsServer
|
||||
from TwitchChannelPointsMiner.classes.Chat import ThreadChat
|
||||
from TwitchChannelPointsMiner.classes.entities.PubsubTopic import PubsubTopic
|
||||
from TwitchChannelPointsMiner.classes.entities.Streamer import (
|
||||
Streamer,
|
||||
@@ -24,6 +26,7 @@ from TwitchChannelPointsMiner.logger import LoggerSettings, configure_loggers
|
||||
from TwitchChannelPointsMiner.utils import (
|
||||
_millify,
|
||||
at_least_one_value_in_settings_is,
|
||||
check_versions,
|
||||
get_user_agent,
|
||||
internet_connection_available,
|
||||
set_default_settings,
|
||||
@@ -33,8 +36,14 @@ from TwitchChannelPointsMiner.utils import (
|
||||
# - chardet.charsetprober - [feed]
|
||||
# - chardet.charsetprober - [get_confidence]
|
||||
# - requests - [Starting new HTTPS connection (1)]
|
||||
# - Flask (werkzeug) logs
|
||||
# - irc.client - [process_data]
|
||||
# - irc.client - [_dispatcher]
|
||||
# - irc.client - [_handle_message]
|
||||
logging.getLogger("chardet.charsetprober").setLevel(logging.ERROR)
|
||||
logging.getLogger("requests").setLevel(logging.ERROR)
|
||||
logging.getLogger("werkzeug").setLevel(logging.ERROR)
|
||||
logging.getLogger("irc.client").setLevel(logging.ERROR)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -62,12 +71,16 @@ class TwitchChannelPointsMiner:
|
||||
username: str,
|
||||
password: str = None,
|
||||
claim_drops_startup: bool = False,
|
||||
# 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(),
|
||||
# Default values for all streamers
|
||||
streamer_settings: StreamerSettings = StreamerSettings(),
|
||||
):
|
||||
Settings.analytics_path = os.path.join(Path().absolute(), "analytics", username)
|
||||
Path(Settings.analytics_path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self.username = username
|
||||
|
||||
# Set as global config
|
||||
@@ -97,9 +110,25 @@ class TwitchChannelPointsMiner:
|
||||
|
||||
self.logs_file = configure_loggers(self.username, logger_settings)
|
||||
|
||||
# Check for the latest version of the script
|
||||
current_version, github_version = check_versions()
|
||||
if github_version == "0.0.0":
|
||||
logger.error(
|
||||
"Unable to detect if you have the latest version of this script"
|
||||
)
|
||||
elif current_version != github_version:
|
||||
logger.info(f"You are running the version {current_version} of this script")
|
||||
logger.info(f"The latest version on GitHub is: {github_version}")
|
||||
|
||||
for sign in [signal.SIGINT, signal.SIGSEGV, signal.SIGTERM]:
|
||||
signal.signal(sign, self.end)
|
||||
|
||||
def analytics(self, host: str = "127.0.0.1", port: int = 5000, refresh: int = 5):
|
||||
http_server = AnalyticsServer(host=host, port=port, refresh=refresh)
|
||||
http_server.daemon = True
|
||||
http_server.name = "Analytics Thread"
|
||||
http_server.start()
|
||||
|
||||
def mine(self, streamers: list = [], blacklist: list = [], followers=False):
|
||||
self.run(streamers=streamers, blacklist=blacklist, followers=followers)
|
||||
|
||||
@@ -139,53 +168,55 @@ class TwitchChannelPointsMiner:
|
||||
)
|
||||
for username in followers_array:
|
||||
if username not in streamers_dict and username not in blacklist:
|
||||
streamers_name.append(username)
|
||||
streamers_dict[username] = username.lower().strip()
|
||||
else:
|
||||
followers_array = []
|
||||
|
||||
streamers_name = list(
|
||||
OrderedDict.fromkeys(streamers_name + followers_array)
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Loading data for {len(streamers_name)} streamers. Please wait...",
|
||||
extra={"emoji": ":nerd_face:"},
|
||||
)
|
||||
for username in streamers_name:
|
||||
time.sleep(random.uniform(0.3, 0.7))
|
||||
try:
|
||||
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
|
||||
)
|
||||
streamer.settings.bet = set_default_settings(
|
||||
streamer.settings.bet, Settings.streamer_settings.bet
|
||||
)
|
||||
self.streamers.append(streamer)
|
||||
except StreamerDoesNotExistException:
|
||||
logger.info(
|
||||
f"Streamer {username} does not exist",
|
||||
extra={"emoji": ":cry:"},
|
||||
)
|
||||
if username in streamers_name:
|
||||
time.sleep(random.uniform(0.3, 0.7))
|
||||
try:
|
||||
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
|
||||
)
|
||||
streamer.settings.bet = set_default_settings(
|
||||
streamer.settings.bet, Settings.streamer_settings.bet
|
||||
)
|
||||
if streamer.settings.join_chat is True:
|
||||
streamer.irc_chat = ThreadChat(
|
||||
self.username,
|
||||
self.twitch.twitch_login.get_auth_token(),
|
||||
streamer.username,
|
||||
)
|
||||
self.streamers.append(streamer)
|
||||
except StreamerDoesNotExistException:
|
||||
logger.info(
|
||||
f"Streamer {username} does not exist",
|
||||
extra={"emoji": ":cry:"},
|
||||
)
|
||||
|
||||
# Populate the streamers with default values.
|
||||
# 1. Load channel points and auto-claim bonus
|
||||
# 2. Check if streamers are online
|
||||
# 3. Check if the user is a Streamer. In thi case you can't do prediction
|
||||
# 3. DEACTIVATED: Check if the user is a moderator. (was used before the 5th of April 2021 to deactivate predictions)
|
||||
for streamer in self.streamers:
|
||||
time.sleep(random.uniform(0.3, 0.7))
|
||||
self.twitch.load_channel_points_context(streamer)
|
||||
self.twitch.check_streamer_online(streamer)
|
||||
self.twitch.viewer_is_mod(streamer)
|
||||
if streamer.viewer_is_mod is True:
|
||||
streamer.settings.make_predictions = False
|
||||
# self.twitch.viewer_is_mod(streamer)
|
||||
|
||||
self.original_streamers = copy.deepcopy(self.streamers)
|
||||
self.original_streamers = [
|
||||
streamer.channel_points for streamer in self.streamers
|
||||
]
|
||||
|
||||
# If we have at least one streamer with settings = make_predictions True
|
||||
make_predictions = at_least_one_value_in_settings_is(
|
||||
@@ -220,10 +251,11 @@ class TwitchChannelPointsMiner:
|
||||
)
|
||||
|
||||
# Subscribe to community-points-user. Get update for points spent or gains
|
||||
user_id = self.twitch.twitch_login.get_user_id()
|
||||
self.ws_pool.submit(
|
||||
PubsubTopic(
|
||||
"community-points-user-v1",
|
||||
user_id=self.twitch.twitch_login.get_user_id(),
|
||||
user_id=user_id,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -232,7 +264,7 @@ class TwitchChannelPointsMiner:
|
||||
self.ws_pool.submit(
|
||||
PubsubTopic(
|
||||
"predictions-user-v1",
|
||||
user_id=self.twitch.twitch_login.get_user_id(),
|
||||
user_id=user_id,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -249,6 +281,7 @@ class TwitchChannelPointsMiner:
|
||||
PubsubTopic("predictions-channel-v1", streamer=streamer)
|
||||
)
|
||||
|
||||
refresh_context = time.time()
|
||||
while self.running:
|
||||
time.sleep(random.uniform(20, 60))
|
||||
# Do an external control for WebSocket. Check if the thread is running
|
||||
@@ -264,11 +297,26 @@ class TwitchChannelPointsMiner:
|
||||
)
|
||||
WebSocketsPool.handle_reconnection(self.ws_pool.ws[index])
|
||||
|
||||
if ((time.time() - refresh_context) // 60) >= 30:
|
||||
refresh_context = time.time()
|
||||
for index in range(0, len(self.streamers)):
|
||||
if self.streamers[index].is_online:
|
||||
self.twitch.load_channel_points_context(
|
||||
self.streamers[index]
|
||||
)
|
||||
|
||||
def end(self, signum, frame):
|
||||
logger.info("CTRL+C Detected! Please wait just a moment!")
|
||||
|
||||
for streamer in self.streamers:
|
||||
if streamer.irc_chat is not None:
|
||||
streamer.leave_chat()
|
||||
if streamer.irc_chat.is_alive() is True:
|
||||
streamer.irc_chat.join()
|
||||
|
||||
self.running = self.twitch.running = False
|
||||
self.ws_pool.end()
|
||||
if self.ws_pool is not None:
|
||||
self.ws_pool.end()
|
||||
|
||||
if self.minute_watcher_thread is not None:
|
||||
self.minute_watcher_thread.join()
|
||||
@@ -276,7 +324,13 @@ class TwitchChannelPointsMiner:
|
||||
if self.sync_campaigns_thread is not None:
|
||||
self.sync_campaigns_thread.join()
|
||||
|
||||
time.sleep(1)
|
||||
# Check if all the mutex are unlocked.
|
||||
# Prevent breaks of .json file
|
||||
for streamer in self.streamers:
|
||||
if streamer.mutex.locked():
|
||||
streamer.mutex.acquire()
|
||||
streamer.mutex.release()
|
||||
|
||||
self.__print_report()
|
||||
|
||||
sys.exit(0)
|
||||
@@ -322,7 +376,7 @@ class TwitchChannelPointsMiner:
|
||||
if self.streamers[streamer_index].history != {}:
|
||||
gained = (
|
||||
self.streamers[streamer_index].channel_points
|
||||
- self.original_streamers[streamer_index].channel_points
|
||||
- self.original_streamers[streamer_index]
|
||||
)
|
||||
logger.info(
|
||||
f"{repr(self.streamers[streamer_index])}, Total Points Gained (after farming - before farming): {_millify(gained)}",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
__version__ = "2.7.1"
|
||||
__version__ = "2.0.5"
|
||||
from .TwitchChannelPointsMiner import TwitchChannelPointsMiner
|
||||
|
||||
__all__ = [
|
||||
|
||||
92
TwitchChannelPointsMiner/classes/AnalyticsServer.py
Normal file
92
TwitchChannelPointsMiner/classes/AnalyticsServer.py
Normal file
@@ -0,0 +1,92 @@
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
from threading import Thread
|
||||
|
||||
from flask import Flask, Response, cli, render_template
|
||||
|
||||
from TwitchChannelPointsMiner.classes.Settings import Settings
|
||||
from TwitchChannelPointsMiner.utils import download_file
|
||||
|
||||
cli.show_server_banner = lambda *_: None
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def streamers_available():
|
||||
path = Settings.analytics_path
|
||||
return [
|
||||
f
|
||||
for f in os.listdir(path)
|
||||
if os.path.isfile(os.path.join(path, f)) and f.endswith(".json")
|
||||
]
|
||||
|
||||
|
||||
def read_json(streamer):
|
||||
path = Settings.analytics_path
|
||||
streamer = streamer if streamer.endswith(".json") else f"{streamer}.json"
|
||||
return Response(
|
||||
open(os.path.join(path, streamer)) if streamer in streamers_available() else [],
|
||||
status=200,
|
||||
mimetype="application/json",
|
||||
)
|
||||
|
||||
|
||||
def index(refresh=5):
|
||||
return render_template(
|
||||
"charts.html",
|
||||
refresh=(refresh * 60 * 1000),
|
||||
streamers=",".join(streamers_available()),
|
||||
)
|
||||
|
||||
|
||||
def download_assets(assets_folder, required_files):
|
||||
Path(assets_folder).mkdir(parents=True, exist_ok=True)
|
||||
logger.info(f"Downloading assets to {assets_folder}")
|
||||
|
||||
for f in required_files:
|
||||
if os.path.isfile(os.path.join(assets_folder, f)) is False:
|
||||
if (
|
||||
download_file(os.path.join("assets", f), os.path.join(assets_folder, f))
|
||||
is True
|
||||
):
|
||||
logger.info(f"Downloaded {f}")
|
||||
|
||||
|
||||
def check_assets():
|
||||
required_files = ["banner.png", "charts.html", "dark-theme.css"]
|
||||
assets_folder = os.path.join(Path().absolute(), "assets")
|
||||
if os.path.isdir(assets_folder) is False:
|
||||
logger.info(f"Assets folder not found at {assets_folder}")
|
||||
download_assets(assets_folder, required_files)
|
||||
else:
|
||||
for f in required_files:
|
||||
if os.path.isfile(os.path.join(assets_folder, f)) is False:
|
||||
logger.info(f"Missing file {f} in {assets_folder}")
|
||||
download_assets(assets_folder, required_files)
|
||||
break
|
||||
|
||||
|
||||
class AnalyticsServer(Thread):
|
||||
def __init__(self, host: str = "127.0.0.1", port: int = 5000, refresh: int = 5):
|
||||
super(AnalyticsServer, self).__init__()
|
||||
|
||||
check_assets()
|
||||
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.refresh = refresh
|
||||
|
||||
self.app = Flask(
|
||||
__name__,
|
||||
template_folder=os.path.join(Path().absolute(), "assets"),
|
||||
static_folder=os.path.join(Path().absolute(), "assets"),
|
||||
)
|
||||
self.app.add_url_rule("/", "index", index, defaults={"refresh": refresh})
|
||||
self.app.add_url_rule("/json/<string:streamer>", "json", read_json)
|
||||
|
||||
def run(self):
|
||||
logger.info(
|
||||
f"Analytics running on http://{self.host}:{self.port}/",
|
||||
extra={"emoji": ":globe_with_meridians:"},
|
||||
)
|
||||
self.app.run(host=self.host, port=self.port, threaded=True)
|
||||
75
TwitchChannelPointsMiner/classes/Chat.py
Normal file
75
TwitchChannelPointsMiner/classes/Chat.py
Normal file
@@ -0,0 +1,75 @@
|
||||
import logging
|
||||
import time
|
||||
from threading import Thread
|
||||
|
||||
from irc.bot import SingleServerIRCBot
|
||||
|
||||
from TwitchChannelPointsMiner.constants import IRC, IRC_PORT
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ClientIRC(SingleServerIRCBot):
|
||||
def __init__(self, username, token, channel):
|
||||
self.token = token
|
||||
self.channel = "#" + channel
|
||||
self.__active = False
|
||||
|
||||
super(ClientIRC, self).__init__(
|
||||
[(IRC, IRC_PORT, f"oauth:{token}")], username, username
|
||||
)
|
||||
|
||||
def on_welcome(self, client, event):
|
||||
client.join(self.channel)
|
||||
|
||||
def start(self):
|
||||
self.__active = True
|
||||
self._connect()
|
||||
while self.__active:
|
||||
try:
|
||||
self.reactor.process_once(timeout=0.2)
|
||||
time.sleep(0.01)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Exception raised: {e}. Thread is active: {self.__active}"
|
||||
)
|
||||
|
||||
def die(self, msg="Bye, cruel world!"):
|
||||
self.connection.disconnect(msg)
|
||||
self.__active = False
|
||||
|
||||
"""
|
||||
def on_join(self, connection, event):
|
||||
logger.info(f"Event: {event}", extra={"emoji": ":speech_balloon:"})
|
||||
|
||||
def on_pubmsg(self, client, message):
|
||||
logger.info(f"Message: {message}", extra={"emoji": ":speech_balloon:"})
|
||||
"""
|
||||
|
||||
|
||||
class ThreadChat(Thread):
|
||||
def __deepcopy__(self, memo):
|
||||
return None
|
||||
|
||||
def __init__(self, username, token, channel):
|
||||
super(ThreadChat, self).__init__()
|
||||
|
||||
self.username = username
|
||||
self.token = token
|
||||
self.channel = channel
|
||||
|
||||
self.chat_irc = None
|
||||
|
||||
def run(self):
|
||||
self.chat_irc = ClientIRC(self.username, self.token, self.channel)
|
||||
logger.info(
|
||||
f"Join IRC Chat: {self.channel}", extra={"emoji": ":speech_balloon:"}
|
||||
)
|
||||
self.chat_irc.start()
|
||||
|
||||
def stop(self):
|
||||
if self.chat_irc is not None:
|
||||
logger.info(
|
||||
f"Leave IRC Chat: {self.channel}", extra={"emoji": ":speech_balloon:"}
|
||||
)
|
||||
self.chat_irc.die()
|
||||
@@ -8,3 +8,7 @@ class StreamerIsOfflineException(Exception):
|
||||
|
||||
class WrongCookiesException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BadCredentialsException(Exception):
|
||||
pass
|
||||
|
||||
@@ -5,6 +5,7 @@ class Priority(Enum):
|
||||
ORDER = auto()
|
||||
STREAK = auto()
|
||||
DROPS = auto()
|
||||
SUBSCRIBED = auto()
|
||||
POINTS_ASCENDING = auto()
|
||||
POINTS_DESCEDING = auto()
|
||||
|
||||
|
||||
@@ -23,8 +23,12 @@ from TwitchChannelPointsMiner.classes.Exceptions import (
|
||||
)
|
||||
from TwitchChannelPointsMiner.classes.Settings import Priority, Settings
|
||||
from TwitchChannelPointsMiner.classes.TwitchLogin import TwitchLogin
|
||||
from TwitchChannelPointsMiner.constants import API, CLIENT_ID, GQLOperations
|
||||
from TwitchChannelPointsMiner.utils import _millify, internet_connection_available
|
||||
from TwitchChannelPointsMiner.constants import CLIENT_ID, GQLOperations
|
||||
from TwitchChannelPointsMiner.utils import (
|
||||
_millify,
|
||||
create_chunks,
|
||||
internet_connection_available,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -139,33 +143,34 @@ class Twitch(object):
|
||||
streamer.set_offline()
|
||||
|
||||
def get_channel_id(self, streamer_username):
|
||||
json_response = self.__do_helix_request(f"/users?login={streamer_username}")
|
||||
if "data" not in json_response:
|
||||
json_data = copy.deepcopy(GQLOperations.ReportMenuItem)
|
||||
json_data["variables"] = {"channelLogin": streamer_username}
|
||||
json_response = self.post_gql_request(json_data)
|
||||
if (
|
||||
"data" not in json_response
|
||||
or "user" not in json_response["data"]
|
||||
or json_response["data"]["user"] is None
|
||||
):
|
||||
raise StreamerDoesNotExistException
|
||||
else:
|
||||
data = json_response["data"]
|
||||
if len(data) >= 1:
|
||||
return data[0]["id"]
|
||||
else:
|
||||
raise StreamerDoesNotExistException
|
||||
return json_response["data"]["user"]["id"]
|
||||
|
||||
def get_followers(self, first=100):
|
||||
followers = []
|
||||
pagination = {}
|
||||
while 1:
|
||||
query = f"/users/follows?from_id={self.twitch_login.get_user_id()}&first={first}"
|
||||
if pagination != {}:
|
||||
query += f"&after={pagination['cursor']}"
|
||||
|
||||
json_response = self.__do_helix_request(query)
|
||||
pagination = json_response["pagination"]
|
||||
followers += [fw["to_login"].lower() for fw in json_response["data"]]
|
||||
time.sleep(random.uniform(0.3, 0.7))
|
||||
|
||||
if pagination == {}:
|
||||
break
|
||||
|
||||
return followers
|
||||
def get_followers(self):
|
||||
json_data = copy.deepcopy(GQLOperations.PersonalSections)
|
||||
json_response = self.post_gql_request(json_data)
|
||||
try:
|
||||
if (
|
||||
"data" in json_response
|
||||
and "personalSections" in json_response["data"]
|
||||
and json_response["data"]["personalSections"] != []
|
||||
):
|
||||
return [
|
||||
fw["user"]["login"]
|
||||
for fw in json_response["data"]["personalSections"][0]["items"]
|
||||
if fw["user"] is not None
|
||||
]
|
||||
except KeyError:
|
||||
return []
|
||||
|
||||
def update_raid(self, streamer, raid):
|
||||
if streamer.raid != raid:
|
||||
@@ -207,14 +212,6 @@ class Twitch(object):
|
||||
)
|
||||
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(
|
||||
@@ -310,6 +307,19 @@ class Twitch(object):
|
||||
if len(streamers_watching) == 2:
|
||||
break
|
||||
|
||||
elif prior == Priority.SUBSCRIBED and len(streamers_watching) < 2:
|
||||
streamers_with_multiplier = [
|
||||
index
|
||||
for index in streamers_index
|
||||
if streamers[index].viewer_has_points_multiplier()
|
||||
]
|
||||
streamers_with_multiplier = sorted(
|
||||
streamers_with_multiplier,
|
||||
key=lambda x: streamers[x].total_points_multiplier(),
|
||||
reverse=True,
|
||||
)
|
||||
streamers_watching += streamers_with_multiplier[:2]
|
||||
|
||||
"""
|
||||
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).
|
||||
@@ -345,14 +355,12 @@ class Twitch(object):
|
||||
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}")
|
||||
@@ -382,6 +390,7 @@ class Twitch(object):
|
||||
channel = response["data"]["community"]["channel"]
|
||||
community_points = channel["self"]["communityPoints"]
|
||||
streamer.channel_points = community_points["balance"]
|
||||
streamer.activeMultipliers = community_points["activeMultipliers"]
|
||||
|
||||
if community_points["availableClaim"] is not None:
|
||||
self.claim_bonus(streamer, community_points["availableClaim"]["id"])
|
||||
@@ -433,7 +442,29 @@ class Twitch(object):
|
||||
"transactionID": token_hex(16),
|
||||
}
|
||||
}
|
||||
return self.post_gql_request(json_data)
|
||||
response = self.post_gql_request(json_data)
|
||||
if (
|
||||
"data" in response
|
||||
and "makePrediction" in response["data"]
|
||||
and "error" in response["data"]["makePrediction"]
|
||||
and response["data"]["makePrediction"]["error"] is not None
|
||||
):
|
||||
error_code = response["data"]["makePrediction"]["error"]["code"]
|
||||
logger.error(
|
||||
f"Failed to place bet, error: {error_code}",
|
||||
extra={
|
||||
"emoji": ":four_leaf_clover:",
|
||||
"color": Settings.logger.color_palette.BET_FAILED,
|
||||
},
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"Bet won't be placed as the amount {_millify(decision['amount'])} is less than the minimum required 10",
|
||||
extra={
|
||||
"emoji": ":four_leaf_clover:",
|
||||
"color": Settings.logger.color_palette.BET_GENERAL,
|
||||
},
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"Oh no! The event is not active anymore! Current status: {event.status}",
|
||||
@@ -489,16 +520,22 @@ class Twitch(object):
|
||||
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()}",
|
||||
}
|
||||
result = []
|
||||
chunks = create_chunks(campaigns, 20)
|
||||
for chunk in chunks:
|
||||
json_data = []
|
||||
for campaign in chunk:
|
||||
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))
|
||||
response = self.post_gql_request(json_data)
|
||||
for r in response:
|
||||
if r["data"]["user"] is not None:
|
||||
result.append(r["data"]["user"]["dropCampaign"])
|
||||
return result
|
||||
|
||||
def __sync_campaigns(self, campaigns):
|
||||
# We need the inventory only for get the real updated value/progress
|
||||
@@ -530,7 +567,21 @@ class Twitch(object):
|
||||
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"
|
||||
# response["data"]["claimDropRewards"] can be null and respose["data"]["errors"] != []
|
||||
# or response["data"]["claimDropRewards"]["status"] === DROP_INSTANCE_ALREADY_CLAIMED
|
||||
if ("claimDropRewards" in response["data"]) and (
|
||||
response["data"]["claimDropRewards"] is None
|
||||
):
|
||||
return False
|
||||
elif ("errors" in response["data"]) and (response["data"]["errors"] != []):
|
||||
return False
|
||||
elif ("claimDropRewards" in response["data"]) and (
|
||||
response["data"]["claimDropRewards"]["status"]
|
||||
in ["ELIGIBLE_FOR_ALL", "DROP_INSTANCE_ALREADY_CLAIMED"]
|
||||
):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except (ValueError, KeyError):
|
||||
return False
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Original Copyright (c) 2020 Rodney
|
||||
# The MIT License (MIT)
|
||||
|
||||
import copy
|
||||
import getpass
|
||||
import logging
|
||||
import os
|
||||
@@ -10,13 +11,17 @@ import pickle
|
||||
import browser_cookie3
|
||||
import requests
|
||||
|
||||
from TwitchChannelPointsMiner.classes.Exceptions import WrongCookiesException
|
||||
from TwitchChannelPointsMiner.classes.Exceptions import (
|
||||
BadCredentialsException,
|
||||
WrongCookiesException,
|
||||
)
|
||||
from TwitchChannelPointsMiner.constants import GQLOperations
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TwitchLogin(object):
|
||||
__self__ = [
|
||||
__slots__ = [
|
||||
"client_id",
|
||||
"token",
|
||||
"login_check_result",
|
||||
@@ -55,11 +60,10 @@ class TwitchLogin(object):
|
||||
|
||||
use_backup_flow = False
|
||||
|
||||
while True:
|
||||
# self.username = input('Enter Twitch username: ')
|
||||
for attempt in range(0, 25):
|
||||
password = (
|
||||
getpass.getpass(f"Enter Twitch password for {self.username}: ")
|
||||
if self.password is None
|
||||
if self.password in [None, ""]
|
||||
else self.password
|
||||
)
|
||||
|
||||
@@ -75,7 +79,7 @@ class TwitchLogin(object):
|
||||
|
||||
if "error_code" in login_response:
|
||||
err_code = login_response["error_code"]
|
||||
if err_code == 3011 or err_code == 3012: # missing 2fa token
|
||||
if err_code in [3011, 3012]: # missing 2fa token
|
||||
if err_code == 3011:
|
||||
logger.info(
|
||||
"Two factor authentication enabled, please enter token below."
|
||||
@@ -87,7 +91,7 @@ class TwitchLogin(object):
|
||||
post_data["authy_token"] = twofa.strip()
|
||||
continue
|
||||
|
||||
elif err_code == 3022 or err_code == 3023: # missing 2fa token
|
||||
elif err_code in [3022, 3023]: # missing 2fa token
|
||||
if err_code == 3022:
|
||||
logger.info("Login Verification code required.")
|
||||
self.email = login_response["obscured_email"]
|
||||
@@ -102,8 +106,17 @@ class TwitchLogin(object):
|
||||
post_data["twitchguard_code"] = twofa.strip()
|
||||
continue
|
||||
|
||||
elif err_code == 3001: # invalid password
|
||||
# invalid password, or password not provided
|
||||
elif err_code in [3001, 3003]:
|
||||
logger.info("Invalid username or password, please try again.")
|
||||
|
||||
# If the password is loaded from run.py, require the user to fix it there.
|
||||
if self.password not in [None, ""]:
|
||||
raise BadCredentialsException(
|
||||
"Username or password is incorrect."
|
||||
)
|
||||
|
||||
# If the user didn't load the password from run.py we can just ask for it again.
|
||||
break
|
||||
elif err_code == 1000:
|
||||
logger.info(
|
||||
@@ -166,14 +179,7 @@ class TwitchLogin(object):
|
||||
if self.token is None:
|
||||
return False
|
||||
|
||||
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"]
|
||||
|
||||
self.login_check_result = self.__set_user_id()
|
||||
return self.login_check_result
|
||||
|
||||
def save_cookies(self, cookies_file):
|
||||
@@ -192,6 +198,7 @@ class TwitchLogin(object):
|
||||
if cookie["name"] == key:
|
||||
if cookie["value"] is not None:
|
||||
return cookie["value"]
|
||||
return None
|
||||
|
||||
def load_cookies(self, cookies_file):
|
||||
if os.path.isfile(cookies_file):
|
||||
@@ -200,7 +207,30 @@ class TwitchLogin(object):
|
||||
raise WrongCookiesException("There must be a cookies file!")
|
||||
|
||||
def get_user_id(self):
|
||||
return int(self.get_cookie_value("persistent").split("%")[0])
|
||||
persistent = self.get_cookie_value("persistent")
|
||||
user_id = (
|
||||
int(persistent.split("%")[0]) if persistent is not None else self.user_id
|
||||
)
|
||||
if user_id is None:
|
||||
if self.__set_user_id() is True:
|
||||
return self.user_id
|
||||
return user_id
|
||||
|
||||
def __set_user_id(self):
|
||||
json_data = copy.deepcopy(GQLOperations.ReportMenuItem)
|
||||
json_data["variables"] = {"channelLogin": self.username}
|
||||
response = self.session.post(GQLOperations.url, json=json_data)
|
||||
|
||||
if response.status_code == 200:
|
||||
json_response = response.json()
|
||||
if (
|
||||
"data" in json_response
|
||||
and "user" in json_response["data"]
|
||||
and json_response["data"]["user"]["id"] is not None
|
||||
):
|
||||
self.user_id = json_response["data"]["user"]["id"]
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_auth_token(self):
|
||||
return self.get_cookie_value("auth-token")
|
||||
|
||||
@@ -43,7 +43,6 @@ class TwitchWebSocket(WebSocketApp):
|
||||
data = {"topics": [str(topic)]}
|
||||
if topic.is_user_topic() and auth_token is not None:
|
||||
data["auth_token"] = auth_token
|
||||
|
||||
nonce = create_nonce()
|
||||
self.send({"type": "LISTEN", "nonce": nonce, "data": data})
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import threading
|
||||
import time
|
||||
from threading import Thread, Timer
|
||||
|
||||
from dateutil import parser
|
||||
|
||||
@@ -13,7 +13,6 @@ from TwitchChannelPointsMiner.classes.Settings import Settings
|
||||
from TwitchChannelPointsMiner.classes.TwitchWebSocket import TwitchWebSocket
|
||||
from TwitchChannelPointsMiner.constants import WEBSOCKET
|
||||
from TwitchChannelPointsMiner.utils import (
|
||||
_millify,
|
||||
get_streamer_index,
|
||||
internet_connection_available,
|
||||
)
|
||||
@@ -68,7 +67,7 @@ class WebSocketsPool:
|
||||
)
|
||||
|
||||
def __start(self, index):
|
||||
thread_ws = threading.Thread(target=lambda: self.ws[index].run_forever())
|
||||
thread_ws = Thread(target=lambda: self.ws[index].run_forever())
|
||||
thread_ws.daemon = True
|
||||
thread_ws.name = f"WebSocket #{self.ws[index].index}"
|
||||
thread_ws.start()
|
||||
@@ -100,7 +99,7 @@ class WebSocketsPool:
|
||||
)
|
||||
WebSocketsPool.handle_reconnection(ws)
|
||||
|
||||
thread_ws = threading.Thread(target=run)
|
||||
thread_ws = Thread(target=run)
|
||||
thread_ws.daemon = True
|
||||
thread_ws.start()
|
||||
|
||||
@@ -111,7 +110,7 @@ class WebSocketsPool:
|
||||
logger.error(f"#{ws.index} - WebSocket error: {error}")
|
||||
|
||||
@staticmethod
|
||||
def on_close(ws):
|
||||
def on_close(ws, close_status_code, close_reason):
|
||||
logger.info(f"#{ws.index} - WebSocket closed")
|
||||
# On close please reconnect automatically
|
||||
WebSocketsPool.handle_reconnection(ws)
|
||||
@@ -143,7 +142,6 @@ class WebSocketsPool:
|
||||
# Why not create a new ws on the same array index? Let's try.
|
||||
self = ws.parent_pool
|
||||
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)
|
||||
@@ -177,11 +175,18 @@ class WebSocketsPool:
|
||||
if streamer_index != -1:
|
||||
try:
|
||||
if message.topic == "community-points-user-v1":
|
||||
if message.type in ["points-earned", "points-spent"]:
|
||||
balance = message.data["balance"]["balance"]
|
||||
ws.streamers[streamer_index].channel_points = balance
|
||||
ws.streamers[streamer_index].persistent_series(
|
||||
event_type=message.data["point_gain"]["reason_code"]
|
||||
if message.type == "points-earned"
|
||||
else "Spent"
|
||||
)
|
||||
|
||||
if message.type == "points-earned":
|
||||
earned = message.data["point_gain"]["total_points"]
|
||||
reason_code = message.data["point_gain"]["reason_code"]
|
||||
balance = message.data["balance"]["balance"]
|
||||
ws.streamers[streamer_index].channel_points = balance
|
||||
logger.info(
|
||||
f"+{earned} → {ws.streamers[streamer_index]} - Reason: {reason_code}.",
|
||||
extra={
|
||||
@@ -194,6 +199,9 @@ class WebSocketsPool:
|
||||
ws.streamers[streamer_index].update_history(
|
||||
reason_code, earned
|
||||
)
|
||||
ws.streamers[streamer_index].persistent_annotations(
|
||||
reason_code, f"+{earned} - {reason_code}"
|
||||
)
|
||||
elif message.type == "claim-available":
|
||||
ws.twitch.claim_bonus(
|
||||
ws.streamers[streamer_index],
|
||||
@@ -238,7 +246,9 @@ class WebSocketsPool:
|
||||
event_dict["prediction_window_seconds"]
|
||||
)
|
||||
# Reduce prediction window by 3/6s - Collect more accurate data for decision
|
||||
prediction_window_seconds -= random.uniform(3, 6)
|
||||
prediction_window_seconds = ws.streamers[
|
||||
streamer_index
|
||||
].get_prediction_window(prediction_window_seconds)
|
||||
event = EventPrediction(
|
||||
ws.streamers[streamer_index],
|
||||
event_id,
|
||||
@@ -252,17 +262,19 @@ class WebSocketsPool:
|
||||
ws.streamers[streamer_index].is_online
|
||||
and event.closing_bet_after(current_tmsp) > 0
|
||||
):
|
||||
if event.streamer.viewer_is_mod is True:
|
||||
logger.info(
|
||||
f"Sorry, you are moderator of {event.streamer}, so you can't bet!"
|
||||
)
|
||||
else:
|
||||
streamer = ws.streamers[streamer_index]
|
||||
bet_settings = streamer.settings.bet
|
||||
if (
|
||||
bet_settings.minimum_points is None
|
||||
or streamer.channel_points
|
||||
> bet_settings.minimum_points
|
||||
):
|
||||
ws.events_predictions[event_id] = event
|
||||
start_after = event.closing_bet_after(
|
||||
current_tmsp
|
||||
)
|
||||
|
||||
place_bet_thread = threading.Timer(
|
||||
place_bet_thread = Timer(
|
||||
start_after,
|
||||
ws.twitch.make_predictions,
|
||||
(ws.events_predictions[event_id],),
|
||||
@@ -277,6 +289,14 @@ class WebSocketsPool:
|
||||
"color": Settings.logger.color_palette.BET_START,
|
||||
},
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"{streamer} have only {streamer.channel_points} channel points and the minimum for bet is: {bet_settings.minimum_points}",
|
||||
extra={
|
||||
"emoji": ":pushpin:",
|
||||
"color": Settings.logger.color_palette.BET_FILTERS,
|
||||
},
|
||||
)
|
||||
|
||||
elif (
|
||||
message.type == "event-updated"
|
||||
@@ -300,63 +320,52 @@ class WebSocketsPool:
|
||||
message.type == "prediction-result"
|
||||
and event_prediction.bet_confirmed
|
||||
):
|
||||
event_result = message.data["prediction"]["result"]
|
||||
result_type = event_result["type"]
|
||||
points_placed = event_prediction.bet.decision["amount"]
|
||||
points_won = (
|
||||
event_result["points_won"]
|
||||
if event_result["points_won"]
|
||||
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"
|
||||
)
|
||||
points = event_prediction.parse_result(
|
||||
message.data["prediction"]["result"]
|
||||
)
|
||||
|
||||
decision = event_prediction.bet.get_decision()
|
||||
choice = event_prediction.bet.decision["choice"]
|
||||
|
||||
logger.info(
|
||||
f"{ws.events_predictions[event_id]} - Result: {result_type}, {action}: {points_prefix}{_millify(points_gained)}",
|
||||
f"{event_prediction} - Decision: {choice}: {decision['title']} ({decision['color']}) - Result: {event_prediction.result['string']}",
|
||||
extra={
|
||||
"emoji": ":bar_chart:",
|
||||
"color": Settings.logger.color_palette.get(
|
||||
f"BET_{result_type}"
|
||||
f"BET_{event_prediction.result['type']}"
|
||||
),
|
||||
},
|
||||
)
|
||||
ws.events_predictions[event_id].final_result = {
|
||||
"type": event_result["type"],
|
||||
"points_won": points_won,
|
||||
"gained": points_gained,
|
||||
}
|
||||
|
||||
ws.streamers[streamer_index].update_history(
|
||||
"PREDICTION", points_gained
|
||||
"PREDICTION", points["gained"]
|
||||
)
|
||||
|
||||
# Remove duplicate history records from previous message sent in community-points-user-v1
|
||||
if result_type == "REFUND":
|
||||
if event_prediction.result["type"] == "REFUND":
|
||||
ws.streamers[streamer_index].update_history(
|
||||
"REFUND",
|
||||
-points_placed,
|
||||
-points["placed"],
|
||||
counter=-1,
|
||||
)
|
||||
elif result_type == "WIN":
|
||||
elif event_prediction.result["type"] == "WIN":
|
||||
ws.streamers[streamer_index].update_history(
|
||||
"PREDICTION",
|
||||
-points_won,
|
||||
-points["won"],
|
||||
counter=-1,
|
||||
)
|
||||
|
||||
if event_prediction.result["type"] != "LOSE":
|
||||
ws.streamers[streamer_index].persistent_annotations(
|
||||
event_prediction.result["type"],
|
||||
f"{ws.events_predictions[event_id].title}",
|
||||
)
|
||||
elif message.type == "prediction-made":
|
||||
event_prediction.bet_confirmed = True
|
||||
ws.streamers[streamer_index].persistent_annotations(
|
||||
"PREDICTION_MADE",
|
||||
f"Decision: {event_prediction.bet.decision['choice']} - {event_prediction.title}",
|
||||
)
|
||||
except Exception:
|
||||
logger.error(
|
||||
f"Exception raised for topic: {message.topic} and message: {message}",
|
||||
|
||||
@@ -41,6 +41,15 @@ class OutcomeKeys(object):
|
||||
DECISION_POINTS = "decision_points"
|
||||
|
||||
|
||||
class DelayMode(Enum):
|
||||
FROM_START = auto()
|
||||
FROM_END = auto()
|
||||
PERCENTAGE = auto()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class FilterCondition(object):
|
||||
__slots__ = [
|
||||
"by",
|
||||
@@ -63,8 +72,11 @@ class BetSettings(object):
|
||||
"percentage",
|
||||
"percentage_gap",
|
||||
"max_points",
|
||||
"minimum_points",
|
||||
"stealth_mode",
|
||||
"filter_condition",
|
||||
"delay",
|
||||
"delay_mode",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
@@ -73,25 +85,34 @@ class BetSettings(object):
|
||||
percentage: int = None,
|
||||
percentage_gap: int = None,
|
||||
max_points: int = None,
|
||||
minimum_points: int = None,
|
||||
stealth_mode: bool = None,
|
||||
filter_condition: FilterCondition = None,
|
||||
delay: float = None,
|
||||
delay_mode: DelayMode = None,
|
||||
):
|
||||
self.strategy = strategy
|
||||
self.percentage = percentage
|
||||
self.percentage_gap = percentage_gap
|
||||
self.max_points = max_points
|
||||
self.minimum_points = minimum_points
|
||||
self.stealth_mode = stealth_mode
|
||||
self.filter_condition = filter_condition
|
||||
self.delay = delay
|
||||
self.delay_mode = delay_mode
|
||||
|
||||
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 20
|
||||
self.max_points = self.max_points if not None else 50000
|
||||
self.minimum_points = self.minimum_points if not None else 0
|
||||
self.stealth_mode = self.stealth_mode if not None else False
|
||||
self.delay = self.delay if not None else 6
|
||||
self.delay_mode = self.delay_mode if not None else DelayMode.FROM_END
|
||||
|
||||
def __repr__(self):
|
||||
return f"BetSettings(strategy={self.strategy}, percentage={self.percentage}, percentage_gap={self.percentage_gap}, max_points={self.max_points}, stealth_mode={self.stealth_mode})"
|
||||
return f"BetSettings(strategy={self.strategy}, percentage={self.percentage}, percentage_gap={self.percentage_gap}, max_points={self.max_points}, minimum_points={self.minimum_points}, stealth_mode={self.stealth_mode})"
|
||||
|
||||
|
||||
class Bet(object):
|
||||
@@ -153,11 +174,18 @@ class Bet(object):
|
||||
self.__clear_outcomes()
|
||||
|
||||
def __repr__(self):
|
||||
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)})"
|
||||
return f"Bet(total_users={millify(self.total_users)}, total_points={millify(self.total_points)}), decision={self.decision})\n\t\tOutcome A({self.get_outcome(0)})\n\t\tOutcome B({self.get_outcome(1)})"
|
||||
|
||||
def get_decision(self, parsed=False):
|
||||
decision = self.outcomes[0 if self.decision["choice"] == "A" else 1]
|
||||
return decision if parsed is False else Bet.__parse_outcome(decision)
|
||||
|
||||
@staticmethod
|
||||
def __parse_outcome(outcome):
|
||||
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 get_outcome(self, index):
|
||||
outcome = self.outcomes[index]
|
||||
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]}%)"
|
||||
return Bet.__parse_outcome(self.outcomes[index])
|
||||
|
||||
def __clear_outcomes(self):
|
||||
for index in range(0, len(self.outcomes)):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from TwitchChannelPointsMiner.classes.entities.Bet import Bet
|
||||
from TwitchChannelPointsMiner.classes.entities.Streamer import Streamer
|
||||
from TwitchChannelPointsMiner.classes.Settings import Settings
|
||||
from TwitchChannelPointsMiner.utils import float_round
|
||||
from TwitchChannelPointsMiner.utils import _millify, float_round
|
||||
|
||||
|
||||
class EventPrediction(object):
|
||||
@@ -12,7 +12,7 @@ class EventPrediction(object):
|
||||
"created_at",
|
||||
"prediction_window_seconds",
|
||||
"status",
|
||||
"final_result",
|
||||
"result",
|
||||
"box_fillable",
|
||||
"bet_confirmed",
|
||||
"bet_placed",
|
||||
@@ -36,7 +36,7 @@ class EventPrediction(object):
|
||||
self.created_at = created_at
|
||||
self.prediction_window_seconds = prediction_window_seconds
|
||||
self.status = status
|
||||
self.final_result: dict = {}
|
||||
self.result: dict = {"string": "", "type": None, "gained": 0}
|
||||
|
||||
self.box_fillable = False
|
||||
self.bet_confirmed = False
|
||||
@@ -60,4 +60,33 @@ class EventPrediction(object):
|
||||
return float_round(self.prediction_window_seconds - self.elapsed(timestamp))
|
||||
|
||||
def print_recap(self) -> str:
|
||||
return f"{self}\n\t\t{self.bet}\n\t\tResult: {self.final_result}"
|
||||
return f"{self}\n\t\t{self.bet}\n\t\tResult: {self.result['string']}"
|
||||
|
||||
def parse_result(self, result) -> dict:
|
||||
result_type = result["type"]
|
||||
|
||||
points = {}
|
||||
points["placed"] = self.bet.decision["amount"]
|
||||
points["won"] = (
|
||||
result["points_won"]
|
||||
if result["points_won"] 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")
|
||||
)
|
||||
|
||||
self.result = {
|
||||
"string": f"{result_type}, {action}: {points['prefix']}{_millify(points['gained'])}",
|
||||
"type": result_type,
|
||||
"gained": points["gained"],
|
||||
}
|
||||
|
||||
return points
|
||||
|
||||
@@ -58,7 +58,11 @@ class Message(object):
|
||||
else (
|
||||
self.data["channel_id"]
|
||||
if "channel_id" in self.data
|
||||
else self.topic_user
|
||||
else (
|
||||
self.data["balance"]["channel_id"]
|
||||
if "balance" in self.data
|
||||
else self.topic_user
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
from threading import Lock
|
||||
|
||||
from TwitchChannelPointsMiner.classes.entities.Bet import BetSettings
|
||||
from TwitchChannelPointsMiner.classes.Chat import ThreadChat
|
||||
from TwitchChannelPointsMiner.classes.entities.Bet import BetSettings, DelayMode
|
||||
from TwitchChannelPointsMiner.classes.entities.Stream import Stream
|
||||
from TwitchChannelPointsMiner.classes.Settings import Settings
|
||||
from TwitchChannelPointsMiner.constants import URL
|
||||
@@ -17,6 +22,7 @@ class StreamerSettings(object):
|
||||
"claim_drops",
|
||||
"watch_streak",
|
||||
"bet",
|
||||
"join_chat",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
@@ -26,22 +32,30 @@ class StreamerSettings(object):
|
||||
claim_drops: bool = None,
|
||||
watch_streak: bool = None,
|
||||
bet: BetSettings = None,
|
||||
join_chat: bool = True,
|
||||
):
|
||||
self.make_predictions = make_predictions
|
||||
self.follow_raid = follow_raid
|
||||
self.claim_drops = claim_drops
|
||||
self.watch_streak = watch_streak
|
||||
self.bet = bet
|
||||
self.join_chat = join_chat
|
||||
|
||||
def default(self):
|
||||
for name in ["make_predictions", "follow_raid", "claim_drops", "watch_streak"]:
|
||||
for name in [
|
||||
"make_predictions",
|
||||
"follow_raid",
|
||||
"claim_drops",
|
||||
"watch_streak",
|
||||
"join_chat",
|
||||
]:
|
||||
if getattr(self, name) is None:
|
||||
setattr(self, name, True)
|
||||
if self.bet is None:
|
||||
self.bet = BetSettings()
|
||||
|
||||
def __repr__(self):
|
||||
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})"
|
||||
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}, join_chat={self.join_chat})"
|
||||
|
||||
|
||||
class Streamer(object):
|
||||
@@ -56,10 +70,13 @@ class Streamer(object):
|
||||
"channel_points",
|
||||
"minute_watched_requests",
|
||||
"viewer_is_mod",
|
||||
"activeMultipliers",
|
||||
"irc_chat",
|
||||
"stream",
|
||||
"raid",
|
||||
"history",
|
||||
"streamer_url",
|
||||
"mutex",
|
||||
]
|
||||
|
||||
def __init__(self, username, settings=None):
|
||||
@@ -73,6 +90,8 @@ class Streamer(object):
|
||||
self.channel_points = 0
|
||||
self.minute_watched_requests = None
|
||||
self.viewer_is_mod = False
|
||||
self.activeMultipliers = None
|
||||
self.irc_chat = None
|
||||
|
||||
self.stream = Stream()
|
||||
|
||||
@@ -81,6 +100,8 @@ class Streamer(object):
|
||||
|
||||
self.streamer_url = f"{URL}/{self.username}"
|
||||
|
||||
self.mutex = Lock()
|
||||
|
||||
def __repr__(self):
|
||||
return f"Streamer(username={self.username}, channel_id={self.channel_id}, channel_points={_millify(self.channel_points)})"
|
||||
|
||||
@@ -95,6 +116,7 @@ class Streamer(object):
|
||||
if self.is_online is True:
|
||||
self.offline_at = time.time()
|
||||
self.is_online = False
|
||||
self.leave_chat()
|
||||
|
||||
logger.info(
|
||||
f"{self} is Offline!",
|
||||
@@ -109,6 +131,7 @@ class Streamer(object):
|
||||
self.online_at = time.time()
|
||||
self.is_online = True
|
||||
self.stream.init_watch_streak()
|
||||
self.join_chat()
|
||||
|
||||
logger.info(
|
||||
f"{self} is Online!",
|
||||
@@ -146,3 +169,87 @@ class Streamer(object):
|
||||
and self.stream.drops_tags is True
|
||||
and self.stream.campaigns_ids != []
|
||||
)
|
||||
|
||||
def viewer_has_points_multiplier(self):
|
||||
return self.activeMultipliers is not None and len(self.activeMultipliers) > 0
|
||||
|
||||
def total_points_multiplier(self):
|
||||
return (
|
||||
sum(
|
||||
map(
|
||||
lambda x: x["factor"],
|
||||
self.activeMultipliers,
|
||||
),
|
||||
)
|
||||
if self.activeMultipliers is not None
|
||||
else 0
|
||||
)
|
||||
|
||||
def get_prediction_window(self, prediction_window_seconds):
|
||||
delay_mode = self.settings.bet.delay_mode
|
||||
delay = self.settings.bet.delay
|
||||
if delay_mode == DelayMode.FROM_START:
|
||||
return min(delay, prediction_window_seconds)
|
||||
elif delay_mode == DelayMode.FROM_END:
|
||||
return max(prediction_window_seconds - delay, 0)
|
||||
elif delay_mode == DelayMode.PERCENTAGE:
|
||||
return prediction_window_seconds * delay
|
||||
else:
|
||||
return prediction_window_seconds
|
||||
|
||||
# === ANALYTICS === #
|
||||
def persistent_annotations(self, event_type, event_text):
|
||||
event_type = event_type.upper()
|
||||
if event_type in ["WATCH_STREAK", "WIN", "PREDICTION_MADE"]:
|
||||
primary_color = (
|
||||
"#45c1ff"
|
||||
if event_type == "WATCH_STREAK"
|
||||
else ("#ffe045" if event_type == "PREDICTION_MADE" else "#54ff45")
|
||||
)
|
||||
data = {
|
||||
"borderColor": primary_color,
|
||||
"label": {
|
||||
"style": {"color": "#000", "background": primary_color},
|
||||
"text": event_text,
|
||||
},
|
||||
}
|
||||
self.__save_json("annotations", data)
|
||||
|
||||
def persistent_series(self, event_type="Watch"):
|
||||
self.__save_json("series", event_type=event_type)
|
||||
|
||||
def __save_json(self, key, data={}, event_type="Watch"):
|
||||
# https://stackoverflow.com/questions/4676195/why-do-i-need-to-multiply-unix-timestamps-by-1000-in-javascript
|
||||
# data.update({"x": round(time.time() * 1000)})
|
||||
now = datetime.now().replace(microsecond=0)
|
||||
data.update({"x": round(datetime.timestamp(now) * 1000)})
|
||||
|
||||
if key == "series":
|
||||
data.update({"y": self.channel_points})
|
||||
if event_type is not None:
|
||||
data.update({"z": event_type.replace("_", " ").title()})
|
||||
|
||||
fname = os.path.join(Settings.analytics_path, f"{self.username}.json")
|
||||
with self.mutex:
|
||||
json_data = json.load(open(fname, "r")) if os.path.isfile(fname) else {}
|
||||
if key not in json_data:
|
||||
json_data[key] = []
|
||||
|
||||
json_data[key].append(data)
|
||||
json.dump(json_data, open(fname, "w"), indent=4)
|
||||
|
||||
def leave_chat(self):
|
||||
if self.irc_chat is not None:
|
||||
self.irc_chat.stop()
|
||||
|
||||
# Recreate a new thread to start again
|
||||
# raise RuntimeError("threads can only be started once")
|
||||
self.irc_chat = ThreadChat(
|
||||
self.irc_chat.username,
|
||||
self.irc_chat.token,
|
||||
self.username,
|
||||
)
|
||||
|
||||
def join_chat(self):
|
||||
if self.irc_chat is not None:
|
||||
self.irc_chat.start()
|
||||
|
||||
1
TwitchChannelPointsMiner/classes/entities/__init__.py
Normal file
1
TwitchChannelPointsMiner/classes/entities/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Twitch endpoints
|
||||
URL = "https://www.twitch.tv"
|
||||
API = "https://api.twitch.tv"
|
||||
IRC = "irc.chat.twitch.tv"
|
||||
IRC_PORT = 6667
|
||||
WEBSOCKET = "wss://pubsub-edge.twitch.tv/v1"
|
||||
CLIENT_ID = "kimne78kx3ncx6brgo4mv6wki5h1ko"
|
||||
DROP_ID = "c2542d6d-cd10-4532-919b-3d19f30a768b"
|
||||
@@ -16,6 +17,12 @@ USER_AGENTS = {
|
||||
},
|
||||
}
|
||||
|
||||
BRANCH = "master"
|
||||
GITHUB_url = (
|
||||
"https://raw.githubusercontent.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2/"
|
||||
+ BRANCH
|
||||
)
|
||||
|
||||
|
||||
class GQLOperations:
|
||||
url = "https://gql.twitch.tv/gql"
|
||||
@@ -116,7 +123,7 @@ class GQLOperations:
|
||||
"extensions": {
|
||||
"persistedQuery": {
|
||||
"version": 1,
|
||||
"sha256Hash": "7da6078b1bfa2f0a4dd061cb47bdcd1ffddf31cccadd966ec192e4cd06666e2b",
|
||||
"sha256Hash": "14b5e8a50777165cfc3971e1d93b4758613fe1c817d5542c398dce70b7a45c05",
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -129,3 +136,30 @@ class GQLOperations:
|
||||
}
|
||||
},
|
||||
}
|
||||
ReportMenuItem = { # Use for replace https://api.twitch.tv/helix/users?login={self.username}
|
||||
"operationName": "ReportMenuItem",
|
||||
"extensions": {
|
||||
"persistedQuery": {
|
||||
"version": 1,
|
||||
"sha256Hash": "8f3628981255345ca5e5453dfd844efffb01d6413a9931498836e6268692a30c",
|
||||
}
|
||||
},
|
||||
}
|
||||
PersonalSections = {
|
||||
"operationName": "PersonalSections",
|
||||
"variables": {
|
||||
"input": {
|
||||
"sectionInputs": ["FOLLOWED_SECTION"],
|
||||
"recommendationContext": {"platform": "web"},
|
||||
},
|
||||
"channelLogin": None,
|
||||
"withChannelUser": False,
|
||||
"creatorAnniversariesExperimentEnabled": False,
|
||||
},
|
||||
"extensions": {
|
||||
"persistedQuery": {
|
||||
"version": 1,
|
||||
"sha256Hash": "9fbdfb00156f754c26bde81eb47436dee146655c92682328457037da1a48ed39",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import logging
|
||||
import os
|
||||
import platform
|
||||
from datetime import datetime
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from pathlib import Path
|
||||
|
||||
import emoji
|
||||
@@ -34,7 +35,7 @@ class GlobalFormatter(logging.Formatter):
|
||||
if "\u2192" in record.msg:
|
||||
record.msg = record.msg.replace("\u2192", "-->")
|
||||
|
||||
# With the update of Stream class It's possible that the Stream Title contains emoji
|
||||
# With the update of Stream class, the Stream Title may contain emoji
|
||||
# Full remove using a method from utils.
|
||||
record.msg = remove_emoji(record.msg)
|
||||
|
||||
@@ -52,6 +53,7 @@ class ColorPalette(object):
|
||||
GAIN_FOR_RAID = Fore.RESET
|
||||
GAIN_FOR_CLAIM = Fore.RESET
|
||||
GAIN_FOR_WATCH = Fore.RESET
|
||||
GAIN_FOR_WATCH_STREAK = Fore.RESET
|
||||
|
||||
BET_WIN = Fore.GREEN
|
||||
BET_LOSE = Fore.RED
|
||||
@@ -103,6 +105,7 @@ class LoggerSettings:
|
||||
file_level: int = logging.DEBUG,
|
||||
emoji: bool = platform.system() != "Windows",
|
||||
colored: bool = False,
|
||||
auto_clear: bool = True,
|
||||
color_palette: ColorPalette = ColorPalette(),
|
||||
):
|
||||
self.save = save
|
||||
@@ -111,6 +114,7 @@ class LoggerSettings:
|
||||
self.file_level = file_level
|
||||
self.emoji = emoji
|
||||
self.colored = colored
|
||||
self.auto_clear = auto_clear
|
||||
self.color_palette = color_palette
|
||||
|
||||
|
||||
@@ -141,11 +145,26 @@ def configure_loggers(username, settings):
|
||||
if settings.save is True:
|
||||
logs_path = os.path.join(Path().absolute(), "logs")
|
||||
Path(logs_path).mkdir(parents=True, exist_ok=True)
|
||||
logs_file = os.path.join(
|
||||
logs_path,
|
||||
f"{username}.{datetime.now().strftime('%Y%m%d-%H%M%S')}.log",
|
||||
)
|
||||
file_handler = logging.FileHandler(logs_file, "w", "utf-8")
|
||||
if settings.auto_clear is True:
|
||||
logs_file = os.path.join(
|
||||
logs_path,
|
||||
f"{username}.log",
|
||||
)
|
||||
file_handler = TimedRotatingFileHandler(
|
||||
logs_file,
|
||||
when="D",
|
||||
interval=1,
|
||||
backupCount=7,
|
||||
encoding="utf-8",
|
||||
delay=False,
|
||||
)
|
||||
else:
|
||||
logs_file = os.path.join(
|
||||
logs_path,
|
||||
f"{username}.{datetime.now().strftime('%Y%m%d-%H%M%S')}.log",
|
||||
)
|
||||
file_handler = logging.FileHandler(logs_file, "w", "utf-8")
|
||||
|
||||
file_handler.setFormatter(
|
||||
logging.Formatter(
|
||||
fmt="%(asctime)s - %(levelname)s - %(name)s - [%(funcName)s]: %(message)s",
|
||||
|
||||
@@ -4,11 +4,13 @@ import socket
|
||||
import time
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timezone
|
||||
from os import path
|
||||
from random import randrange
|
||||
|
||||
import requests
|
||||
from millify import millify
|
||||
|
||||
from TwitchChannelPointsMiner.constants import USER_AGENTS
|
||||
from TwitchChannelPointsMiner.constants import USER_AGENTS, GITHUB_url
|
||||
|
||||
|
||||
def _millify(input, precision=2):
|
||||
@@ -150,3 +152,50 @@ def internet_connection_available(host="8.8.8.8", port=53, timeout=3):
|
||||
|
||||
def percentage(a, b):
|
||||
return 0 if a == 0 else int((a / b) * 100)
|
||||
|
||||
|
||||
def create_chunks(lst, n):
|
||||
return [lst[i : (i + n)] for i in range(0, len(lst), n)] # noqa: E203
|
||||
|
||||
|
||||
def download_file(name, fpath):
|
||||
r = requests.get(
|
||||
path.join(GITHUB_url, name),
|
||||
headers={"User-Anget": get_user_agent("FIREFOX")},
|
||||
stream=True,
|
||||
)
|
||||
if r.status_code == 200:
|
||||
with open(fpath, "wb") as f:
|
||||
for chunk in r.iter_content(chunk_size=1024):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
return True
|
||||
|
||||
|
||||
def read(fname):
|
||||
return open(path.join(path.dirname(__file__), fname), encoding="utf-8").read()
|
||||
|
||||
|
||||
def init2dict(content):
|
||||
return dict(re.findall(r"""__([a-z]+)__ = "([^"]+)""", content))
|
||||
|
||||
|
||||
def check_versions():
|
||||
try:
|
||||
current_version = init2dict(read("__init__.py"))
|
||||
current_version = (
|
||||
current_version["version"] if "version" in current_version else "0.0.0"
|
||||
)
|
||||
except Exception:
|
||||
current_version = "0.0.0"
|
||||
try:
|
||||
r = requests.get(
|
||||
path.join(GITHUB_url, "TwitchChannelPointsMiner", "__init__.py")
|
||||
)
|
||||
github_version = init2dict(r.text)
|
||||
github_version = (
|
||||
github_version["version"] if "version" in github_version else "0.0.0"
|
||||
)
|
||||
except Exception:
|
||||
github_version = "0.0.0"
|
||||
return current_version, github_version
|
||||
|
||||
BIN
assets/chart-analytics-dark.png
Normal file
BIN
assets/chart-analytics-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
BIN
assets/chart-analytics-light.png
Normal file
BIN
assets/chart-analytics-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
256
assets/charts.html
Normal file
256
assets/charts.html
Normal file
@@ -0,0 +1,256 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Twitch-Channel-Points-Miner-v2</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" rel="stylesheet" />
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.1/css/bulma.css" rel="stylesheet" />
|
||||
|
||||
<link href="{{url_for('static', filename='dark-theme.css')}}" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body style="height: 100vh;">
|
||||
<div class="container">
|
||||
<div style="text-align: center">
|
||||
<img style="margin-top: -20px;" width="600px" src="{{url_for('static', filename='banner.png')}}"
|
||||
alt="banner">
|
||||
<br>
|
||||
<div style="margin-top: -15px;">
|
||||
<a style="text-decoration: none;"
|
||||
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 style="text-decoration: none;" 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 style="text-decoration: none;"
|
||||
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 style="text-decoration: none;"
|
||||
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 style="text-decoration: none;"
|
||||
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 style="text-decoration: none;" 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>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<div class="columns">
|
||||
<div class="column is-10">
|
||||
<div class="tabs">
|
||||
<ul>
|
||||
{% for streamer in streamers.split(',')|sort %}
|
||||
<li onClick="changeStreamer('{{ streamer }}', {{ loop.index }})"><a>{{ streamer.replace(".json",
|
||||
"") }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column" style="text-align: right">
|
||||
<label class="checkbox">
|
||||
Annotations
|
||||
<input type="checkbox" checked="true" id="annotations">
|
||||
</label>
|
||||
<label class="checkbox">
|
||||
Dark mode
|
||||
<input type="checkbox" checked="true" id="dark-mode">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="box" id="chart" style="padding: 0.30rem;"></div>
|
||||
|
||||
<!-- <hr style="margin: 1.2rem 0;"> -->
|
||||
<div style="text-align: center">If you want to help on this project, please leave a star 🌟 and share it with
|
||||
your friends! 😎 - If you want to offer me a coffee, I would be grateful ❤️
|
||||
<a href="https://www.buymeacoffee.com/tkdalex" target="_blank">
|
||||
<img style="margin-bottom: -8px; margin-left: 10px;"
|
||||
src="https://cdn.buymeacoffee.com/buttons/lato-yellow.png" alt="Buy Me A Coffee" height="5"
|
||||
width="138">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
</div>
|
||||
</body>
|
||||
<script type="text/javascript">
|
||||
// https://apexcharts.com/javascript-chart-demos/line-charts/zoomable-timeseries/
|
||||
var options = {
|
||||
series: [],
|
||||
chart: {
|
||||
type: 'area',
|
||||
stacked: false,
|
||||
height: 400,
|
||||
zoom: {
|
||||
type: 'x',
|
||||
enabled: true,
|
||||
autoScaleYaxis: true
|
||||
},
|
||||
// background: '#2B2D3E',
|
||||
foreColor: '#fff'
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
stroke: {
|
||||
curve: 'smooth',
|
||||
},
|
||||
markers: {
|
||||
size: 0,
|
||||
},
|
||||
title: {
|
||||
text: 'Channel points (dates are displayed in UTC)',
|
||||
align: 'left'
|
||||
},
|
||||
colors: ["#f9826c"],
|
||||
fill: {
|
||||
type: 'gradient',
|
||||
gradient: {
|
||||
shadeIntensity: 1,
|
||||
inverseColors: false,
|
||||
opacityFrom: 0.5,
|
||||
opacityTo: 0,
|
||||
stops: [0, 90, 100]
|
||||
},
|
||||
},
|
||||
yaxis: {
|
||||
title: {
|
||||
text: 'Channel points'
|
||||
},
|
||||
},
|
||||
xaxis: {
|
||||
type: 'datetime',
|
||||
labels: {
|
||||
datetimeUTC: false
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
theme: 'dark',
|
||||
shared: false,
|
||||
x: {
|
||||
show: true,
|
||||
format: 'HH:mm:ss dd MMM',
|
||||
},
|
||||
custom: ({ series, seriesIndex, dataPointIndex, w }) => {
|
||||
return (`<div class="apexcharts-active">
|
||||
<div class="apexcharts-tooltip-title">${w.globals.seriesNames[seriesIndex]}</div>
|
||||
<div class="apexcharts-tooltip-series-group apexcharts-active" style="order: 1; display: flex; padding-bottom: 0px !important;">
|
||||
<div class="apexcharts-tooltip-text">
|
||||
<div class="apexcharts-tooltip-y-group">
|
||||
<span class="apexcharts-tooltip-text-label"><b>Points</b>: ${series[seriesIndex][dataPointIndex]}</span><br>
|
||||
<span class="apexcharts-tooltip-text-label"><b>Reason</b>: ${w.globals.seriesZ[seriesIndex][dataPointIndex] ? w.globals.seriesZ[seriesIndex][dataPointIndex] : ''}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
)
|
||||
}
|
||||
},
|
||||
noData: {
|
||||
text: 'Loading...'
|
||||
}
|
||||
};
|
||||
|
||||
var chart = new ApexCharts(document.querySelector("#chart"), options);
|
||||
var currentStreamer = null;
|
||||
var annotations = [];
|
||||
|
||||
var refresh = parseInt("{{ refresh }}");
|
||||
|
||||
$(document).ready(function () {
|
||||
chart.render();
|
||||
$("li").eq(0).click();
|
||||
|
||||
if (!localStorage.getItem("annotations")) localStorage.setItem("annotations", true);
|
||||
if (!localStorage.getItem("dark-mode")) localStorage.setItem("dark-mode", true);
|
||||
|
||||
$('#annotations').prop("checked", localStorage.getItem("annotations") === "true");
|
||||
$('#dark-mode').prop("checked", localStorage.getItem("dark-mode") === "true");
|
||||
|
||||
updateAnnotations();
|
||||
toggleDarkMode();
|
||||
});
|
||||
|
||||
function changeStreamer(streamer, index) {
|
||||
$("li").removeClass("is-active")
|
||||
$("li").eq(index - 1).addClass('is-active');
|
||||
currentStreamer = streamer;
|
||||
getStreamerData(streamer);
|
||||
}
|
||||
|
||||
function getStreamerData(streamer) {
|
||||
if (currentStreamer == streamer) {
|
||||
$.getJSON(`./json/${streamer}`, function (response) {
|
||||
chart.updateSeries([{
|
||||
name: streamer.replace(".json", ""),
|
||||
data: response["series"]
|
||||
}], true)
|
||||
clearAnnotations();
|
||||
annotations = response["annotations"];
|
||||
updateAnnotations();
|
||||
setTimeout(function () { getStreamerData(streamer); }, 300000); // 5 minutes
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateAnnotations() {
|
||||
if ($('#annotations').prop("checked") === true) {
|
||||
clearAnnotations()
|
||||
if (annotations && annotations.length > 0)
|
||||
annotations.forEach((annotation, index) => {
|
||||
annotations[index]['id'] = `id-${index}`
|
||||
chart.addXaxisAnnotation(annotation, true)
|
||||
})
|
||||
localStorage.setItem('annotations', true);
|
||||
} else {
|
||||
clearAnnotations();
|
||||
localStorage.setItem('annotations', false);
|
||||
}
|
||||
}
|
||||
|
||||
function clearAnnotations() {
|
||||
if (annotations && annotations.length > 0)
|
||||
annotations.forEach((annotation, index) => {
|
||||
chart.removeAnnotation(annotation['id'])
|
||||
})
|
||||
chart.clearAnnotations();
|
||||
}
|
||||
|
||||
function toggleDarkMode() {
|
||||
var darkMode = $('#dark-mode').prop("checked")
|
||||
localStorage.setItem('dark-mode', darkMode);
|
||||
$("link[href='{{url_for('static', filename='dark-theme.css')}}']").prop("disabled", !darkMode);
|
||||
chart.updateOptions({
|
||||
colors: darkMode === true ? ["#f9826c"] : ['#008ffb'],
|
||||
chart: {
|
||||
foreColor: darkMode === true ? "#fff" : '#373d3f'
|
||||
},
|
||||
tooltip: {
|
||||
theme: darkMode === true ? "dark" : "light"
|
||||
}
|
||||
})
|
||||
// if (darkMode === true) $("#chart").addClass("box")
|
||||
// else $("#chart").removeClass("box")
|
||||
}
|
||||
|
||||
$('#annotations').click(() => { updateAnnotations(); });
|
||||
$('#dark-mode').click(() => { toggleDarkMode(); });
|
||||
|
||||
</script>
|
||||
|
||||
</html>
|
||||
29
assets/dark-theme.css
Normal file
29
assets/dark-theme.css
Normal file
@@ -0,0 +1,29 @@
|
||||
body {
|
||||
background: #343E59;
|
||||
color: #fff;
|
||||
}
|
||||
.box {
|
||||
background-color: #2B2D3E;
|
||||
}
|
||||
.tabs a {
|
||||
border-bottom-color: #dbdbdb;
|
||||
color: #fff;
|
||||
border-bottom-style: none;
|
||||
}
|
||||
.tabs li.is-active a {
|
||||
border-bottom-color: #f9826c;
|
||||
color: #fff;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
.tabs a:hover {
|
||||
border-bottom-color: #dbdbdb;
|
||||
color: #dbdbdb;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
.tabs ul{
|
||||
margin-bottom: 5px;
|
||||
border-bottom-style: none;
|
||||
}
|
||||
.checkbox:hover{
|
||||
color: #f9826c;
|
||||
}
|
||||
30
example.py
30
example.py
@@ -5,27 +5,27 @@ from colorama import Fore
|
||||
from TwitchChannelPointsMiner import TwitchChannelPointsMiner
|
||||
from TwitchChannelPointsMiner.logger import LoggerSettings, ColorPalette
|
||||
from TwitchChannelPointsMiner.classes.Settings import Priority
|
||||
from TwitchChannelPointsMiner.classes.entities.Bet import Strategy, BetSettings, Condition, OutcomeKeys, FilterCondition
|
||||
from TwitchChannelPointsMiner.classes.entities.Bet import Strategy, BetSettings, Condition, OutcomeKeys, FilterCondition, DelayMode
|
||||
from TwitchChannelPointsMiner.classes.entities.Streamer import Streamer, StreamerSettings
|
||||
|
||||
twitch_miner = TwitchChannelPointsMiner(
|
||||
username="your-twitch-username",
|
||||
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
|
||||
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 the 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)
|
||||
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 the drops claimed and no watch-streak available, use the order priority (POINTS_ASCENDING, POINTS_DESCEDING)
|
||||
],
|
||||
logger_settings=LoggerSettings(
|
||||
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)
|
||||
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
|
||||
colored=True, # If you want to print colored text
|
||||
color_palette=ColorPalette( # You can also create a custom palette color (for the common message).
|
||||
STREAMER_online="GREEN", # Don't worry about lower/upper case the script will be parse all the values.
|
||||
STREAMER_online="GREEN", # Don't worry about lower/upper case. The script will parse all the values.
|
||||
streamer_offline="red", # Read more in README.md
|
||||
BET_wiN=Fore.MAGENTA # Color allowed are: [BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, RESET].
|
||||
)
|
||||
@@ -34,16 +34,20 @@ twitch_miner = TwitchChannelPointsMiner(
|
||||
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
|
||||
watch_streak=True, # If a streamer go online change the priority of streamers array and catch the watch streak. Issue #11
|
||||
join_chat=True, # Join irc chat to increase watch-time
|
||||
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)
|
||||
percentage_gap=20, # Gap difference between outcomesA and outcomesB (for SMART strategy)
|
||||
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
|
||||
stealth_mode=True, # If the calculated amount of channel points is GT the highest bet, place the highest value minus 1-2 points Issue #33
|
||||
delay_mode=DelayMode.FROM_END, # When placing a bet, we will wait until `delay` seconds before the end of the timer
|
||||
delay=6,
|
||||
minimum_points=2000, # Place the bet only if we have at least 20k points. Issue #113
|
||||
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
|
||||
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
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
requests
|
||||
websocket-client
|
||||
browser_cookie3
|
||||
pillow
|
||||
python-dateutil
|
||||
emoji
|
||||
millify
|
||||
pre-commit
|
||||
colorama
|
||||
requests==2.25.1
|
||||
websocket-client==1.0.1
|
||||
browser_cookie3==0.12.1
|
||||
pillow==8.3.2
|
||||
python-dateutil==2.8.1
|
||||
emoji==1.2.0
|
||||
millify==0.1.1
|
||||
pre-commit==2.13.0
|
||||
colorama==0.4.4
|
||||
flask==2.0.1
|
||||
irc==19.0.1
|
||||
@@ -1,86 +0,0 @@
|
||||
[
|
||||
{
|
||||
"settings": null,
|
||||
"username": "r_eqxxx"
|
||||
},
|
||||
{
|
||||
"settings": null,
|
||||
"username": "vhauss"
|
||||
},
|
||||
{
|
||||
"settings": null,
|
||||
"username": "carriedbyoujua"
|
||||
},
|
||||
{
|
||||
"settings": null,
|
||||
"username": "robertcrg"
|
||||
},
|
||||
{
|
||||
"settings": null,
|
||||
"username": "blu3berrycupcake"
|
||||
},
|
||||
{
|
||||
"settings": null,
|
||||
"username": "brbteabreak"
|
||||
},
|
||||
{
|
||||
"settings": null,
|
||||
"username": "nut9tv"
|
||||
},
|
||||
{
|
||||
"settings": null,
|
||||
"username": "joaoboavista"
|
||||
},
|
||||
{
|
||||
"settings": null,
|
||||
"username": "hyeaga"
|
||||
},
|
||||
{
|
||||
"settings": null,
|
||||
"username": "chaosmachinegr"
|
||||
},
|
||||
{
|
||||
"settings": null,
|
||||
"username": "malteseknight_"
|
||||
},
|
||||
{
|
||||
"settings": null,
|
||||
"username": "daniel_rusev"
|
||||
},
|
||||
{
|
||||
"settings": null,
|
||||
"username": "serwinterofficial"
|
||||
},
|
||||
{
|
||||
"settings": null,
|
||||
"username": "shadowfrax"
|
||||
},
|
||||
{
|
||||
"settings": null,
|
||||
"username": "boxbox"
|
||||
},
|
||||
{
|
||||
"settings": null,
|
||||
"username": "ash_on_lol"
|
||||
},
|
||||
{
|
||||
"settings": null,
|
||||
"username": "dyannatv"
|
||||
},
|
||||
{
|
||||
"settings": null,
|
||||
"username": "mammoth"
|
||||
},
|
||||
{
|
||||
"settings": null,
|
||||
"username": "bchillz"
|
||||
},
|
||||
{
|
||||
"settings": null,
|
||||
"username": "buddha"
|
||||
},
|
||||
{
|
||||
"settings": null,
|
||||
"username": "trausi"
|
||||
}
|
||||
]
|
||||
18
setup.py
18
setup.py
@@ -25,12 +25,22 @@ setuptools.setup(
|
||||
url="https://github.com/Tkd-Alex/Twitch-Channel-Points-Miner-v2",
|
||||
packages=setuptools.find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=read("requirements.txt").splitlines(),
|
||||
install_requires=[
|
||||
"requests==2.25.1",
|
||||
"websocket-client==1.0.1",
|
||||
"browser_cookie3==0.12.1",
|
||||
"pillow==8.3.2",
|
||||
"python-dateutil==2.8.1",
|
||||
"emoji==1.2.0",
|
||||
"millify==0.1.1",
|
||||
"pre-commit==2.13.0",
|
||||
"colorama==0.4.4",
|
||||
"flask==2.0.1",
|
||||
"irc==19.0.1",
|
||||
],
|
||||
long_description=read("README.md"),
|
||||
long_description_content_type="text/markdown",
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
@@ -43,5 +53,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.*, !=3.4.*",
|
||||
python_requires=">=3.6",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user