diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..224cfd3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,126 @@ + +# Created by https://www.gitignore.io/api/python +# Edit at https://www.gitignore.io/?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +### Python Patch ### +.venv/ + +# End of https://www.gitignore.io/api/python diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..e6cb5ad --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: python app.py \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..997fa73 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +## Telegram 音乐盒子 +在Telegram上搜索并下载某易云音乐 + + +### 效果展示 + +![效果展示](./images/music_box_0.png) + +在发出我想听XXX(歌曲名)后,bot返回的对话框状态由 搜索中 -> 下载中 -> 发送中 -> 歌曲 自动转换 + +搜索中 +![效果展示](./images/music_box_1.png) + +下载中 +![效果展示](./images/music_box_2.png) + + +发送中 +![效果展示](./images/music_box_3.png) + +发送成功 +![效果展示](./images/music_box_4.png) + + +### 部署到Heroku + +1. 第一步,根据[此项目](https://github.com/Binaryify/NeteaseCloudMusicApi)建立音乐API 并部署到heroku。 +2. 在 app.py 中修改对应地方的变量,如 机器人Token,上一步中音乐API的地址等。 +3. 修改之后即可将本项目一同部署到heroku,即可。 + diff --git a/app.json b/app.json new file mode 100644 index 0000000..3f24db5 --- /dev/null +++ b/app.json @@ -0,0 +1,7 @@ +{ + "name": "MusicBox", + "description": "Telegram Music Box", + "image": "heroku/python", + "repository": "{部署代码的仓库地址}", + "keywords": ["python"] +} \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..7be9b23 --- /dev/null +++ b/app.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- + +import os +import io +import re +from functools import wraps +import logging +from collections import namedtuple +logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=logging.INFO) + +import requests +import telegram +from telegram import ChatAction +from telegram.ext import Updater, CommandHandler, MessageHandler, Filters +from telegram.error import (TelegramError, Unauthorized, BadRequest, + TimedOut, ChatMigrated, NetworkError) + +# 机器人的 TOKEN 填写在这里 +TOKEN = "{YOUR_BOT_TOKEN}" +PORT = int(os.environ.get('PORT', '80')) + +def error_callback(bot, context, error): + try: + raise error + except Unauthorized: + # remove update.message.chat_id from conversation list + bot.sendMessage(chat_id=context.message.chat.id, + text = '网络不稳定, 请稍后再试(1)') + except BadRequest: + # handle malformed requests - read more below! + bot.sendMessage(chat_id=context.message.chat.id, + text = '网络不稳定, 请稍后再试(2)') + except TimedOut: + # handle slow connection problems + bot.sendMessage(chat_id=context.message.chat.id, + text = '网络不稳定, 请稍后再试(3)') + except NetworkError: + # handle other connection problems + bot.sendMessage(chat_id=context.message.chat.id, + text = '网络不稳定, 请稍后再试(4)') + except ChatMigrated as e: + # the chat_id of a group has changed, use e.new_chat_id instead + bot.sendMessage(chat_id=context.message.chat.id, + text = '网络不稳定, 请稍后再试(5)') + except TelegramError: + # handle all other telegram related errors + bot.sendMessage(chat_id=context.message.chat.id, + text = '网络不稳定, 请稍后再试(6)') + + +def getMusicNameFromUser(context): + message = context.message.text + if not message.startswith('我想听'): + return None + name = message.replace('我想听','').strip() + return name + + +def getMusicInfoFromInternet(bot, context, musicName, msg): + + # 这里添加网易云音乐 API 的地址 + baseUrl = '{网易云音乐API的地址}' + + music = namedtuple('music_info', ['name', 'id', 'author', 'thumb', 'url', 'lyric']) + resp = requests.get(baseUrl + f'/search?keywords={musicName}').json() + music_info = None + try: + music_info = resp['result']['songs'][0] + except KeyError as e: + bot.edit_message_text(chat_id=context.message.chat_id, + message_id=msg.message_id, + text='「'+ musicName +'」- 找不到该歌曲') + return None + + music_id = music_info['id'] + music_name = music_info['name'] + music_author = music_info['artists'][0]['name'] + music_url = f'https://music.163.com/song/media/outer/url?id={music_id}.mp3' + thumb_url = None + music_lyric = None + + try: + resp = requests.get(baseUrl + f'/song/detail?ids={music_id}').json() + music_info = resp['songs'][0] + thumb_url = music_info['al']['picUrl'] + except KeyError as e: + logging.info('cannot find music detail info !') + + + try: + resp = requests.get(baseUrl + f'/lyric?id={music_id}').json() + music_lyric = handle_lyric(resp['lrc']['lyric']) + except KeyError as e: + logging.info('cannot find music lyric !') + + return music(music_name, music_id, music_author, thumb_url, music_url, music_lyric) + + +def handle_lyric(old_lyric): + lyric = re.sub(r'\[.*\]','', old_lyric).split('\n') + return '\n'.join(l.strip() for l in lyric) + + +def downloadMusicWithProgress(bot, context, music, msg): + logging.info(f'music: {music}') + if music.url == '': + return msg, None + resp = requests.get(music.url, stream=True) + total_length = resp.headers.get('Content-Length') + dl = 0 + total_length = int(total_length) + output = io.BytesIO() + for data in resp.iter_content(chunk_size=1024*1024): + output.write(data) + dl += len(data) + done = int(25 * dl / total_length) + msg = bot.edit_message_text(chat_id=context.message.chat_id, + message_id=msg.message_id, + text="「"+ music.name +"」- " + music.author + ", 下载中....\n\r`[%s%s]`" % ('█' * done, '.' * (25-done)), + parse_mode='Markdown') + return msg, output + + +def echo(bot, context): + name = getMusicNameFromUser(context) + if not name: return + + msg = context.message.reply_text("「"+ name +"」 搜索中...", + reply_to_message_id=context.message.message_id) + + music = getMusicInfoFromInternet(bot, context, name, msg) + if not music: return + + msg, output = downloadMusicWithProgress(bot, context, music, msg) + + if output is not None: + msg = bot.edit_message_text(chat_id=context.message.chat_id, + message_id=msg.message_id, + text='mp3发送中....') + + bot.sendAudio(chat_id=context.message.chat.id, + audio=io.BytesIO(output.getvalue()), + timeout=3000, + title=music.name, + performer=music.author, + thumb=music.thumb, + caption=music.lyric) + else: + bot.sendMessage(chat_id=context.message.chat.id, + text = '_该歌曲由于版权或者其他原因无法下载_\n\n' + music.author + ' - ' + music.name + '\n' + music.lyric, + parse_mode='Markdown') + + bot.delete_message(chat_id=context.message.chat_id, + message_id=msg.message_id) + + +def main(): + updater = Updater(TOKEN) + + updater.start_webhook(listen="0.0.0.0",port=PORT,url_path=TOKEN) + updater.bot.set_webhook("{部署代码的URL}" + TOKEN) + + dp = updater.dispatcher + dp.add_handler(MessageHandler(Filters.text, echo)) + dp.add_error_handler(error_callback) + + updater.idle() + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/images/music_box_0.png b/images/music_box_0.png new file mode 100644 index 0000000..01b64bb Binary files /dev/null and b/images/music_box_0.png differ diff --git a/images/music_box_1.png b/images/music_box_1.png new file mode 100644 index 0000000..7fe10a2 Binary files /dev/null and b/images/music_box_1.png differ diff --git a/images/music_box_2.png b/images/music_box_2.png new file mode 100644 index 0000000..3a519b5 Binary files /dev/null and b/images/music_box_2.png differ diff --git a/images/music_box_3.png b/images/music_box_3.png new file mode 100644 index 0000000..c31fa89 Binary files /dev/null and b/images/music_box_3.png differ diff --git a/images/music_box_4.png b/images/music_box_4.png new file mode 100644 index 0000000..a19d4d4 Binary files /dev/null and b/images/music_box_4.png differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5b3d8e9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,12 @@ +asn1crypto==0.24.0 +certifi==2018.11.29 +cffi==1.12.2 +chardet==3.0.4 +cryptography==2.6.1 +future==0.17.1 +idna==2.8 +pycparser==2.19 +python-telegram-bot==11.1.0 +requests==2.21.0 +six==1.12.0 +urllib3==1.24.1