├── requirement.txt ├── Config ├── __init__.py ├── ConfigGetter.py └── setting.py.example ├── Util ├── Function.py ├── __init__.py ├── Singleton.py ├── LazyProperty.py ├── Printer.py ├── EncryptParams.py ├── Notification.py └── NetEaseLogin.py ├── LICENSE ├── .gitignore ├── netease.sql ├── README.md ├── Db └── DbClient.py └── main.py /requirement.txt: -------------------------------------------------------------------------------- 1 | requests 2 | pymysql 3 | pycryptodome 4 | retry -------------------------------------------------------------------------------- /Config/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | - author: Lkeme 5 | - contact: Useri@live.cn 6 | - file: __init__ 7 | - time: 2019/10/21 18:00 8 | - desc: 9 | """ -------------------------------------------------------------------------------- /Util/Function.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | - author: Lkeme 5 | - contact: Useri@live.cn 6 | - file: functions 7 | - time: 2019/10/21 19:28 8 | - desc: 9 | """ 10 | import time 11 | 12 | 13 | # 时间戳 14 | def current_unix(): 15 | now = (int(time.time() * 1000)) 16 | return now 17 | -------------------------------------------------------------------------------- /Util/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | - author: Lkeme 5 | - contact: Useri@live.cn 6 | - file: __init__ 7 | - time: 2019/10/21 17:32 8 | - desc: 9 | """ 10 | from Util.Printer import Printer 11 | from Util.Singleton import Singleton 12 | from Util.LazyProperty import LazyProperty 13 | from Util.EncryptParams import EncryptParams 14 | from Util.NetEaseLogin import NetEaseLogin 15 | from Util.Notification import Notification 16 | -------------------------------------------------------------------------------- /Util/Singleton.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | - author: Lkeme 5 | - contact: Useri@live.cn 6 | - file: UtilClass 7 | - time: 2019/10/21 17:30 8 | - desc: 9 | """ 10 | 11 | 12 | class Singleton(type): 13 | """ 14 | Singleton Metaclass 15 | """ 16 | 17 | _inst = {} 18 | 19 | def __call__(cls, *args, **kwargs): 20 | if cls not in cls._inst: 21 | cls._inst[cls] = super(Singleton, cls).__call__(*args) 22 | return cls._inst[cls] 23 | -------------------------------------------------------------------------------- /Util/LazyProperty.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | - author: Lkeme 5 | - contact: Useri@live.cn 6 | - file: LazyProperty 7 | - time: 2019/10/21 18:02 8 | - desc: 9 | """ 10 | 11 | 12 | class LazyProperty(object): 13 | """ 14 | LazyProperty 15 | explain: http://www.spiderpy.cn/blog/5/ 16 | """ 17 | 18 | def __init__(self, func): 19 | self.func = func 20 | 21 | def __get__(self, instance, owner): 22 | if instance is None: 23 | return self 24 | else: 25 | value = self.func(instance) 26 | setattr(instance, self.func.__name__, value) 27 | return value 28 | -------------------------------------------------------------------------------- /Util/Printer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | - author: Lkeme 5 | - contact: Useri@live.cn 6 | - file: Printer 7 | - time: 2019/10/21 17:41 8 | - desc: 9 | """ 10 | import time 11 | 12 | 13 | class Printer: 14 | # 格式化时间 15 | @staticmethod 16 | def current_time(): 17 | return f"[{str(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))}]" 18 | 19 | # 格式化打印 20 | def printer(self, genre, info, *args): 21 | # flag = "," if len(args) else " " 22 | content = f'{self.current_time()} [{genre}] {info} {" ".join(f"{str(arg)}" for arg in args)}' 23 | print(content, flush=True) 24 | 25 | 26 | if __name__ == '__main__': 27 | printer = Printer() 28 | printer.printer("TEST", "ENABLE") 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Config/ConfigGetter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | - author: Lkeme 5 | - contact: Useri@live.cn 6 | - file: ConfigGetter 7 | - time: 2019/10/21 18:01 8 | - desc: 9 | """ 10 | from Util import LazyProperty 11 | from Config.setting import * 12 | 13 | 14 | class ConfigGetter(object): 15 | """ 16 | get config 17 | """ 18 | 19 | def __init__(self): 20 | pass 21 | 22 | @LazyProperty 23 | def db_name(self): 24 | return DATABASES.get("default", {}).get("DATABASE", "netease") 25 | 26 | @LazyProperty 27 | def db_host(self): 28 | return DATABASES.get("default", {}).get("HOST", "localhost") 29 | 30 | @LazyProperty 31 | def db_port(self): 32 | return DATABASES.get("default", {}).get("PORT", 3306) 33 | 34 | @LazyProperty 35 | def db_user(self): 36 | return DATABASES.get("default", {}).get("USERNAME", "root") 37 | 38 | @LazyProperty 39 | def db_password(self): 40 | return DATABASES.get("default", {}).get("PASSWORD", "123456") 41 | 42 | @LazyProperty 43 | def user_accounts(self): 44 | return ACCOUNTS 45 | 46 | @LazyProperty 47 | def notification(self): 48 | return NOTIFICATION 49 | 50 | 51 | config = ConfigGetter() 52 | 53 | if __name__ == '__main__': 54 | print(config.user_accounts) 55 | -------------------------------------------------------------------------------- /Config/setting.py.example: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | - author: Lkeme 5 | - contact: Useri@live.cn 6 | - file: setting 7 | - time: 2019/10/21 18:01 8 | - desc: 9 | """ 10 | 11 | """ 网易云账号配置 """ 12 | ACCOUNTS = [ 13 | # default 扫描账号 (扫描、转发、删除等) 必须定义一个 14 | # valid 有效账号 (转发、删除等) 视情况定义增加 15 | # invalid 无效账号 (不做任何操作) 16 | { 17 | "user_id": "your user id", 18 | "username": "your user name", 19 | "password": "your password", 20 | "type": "default", 21 | }, 22 | { 23 | "user_id": "your user id", 24 | "username": "your user name", 25 | "password": "your password", 26 | "type": "valid", 27 | }, 28 | { 29 | "user_id": "your user id", 30 | "username": "your user name", 31 | "password": "your password", 32 | "type": "invalid", 33 | } 34 | ] 35 | 36 | """ MYSQL数据库配置 """ 37 | DATABASES = { 38 | "default": { 39 | "HOST": "localhost", 40 | "PORT": 3306, 41 | "USERNAME": "root", 42 | "PASSWORD": "", 43 | "DATABASE": "netease", 44 | } 45 | } 46 | 47 | """ 通知服务配置 """ 48 | NOTIFICATION = { 49 | # 开关 50 | "enable": False, 51 | "type": "server_chan", 52 | # Server酱 53 | "server_chan": 54 | { 55 | "key": "", 56 | }, 57 | # tg_bot https://github.com/Fndroid/tg_push_bot 58 | "tg_bot": 59 | { 60 | "api": "https://xxxx.com/sendMessage/:Token", 61 | }, 62 | # 自用通知服务 63 | "personal": 64 | { 65 | "url": "", 66 | "channel": "" 67 | } 68 | } 69 | 70 | 71 | class ConfigError(BaseException): 72 | pass 73 | 74 | 75 | def check_config(): 76 | # raise ConfigError("test") 77 | pass 78 | 79 | 80 | check_config() 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # Custom 107 | /g20191022 108 | /s20191022 109 | /Config/setting.py 110 | test.py 111 | backup/ -------------------------------------------------------------------------------- /netease.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat Premium Data Transfer 3 | 4 | Source Server : Localhost 5 | Source Server Type : MySQL 6 | Source Server Version : 50724 7 | Source Host : localhost:3306 8 | Source Schema : netease 9 | 10 | Target Server Type : MySQL 11 | Target Server Version : 50724 12 | File Encoding : 65001 13 | 14 | Date: 22/10/2019 19:45:42 15 | */ 16 | 17 | SET NAMES utf8mb4; 18 | SET FOREIGN_KEY_CHECKS = 0; 19 | 20 | -- ---------------------------- 21 | -- Table structure for raw_event 22 | -- ---------------------------- 23 | DROP TABLE IF EXISTS `raw_event`; 24 | CREATE TABLE `raw_event` ( 25 | `id` int(11) NOT NULL AUTO_INCREMENT, 26 | `uid` int(11) NULL DEFAULT NULL, 27 | `event_msg` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL, 28 | `event_id` bigint(20) NULL DEFAULT NULL, 29 | `lottery_id` int(11) NULL DEFAULT NULL, 30 | `lottery_time` bigint(20) NULL DEFAULT NULL, 31 | `crt_time` bigint(20) NULL DEFAULT NULL, 32 | `is_reposted` int(255) NULL DEFAULT NULL, 33 | `is_deleted` int(255) NULL DEFAULT NULL, 34 | PRIMARY KEY (`id`) USING BTREE 35 | ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; 36 | 37 | -- ---------------------------- 38 | -- Table structure for used_event 39 | -- ---------------------------- 40 | DROP TABLE IF EXISTS `used_event`; 41 | CREATE TABLE `used_event` ( 42 | `id` int(11) NOT NULL AUTO_INCREMENT, 43 | `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, 44 | `pre_event_id` bigint(20) NULL DEFAULT NULL, 45 | `raw_event_id` int(11) NULL DEFAULT NULL, 46 | `crt_time` bigint(20) NULL DEFAULT NULL, 47 | PRIMARY KEY (`id`) USING BTREE 48 | ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; 49 | 50 | SET FOREIGN_KEY_CHECKS = 1; 51 | -------------------------------------------------------------------------------- /Util/EncryptParams.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | - author: Lkeme 5 | - contact: Useri@live.cn 6 | - file: EncryptParams 7 | - time: 2019/10/21 17:47 8 | - desc: 9 | """ 10 | import os 11 | import json 12 | import codecs 13 | import base64 14 | from Crypto.Cipher import AES 15 | 16 | 17 | class EncryptParams: 18 | 19 | def __init__(self): 20 | self.modulus = '00e0b509f6259df8642dbc35662901477df22677ec152b5ff68ace615bb7b725152b3ab17a876aea8a5aa76d2e417629ec4ee341f56135fccf695280104e0312ecbda92557c93870114af6c9d05c4f7f0c3685b7a46bee255932575cce10b424d813cfe4875d3e82047b97ddef52741d546b8e289dc6935b3ece0462db0a22b8e7' 21 | self.nonce = '0CoJUm6Qyw8W8jud' 22 | self.pubKey = '010001' 23 | 24 | def get(self, text): 25 | text = json.dumps(text) 26 | secKey = self._createSecretKey(16) 27 | encText = self._aesEncrypt(self._aesEncrypt(text, self.nonce), secKey) 28 | encSecKey = self._rsaEncrypt(secKey, self.pubKey, self.modulus) 29 | post_data = { 30 | 'params': encText, 31 | 'encSecKey': encSecKey 32 | } 33 | return post_data 34 | 35 | def _aesEncrypt(self, text, secKey): 36 | pad = 16 - len(text) % 16 37 | if isinstance(text, bytes): 38 | text = text.decode('utf-8') 39 | text = text + str(pad * chr(pad)) 40 | secKey = secKey.encode('utf-8') 41 | encryptor = AES.new(secKey, 2, b'0102030405060708') 42 | text = text.encode('utf-8') 43 | ciphertext = encryptor.encrypt(text) 44 | ciphertext = base64.b64encode(ciphertext) 45 | return ciphertext 46 | 47 | def _rsaEncrypt(self, text, pubKey, modulus): 48 | text = text[::-1] 49 | rs = int(codecs.encode(text.encode('utf-8'), 'hex_codec'), 16) ** int( 50 | pubKey, 16) % int(modulus, 16) 51 | return format(rs, 'x').zfill(256) 52 | 53 | def _createSecretKey(self, size): 54 | return (''.join( 55 | map(lambda xx: (hex(ord(xx))[2:]), str(os.urandom(size)))))[0:16] 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NeteaseMusicLottery 2 | 3 | ## 公告 4 | 网易云音乐动态互动抽奖测试学习 5 | 6 | ## 安装 7 | 1. 克隆项目代码 8 | ```bash 9 | git clone https://github.com/lkeme/NeteaseMusicLottery.git 10 | cd NeteaseMusicLottery 11 | ``` 12 | 2. 安装环境依赖 **env python3.6+** 13 | ```bash 14 | pip install -r requirement.txt -i https://pypi.tuna.tsinghua.edu.cn/simple 15 | ``` 16 | 17 | 3. 导入数据文件 18 | mysql中创建数据库 `netease`, 并导入数据结构`netease.sql` 19 | ```sql 20 | create DATABASE netease; 21 | use netease; 22 | source /your path/netease.sql; 23 | ``` 24 | 25 | 4. 复制配置文件 26 | ```bash 27 | # linux 28 | cd Config && cp setting.py.example setting.py 29 | # windows 30 | 手动重命名 31 | ``` 32 | 33 | 5. 填写配置信息 34 | ```python 35 | # 修改文件 --> Config/setting.py 36 | # """ 网易云账号配置 """ 37 | ACCOUNTS = [ 38 | # default 扫描账号 (扫描、转发、删除等) 必须定义一个 39 | # valid 有效账号 (转发、删除等) 视情况定义增加 40 | # invalid 无效账号 (不做任何操作) 41 | { 42 | "user_id": "", 43 | "username": "", 44 | "password": "", 45 | "type": "default", 46 | }, 47 | { 48 | "user_id": "0", 49 | "username": "your user name", 50 | "password": "your password", 51 | "type": "invalid", 52 | }, 53 | { 54 | "user_id": "0", 55 | "username": "your user name", 56 | "password": "your password", 57 | "type": "invalid", 58 | } 59 | ] 60 | 61 | """ 通知服务配置 """ 62 | NOTIFICATION = { 63 | # 开关 64 | "enable": True, 65 | "type": "server_chan", 66 | # Server酱 67 | "server_chan": 68 | { 69 | "key": "", 70 | }, 71 | # tg_bot https://github.com/Fndroid/tg_push_bot 72 | "tg_bot": 73 | { 74 | "api": "https://xxxx.com/sendMessage/:Token", 75 | }, 76 | # 自用通知服务 77 | "personal": 78 | { 79 | "url": "", 80 | "channel": "" 81 | } 82 | } 83 | 84 | """ MYSQL数据库配置 """ 85 | DATABASES = { 86 | "default": { 87 | "HOST": "localhost", 88 | "PORT": 3306, 89 | "USERNAME": "root", 90 | "PASSWORD": "123456", 91 | "DATABASE": "netease", 92 | } 93 | } 94 | ``` 95 | 96 | ## 使用 97 | ```bash 98 | python main.py 99 | ``` 100 | 101 | ## 打赏 102 | 103 | ![](https://i.loli.net/2019/07/13/5d2963e5cc1eb22973.png) 104 | 105 | ## 流程 106 | 1. 扫描分为匹配扫描(页面匹配) 、去重扫描(区间穷举)两种。 107 | 2. 中奖检测(1 * 60 * 60 == 3600),开奖后一小时的动态。 108 | 2. 转发时间(12 * 60 * 60 == 43200) ,12小时内开奖的动态。 109 | 3. 删除时间(6 * 60 * 60 == 21600) ,开奖后6小时的动态。 110 | 5. 中奖标记删除但实际未删除,删除动态每1小时检测但12小时才会删除。 111 | 6. 考虑server酱限制,允许错误10次,每次休眠30s,未成功直接跳过。 112 | 7. 完整扫描部分4小时一次, 防止异常,分段扫描, 每段1000(可调节)。 113 | 7. 迷你扫描部分14小时一次, 会加上去重扫描消耗时间,理论时间不和完整扫描冲突。 114 | 8. 错误统一休眠60s一次。 115 | 9. 去重扫描实际是阻塞的,部分功能的时间可能会出现延迟。 116 | 10. 自用设置不用管,留空就行 117 | 118 | ps. 以上的时间并不精准,可能会出现正负值。 119 | 120 | ## License 许可证 121 | MIT 122 | -------------------------------------------------------------------------------- /Util/Notification.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | - author: Lkeme 5 | - contact: Useri@live.cn 6 | - file: Notification 7 | - time: 2019/10/21 20:33 8 | - desc: 9 | """ 10 | import requests 11 | from Config.ConfigGetter import config 12 | from Util import Printer 13 | from retry import retry 14 | 15 | 16 | class NotificationError(BaseException): 17 | pass 18 | 19 | 20 | class Notification: 21 | def __init__(self): 22 | self.session = requests.Session() 23 | self.log = Printer() 24 | self.time_out = 30 25 | self.title = '' 26 | self.content = '' 27 | 28 | # 通知分发 29 | @retry(NotificationError, tries=5, delay=60) 30 | def notice_handler(self, title, content): 31 | notification = config.notification 32 | if not notification['enable']: 33 | return 34 | if notification['type'] not in notification.keys(): 35 | return 36 | for key, value in notification[notification['type']].items(): 37 | if not value: 38 | return 39 | self.title = title 40 | self.content = content 41 | if notification['type'] == 'server_chan': 42 | self.server_chan( 43 | notification['server_chan']['key'] 44 | ) 45 | elif notification['type'] == 'tg_bot': 46 | self.tg_bot( 47 | notification['tg_bot']['api'], 48 | ) 49 | elif notification['type'] == 'personal': 50 | self.personal( 51 | notification['personal']['url'], 52 | notification['personal']['channel'] 53 | ) 54 | else: 55 | pass 56 | 57 | # server酱 58 | def server_chan(self, sc_key): 59 | try: 60 | url = f'https://sc.ftqq.com/{sc_key}.send' 61 | data = { 62 | 'text': self.title, 63 | 'desp': self.content 64 | } 65 | headers = { 66 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 67 | 'Referer': 'http://sc.ftqq.com/?c=code', 68 | } 69 | response = self.session.post( 70 | url, headers=headers, data=data, timeout=self.time_out 71 | ).json() 72 | 73 | self.log.printer('SERVERCHAN', response) 74 | if response['errmsg'] == 'success': 75 | return 76 | except Exception as e: 77 | pass 78 | raise NotificationError("NotificationError") 79 | 80 | # tg_bot 81 | def tg_bot(self, url): 82 | try: 83 | data = { 84 | 'text': f"{self.title}\r\n{self.content}" 85 | } 86 | response = self.session.post( 87 | url, data=data, timeout=self.time_out 88 | ) 89 | self.log.printer('TGBOT', response.text) 90 | if response.status_code == 200: 91 | return True 92 | except Exception as e: 93 | pass 94 | raise NotificationError("NotificationError") 95 | 96 | # 自用 97 | def personal(self, url, channel): 98 | try: 99 | json = { 100 | "channelName": channel, 101 | "text": f"{self.title}\r\n{self.content}" 102 | } 103 | headers = { 104 | 'content-type': 'application/json' 105 | } 106 | response = self.session.post( 107 | url, headers=headers, json=json, timeout=self.time_out 108 | ).json() 109 | # {"error":0,"message":"Done!"} 110 | self.log.printer('PERSONAL', response) 111 | if response['message'] == 'Done!': 112 | return True 113 | except Exception as e: 114 | pass 115 | raise NotificationError("NotificationError") 116 | 117 | 118 | if __name__ == '__main__': 119 | n = Notification() 120 | n.notice_handler("测试标题", "测试内容") 121 | -------------------------------------------------------------------------------- /Db/DbClient.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | - author: Lkeme 5 | - contact: Useri@live.cn 6 | - file: DbClient 7 | - time: 2019/10/21 17:27 8 | - desc: 9 | """ 10 | import os 11 | import sys 12 | import pymysql 13 | from Util import Singleton, Printer 14 | from Util.Function import current_unix 15 | from Config.ConfigGetter import config 16 | 17 | sys.path.append(os.path.dirname(os.path.abspath(__file__))) 18 | 19 | 20 | class DbClient(object): 21 | """ 22 | DbClient DB工厂类 23 | """ 24 | 25 | __metaclass__ = Singleton 26 | 27 | def __init__(self): 28 | """ 29 | init 30 | :return: 31 | """ 32 | self.name = None 33 | self.client = None 34 | self.cursor = None 35 | self.log = Printer() 36 | self.db_conn() 37 | 38 | def db_conn(self): 39 | """ 40 | init DB Client 41 | :return: 42 | """ 43 | self.client = pymysql.connect( 44 | host=config.db_host, 45 | user=config.db_user, 46 | password=config.db_password, 47 | db=config.db_name, 48 | port=config.db_port 49 | ) 50 | self.cursor = self.client.cursor(cursor=pymysql.cursors.DictCursor) 51 | 52 | # 查询key 53 | def query_key(self, key): 54 | sql = f"select {key} from {self.name}" 55 | self.cursor.execute(sql) 56 | row_list = self.cursor.fetchall() 57 | new_row_list = [row[key] for row in row_list] 58 | return new_row_list 59 | 60 | # 查询有效数据 61 | def query_valid(self): 62 | sql = f"select * from {self.name} where lottery_time > %s" 63 | self.cursor.execute(sql, (current_unix(),)) 64 | row_list = self.cursor.fetchall() 65 | self.log.printer("DB", 66 | f"查询有效数据库完毕 data->{self.length(row_list)}") 67 | return row_list 68 | 69 | # 查询转发数据库 70 | def query_forward(self): 71 | sql = f"select * from {self.name} where ((lottery_time - 43200*1000) < %s and lottery_time > %s and is_reposted=0)" 72 | self.cursor.execute(sql, (current_unix(), current_unix())) 73 | row_list = self.cursor.fetchall() 74 | self.log.printer("DB", f"查询转发数据库完毕 data->{self.length(row_list)}") 75 | return row_list 76 | 77 | # 查询删除数据库 78 | def query_delete(self): 79 | sql = f"select * from {self.name} where ((lottery_time + 3600*1000) < %s and is_reposted=1 and is_deleted=0)" 80 | self.cursor.execute(sql, (current_unix(),)) 81 | row_list = self.cursor.fetchall() 82 | self.log.printer("DB", f"查询删除数据库完毕 data->{self.length(row_list)}") 83 | return row_list 84 | 85 | # 查询event_raw_id 2 pre_event_id数据库 86 | def query_pre_event(self, username, event_id): 87 | sql = f"select pre_event_id from {self.name} where (username = %s and raw_event_id = %s)" 88 | self.cursor.execute(sql, (username, event_id)) 89 | row_list = self.cursor.fetchone() 90 | self.log.printer("DB", f"查询PRE数据库完毕 data->{self.length(row_list)}") 91 | return row_list 92 | 93 | # 插入原始数据 94 | def insert_raw(self, uid, event_msg, event_id, lottery_id, lottery_time): 95 | sql = f"INSERT INTO {self.name}( uid, event_msg, event_id, lottery_id, lottery_time,crt_time,is_reposted, is_deleted) values (%s,%s,%s,%s,%s,%s,%s,%s)" 96 | try: 97 | self.cursor.execute(sql, ( 98 | uid, event_msg, event_id, lottery_id, 99 | lottery_time, current_unix(), 0, 0 100 | )) 101 | self.client.commit() 102 | except Exception as e: 103 | self.client.rollback() 104 | self.log.printer("DB", "插入原始数据", e) 105 | finally: 106 | self.log.printer("DB", "插入原始数据", "ok") 107 | 108 | # 插入使用数据 109 | def insert_used(self, username, pre_event_id, raw_event_id): 110 | sql = f"INSERT INTO {self.name}( username, pre_event_id, raw_event_id, crt_time) values (%s,%s,%s,%s)" 111 | try: 112 | self.cursor.execute(sql, ( 113 | username, pre_event_id, raw_event_id, current_unix() 114 | )) 115 | self.client.commit() 116 | except Exception as e: 117 | self.client.rollback() 118 | self.log.printer("DB", "插入使用数据", e) 119 | finally: 120 | self.log.printer("DB", "插入使用数据", "ok") 121 | 122 | # 更新原始删除 123 | def update_raw_deleted(self, lottery_id): 124 | sql = f"UPDATE {self.name} SET is_deleted = 1 WHERE lottery_id = %s" 125 | try: 126 | self.cursor.execute(sql, (lottery_id,)) 127 | self.client.commit() 128 | except Exception as e: 129 | self.client.rollback() 130 | self.log.printer("DB", "更新删除数据库", e) 131 | 132 | finally: 133 | self.log.printer("DB", "更新删除数据库", "ok") 134 | 135 | # 更新原始转发 136 | def update_raw_reposted(self, lottery_id): 137 | sql = f"UPDATE {self.name} SET is_reposted = 1 WHERE lottery_id = %s" 138 | try: 139 | self.cursor.execute(sql, (lottery_id,)) 140 | self.client.commit() 141 | except Exception as e: 142 | self.client.rollback() 143 | self.log.printer("DB", "更新删除数据库", e) 144 | 145 | finally: 146 | self.log.printer("DB", "更新删除数据库", "ok") 147 | 148 | # 改变表 149 | def change_table(self, name): 150 | self.name = name 151 | 152 | @staticmethod 153 | def length(data): 154 | return 0 if data is None else len(data) 155 | 156 | 157 | if __name__ == '__main__': 158 | pass 159 | -------------------------------------------------------------------------------- /Util/NetEaseLogin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3.6 2 | # -*- coding: utf-8 -*- 3 | 4 | import hashlib 5 | import re 6 | import random 7 | import requests 8 | from Util import EncryptParams, Printer 9 | import faker 10 | 11 | fake = faker.Faker(locale='zh_CN') 12 | 13 | 14 | class NetEaseLogin: 15 | 16 | def __init__(self): 17 | self.ua_list = [ 18 | 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_9_7 rv:2.0; ff-SN) AppleWebKit/531.5.1 (KHTML, like Gecko) Version/5.0.2 Safari/531.5.1', 19 | 'Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.1)', 20 | 'Opera/8.14.(X11; Linux i686; lzh-TW) Presto/2.9.176 Version/10.00', 21 | 'Opera/8.18.(Windows NT 5.01; ku-TR) Presto/2.9.181 Version/12.00', 22 | 'Mozilla/5.0 (compatible; MSIE 5.0; Windows NT 4.0; Trident/4.1)', 23 | 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_9_5; rv:1.9.5.20) Gecko/2012-08-22 13:50:54 Firefox/11.0', 24 | 'Opera/8.44.(Windows NT 4.0; da-DK) Presto/2.9.189 Version/11.00', 25 | 'Mozilla/5.0 (Windows NT 6.0) AppleWebKit/5341 (KHTML, like Gecko) Chrome/35.0.899.0 Safari/5341', 26 | 'Mozilla/5.0 (Windows; U; Windows 98; Win 9x 4.90) AppleWebKit/534.50.5 (KHTML, like Gecko) Version/5.1 Safari/534.50.5', 27 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:46.0) Gecko/20100101 Firefox/46.0', 28 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36', 29 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.1.1 Safari/603.2.4', 30 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:46.0) Gecko/20100101 Firefox/46.0', 31 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36', 32 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/13.10586', 33 | 'Mozilla/5.0 (iPod; U; CPU iPhone OS 3_0 like Mac OS X; fr-CH) AppleWebKit/535.40.2 (KHTML, like Gecko) Version/4.0.5 Mobile/8B113 Safari/6535.40.2', 34 | ] 35 | self.headers = { 36 | 'Content-Type': 'application/x-www-form-urlencoded', 37 | 'Origin': 'https://music.163.com', 38 | 'Referer': 'https://music.163.com/', 39 | 'Cookie': 'os=pc', 40 | 'User-Agent': random.choice(self.ua_list), 41 | } 42 | self.enc = EncryptParams() 43 | self.log = Printer() 44 | self.session = requests.Session() 45 | 46 | # 更新 Session 47 | def update_session(self): 48 | self.session.headers.update( 49 | { 50 | 'Origin': 'https://music.163.com', 51 | 'Referer': 'https://music.163.com/', 52 | 'User-Agent': fake.user_agent(), 53 | } 54 | ) 55 | # 利用RequestsCookieJar获取 56 | jar = requests.cookies.RequestsCookieJar() 57 | jar.set('os', random.choice(['pc', 'osx', 'android'])) 58 | self.session.cookies.update(jar) 59 | return self.session 60 | 61 | # 邮箱登录 62 | def email_login(self, username, password): 63 | url = 'https://music.163.com/weapi/login?csrf_token=' 64 | text = { 65 | 'username': username, 66 | 'password': password, 67 | 'rememberLogin': 'true', 68 | 'csrf_token': '' 69 | } 70 | payload = self.enc.get(text) 71 | try: 72 | return self.session.post(url, headers=self.headers, data=payload) 73 | except Exception as e: 74 | # print(e) 75 | return {'code': 501, 'msg': str(e)} 76 | 77 | # 手机登录 78 | def phone_login(self, username, password): 79 | url = 'https://music.163.com/weapi/login/cellphone' 80 | text = { 81 | 'phone': username, 82 | 'password': password, 83 | 'rememberLogin': 'true', 84 | 'csrf_token': '' 85 | } 86 | payload = self.enc.get(text) 87 | try: 88 | return self.session.post(url, headers=self.headers, data=payload) 89 | except Exception as e: 90 | return {'code': 501, 'msg': str(e)} 91 | 92 | # 登录 93 | def login(self, username, password): 94 | # printer(username, password) 95 | account_type = self.match_login_type(username) 96 | md5 = hashlib.md5() 97 | md5.update(password.encode('utf-8')) 98 | password = md5.hexdigest() 99 | 100 | # 为了后期考虑,暂时拆分登陆 101 | if account_type == 'phone': 102 | response = self.phone_login(username, password) 103 | else: 104 | response = self.email_login(username, password) 105 | # 判断字典 106 | if not isinstance(response, dict): 107 | json_resp = response.json() 108 | else: 109 | json_resp = response 110 | if json_resp['code'] == 200: 111 | self.log.printer( 112 | "LOGIN", 113 | f"Account -> {username}, login successfully..." 114 | ) 115 | return self.update_session() 116 | elif json_resp['code'] == 501: 117 | self.log.printer( 118 | "LOGIN", 119 | f"[ERROR]: Account -> {username}, fail to login, {json_resp['msg']}..." 120 | ) 121 | else: 122 | self.log.printer( 123 | "LOGIN", 124 | f"[ERROR]: Account -> {username}, fail to login, {json_resp}..." 125 | ) 126 | return False 127 | 128 | # 匹配登录类型 129 | def match_login_type(self, username): 130 | # 正则方案 131 | pattern = re.compile(r'^1\d{2,3}\d{7,8}$|^1[34578]\d{9}$') 132 | return 'phone' if pattern.match(username) else 'email' 133 | # int报错方案 134 | # try: 135 | # int(username) 136 | # login_type = 'phone' 137 | # except: 138 | # login_type = 'email' 139 | # return login_type 140 | 141 | 142 | if __name__ == '__main__': 143 | pass 144 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import re 4 | import time 5 | import faker 6 | import random 7 | import asyncio 8 | import traceback 9 | from Db.DbClient import DbClient 10 | from Util.Function import current_unix 11 | from Config.ConfigGetter import config 12 | from Util import EncryptParams, Printer, NetEaseLogin, Notification 13 | 14 | fake = faker.Faker(locale='zh_CN') 15 | 16 | 17 | class NetEaseLottery: 18 | 19 | def __init__(self): 20 | # 加密 日志 数据库 21 | self.enc = EncryptParams() 22 | self.log = Printer() 23 | self.db = DbClient() 24 | # 数据表 25 | self.raw_event = 'raw_event' 26 | self.used_event = 'used_event' 27 | # pass 28 | self.session = None 29 | self.user_box = [] 30 | self.lottery_list = [] 31 | self.pre_scan_list = [] 32 | self.designation_list = [] 33 | self.lock = { 34 | 'scan': 0, 35 | } 36 | self.__init_user_manager() 37 | 38 | # 初始化用户管理 39 | def __init_user_manager(self): 40 | if self.user_box and self.session: 41 | return 42 | temp = [] 43 | accounts = config.user_accounts 44 | for account in accounts: 45 | if account['user_id'] in temp: 46 | continue 47 | for key, value in account.items(): 48 | if not value or value == 'invalid': 49 | break 50 | else: 51 | try: 52 | account['session'] = self.login( 53 | account['username'], account['password'] 54 | ) 55 | if account['type'] == 'default': 56 | self.session = account['session'] 57 | self.user_box.append(account) 58 | except Exception as e: 59 | self.log.printer("LOGIN", e) 60 | continue 61 | temp.append(account['user_id']) 62 | if not self.user_box: 63 | exit("有效用户为0,请检查配置!") 64 | if not self.session: 65 | self.session = self.user_box[0]['session'] 66 | self.user_box[0]['type'] = 'default' 67 | 68 | # 请求中心 69 | def _requests(self, method, url, decode=2, retry=10, timeout=15, **kwargs): 70 | if method in ["get", "post"]: 71 | for _ in range(retry + 1): 72 | try: 73 | response = getattr(self.session, method)( 74 | url, timeout=timeout, **kwargs 75 | ) 76 | return response.json() if decode == 2 else response.content if decode == 1 else response 77 | except Exception as e: 78 | self.log.printer("REQUEST", "出现错误 {e}") 79 | time.sleep(1) 80 | return None 81 | 82 | # 请求中心 83 | def _multi_requests(self, session, method, url, decode=2, retry=10, 84 | timeout=15, 85 | **kwargs): 86 | if method in ["get", "post"]: 87 | for _ in range(retry + 1): 88 | try: 89 | response = getattr(session, method)( 90 | url, timeout=timeout, **kwargs 91 | ) 92 | return response.json() if decode == 2 else response.content if decode == 1 else response 93 | except Exception as e: 94 | self.log.printer("MULTI_REQUEST", "出现错误 {e}") 95 | time.sleep(1) 96 | return None 97 | 98 | # 登陆 99 | def login(self, username, password): 100 | return NetEaseLogin().login(username=username, password=password) 101 | 102 | # 用户相关 103 | async def users(self): 104 | while True: 105 | try: 106 | user_len = len(self.user_box) 107 | for _ in range(user_len): 108 | await asyncio.sleep(30) 109 | user = self.user_box.pop(0) 110 | survive_status = self.check_survive(user) 111 | if not survive_status: 112 | user = self.flush_session(user) 113 | self.user_box.append(user) 114 | self.log.printer("SURVIVE", "休眠12小时后继续") 115 | await asyncio.sleep(12 * 60 * 60) 116 | except Exception as e: 117 | self.log.printer("SURVIVE", f"流程出错 {e},稍后重试") 118 | self.user_box.append(user) 119 | await asyncio.sleep(60) 120 | 121 | # 转发回滚 122 | async def client(self): 123 | while True: 124 | try: 125 | self.forward() 126 | self.rollback() 127 | self.log.printer("CLIENT", "休眠1小时后继续") 128 | await asyncio.sleep(60 * 60) 129 | except Exception as e: 130 | self.log.printer("CLIENT", f"流程出错 {e},稍后重试") 131 | await asyncio.sleep(60) 132 | 133 | # 迷你扫描 134 | async def mini_scan(self): 135 | while True: 136 | try: 137 | await asyncio.sleep(3 * 60) 138 | self.scan_lottery_id() 139 | self.repeat_lottery() 140 | self.log.printer("SERVER_MASTER", "休眠14小时后继续") 141 | await asyncio.sleep(14 * 60 * 60) 142 | except Exception as e: 143 | self.log.printer("SERVER_MASTER", f"流程出错 {e},稍后重试") 144 | await asyncio.sleep(60) 145 | 146 | # 完整扫描 147 | async def full_scan(self): 148 | while True: 149 | try: 150 | await asyncio.sleep(10 * 60) 151 | self.designation_section() 152 | self.repeat_lottery_scan() 153 | self.log.printer("SERVER_SLAVE", "休眠4小时后继续") 154 | await asyncio.sleep(4 * 60 * 60) 155 | except Exception as e: 156 | self.log.printer("SERVER_SLAVE", f"流程出错 {e},稍后重试") 157 | await asyncio.sleep(60) 158 | 159 | # 字符匹配抽奖id 160 | def match_lottery_id(self, json_data): 161 | pattern = r'\"lotteryId\":(\d+),\"status\":(\d)' 162 | str_data = str(json_data) \ 163 | .replace(" ", "") \ 164 | .replace("\n", "") \ 165 | .replace("\r", "") 166 | return re.findall(pattern, str_data) 167 | 168 | # 去重 169 | def repeat_lottery(self): 170 | self.db.change_table(self.raw_event) 171 | temp_lottery_list = self.db.query_key('lottery_id') 172 | self.log.printer("M_SCAN", f"当前共有 {len(self.lottery_list)} 个动态抽奖需要去重") 173 | for lottery_id in self.lottery_list: 174 | time.sleep(2) 175 | if lottery_id in temp_lottery_list: 176 | continue 177 | data = self.fetch_lottery_info(lottery_id) 178 | # if data['status'] == 2: 179 | # continue 180 | self.db.change_table(self.raw_event) 181 | self.db.insert_raw( 182 | data['uid'], data['event_msg'], data['event_id'], 183 | data['lottery_id'], data['lottery_time'] 184 | ) 185 | self.lottery_list = [] 186 | self.log.printer("M_SCAN", f"动态抽奖库存去重完成") 187 | 188 | # 扫描动态 189 | def scan_lottery_id(self): 190 | url = 'http://music.163.com/weapi/act/event?csrf_token=' 191 | scan_page = 10 192 | max_page = '100' 193 | last_time = '-1' 194 | act_ids = { 195 | '互动抽奖': '44196506', 196 | '抽奖活动': '17731067', 197 | '转发抽奖': '20397151', 198 | '抽奖福利': '19873053', 199 | '粉丝福利': '3753053' 200 | } 201 | for act_id in act_ids.keys(): 202 | for page in range(0, scan_page): 203 | self.log.printer("M_SCAN", f'开始扫描 {act_id} 第 {page + 1} 页') 204 | while True: 205 | try: 206 | params = { 207 | "actid": act_ids[act_id], 208 | "total": "true", 209 | "limit": "20", 210 | "lasttime": last_time, 211 | "pagesize": max_page, 212 | "getcounts": "true", 213 | "csrf_token": "" 214 | } 215 | params = self.enc.get(params) 216 | response = self.session.post(url, params=params).json() 217 | # 匹配 未过滤 218 | match_data_all = self.match_lottery_id(response) 219 | 220 | for match_data in match_data_all: 221 | try: 222 | lottery_id = int(match_data[0]) 223 | lottery_status = int(match_data[1]) 224 | except Exception as e: 225 | continue 226 | status = 'valid' if lottery_status == 1 else 'invalid' 227 | self.log.printer( 228 | "M_SCAN", 229 | f"title: {act_id}, lotteryId: {lottery_id}, status: {status}" 230 | ) 231 | if lottery_id in self.lottery_list: 232 | # or lottery_status == 2: 233 | continue 234 | 235 | self.lottery_list.append(lottery_id) 236 | last_time = response['lasttime'] 237 | break 238 | except Exception as e: 239 | traceback.print_exc() 240 | self.log.printer("M_SCAN", f"扫描动态出现错误 {e}, 稍后重试!") 241 | 242 | if not response['more']: 243 | break 244 | self.log.printer("M_SCAN", 245 | f"此次扫描已结束,当前库存 {len(self.lottery_list)} 个未验证动态抽奖") 246 | 247 | # 分配扫描段 248 | def designation_section(self): 249 | if len(self.pre_scan_list) == 0: 250 | self.db.change_table(self.raw_event) 251 | lottery_data = self.db.query_valid() 252 | valid_lottery_list = [] 253 | for lottery in lottery_data: 254 | valid_lottery_list.append(lottery['lottery_id']) 255 | self.calc_section(valid_lottery_list) 256 | 257 | for _ in range(1000): 258 | if len(self.pre_scan_list) == 0: 259 | break 260 | self.designation_list.append(self.pre_scan_list.pop(0)) 261 | self.log.printer( 262 | "F_SCAN", 263 | f"预备扫描 {len(self.pre_scan_list)} 分配扫描 {len(self.designation_list)}" 264 | ) 265 | 266 | # 去重扫描 267 | def repeat_lottery_scan(self): 268 | self.db.change_table(self.raw_event) 269 | exist_lottery_list = self.db.query_key('lottery_id') 270 | self.log.printer("F_SCAN", 271 | f"当前分配 {len(self.designation_list)} 个动态抽奖需要扫描") 272 | for lottery_id in self.designation_list: 273 | time.sleep(random.uniform(1.5, 2)) 274 | if lottery_id in exist_lottery_list: 275 | continue 276 | data = self.fetch_lottery_info(lottery_id) 277 | if data is None: 278 | continue 279 | self.db.change_table(self.raw_event) 280 | self.db.insert_raw( 281 | data['uid'], data['event_msg'], data['event_id'], 282 | data['lottery_id'], data['lottery_time'] 283 | ) 284 | 285 | self.designation_list = [] 286 | self.log.printer("F_SCAN", f"动态抽奖去重扫描完成") 287 | 288 | # 生存检测 289 | def check_survive(self, user): 290 | url = f"http://music.163.com/api/lottery/event/get?lotteryId=1" 291 | try: 292 | response = user['session'].get(url).json() 293 | # {"code":301,"message":"系统错误","debugInfo":null} 294 | if response['code'] == 301: 295 | self.log.printer("SURVIVE", f"{user['username']} 存活检测 -> fail") 296 | return False 297 | self.log.printer("SURVIVE", f"{user['username']} 存活检测 -> success") 298 | return True 299 | except Exception as e: 300 | self.log.printer("SURVIVE", f"{user['username']} 存活检测 -> {e}") 301 | return None 302 | 303 | # 刷新session 304 | def flush_session(self, user): 305 | try: 306 | user['session'] = self.login( 307 | user['username'], user['password'] 308 | ) 309 | if user['type'] == 'default': 310 | self.session = user['session'] 311 | self.log.printer( 312 | "SURVIVE", 313 | f"{user['username']} 刷新SESSION -> success" 314 | ) 315 | except Exception as e: 316 | self.log.printer("SURVIVE", f"{user['username']} 刷新SESSION -> {e}") 317 | return user 318 | 319 | # 取动态抽奖信息 320 | def fetch_lottery_info(self, lottery_id): 321 | url = f"http://music.163.com/api/lottery/event/get?lotteryId={lottery_id}" 322 | try: 323 | response = self.session.get(url).json() 324 | if response['code'] == 200: 325 | data = { 326 | 'uid': response['data']['user']['userId'], 327 | 'event_msg': response['data']['event']['eventMsg'], 328 | 'event_id': response['data']['lottery']['eventId'], 329 | 'lottery_id': response['data']['lottery']['lotteryId'], 330 | 'lottery_time': response['data']['lottery'][ 331 | 'lotteryTime'], 332 | 'prizes': response['data']['prize'], 333 | 'status': response['data']['lottery']['status'] 334 | } 335 | self.log.printer("LOTTERY_INFO", f"{lottery_id} -> 命中抽奖") 336 | return data 337 | elif response['code'] == 404: 338 | self.log.printer( 339 | "LOTTERY_INFO", 340 | f"{lottery_id} -> {response['message']}" 341 | ) 342 | else: 343 | self.log.printer("LOTTERY_INFO", 344 | f"{lottery_id} -> {response}") 345 | return None 346 | except Exception as e: 347 | self.log.printer("LOTTERY_INFO", f"{lottery_id} -> {e}") 348 | # {"code":404,"message":"动态资源不存在","debugInfo":null} 349 | return None 350 | 351 | # 计算区间 352 | def calc_section(self, lottery_id_list): 353 | scan_list = [] 354 | for lottery_id in lottery_id_list: 355 | if int(lottery_id) < 10000: 356 | start = 1 357 | end = 9999 358 | else: 359 | start = int(f"{str(lottery_id)[:-4]}0000") 360 | end = int(f"{str(lottery_id)[:-4]}9999") 361 | scan_list.append([start, end]) 362 | for scan in scan_list: 363 | if scan_list.count(scan) == 1: 364 | continue 365 | # 第一方案 366 | self.pre_scan_list += list(range(scan[0], scan[1])) 367 | self.pre_scan_list = list(set(self.pre_scan_list)) 368 | # 第二方案 369 | # self.pre_scan_list = list(set(self.pre_scan_list).union(set(list(range(start, end))))) 370 | # self.log.printer(f"当前区间 {start},{end} 已有数据 {len(self.pre_scan_list)}") 371 | 372 | # 转发 373 | def publish(self, user, raw_event_id, event_id, event_uid, msg): 374 | try: 375 | csrf = self.get_csrf(user) 376 | url = f"http://music.163.com/weapi/event/forward?csrf_token={csrf}" 377 | params = { 378 | "forwards": msg, 379 | "id": event_id, 380 | "eventUserId": event_uid, 381 | "checkToken": "", 382 | "csrf_token": csrf 383 | } 384 | params = self.enc.get(params) 385 | response = user['session'].post(url, params=params).json() 386 | self.log.printer("FORWARD", response) 387 | self.db.change_table(self.used_event) 388 | self.db.insert_used( 389 | user['username'], response['data']['eventId'], raw_event_id 390 | ) 391 | url = f'http://music.163.com/weapi/feedback/weblog?csrf_token={csrf}' 392 | params = { 393 | "logs": '[{"action": "eventclick","json":{"id": %s,"sourceid": %s,"alg": "","contentType": "user_event","actionType": "forward"}}]' % ( 394 | event_id, event_uid), 395 | "csrf_token": csrf 396 | } 397 | params = self.enc.get(params) 398 | response = user['session'].post(url, params=params).json() 399 | self.log.printer("FORWARD", response) 400 | except Exception as e: 401 | self.log.printer("FORWARD", f"{response} -> {e}") 402 | return False 403 | return True 404 | 405 | # 获取csrf 406 | def get_csrf(self, user): 407 | return (user['session'].cookies.get_dict())['__csrf'] 408 | 409 | # 关注 410 | def follow(self, user, follow_uid): 411 | csrf = self.get_csrf(user) 412 | url = f"http://music.163.com/weapi/user/follow/{follow_uid}?csrf_token={csrf}" 413 | user['session'].headers.update( 414 | { 415 | 'Host': 'music.163.com', 416 | 'Origin': 'http://music.163.com', 417 | 'Referer': f"http://music.163.com/user/home?id={follow_uid}", 418 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36', 419 | } 420 | ) 421 | params = { 422 | "followId": follow_uid, 423 | "checkToken": "", 424 | "csrf_token": csrf 425 | } 426 | params = self.enc.get(params) 427 | response = user['session'].post(url, params=params) 428 | self.log.printer("FOLLOW", response.json()) 429 | 430 | # 取消关注 431 | def un_follow(self, user, follow_uid): 432 | csrf = self.get_csrf(user) 433 | url = f'http://music.163.com/weapi/user/delfollow/{follow_uid}?csrf_token={csrf}' 434 | params = { 435 | "followId": follow_uid, 436 | "csrf_token": csrf 437 | } 438 | params = self.enc.get(params) 439 | response = user['session'].post(url, params=params).json() 440 | self.log.printer("UN_FOLLOW", response) 441 | 442 | # 过滤关键词 钓鱼测试 443 | def filter_keywords(self, desp): 444 | keys = [ 445 | '禁言', '测试', 'vcf', '体验中奖', '中奖的感觉', '赶脚', '感脚', '感jio', 446 | '黑名单', '拉黑', '拉黑', '脸皮厚', '没有奖品', '无奖', '脸皮厚', 'ceshi', 447 | '测试', '脚本', '抽奖号', '不要脸', '至尊vip会员7天', '高级会员7天', '万兴神剪手', 448 | '测试', '加密', 'test', 'TEST', '钓鱼', '炸鱼', '调试' 449 | ] 450 | for key in keys: 451 | if key in desp: 452 | return False 453 | return True 454 | 455 | # 过滤关键词 奖品 456 | def filter_prizes(self, prizes): 457 | keys = [ 458 | '编曲', '作词', '半价', '打折', '机器', '禁言', '测试', 'vcf', '体验中奖', 459 | '中奖的感觉', '录歌', '混音', '一毛', '0.1元', '1角', '0.5元', '5毛', 460 | '赶脚', '感脚', '曲风', '专辑封面', '封面', '一元红包', '感jio', '名片赞', 461 | '黑名单', '拉黑', '拉黑', '脸皮厚', '没有奖品', '无奖', '脸皮厚', 462 | '测试', '脚本', '抽奖号', '不要脸', '至尊vip会员7天', '高级会员7天', 463 | '测试', '加密', 'test', 'TEST', '钓鱼', '炸鱼', '调试', '歌曲定制', 464 | '学习视频', '修图视频', '作词', '免费编曲', '后期制作', '编曲搬家', 465 | '内容自定', '音乐人一个', '私人唱歌', '感恩', '作业', '八字', '算命', 466 | '电台', '情感视频', '万兴神剪手', '学习修图', '写一首歌', 'ceshi', 467 | '管饱', 'dong tai ga', '电话唱歌', '感谢转发', '非独家使用权', '前排沙发', 468 | '琴谱', '有就送', '什么也不给', '什么都没有', '租赁', '伴奏', '定制beat', 469 | '定制logo', '惊喜软件', '终于中奖', '加群', '第一批粉丝', '祝大家', 470 | '内部群', '老粉', '仅关注', '仅我关注', '打字粉丝ID', '手打粉丝ID', 471 | '人声采集', '采样包', '约稿', 'remix', '明信片', '感受中奖', '快落', 472 | '中奖的快乐', '单曲', '主题创作', '猎妈', '签名照', '数字专辑', '除夕夜', 473 | '专辑', '励志的话', '亲笔签名', 'up', '扫码进群', '粉丝群', '签名写真', 474 | '纹身', '祝你', '红包雨', '电子书', '我', '专辑', '好友位', '豪车优惠', 475 | '观众老爷', '的支持', '拉黑', '黑名单', '脸皮厚', '没有奖品', '无奖', 476 | '测试', '测试', '测试', '脚本', '抽奖号', '星段位', '星段位', '圣晶石', 477 | '水晶', '水晶', '万兴神剪手', '万兴神剪手', 'ceshi', 'QQ', '测试', 478 | '自付邮费', '自付邮费', 'test', 'Test', 'TEST', '加密', '测试用', 479 | 'VX', 'vx', 'ce', 'shi', '这是一个', 'lalall', '第一波', '第二波', 480 | '策是', '我是抽奖', '照片', '穷', '0.5', '一角', '冥币', '加速器', 481 | '硬币', '无奖品', '白名单', '圣晶石', '奖品B', '奖品C', '五毛', '第三波', 482 | '0.1', '五毛二', '一分', '一毛', '0.52', '0.66', '0.01', '0.77', '0.16', 483 | '0.88', '双排', '1毛', '1分', '1角', 'P口罩', '素颜', '写真', '图包', 484 | '自拍', '日历', '0.22', '房间抽奖', 'CESHI', '脸皮厚', 'ceshi', '奖品A', 485 | '抽奖标题', '測試', '越南盾', '啥都没有', '哈哈哈', '作曲', '一首', '手绘', 486 | '学霸', 'buff', '头像', '剩的', '中奖的', 'Ziyoda', 'Hilola', 'beden', '新专', 487 | '采样', '音频', '海报', '关注', '粉丝ID', '电子书', '我', '半价', '优惠券', 488 | '微博', '互粉', '真心话', '回答', '签名海报', '不想要', '抱抱', '拥抱', 'WAV', 489 | '伴奏', '邀请函', '你猜猜', '什么也没有', '什么都', '什么也', '这不是抽奖', 490 | '真欧', '很欧', '使用权', '曲谱', '啥也没有', '木有', '哈哈哈', '车载音乐', 491 | '中奖的', '中奖滴', '会员歌曲', '一首歌', '必唱', '发文件', '词作', '购买资格', 492 | '粉群', '优惠', '折扣', 'hoholive', 'surat', 'hisyat', '免费观', '免费演', 493 | '免费门', '谢谢参与', 'vx call u', '新婚快乐', '歌曲使用权', '普通mp3使用权', 494 | '破解版', '土嗨', '给你写', '普通mp3', '啥也不是', '歌曲大礼包', '歌手大礼包', 495 | '无损wav', 'mp3使用权', 'wav使用权', '曲谱', '阿里嘎多', '代金券', '你的想法', 496 | '封面制作', '一句喵喵喵', '互关', '逢考必过', '云豆', '擁抱', '音色包', '点赞', 497 | '动态', '冠军照', '手抄', '邮费自理', '*7天', '7天', '周卡', '七日', '七天', 498 | '钥匙扣', '中奖辣', '7日', '给我转账', '体验卡', '吻', '么么', '没有', '分享图片', 499 | '黑胶会员7天', '黑胶会员5天', '评论区', '热评', '微信', 'vx', '原创', '参与', 500 | '文档', '谢谢', '感谢', '祝福', '沾沾福', '一周', '歌曲', '评论', '赞赏', ',', 501 | '签名', '文件', '英语四级', '新年快乐', 502 | ] 503 | # 过滤 一等奖 奖品 504 | for prize in prizes: 505 | for key in keys: 506 | if key in prize['name']: 507 | return False 508 | break 509 | return True 510 | 511 | # 过滤404动态 512 | def filter_dynamic(self, event_id): 513 | pass 514 | 515 | # 概率性抽奖 516 | def filter_probability(self): 517 | pass 518 | 519 | # 过滤转发数据 520 | def filter_repost(self, lottery_id): 521 | data = self.fetch_lottery_info(lottery_id) 522 | # 抽奖存在异常 523 | if data is None: 524 | return False, '获取动态抽奖信息异常' 525 | # 抽奖存在异常关键字返回假 526 | if not self.filter_keywords(data['event_msg']): 527 | return False, '标题内容存在异常关键字' 528 | # 抽奖奖品存在异常 529 | if not self.filter_prizes(data['prizes']): 530 | return False, '奖品内容存在异常关键字' 531 | return True, None 532 | 533 | # 转发动态 534 | def forward(self): 535 | messages = [ 536 | '转发动态', '转发动态', '', '好运来', '啊~', '哈哈哈', '抽奖奖(⌒▽⌒)', 537 | '中奖绝缘体', '绝缘体', '求脱非入欧', '好运', '中奖绝缘体表示想中!', 538 | '呜呜呜非洲人来了', '选我吧', '一定会中', '好运bufff', '滴滴滴', '哇哇哇哇', 539 | '拉低中奖率', '万一呢', '非酋日常', '加油', '抽中吧', '我要', '想欧一次!', 540 | '拉低中奖率233', '想要...', '路过拉低中奖率', '希望有个好运气', 541 | '中奖', '什么时候才会抽到我呢?', '试试水,看看能不能中', '过来水一手', 542 | '这辈子都不可能中奖的', '先拉低中奖率23333', '先抽奖,抽不到再说', 543 | '嘤嘤嘤', '捞一把', '我就想中一次', '拉低拉低', '试一试', '搞一搞', 544 | '中奖什么的不可能的( ̄▽ ̄)', '听说我中奖了?', '脱非转欧', 'emm', 545 | ] 546 | self.db.change_table(self.raw_event) 547 | data = self.db.query_forward() 548 | for d in data: 549 | event_status, status_msg = self.filter_repost(d['lottery_id']) 550 | if not event_status: 551 | self.log.printer( 552 | "REPOST", f"当前动态 {d['lottery_id']} {status_msg} 跳过" 553 | ) 554 | continue 555 | for user in self.user_box: 556 | message = random.choice(messages) 557 | status = self.publish(user, d['id'], d['event_id'], d['uid'], 558 | message) 559 | if status: 560 | self.follow(user, d['uid']) 561 | self.db.change_table(self.raw_event) 562 | self.db.update_raw_reposted(d['lottery_id']) 563 | 564 | # 删除动态 565 | def del_event(self, user, event_id): 566 | csrf = self.get_csrf(user) 567 | url = f'http://music.163.com/weapi/event/delete?csrf_token={csrf}' 568 | params = { 569 | "type": "delete", 570 | "id": event_id, 571 | "transcoding": "false", 572 | "csrf_token": csrf 573 | } 574 | params = self.enc.get(params) 575 | response = user['session'].post(url, params=params).json() 576 | self.log.printer("DEL_EVENT", response) 577 | 578 | # 回滚 579 | def rollback(self): 580 | self.db.change_table(self.raw_event) 581 | data = self.db.query_delete() 582 | for d in data: 583 | event_status, status_msg = self.filter_repost(d['lottery_id']) 584 | for user in self.user_box: 585 | if not event_status: 586 | self.log.printer( 587 | "ROLLBACK", f"当前动态 {d['lottery_id']} {status_msg} 跳过" 588 | ) 589 | self.db.change_table(self.used_event) 590 | pre_d = self.db.query_pre_event(user['username'], d['id']) 591 | if pre_d is not None: 592 | self.del_event(user, pre_d['pre_event_id']) 593 | # 如果没查询到 取消关注 TODO 错误回滚 594 | self.un_follow(user, d['uid']) 595 | continue 596 | 597 | if not self.win_check(user, d['lottery_id']): 598 | if (d['lottery_time'] + 5 * 60 * 60) > current_unix(): 599 | continue 600 | self.db.change_table(self.used_event) 601 | pre_d = self.db.query_pre_event(user['username'], d['id']) 602 | if pre_d is not None: 603 | self.del_event(user, pre_d['pre_event_id']) 604 | # 如果没查询到 取消关注 TODO 错误回滚 605 | self.un_follow(user, d['uid']) 606 | self.db.change_table(self.raw_event) 607 | self.db.update_raw_deleted(d['lottery_id']) 608 | 609 | # 中奖检测 610 | def win_check(self, user, lottery_id): 611 | url = f"http://music.163.com/api/lottery/event/get?lotteryId={lottery_id}" 612 | response = self.session.get(url).json() 613 | prize_ids = response['data']['lottery']['prizeId'] 614 | prize_ids = prize_ids.strip('[').strip(']').split(',') 615 | 616 | try: 617 | for prize_id in prize_ids: 618 | data = response['data']['luckUsers'][prize_id] 619 | for d in data: 620 | if d['userId'] == int(user['user_id']): 621 | prizes = response['data']['prize'] 622 | for index, prize in enumerate(prizes): 623 | if str(prize['id']) == prize_id: 624 | prize_level = index + 1 625 | prize_name = prize['name'] 626 | 627 | info = f"""亲爱的 [{user['username']} -> {user['user_id']}] 您好: 628 | 恭喜您在【{response['data']['user']['nickname']}】发布的动态互动抽奖活动中,喜获奖品啦! 629 | >>> 互动抽奖{lottery_id} -> {prize_level}等奖 -> {prize_name}] <<< 630 | 请前往网易云音乐APP查看详情,尽快填写中奖信息或领取奖品。""" 631 | self.log.printer("WIN", info) 632 | # (https://music.163.com/st/m#/lottery/detail?id={lottery_id}) 633 | # 中奖提醒 634 | Notification().notice_handler('网易云互动抽奖', info) 635 | return True 636 | except Exception as e: 637 | return False 638 | return False 639 | 640 | 641 | if __name__ == '__main__': 642 | net_ease = NetEaseLottery() 643 | tasks = [ 644 | net_ease.client(), 645 | net_ease.users(), 646 | net_ease.mini_scan(), 647 | net_ease.full_scan() 648 | ] 649 | loop = asyncio.get_event_loop() 650 | loop.run_until_complete(asyncio.wait(tasks)) 651 | loop.close() 652 | --------------------------------------------------------------------------------