├── requirements.txt ├── Procfile ├── Dockerfile ├── docker-compose.yml ├── bot ├── main.py ├── zh_TW.py ├── rest.py ├── database.py └── bot.py ├── LICENSE ├── README-zh_tw.md └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | aiotg>=0.7.3 2 | motor>=0.5 -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn gettingstarted.wsgi --log-file - 2 | web: python ./bot/main.py -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python 2 | 3 | ADD requirements.txt /bot/ 4 | WORKDIR /bot 5 | RUN pip install -r ./requirements.txt 6 | 7 | ADD bot /bot 8 | 9 | CMD ["python", "./main.py"] 10 | 11 | EXPOSE 8080 12 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | services: 3 | musicbot: 4 | build: . 5 | links: 6 | - mongo 7 | environment: 8 | - BOT_NAME= 9 | - API_TOKEN= 10 | - BOTAN_TOKEN=Optional botan token 11 | - MONGO_HOST=MONGODB_URI 12 | - CHANNEL= 13 | - CHANNEL_NAME= 14 | - LOGCHN_ID= 15 | - REST_PORT= 16 | - REST_HOST= 17 | - MONGO_DB_NAME= 18 | - LANG=zh-TW 19 | mongo: 20 | image: mongo 21 | -------------------------------------------------------------------------------- /bot/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import asyncio 3 | import os 4 | 5 | from bot import bot 6 | from rest import RestBridge 7 | from database import prepare_index 8 | 9 | rest = RestBridge(bot) 10 | 11 | async def start(): 12 | await prepare_index() 13 | await rest.start() 14 | await bot.loop() 15 | 16 | 17 | async def stop(): 18 | await rest.stop() 19 | 20 | 21 | if __name__ == '__main__': 22 | loglevel = logging.DEBUG if os.getenv("DEBUG") else logging.INFO 23 | logging.basicConfig(level=loglevel) 24 | 25 | loop = asyncio.get_event_loop() 26 | try: 27 | loop.run_until_complete(start()) 28 | except KeyboardInterrupt: 29 | pass 30 | finally: 31 | loop.run_until_complete(stop()) 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Rex Tseng 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 | -------------------------------------------------------------------------------- /README-zh_tw.md: -------------------------------------------------------------------------------- 1 | # Telegram 音樂 bot 2 | 3 | [![部屬到 OpenShift](http://launch-shifter.rhcloud.com/launch/light/部屬到.svg)](https://openshift.redhat.com/app/console/application_type/custom?&cartridges[]=python-3.5&initial_git_url=https://github.com/rexx0520/Telegram-Music-Bot&name=Telegram%20Music%20Bot) 4 | 5 | ## 簡介 6 | 7 | 這是一個 Telegram 的 音樂 Bot 8 | 9 | 由 [szastupov/musicbot](//github.com/szastupov/musicbot) 改進而成。 10 | 11 | ### 改進項目 12 | 13 | - 重寫的搜尋邏輯,支援模糊搜尋。 14 | 15 | - 支援更多格式 16 | 17 | - 支援搜尋參數,可限定藝術家、曲名和檔案格式 18 | 19 | - Log Channel 支援 20 | 21 | - 管理員功能,支援刪除資料 22 | 23 | ## 用法 24 | 25 | 輸入關鍵字來搜尋音樂資料庫,傳送音樂檔案以增加至資料庫。 26 | 27 | 輸入 `/help` 來獲取說明 28 | 29 | 在關鍵字後輸入`type:TYPE`來限定音樂格式,像這樣: 30 | 31 | 32 | >```棒棒勝 type:flac``` 33 | > 34 | >```棒棒勝 type:mp3``` 35 | > 36 | >```棒棒勝 type:mpeg``` 37 | 38 | 若同時想搜尋作者和曲名,請用 `>` 隔開 (預設為作者、曲名都納入搜尋),像這樣: 39 | 40 | 41 | >```棒棒勝>洨安之歌``` 42 | 43 | 也可以搭配`type`指令,像這樣: 44 | 45 | 46 | >```棒棒勝>洨安之歌 type:flac``` 47 | 48 | 輸入 `/stats` 來獲取 bot 資訊。 49 | 50 | 用 `/music` 指令來在群聊內使用棒棒勝 Music Bot,像這樣: 51 | 52 | 53 | >```/music 棒棒勝``` 54 | 55 | 對於一個音樂文件回覆 `/add` 來新增至資料庫。 56 | 57 | 丟進音樂頻道的歌曲將會被同步至資料庫。 58 | 59 | log 頻道的管理員將獲得 Bot 管理員權限。 60 | 61 | ### 管理員指令: 62 | 63 | >```/delete 棒棒勝``` 64 | 65 | `/admin` 可以看到目前的管理員列表。 66 | 67 | ## 環境變數 68 | 69 | **API_TOKEN** : Bot 的 API Token 70 | 71 | **BOT_NAME** : Bot 的名字 72 | 73 | 74 | **CHANNEL** : 音樂頻道 ID 75 | 76 | **CHANNEL_NAME** : 音樂頻道的名字 77 | 78 | **LOGCHN_ID** : log 頻道的 chat ID (記得帶負號) 79 | 80 | 81 | **REST_PORT** : REST API 的埠 82 | 83 | **REST_HOST** : REST API 的主機IP,通常是 `0.0.0.0` 84 | 85 | 86 | **MONGO_HOST** : MongoDB 的網址 87 | 88 | 例如 : `mongodb://user:pwd@host/python` 89 | 90 | **MONGO_DB_NAME** : MongoDB 資料庫的名字 91 | 92 | **LANG** : 語言設定 93 | 94 | 可用語言:zh-TW 95 | 96 | 歡迎發 PR 新增翻譯! 97 | -------------------------------------------------------------------------------- /bot/zh_TW.py: -------------------------------------------------------------------------------- 1 | greeting = """ 2 | ✋ 歡迎來到棒棒勝 Music 的 Bot ! 🎧 3 | 輸入關鍵字來搜尋音樂資料庫,傳送音樂檔案以增加至資料庫。 4 | 輸入 `/help` 來獲取說明! 5 | ** 丟進本 Bot 的音樂不會同步到頻道唷!只有頻道的會同步過來 owo ** 6 | """ 7 | 8 | help = """ 9 | 輸入關鍵字來搜尋音樂資料庫。 10 | 在關鍵字後輸入`type:TYPE`來限定音樂格式,像這樣: 11 | ```棒棒勝 type:flac``` 12 | ```棒棒勝 type:mp3``` 13 | ```棒棒勝 type:mpeg``` 14 | 若同時想搜尋作者和曲名,請用 `>` 隔開 (預設為作者、曲名都納入搜尋),像這樣: 15 | ```棒棒勝>洨安之歌``` 16 | 也可以搭配`type`指令,像這樣: 17 | ```棒棒勝>洨安之歌 type:flac``` 18 | 輸入 `/stats` 來獲取 bot 資訊。 19 | 用 `/music` 指令來在群聊內使用棒棒勝 Music Bot,像這樣: 20 | `/music 棒棒勝` 21 | 對於一個音樂文件回覆 `/add` 來新增至資料庫。 22 | 此外,本 bot 也支援 inline mode。 23 | 在所有地方輸入 `@music_Index_bot` 加空格後便可搜尋音樂。 24 | """ 25 | 26 | not_found = """ 27 | 找不到資料 :/ 28 | """ 29 | 30 | texts = { 31 | 'tagNotFound': "傳送失敗...是不是你的音樂檔案少了資訊標籤? :(", 32 | 'musicExists': "資料庫裡已經有這首囉 owo", 33 | 'sentExistedMusic': lambda sender, artist, title: sender +" 傳送了重複的歌曲 "+ artist +" - "+ title, 34 | 'replaced': "檔案大小較資料庫內的大,已取代!", 35 | 'sentLargerMusic': lambda sender, artist, title: sender + " 傳送了大小較大的歌曲 " + artist + " - " + title, 36 | 'addMusic': lambda sender, artist, title: sender + " 新增了 " + artist + " - " + title, 37 | 'inquiredAdminListRefused': lambda user: user + ' 查詢了管理員名單,遭到拒絕。', 38 | 'denied': "存取遭拒。", 39 | 'inquiredAdminList': lambda user: user + ' 查詢了管理員名單', 40 | 'deleteRefused': lambda user, keyword: user + ' 意圖刪除 ' + keyword + ',遭到拒絕。', 41 | 'deleteNumTypeArt': lambda sender, num, type, artist, title: sender + " 刪除了 " + num + ' 個 ' + type + " 格式的 " + artist + "的" + title, 42 | 'deleteNumArt': lambda sender, num, artist, title: sender + " 刪除了 " + num + ' 個 ' + artist + "的" + title, 43 | 'deleteNumType':lambda sender, num, type, keyword: sender + " 刪除了 " + num + ' 個 ' + type + " 格式的 " + keyword, 44 | 'deleteNum': lambda sender, num, keyword: sender + " 刪除了 " + num + ' 個 ' + keyword, 45 | 'deleteError': "刪除元素個數有問題RRR", 46 | 'nextPage': "下一頁", 47 | 'searchTypeArt': lambda sender, type, artist, title: sender + " 搜尋了 " + type + " 格式的 " + artist + "的" + title, 48 | 'searchArt': lambda sender, artist, title: sender + " 搜尋了 " + artist + "的" + title, 49 | 'searchType': lambda sender, type, keyword: sender + " 搜尋了 " + type + " 格式的 " + keyword, 50 | 'search': lambda user, keyword: user + " 搜尋了 " + keyword, 51 | 'searchError': "元素個數有問題RRR", 52 | 'newUser': lambda user: "新用戶 " + user, 53 | 'exit': lambda user: user + " 退出了", 54 | 'bye': "掰掰! 😢", 55 | 'statsNotReady': "統計資訊還沒好!", 56 | 'musicCalc': lambda count, size: count + '首歌曲' + size, 57 | 'unknownArtist': "未知藝術家", 58 | 'untitled': "無標題" 59 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telegram Music Bot 2 | 3 | [![LAUNCH ON OpenShift](http://launch-shifter.rhcloud.com/launch/light/LAUNCH%20ON.svg)](https://openshift.redhat.com/app/console/application_type/custom?&cartridges[]=python-3.5&initial_git_url=https://github.com/rexx0520/Telegram-Music-Bot&name=Telegram%20Music%20Bot) 4 | 5 | ## Description 6 | 7 | This is a Telegram music catalog bot. 8 | Was originated and improved from [szastupov/musicbot](//github.com/szastupov/musicbot) . 9 | 10 | ### Improvements 11 | 12 | - Rewrote search logic, fuzzy search is supported. 13 | 14 | - More formats is supported. 15 | 16 | - Various searching parameters, including artist, title and formats. 17 | 18 | - Log channel support 19 | 20 | - Admin function, `/delete` is supported. 21 | 22 | - Duplicates detection. Will only keep the one with a highest resolution. 23 | 24 | ## Usage 25 | 26 | Simply send keywords to search from the database. 27 | 28 | Send music files to add it to the database. 29 | 30 | Command `/help` for help. 31 | 32 | `type:TYPE` after keywords to restrict the type of result. 33 | 34 | 35 | >```Xiaoan type:flac``` 36 | > 37 | >```Xiaoan type:mp3``` ( `mp3` was converted to `mpeg` in bot since mp3 is not a mime-type.) 38 | > 39 | >```Xiaoan type:mpeg``` 40 | 41 | Seperate the artist and song by `>` . 42 | 43 | 44 | >```Xiaoan>The song of early-spring``` 45 | 46 | It also works great with `type`. 47 | 48 | 49 | >```Xiaoan>The song of early-spring type:flac``` 50 | 51 | Command `/stats` for the status of bot. 52 | 53 | Command `/music` to send music files from this bot in a group chat. 54 | 55 | 56 | >```/music Xiaoan``` 57 | 58 | Reply `/add` to a music file in a group chat to add music file to the database. 59 | 60 | Songs which was uploaded to the music channel will be sync to the database. 61 | 62 | Add admins assigning as an admin of the log channel. 63 | 64 | ### Admin commands 65 | 66 | >```/delete The Song of early-spring``` 67 | 68 | `/admin` to return a list of admin. 69 | 70 | ## Environment Variables 71 | 72 | **API_TOKEN** : Bot's API Token. 73 | 74 | **BOT_NAME** : Bot's name. 75 | 76 | 77 | **CHANNEL** : Music channel's ID. 78 | 79 | **CHANNEL_NAME** : Music channel's name. 80 | 81 | **LOGCHN_ID** : log channel's chat ID. (with `-`) 82 | 83 | 84 | **REST_PORT** : REST API's port. Usually `8080`. 85 | 86 | **REST_HOST** : REST API's host. Usually `0.0.0.0`. 87 | 88 | 89 | **MONGO_HOST** : MongoDB 's URL. 90 | 91 | e.g. : `mongodb://user:pwd@host/python` 92 | 93 | **MONGO_DB_NAME** : MongoDB Database's name. 94 | 95 | **LANG** : Language. 96 | 97 | Currently Chinese(Taiwan) only. [zh-TW] 98 | 99 | Feel free to open a PR! 100 | -------------------------------------------------------------------------------- /bot/rest.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import aiohttp 3 | import logging 4 | import os 5 | 6 | from aiohttp import web 7 | from database import db, text_search 8 | 9 | 10 | logger = logging.getLogger("rest") 11 | chunk_size = 8192 12 | 13 | 14 | class RestBridge: 15 | def __init__(self, bot): 16 | self.bot = bot 17 | 18 | app = aiohttp.web.Application() 19 | app.router.add_route('GET', '/tracks', self.search) 20 | app.router.add_route('GET', '/files/{file_id}', self.download_file) 21 | 22 | self.app = app 23 | self.handler = app.make_handler() 24 | 25 | async def search(self, request): 26 | text = request.GET.get("text") 27 | offset = int(request.GET.get("offset", 0)) 28 | limit = int(request.GET.get("limit", 10)) 29 | 30 | cursor = text_search(text) if text else db.tracks.find({}) 31 | total = await cursor.count() 32 | results = await cursor.skip(offset).limit(limit).to_list(limit) 33 | for r in results: 34 | del r["_id"] 35 | 36 | return web.json_response({ 37 | "tracks": results, 38 | "offset": offset, 39 | "limit": limit, 40 | "total": total 41 | }) 42 | 43 | async def download_file(self, request): 44 | file_id = request.match_info['file_id'] 45 | 46 | record = await db.tracks.find_one({ "file_id": file_id }) 47 | if not record: 48 | return web.HTTPNotFound() 49 | 50 | file = await self.bot.get_file(file_id) 51 | file_path = file["file_path"] 52 | range = request.headers.get("range") 53 | copy_headers = ["content-length", "content-range", "etag", "last-modified"] 54 | 55 | async with self.bot.download_file(file_path, range) as r: 56 | # Prepare headers 57 | resp = web.StreamResponse(status=r.status) 58 | resp.content_type = record["mime_type"] 59 | for h in copy_headers: 60 | val = r.headers.get(h) 61 | if val: 62 | resp.headers[h] = val 63 | 64 | await resp.prepare(request) 65 | 66 | # Send content 67 | while True: 68 | chunk = await r.content.read(chunk_size) 69 | if not chunk: 70 | break 71 | resp.write(chunk) 72 | 73 | return resp 74 | 75 | async def start(self): 76 | loop = asyncio.get_event_loop() 77 | srv = await loop.create_server(self.handler, os.environ.get("REST_HOST"), os.environ.get("REST_PORT")) 78 | logger.info('serving REST on %s', srv.sockets[0].getsockname()) 79 | self.srv = srv 80 | 81 | async def stop(self): 82 | await self.handler.finish_connections(1.0) 83 | self.srv.close() 84 | await self.srv.wait_closed() 85 | await self.app.finish() 86 | -------------------------------------------------------------------------------- /bot/database.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pymongo 3 | from motor.motor_asyncio import AsyncIOMotorClient 4 | import re 5 | from functools import reduce 6 | 7 | client = AsyncIOMotorClient(host=os.environ.get('MONGO_HOST')) 8 | db = client[os.environ.get('MONGO_DB_NAME')] 9 | 10 | 11 | 12 | async def text_search(query): 13 | typel = query.split(" type:") 14 | if (query.find(">") == -1): 15 | if (len(typel) == 1): 16 | typef = 'audio' 17 | elif (typel[1] == 'mp3'): 18 | typef = 'mpeg' 19 | else: 20 | typef = typel[1] 21 | keyword = typel[0].split(" ") 22 | keyword_regex = re.compile (reduce(lambda x,y: x+'(?=.*?'+y+')', keyword) + '.*?', re.IGNORECASE) 23 | return db.tracks.find( 24 | {"$and":[ 25 | {'mime_type': re.compile (typef, re.IGNORECASE)}, 26 | {"$or":[ 27 | {'title': keyword_regex}, 28 | {'performer': keyword_regex} 29 | ]}]}, 30 | { 'score': { '$meta': 'textScore' } }).sort([('score', {'$meta': 'textScore'})]) 31 | elif (query.find(">") != -1): 32 | art = typel[0].split(">") 33 | if (len(typel) == 1): 34 | typef = 'audio' 35 | elif (typel[1] == 'mp3'): 36 | typef = 'mpeg' 37 | else: 38 | typef = typel[1] 39 | title = art[1].split(" ") 40 | performer = art[0].split(" ") 41 | return db.tracks.find( 42 | {"$and":[ 43 | {'mime_type': re.compile (typef, re.IGNORECASE)}, 44 | {"$and":[ 45 | {'title': re.compile (reduce(lambda x,y: x+'(?=.*?'+y+')', title) + '.*?', re.IGNORECASE)}, 46 | {'performer': re.compile (reduce(lambda x,y: x+'(?=.*?'+y+')', performer) + '.*?', re.IGNORECASE)} 47 | ]}]}, 48 | { 'score': { '$meta': 'textScore' } }).sort([('score', {'$meta': 'textScore'})]) 49 | else: 50 | logger.info("DATABASE ERROR!") 51 | await bot.send_message(logChannelID,"DATABASE ERROR!") 52 | 53 | 54 | async def text_delete(query): 55 | typel = query.split(" type:") 56 | if (query.find(">") == -1): 57 | if (len(typel) == 1): 58 | typef = 'audio' 59 | elif (typel[1] == 'mp3'): 60 | typef = 'mpeg' 61 | else: 62 | typef = typel[1] 63 | keyword = typel[0].split(" ") 64 | keyword_regex = re.compile (reduce(lambda x,y: x+'(?=.*?'+y+')', keyword) + '.*?', re.IGNORECASE) 65 | result = await db.tracks.delete_many( 66 | {"$and":[ 67 | {'mime_type': re.compile (typef, re.IGNORECASE)}, 68 | {"$or":[ 69 | {'title': keyword_regex}, 70 | {'performer': keyword_regex} 71 | ]}]}) 72 | return result.deleted_count 73 | elif (query.find(">") != -1): 74 | art = typel[0].split(">") 75 | if (len(typel) == 1): 76 | typef = 'audio' 77 | elif (typel[1] == 'mp3'): 78 | typef = 'mpeg' 79 | else: 80 | typef = typel[1] 81 | title = art[1].split(" ") 82 | performer = art[0].split(" ") 83 | result = await db.tracks.delete_many( 84 | {"$and":[ 85 | {'mime_type': re.compile (typef, re.IGNORECASE)}, 86 | {"$and":[ 87 | {'title': re.compile (reduce(lambda x,y: x+'(?=.*?'+y+')', title) + '.*?', re.IGNORECASE)}, 88 | {'performer': re.compile (reduce(lambda x,y: x+'(?=.*?'+y+')', performer) + '.*?', re.IGNORECASE)} 89 | ]}]}) 90 | return result.deleted_count 91 | else: 92 | logger.info("DATABASE ERROR!") 93 | await bot.send_message(logChannelID,"DATABASE ERROR!") 94 | 95 | async def prepare_index(): 96 | await db.tracks.create_index([ 97 | ("title", pymongo.TEXT), 98 | ("performer", pymongo.TEXT) 99 | ]) 100 | await db.tracks.create_index([ 101 | ("file_id", pymongo.ASCENDING) 102 | ]) 103 | await db.users.create_index("id") 104 | -------------------------------------------------------------------------------- /bot/bot.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import json 4 | import math 5 | import re 6 | import random 7 | import ast 8 | 9 | from aiotg import Bot, chat 10 | from database import db, text_search, text_delete 11 | 12 | if os.environ.get('LANG') == 'zh-TW': 13 | from zh_TW import greeting, help, not_found, texts 14 | elif os.environ.get('LANG') == 'en-US': 15 | from en_US import greeting, help, not_found, texts 16 | else: 17 | from zh_TW import greeting, help, not_found, texts 18 | 19 | bot = Bot( 20 | api_token=os.environ.get('API_TOKEN'), 21 | name=os.environ.get('BOT_NAME'), 22 | botan_token=os.environ.get("BOTAN_TOKEN") 23 | ) 24 | logger = logging.getLogger("musicbot") 25 | channel = bot.channel(os.environ.get('CHANNEL')) 26 | logChannelID = os.environ.get("LOGCHN_ID") 27 | 28 | async def getAdmin(ID=logChannelID): 29 | raw = ast.literal_eval(str(await bot.api_call("getChatAdministrators",chat_id=ID))) 30 | i=0 31 | adminDict = [] 32 | while i < len(raw['result']): 33 | if 'last_name' in raw['result'][i]['user']: 34 | adminDict.append({ 35 | 'id':raw['result'][i]['user']['id'], 36 | 'username':raw['result'][i]['user']['username'], 37 | 'first_name':raw['result'][i]['user']['first_name'], 38 | 'last_name':raw['result'][i]['user']['last_name']}) 39 | else: 40 | adminDict.append({ 41 | 'id':raw['result'][i]['user']['id'], 42 | 'username':raw['result'][i]['user']['username'], 43 | 'first_name':raw['result'][i]['user']['first_name'], 44 | 'last_name':''}) 45 | i += 1 46 | return adminDict 47 | 48 | async def isAdmin(ID): 49 | i=0 50 | adminList = await getAdmin() 51 | while i int(matchedMusic["file_size"]): 75 | await chat.send_text(texts['musicExists']) 76 | await say(texts['sentExistedMusic'](sendervar, str(audio.get("performer")), str(audio.get("title")))) 77 | return 78 | else: 79 | await text_delete(str(audio.get("performer"))+ '>' + str(audio.get("title"))) 80 | doc = audio.copy() 81 | try: 82 | if (chat.sender["id"]): 83 | doc["sender"] = chat.sender["id"] 84 | except: 85 | doc["sender"] = os.environ.get("CHANNEL") 86 | await db.tracks.insert(doc) 87 | await chat.send_text(texts['replaced']) 88 | await say(texts['sentLargerMusic'](sendervar, str(audio.get("performer")), str(audio.get("title")))) 89 | return 90 | doc = audio.copy() 91 | try: 92 | if (chat.sender["id"]): 93 | doc["sender"] = chat.sender["id"] 94 | except: 95 | doc["sender"] = os.environ.get("CHANNEL") 96 | 97 | await db.tracks.insert(doc) 98 | await say(texts['addMusic'](sendervar, str(doc.get("performer")), str(doc.get("title")))) 99 | if (sendervar != os.environ.get('CHANNEL_NAME')): 100 | await chat.send_text(texts['addMusic'](sendervar, str(doc.get("performer")), str(doc.get("title"))) + " !") 101 | 102 | @bot.command(r'/add') 103 | async def add(chat, match): 104 | audio = chat.message['reply_to_message']['audio'] 105 | if "title" not in audio: 106 | await chat.send_text(texts['tagNotFound']) 107 | return 108 | 109 | if (str(chat.sender) == 'N/A'): 110 | sendervar = os.environ.get('CHANNEL_NAME') 111 | else: 112 | sendervar = str(chat.sender) 113 | if (await db.tracks.find_one({ "file_id": audio["file_id"] })): 114 | await chat.send_text(texts['musicExists']) 115 | await say(texts['sentExistedMusic'](sendervar, str(audio.get("performer")), str(audio.get("title")))) 116 | return 117 | 118 | doc = audio.copy() 119 | try: 120 | if (chat.sender["id"]): 121 | doc["sender"] = chat.sender["id"] 122 | except: 123 | doc["sender"] = os.environ.get("CHANNEL") 124 | 125 | await db.tracks.insert(doc) 126 | await say(texts['addMusic'](sendervar, str(doc.get("performer")), str(doc.get("title")))) 127 | if (sendervar != os.environ.get('CHANNEL_NAME')): 128 | await chat.send_text(texts['addMusic'](sendervar, str(doc.get("performer")), str(doc.get("title"))) + " !") 129 | 130 | @bot.command(r'/admin') 131 | async def admin(chat, match): 132 | if not await isAdmin(chat.sender['id']): 133 | await say(texts['inquiredAdminListRefused'](str(chat.sender))) 134 | await chat.send_text(texts['denied']) 135 | return 136 | else: 137 | await say(texts['inquiredAdminList'](str(chat.sender))) 138 | raw = await getAdmin() 139 | adminStr='' 140 | i=0 141 | while i') 156 | i=0 157 | cursor = await text_delete(text) 158 | 159 | if (len(art) == 2): 160 | if (len(msg) == 2): 161 | await say(texts['deleteNumTypeArt'](chat.sender, str(cursor), msg[1].upper(), art[0], art[1])) 162 | elif (len(msg) == 1): 163 | await say(texts['deleteNumArt'](chat.sender, str(cursor), art[0], art[1])) 164 | elif (len(msg) == 2): 165 | await say(texts['deleteNumType'](chat.sender, str(cursor), msg[1].upper(), msg[0])) 166 | elif (len(msg) == 1): 167 | await say(texts['deleteNum'](chat.sender, str(cursor), text)) 168 | else: 169 | await say(texts['deleteError']) 170 | await say("(text , msg , len(msg)) = " + str(text) + " , " + str(msg) + " , " + str(len(msg))) 171 | 172 | @bot.command(r'@%s (.+)' % bot.name) 173 | @bot.command(r'/music@%s (.+)' % bot.name) 174 | @bot.command(r'/music (.+)') 175 | def music(chat, match): 176 | return search_tracks(chat, match.group(1)) 177 | 178 | @bot.command(r'/me') 179 | def whoami(chat, match): 180 | return chat.reply(chat.sender["id"]) 181 | 182 | @bot.command(r'\((\d+)/\d+\) %s "(.+)"' % texts['nextPage']) 183 | def more(chat, match): 184 | page = int(match.group(1)) 185 | return search_tracks(chat, match.group(2), page) 186 | 187 | 188 | @bot.default 189 | def default(chat, message): 190 | return search_tracks(chat, message["text"]) 191 | 192 | @bot.inline 193 | async def inline(iq): 194 | msg = iq.query.split(" type:") 195 | art = msg[0].split('>') 196 | if (len(art) == 2): 197 | if (len(msg) == 2): 198 | await say(texts['searchTypeArt'](str(iq.sender), msg[1].upper(), art[0], art[1])) 199 | cursor = await text_search(iq.query) 200 | results = [inline_result(iq.query, t) for t in await cursor.to_list(10)] 201 | await iq.answer(results) 202 | elif (len(msg) == 1): 203 | await say(texts['searchArt'](str(iq.sender), art[0], art[1])) 204 | cursor = await text_search(iq.query) 205 | results = [inline_result(iq.query, t) for t in await cursor.to_list(10)] 206 | await iq.answer(results) 207 | elif (len(msg) == 2): 208 | await say(texts['searchType'](str(iq.sender), msg[1].upper(), msg[0])) 209 | cursor = await text_search(iq.query) 210 | results = [inline_result(iq.query, t) for t in await cursor.to_list(10)] 211 | await iq.answer(results) 212 | elif (len(msg) == 1): 213 | await say(texts['search'](str(iq.sender), iq.query)) 214 | cursor = await text_search(iq.query) 215 | results = [inline_result(iq.query, t) for t in await cursor.to_list(10)] 216 | await iq.answer(results) 217 | else: 218 | await say(texts['searchError']) 219 | await say("(iq.query , msg , len(msg)) = " + str(iq.query) + " , " + str(msg) + " , " + str(len(msg))) 220 | 221 | 222 | @bot.command(r'/music(@%s)?$' % bot.name) 223 | def usage(chat, match): 224 | return chat.send_text(greeting, parse_mode='Markdown') 225 | 226 | 227 | @bot.command(r'/start') 228 | async def start(chat, match): 229 | tuid = chat.sender["id"] 230 | if not (await db.users.find_one({ "id": tuid })): 231 | await say(texts['newUser'](str(chat.sender))) 232 | await db.users.insert(chat.sender.copy()) 233 | 234 | await chat.send_text(greeting, parse_mode='Markdown') 235 | 236 | 237 | @bot.command(r'/stop') 238 | async def stop(chat, match): 239 | tuid = chat.sender["id"] 240 | await db.users.remove({ "id": tuid }) 241 | 242 | await say(texts['exit'](str(chat.sender))) 243 | await chat.send_text(texts['bye']) 244 | 245 | 246 | @bot.command(r'/help') 247 | def usage(chat, match): 248 | return chat.send_text(help, parse_mode='Markdown') 249 | 250 | 251 | @bot.command(r'/stats') 252 | async def stats(chat, match): 253 | count = await db.tracks.count() 254 | group = { 255 | "$group": { 256 | "_id": None, 257 | "size": {"$sum": "$file_size"} 258 | } 259 | } 260 | cursor = db.tracks.aggregate([group]) 261 | aggr = await cursor.to_list(1) 262 | 263 | if len(aggr) == 0: 264 | return (await chat.send_text(texts['statsNotReady'])) 265 | 266 | size = human_size(aggr[0]["size"]) 267 | text = texts['musicCalc'](str(count), str(size)) 268 | 269 | return (await chat.send_text(text)) 270 | 271 | 272 | def human_size(nbytes): 273 | suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] 274 | rank = int((math.log10(nbytes)) / 3) 275 | rank = min(rank, len(suffixes) - 1) 276 | human = nbytes / (1024.0 ** rank) 277 | f = ('%.2f' % human).rstrip('0').rstrip('.') 278 | return '%s %s' % (f, suffixes[rank]) 279 | 280 | 281 | def send_track(chat, keyboard, track): 282 | return chat.send_audio( 283 | audio=track["file_id"], 284 | title=track.get("title"), 285 | performer=track.get("performer"), 286 | duration=track.get("duration"), 287 | reply_markup=json.dumps(keyboard) 288 | ) 289 | 290 | 291 | async def search_tracks(chat, query, page=1): 292 | if(str(chat.sender) != "N/A"): 293 | typel = query.split(" type:") 294 | if (query.find(">") != -1): 295 | art = typel[0].split('>') 296 | author = art[0] 297 | song = art[1] 298 | if (len(typel) == 1): 299 | await say(texts['searchArt'](str(chat.sender), author, song)) 300 | else: 301 | await say(texts['searchTypeArt'](str(chat.sender), typel[1].upper(), author, song)) 302 | elif (len(typel) == 1): 303 | await say(texts['search'](str(chat.sender), query)) 304 | else: 305 | await say(texts['searchType'](str(chat.sender), typel[1].upper(), typel[0])) 306 | 307 | limit = 3 308 | offset = (page - 1) * limit 309 | 310 | tempCursor = await text_search(query) 311 | cursor = tempCursor.skip(offset).limit(limit) 312 | count = await cursor.count() 313 | results = await cursor.to_list(limit) 314 | 315 | if count == 0: 316 | await chat.send_text(not_found) 317 | return 318 | 319 | # Return single result if we have exact match for title and performer 320 | if results[0]['score'] > 2: 321 | limit = 1 322 | results = results[:1] 323 | 324 | newoff = offset + limit 325 | show_more = count > newoff 326 | 327 | if show_more: 328 | pages = math.ceil(count / limit) 329 | kb = [['(%d/%d) %s "%s"' % (page+1, pages, texts['nextPage'], query)]] 330 | keyboard = { 331 | "keyboard": kb, 332 | "resize_keyboard": True 333 | } 334 | else: 335 | keyboard = { "hide_keyboard": True } 336 | 337 | for track in results: 338 | await send_track(chat, keyboard, track) 339 | 340 | 341 | def inline_result(query, track): 342 | global seed 343 | seed = query + str(random.randint(0,9999999)) 344 | random.seed(query + str(random.randint(0,9999999))) 345 | results = { 346 | "type": "document", 347 | "id": track["file_id"] + str(random.randint(0,99)), 348 | "document_file_id": track["file_id"], 349 | "title" : "{} - {}".format(track.get("performer", texts['unknownArtist']),track.get("title", texts['untitled'])), 350 | } 351 | return results --------------------------------------------------------------------------------