├── rooms.json ├── sources ├── __init__.py ├── video │ └── __init__.py ├── base │ ├── Media.py │ ├── Text.py │ ├── Picture.py │ ├── interface.py │ ├── SearchResult.py │ └── __init__.py └── audio │ ├── __init__.py │ └── kuwo.py ├── frontend ├── static │ └── .gitkeep ├── .eslintignore ├── config │ ├── prod.env.js │ ├── dev.env.js │ └── index.js ├── src │ ├── views │ │ ├── NotFound.vue │ │ ├── CurrentLyric.vue │ │ ├── CurrentPlaying.vue │ │ ├── CurrentCover.vue │ │ ├── Home.vue │ │ ├── PlaylistView.vue │ │ └── TextInfo.vue │ ├── assets │ │ └── logo.png │ ├── components │ │ ├── NotFound.vue │ │ ├── PlayingLyric.vue │ │ ├── PlayingCover.vue │ │ ├── PlayingInfo.vue │ │ ├── Playlist.vue │ │ └── Home.vue │ ├── main.js │ ├── App.vue │ ├── router │ │ └── index.js │ └── api │ │ └── AudioBotWS.js ├── .editorconfig ├── .gitignore ├── .postcssrc.js ├── index.html ├── build │ ├── dev-client.js │ ├── vue-loader.conf.js │ ├── build.js │ ├── webpack.dev.conf.js │ ├── check-versions.js │ ├── webpack.base.conf.js │ ├── utils.js │ ├── dev-server.js │ └── webpack.prod.conf.js ├── .babelrc ├── .eslintrc.js └── package.json ├── plugins └── danmaku │ ├── paramgen │ ├── __init__.py │ ├── enc.py │ ├── arcparam.py │ └── liveparam.py │ ├── tars │ ├── tars │ │ ├── __init__.py │ │ ├── QueryF.tars │ │ └── EndpointF.tars │ ├── exception.py │ ├── __init__.py │ ├── core.py │ ├── EndpointF.py │ ├── __logger.py │ ├── __tup.py │ └── __packet.py │ ├── twitch.py │ ├── log.py │ ├── douyu.py │ ├── __init__.py │ ├── huya.py │ └── bilibili.py ├── liveroom ├── danmaku │ ├── __init__.py │ ├── base.py │ ├── huya.py │ └── bilibili.py ├── platform.py ├── __init__.py ├── message.py ├── manager.py └── LiveRoom.py ├── config ├── blacklist.json ├── cookies.json ├── audiobot_template.txt └── config.json ├── resource ├── favicon.ico ├── favicon.png └── translation.json ├── addons ├── handler │ ├── bypassproxy.py │ ├── connectwhenstart.py │ ├── skipwhentimereach.py │ └── skiplong.py └── cmd │ ├── adminvolume.py │ ├── forcediange.py │ └── chaduidiange.py ├── audiobot ├── event │ ├── base.py │ ├── lyric.py │ ├── blacklist.py │ ├── __init__.py │ ├── playlist.py │ └── audiobot.py ├── handlers │ ├── __init__.py │ └── SkipCover.py ├── commands │ ├── __init__.py │ ├── qiege.py │ └── diange.py ├── audio.py ├── user.py ├── __init__.py ├── command.py ├── handler.py ├── MatchEngine.py ├── lyric.py └── playlist.py ├── utils ├── etc.py ├── vasyncio.py ├── vwrappers.py ├── vtranslation.py ├── vfile.py ├── vhttp.py ├── formats.py ├── command.py └── bilibili.py ├── gui ├── factory │ ├── TextEntry.py │ ├── ToolTip.py │ ├── PlayerProgressBar.py │ └── ConfigGUIFactory.py ├── RoomGUI.py ├── __init__.py ├── WYLoginGUI.py └── MPVGUI.py ├── backend └── aioserver │ ├── __init__.py │ ├── router_handlers.py │ └── aiosocket_server.py ├── requirements.txt ├── apis ├── bilibili │ ├── audiolist.py │ ├── live.py │ └── audio.py ├── kuwo.py └── __init__.py ├── AudioBot.py ├── README.md ├── player └── mpv.py └── config.py /rooms.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /sources/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugins/danmaku/paramgen/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugins/danmaku/tars/tars/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | -------------------------------------------------------------------------------- /liveroom/danmaku/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import LiveRoom -------------------------------------------------------------------------------- /config/blacklist.json: -------------------------------------------------------------------------------- 1 | { 2 | "song_name": [], 3 | "song_id": [], 4 | "username": [] 5 | } -------------------------------------------------------------------------------- /liveroom/platform.py: -------------------------------------------------------------------------------- 1 | class LivePlatform(): 2 | Bilibili = "bilibili" 3 | Huya = "huya" 4 | -------------------------------------------------------------------------------- /resource/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AynaLivePlayer/BiliAudioBot/HEAD/resource/favicon.ico -------------------------------------------------------------------------------- /resource/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AynaLivePlayer/BiliAudioBot/HEAD/resource/favicon.png -------------------------------------------------------------------------------- /frontend/config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"' 4 | } 5 | -------------------------------------------------------------------------------- /addons/handler/bypassproxy.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | os.environ['NO_PROXY'] = '*' 4 | os.environ['no_proxy'] = '*' -------------------------------------------------------------------------------- /frontend/src/views/NotFound.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AynaLivePlayer/BiliAudioBot/HEAD/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /frontend/src/components/NotFound.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /config/cookies.json: -------------------------------------------------------------------------------- 1 | { 2 | "bilibili": { 3 | "default": "" 4 | }, 5 | "netease": { 6 | "pyncm": "session=" 7 | } 8 | } -------------------------------------------------------------------------------- /plugins/danmaku/tars/tars/QueryF.tars: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AynaLivePlayer/BiliAudioBot/HEAD/plugins/danmaku/tars/tars/QueryF.tars -------------------------------------------------------------------------------- /plugins/danmaku/tars/tars/EndpointF.tars: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AynaLivePlayer/BiliAudioBot/HEAD/plugins/danmaku/tars/tars/EndpointF.tars -------------------------------------------------------------------------------- /liveroom/__init__.py: -------------------------------------------------------------------------------- 1 | from liveroom.manager import RoomManager 2 | 3 | print("Initialize global room manager") 4 | Global_Room_Manager = RoomManager() -------------------------------------------------------------------------------- /frontend/config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const merge = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | 5 | module.exports = merge(prodEnv, { 6 | NODE_ENV: '"development"' 7 | }) 8 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Editor directories and files 9 | .idea 10 | .vscode 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | -------------------------------------------------------------------------------- /audiobot/event/base.py: -------------------------------------------------------------------------------- 1 | class BaseAudioBotEvent(): 2 | __event_name__ = "base" 3 | 4 | class CancellableEvent(): 5 | 6 | def isCancelled(self): 7 | return False 8 | 9 | def setCancelled(self,b): 10 | pass 11 | -------------------------------------------------------------------------------- /frontend/.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | "plugins": { 5 | // to edit target browsers: use "browserslist" field in package.json 6 | "autoprefixer": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | BiliAudioBot 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /utils/etc.py: -------------------------------------------------------------------------------- 1 | import pyncm 2 | 3 | def patchPyncm(): 4 | pyncm.GetCurrentSession().headers['X-Real-IP'] = '118.88.88.88' 5 | 6 | def filterTclSpecialCharacter(chars): 7 | return "".join(map(lambda s: s if 0<= ord(s) <= 65535 else "?",chars)) 8 | -------------------------------------------------------------------------------- /utils/vasyncio.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | from functools import wraps 3 | import requests,asyncio 4 | 5 | def asyncwrapper(func): 6 | @wraps(func) 7 | async def wrapper(*args,**kwargs): 8 | func(*args,**kwargs) 9 | return wrapper 10 | 11 | 12 | -------------------------------------------------------------------------------- /config/audiobot_template.txt: -------------------------------------------------------------------------------- 1 | {{current_title[:16:]}} - {{current_artist[:16:]}} - {{current_username[:16:]}} 2 | Lyric: {{current_lyric}} 3 | {% for item in playlist -%} 4 | #{{item.index}} - {{item.title[:16:]}} - {{item.artist[:16:]}} - {{item.username[:16:]}} 5 | {% endfor -%} -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App' 3 | import router from './router' 4 | 5 | Vue.config.productionTip = false 6 | 7 | /* eslint-disable no-new */ 8 | new Vue({ 9 | el: '#app', 10 | router, 11 | render: h => h(App) 12 | }) 13 | -------------------------------------------------------------------------------- /gui/factory/TextEntry.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | 3 | 4 | def getTextEntry(master,text,**kwargs): 5 | t_var = tk.StringVar() 6 | t_var.set(text) 7 | entry = tk.Entry(master,textvariable=t_var,state="readonly",readonlybackground="white",**kwargs) 8 | return entry -------------------------------------------------------------------------------- /audiobot/event/lyric.py: -------------------------------------------------------------------------------- 1 | from audiobot.event.base import BaseAudioBotEvent 2 | 3 | 4 | class LyricUpdateEvent(BaseAudioBotEvent): 5 | __event_name__ = "lyric_update" 6 | 7 | def __init__(self, lyrics, lyric): 8 | self.lyrics = lyrics 9 | self.lyric = lyric 10 | -------------------------------------------------------------------------------- /frontend/src/components/PlayingLyric.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | 14 | 18 | -------------------------------------------------------------------------------- /frontend/build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict' 3 | require('eventsource-polyfill') 4 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 5 | 6 | hotClient.subscribe(function (event) { 7 | if (event.action === 'reload') { 8 | window.location.reload() 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /sources/video/__init__.py: -------------------------------------------------------------------------------- 1 | from sources.base import CommonSource 2 | 3 | 4 | class VideoSource(CommonSource): 5 | __source_name__ = "base" 6 | 7 | @classmethod 8 | def getSourceName(cls): 9 | return "video.%s" % cls.__source_name__ 10 | 11 | @property 12 | def video(self): 13 | return None -------------------------------------------------------------------------------- /utils/vwrappers.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | import traceback 3 | 4 | def TryExceptRetNone(func): 5 | @wraps(func) 6 | def wrapper(*args,**kwargs): 7 | try: 8 | return func(*args,**kwargs) 9 | except: 10 | traceback.print_exc() 11 | return None 12 | return wrapper 13 | -------------------------------------------------------------------------------- /audiobot/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | import config 2 | 3 | from os import getcwd 4 | from os.path import basename, isfile, join 5 | import glob,importlib 6 | 7 | 8 | for f in glob.glob(join(getcwd(), config.Config.addon_handler_path, "*.py")): 9 | name = basename(f)[:-3:] 10 | if isfile(f): 11 | importlib.import_module("addons.handler." + name) -------------------------------------------------------------------------------- /backend/aioserver/__init__.py: -------------------------------------------------------------------------------- 1 | from aiohttp import web 2 | from config import Config 3 | from utils import vfile 4 | 5 | ENV = Config.environment 6 | DIST_DIR = vfile.getResourcePath("./frontend/dist") 7 | 8 | app = web.Application() 9 | routes = web.RouteTableDef() 10 | 11 | from backend.aioserver import router_handlers 12 | 13 | app.add_routes(routes) -------------------------------------------------------------------------------- /frontend/src/components/PlayingCover.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | 14 | 18 | -------------------------------------------------------------------------------- /audiobot/commands/__init__.py: -------------------------------------------------------------------------------- 1 | import config 2 | from audiobot.commands import diange,qiege 3 | 4 | from os import getcwd 5 | from os.path import basename, isfile, join 6 | import glob,importlib 7 | 8 | for f in glob.glob(join(getcwd(), config.Config.addon_cmd_path, "*.py")): 9 | name = basename(f)[:-3:] 10 | if isfile(f): 11 | importlib.import_module("addons.cmd." + name) -------------------------------------------------------------------------------- /frontend/src/components/PlayingInfo.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | 16 | 20 | -------------------------------------------------------------------------------- /frontend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 7 | } 8 | }], 9 | "stage-2" 10 | ], 11 | "plugins": ["transform-runtime"], 12 | "env": { 13 | "test": { 14 | "presets": ["env", "stage-2"], 15 | "plugins": ["istanbul"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 22 | -------------------------------------------------------------------------------- /audiobot/event/blacklist.py: -------------------------------------------------------------------------------- 1 | from audiobot.event.base import BaseAudioBotEvent 2 | 3 | class BlacklistUpdateEvent(BaseAudioBotEvent): 4 | __event_name__ = "blacklist_update" 5 | 6 | def __init__(self, blacklist): 7 | self.blacklist = blacklist 8 | 9 | 10 | class BlacklistLoadedEvent(BaseAudioBotEvent): 11 | __event_name__ = "blacklist_loaded" 12 | 13 | def __init__(self, blacklist): 14 | self.blacklist = blacklist -------------------------------------------------------------------------------- /audiobot/audio.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from audiobot.user import User 4 | from sources.base import CommonSource 5 | from sources.base.interface import AudioBotInfoSource 6 | 7 | 8 | class AudioItem(): 9 | def __init__(self, source: Union[CommonSource, AudioBotInfoSource], user: User, keyword): 10 | self.source = source 11 | self.user: User = user 12 | self.keyword = keyword 13 | 14 | @property 15 | def username(self): 16 | return self.user.username 17 | -------------------------------------------------------------------------------- /sources/base/Media.py: -------------------------------------------------------------------------------- 1 | 2 | from sources.base import BaseSource 3 | from sources.base.interface import WatchableSource 4 | from utils import formats,vfile 5 | import os 6 | 7 | class MediaSource(BaseSource,WatchableSource): 8 | __source_name__ = "media" 9 | 10 | def __init__(self,url,headers,filename): 11 | self.url = url 12 | self.headers = headers 13 | self.filename = filename 14 | 15 | @property 16 | def suffix(self): 17 | return self.filename.split(".")[-1] 18 | -------------------------------------------------------------------------------- /sources/base/Text.py: -------------------------------------------------------------------------------- 1 | from sources.base.interface import DownloadableSource 2 | from utils import vfile 3 | 4 | from sources.base import BaseSource 5 | 6 | 7 | class TextSource(BaseSource): 8 | __source_name__ = "text" 9 | 10 | def __init__(self, url, headers, filename, filecontent): 11 | self.url = url 12 | self.headers = headers 13 | self.filename = filename 14 | self.filecontent = filecontent 15 | 16 | @property 17 | def suffix(self): 18 | return self.filename.split(".")[-1] -------------------------------------------------------------------------------- /audiobot/user.py: -------------------------------------------------------------------------------- 1 | class User(): 2 | def __init__(self, username): 3 | self.username = username 4 | 5 | 6 | class DanmakuUser(User): 7 | def __init__(self, username, identifier, platform): 8 | super().__init__(username) 9 | self.identifier = identifier 10 | self.platform = platform 11 | 12 | 13 | class SystemUser(User): 14 | def __init__(self, username="system"): 15 | super().__init__(username) 16 | 17 | 18 | DefaultUser = SystemUser() 19 | PlaylistUser = SystemUser(username="playlist") 20 | -------------------------------------------------------------------------------- /frontend/build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | 6 | module.exports = { 7 | loaders: utils.cssLoaders({ 8 | sourceMap: isProduction 9 | ? config.build.productionSourceMap 10 | : config.dev.cssSourceMap, 11 | extract: isProduction 12 | }), 13 | transformToRequire: { 14 | video: 'src', 15 | source: 'src', 16 | img: 'src', 17 | image: 'xlink:href' 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sources/audio/__init__.py: -------------------------------------------------------------------------------- 1 | from sources.base import CommonSource, TextSource 2 | 3 | 4 | class AudioSource(CommonSource): 5 | __source_name__ = "base" 6 | 7 | @classmethod 8 | def getSourceName(cls): 9 | return "audio.%s" % cls.__source_name__ 10 | 11 | @property 12 | def audio(self): 13 | return None 14 | 15 | @property 16 | def lyric(self) -> TextSource: 17 | return None 18 | 19 | from .netease import NeteaseMusicSource 20 | from .bilibili import BiliAudioSource 21 | from .kuwo import KuwoMusicSource -------------------------------------------------------------------------------- /audiobot/event/__init__.py: -------------------------------------------------------------------------------- 1 | from audiobot.event.audiobot import AudioBotPlayEvent 2 | from audiobot.event.playlist import PlaylistAppendEvent 3 | 4 | 5 | class AudioBotEvents(): 6 | AUDIOBOT_PLAY = AudioBotPlayEvent 7 | PLAYLIST_APPEND = PlaylistAppendEvent 8 | 9 | values = [AUDIOBOT_PLAY,PLAYLIST_APPEND] 10 | names = [x.__event_name__ for x in values] 11 | 12 | @classmethod 13 | def getByName(cls,name): 14 | for e in cls.values: 15 | if name == e.__event_name__: 16 | return e 17 | return None -------------------------------------------------------------------------------- /plugins/danmaku/paramgen/enc.py: -------------------------------------------------------------------------------- 1 | def vn(val): 2 | if val < 0: 3 | raise ValueError 4 | buf = b"" 5 | while val >> 7: 6 | m = val & 0xFF | 0x80 7 | buf += m.to_bytes(1, "big") 8 | val >>= 7 9 | buf += val.to_bytes(1, "big") 10 | return buf 11 | 12 | 13 | def tp(a, b, ary): 14 | return vn((b << 3) | a) + ary 15 | 16 | 17 | def rs(a, ary): 18 | if isinstance(ary, str): 19 | ary = ary.encode() 20 | return tp(2, a, vn(len(ary)) + ary) 21 | 22 | 23 | def nm(a, ary): 24 | return tp(0, a, vn(ary)) 25 | -------------------------------------------------------------------------------- /frontend/src/components/Playlist.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 20 | 21 | 28 | -------------------------------------------------------------------------------- /liveroom/message.py: -------------------------------------------------------------------------------- 1 | from audiobot.user import DanmakuUser 2 | 3 | 4 | class DanmakuMessage(): 5 | def __init__(self, user: DanmakuUser, message): 6 | self._user = user 7 | self._message = message 8 | 9 | @property 10 | def user(self) -> DanmakuUser: 11 | return self._user 12 | 13 | @property 14 | def message(self) -> str: 15 | return self._message 16 | 17 | @property 18 | def admin(self) -> bool: 19 | return False 20 | 21 | @property 22 | def privilege_level(self) -> int: 23 | return 0 24 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | aiohttp-jinja2 3 | altgraph 4 | async-timeout 5 | attrs 6 | beautifulsoup4 7 | bs4 8 | certifi 9 | chardet 10 | click 11 | colorama 12 | coloredlogs 13 | future 14 | humanfriendly 15 | idna 16 | iso8601 17 | itsdangerous 18 | Jinja2 19 | m3u8 20 | MarkupSafe 21 | mttkinter 22 | multidict 23 | mutagen 24 | nest-asyncio 25 | pefile 26 | Pillow 27 | pyaria2 28 | pycryptodome 29 | pyinstaller 30 | pyinstaller-hooks-contrib 31 | pyncm 32 | pyparsing 33 | pyreadline 34 | pywin32-ctypes 35 | qrcode 36 | requests 37 | six 38 | soupsieve 39 | typing-extensions 40 | urllib3 41 | Werkzeug 42 | yarl 43 | -------------------------------------------------------------------------------- /addons/handler/connectwhenstart.py: -------------------------------------------------------------------------------- 1 | from audiobot import Global_Audio_Bot 2 | from audiobot.event.audiobot import AudioBotStartEvent 3 | from liveroom import Global_Room_Manager 4 | from config import Config 5 | 6 | @Global_Audio_Bot.handlers.register(AudioBotStartEvent, 7 | "addon.connect_when_start") 8 | def connect_when_start(event: AudioBotStartEvent): 9 | if Config.default_room != "": 10 | Global_Room_Manager.stop_all() 11 | lr = Global_Room_Manager.add_live_room(Config.default_room) 12 | Global_Room_Manager.start_room(Config.default_room) 13 | Global_Audio_Bot.setLiveRoom(lr) 14 | print("live room connected") 15 | 16 | -------------------------------------------------------------------------------- /audiobot/event/playlist.py: -------------------------------------------------------------------------------- 1 | from audiobot.event.base import BaseAudioBotEvent, CancellableEvent 2 | 3 | 4 | class PlaylistAppendEvent(BaseAudioBotEvent, 5 | CancellableEvent): 6 | __event_name__ = "playlist_append" 7 | 8 | def __init__(self, playlist, item,index): 9 | self.playlist = playlist 10 | self.item = item 11 | self.index = index 12 | self.cancel = False 13 | 14 | def isCancelled(self): 15 | return self.cancel 16 | 17 | def setCancelled(self, b): 18 | self.cancel = b 19 | 20 | 21 | class PlaylistUpdateEvent(BaseAudioBotEvent): 22 | __event_name__ = "playlist_update" 23 | 24 | def __init__(self, playlist): 25 | self.playlist = playlist -------------------------------------------------------------------------------- /frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | const routerOptions = [ 5 | {path: '/', component: 'Home'}, 6 | {path: '/textinfo', component: 'TextInfo'}, 7 | {path: '/currentplaying', component: 'CurrentPlaying'}, 8 | {path: '/currentcover', component: 'CurrentCover'}, 9 | {path: '/currentlyric', component: 'CurrentLyric'}, 10 | {path: '/playlist', component: 'PlaylistView'}, 11 | {path: '*', component: 'NotFound'} 12 | ] 13 | const routes = routerOptions.map(route => { 14 | return { 15 | ...route, 16 | component: () => import(`@/views/${route.component}.vue`) 17 | } 18 | }) 19 | Vue.use(Router) 20 | export default new Router({ 21 | routes, 22 | mode: 'history' 23 | }) 24 | -------------------------------------------------------------------------------- /sources/base/Picture.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | from sources.base import BaseSource 4 | 5 | 6 | class PictureSource(BaseSource): 7 | __source_name__ = "picture" 8 | 9 | def __init__(self, url, headers, filename,filecontent): 10 | self.url = url 11 | self.headers = headers 12 | self.filename = filename 13 | self.filecontent = filecontent 14 | 15 | @property 16 | def suffix(self): 17 | return self.filename.split(".")[-1] 18 | 19 | @classmethod 20 | def initFromBase64(cls,filename,src): 21 | data = src.split(',') 22 | if (len(data) == 2): 23 | return cls("",{},filename,base64.b64decode(data[1])) 24 | else: 25 | return cls("", {}, filename, src) -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | sourceType: 'module' 8 | }, 9 | env: { 10 | browser: true, 11 | }, 12 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 13 | extends: 'standard', 14 | // required to lint *.vue files 15 | plugins: [ 16 | 'html' 17 | ], 18 | // add your custom rules here 19 | 'rules': { 20 | // allow paren-less arrow functions 21 | 'arrow-parens': 0, 22 | // allow async-await 23 | 'generator-star-spacing': 0, 24 | // allow debugger during development 25 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/views/CurrentLyric.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 32 | 33 | 36 | -------------------------------------------------------------------------------- /audiobot/__init__.py: -------------------------------------------------------------------------------- 1 | from pyncm import SetCurrentSession, LoadSessionFromString 2 | 3 | from audiobot.audiobot import AudioBot 4 | from audiobot.command import CommandManager 5 | from config import Config 6 | 7 | pyncmcookie = Config.getCookie("netease", "pyncm") 8 | if pyncmcookie.get("session") is not None: 9 | try: 10 | SetCurrentSession(LoadSessionFromString(pyncmcookie.get("session"))) 11 | except: 12 | pass 13 | 14 | print("Initialize global audio bot") 15 | Global_Audio_Bot = AudioBot() 16 | # register hooks 17 | from audiobot.handlers import * 18 | 19 | Global_Audio_Bot._loadSystemPlaylist(Config.system_playlist) 20 | 21 | Global_Command_Manager = CommandManager() 22 | # register commands 23 | from audiobot.commands import * 24 | 25 | Global_Audio_Bot.registerCommandExecutors(Global_Command_Manager.commands) 26 | -------------------------------------------------------------------------------- /audiobot/event/audiobot.py: -------------------------------------------------------------------------------- 1 | from audiobot.event.base import BaseAudioBotEvent, CancellableEvent 2 | 3 | 4 | class AudioBotStartEvent(BaseAudioBotEvent): 5 | __event_name__ = "audiobot_start" 6 | 7 | def __init__(self, audio_bot): 8 | self.audio_bot = audio_bot 9 | 10 | class AudioBotPlayEvent(BaseAudioBotEvent): 11 | __event_name__ = "audiobot_play" 12 | 13 | def __init__(self, audio_bot, item): 14 | self.audio_bot = audio_bot 15 | self.item = item 16 | 17 | 18 | class FindSearchResultEvent(BaseAudioBotEvent, 19 | CancellableEvent): 20 | def __init__(self, search_result): 21 | self.search_result = search_result 22 | self.cancelled = False 23 | 24 | def isCancelled(self): 25 | return self.cancelled 26 | 27 | def setCancelled(self, b): 28 | self.cancelled = b -------------------------------------------------------------------------------- /audiobot/handlers/SkipCover.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from audiobot.event.audiobot import FindSearchResultEvent 4 | from sources.base import SearchResult 5 | from sources.base.interface import AudioBotInfoSource 6 | 7 | coverlist = [r"\(.*cover.*\)", 8 | r"\(.*翻唱.*\)", 9 | r"\(.*翻自.*\)", 10 | r"\(.*remix.*\)", 11 | r"(.*cover.*)", 12 | r"(.*翻唱.*\)", 13 | r"(.*翻自.*\)", 14 | r"\(.*remix.*\)", 15 | ] 16 | 17 | 18 | def skip_cover(event: FindSearchResultEvent): 19 | if event.isCancelled(): 20 | return 21 | result: SearchResult = event.search_result 22 | if not isinstance(result.source, AudioBotInfoSource): 23 | return 24 | for pattern in coverlist: 25 | if re.search(pattern, result.source.getTitle()) is not None: 26 | event.setCancelled(True) 27 | return 28 | -------------------------------------------------------------------------------- /utils/vtranslation.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import traceback 4 | 5 | from config import Config 6 | from utils import vfile 7 | 8 | TRANSLATION_DICT = {} 9 | 10 | try: 11 | with open(vfile.getResourcePath(Config.translation["path"]),"r",encoding="utf-8") as f: 12 | TRANSLATION_DICT = json.loads(f.read()) 13 | except: 14 | traceback.print_exc() 15 | pass 16 | 17 | 18 | def getTranslatedText(text): 19 | if TRANSLATION_DICT.get(text) is None: 20 | TRANSLATION_DICT[text] = text 21 | if Config.environment == "development": 22 | vfile.writeToFile(json.dumps(TRANSLATION_DICT,indent=4,ensure_ascii=False), 23 | os.path.dirname(Config.translation["path"]), 24 | os.path.basename(Config.translation["path"])) 25 | if Config.translation["enable"]: 26 | return TRANSLATION_DICT[text] 27 | else: 28 | return text -------------------------------------------------------------------------------- /audiobot/command.py: -------------------------------------------------------------------------------- 1 | from typing import Union, List, Type 2 | 3 | from plugins.blivedm import DanmakuMessage 4 | 5 | 6 | class CommandExecutor(): 7 | def __init__(self, audiobot, commands: Union[str, List[str]]): 8 | self.audiobot = audiobot 9 | if isinstance(commands, list): 10 | self.commands = commands 11 | else: 12 | self.commands = [commands] 13 | 14 | def applicable(self, command): 15 | return command in self.commands 16 | 17 | def process(self, command, dmkMsg: DanmakuMessage): 18 | pass 19 | 20 | 21 | class CommandManager(): 22 | def __init__(self): 23 | self.commands = {} 24 | 25 | def _register(self, id, command: Type[CommandExecutor]): 26 | self.commands[id] = command 27 | 28 | def register(self,id): 29 | def add(fun): 30 | self._register(id,fun) 31 | return fun 32 | return add -------------------------------------------------------------------------------- /frontend/src/views/CurrentPlaying.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 38 | 39 | 42 | -------------------------------------------------------------------------------- /audiobot/commands/qiege.py: -------------------------------------------------------------------------------- 1 | from audiobot.command import CommandExecutor 2 | from audiobot import Global_Command_Manager 3 | from config import Config 4 | from liveroom.message import DanmakuMessage 5 | 6 | 7 | @Global_Command_Manager.register("qiege") 8 | class QiegeCommand(CommandExecutor): 9 | def __init__(self,audiobot): 10 | super().__init__(audiobot,["切歌"]) 11 | 12 | def __hasPermission(self, dmkMsg: DanmakuMessage): 13 | config = Config.commands["qiege"] 14 | try: 15 | if config["self"] and (dmkMsg.user.username == self.audiobot.current.username): 16 | return True 17 | if config["admin"] and dmkMsg.admin: 18 | return True 19 | if config["guard"] and dmkMsg.privilege_level > 0: 20 | return True 21 | except: 22 | return False 23 | 24 | def process(self, command,dmkMsg): 25 | if self.__hasPermission(dmkMsg): 26 | self.audiobot.playNext() -------------------------------------------------------------------------------- /frontend/src/views/CurrentCover.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 37 | 38 | 44 | -------------------------------------------------------------------------------- /frontend/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 42 | -------------------------------------------------------------------------------- /frontend/src/components/Home.vue: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 42 | -------------------------------------------------------------------------------- /frontend/src/views/PlaylistView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 35 | 36 | 46 | -------------------------------------------------------------------------------- /sources/base/interface.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | from sources.base.Picture import PictureSource 4 | from sources.base.SearchResult import SearchResults 5 | 6 | 7 | class BaseInterface(metaclass=ABCMeta): 8 | pass 9 | 10 | 11 | class DownloadableSource(BaseInterface): 12 | @abstractmethod 13 | def download(self, downloader, saveroute, **kwargs): 14 | pass 15 | 16 | @property 17 | @abstractmethod 18 | def suffix(self): 19 | return "" 20 | 21 | 22 | class SearchableSource(BaseInterface): 23 | @classmethod 24 | @abstractmethod 25 | def search(cls, keyword, *args, **kwargs) -> SearchResults: 26 | pass 27 | 28 | 29 | class WatchableSource(BaseInterface): 30 | pass 31 | 32 | class AudioBotInfoSource(): 33 | @abstractmethod 34 | def getUniqueId(self): 35 | return None 36 | 37 | @abstractmethod 38 | def getTitle(self): 39 | return None 40 | 41 | @abstractmethod 42 | def getArtist(self): 43 | return None 44 | 45 | @abstractmethod 46 | def getCover(self) -> PictureSource: 47 | return None 48 | -------------------------------------------------------------------------------- /addons/handler/skipwhentimereach.py: -------------------------------------------------------------------------------- 1 | from audiobot import Global_Audio_Bot 2 | from audiobot.event import AudioBotPlayEvent 3 | from audiobot.event.audiobot import AudioBotStartEvent 4 | from player.mpv import MPVProperty 5 | 6 | max_sec = 120 7 | has_skip = True 8 | 9 | def skip_when_time_reach(property, val, *args): 10 | global has_skip 11 | time_pos = 0 if val is None else int(val) 12 | if time_pos >= max_sec and has_skip: 13 | has_skip = False 14 | Global_Audio_Bot.playNext() 15 | 16 | def check_play_next(event:AudioBotPlayEvent): 17 | global has_skip 18 | has_skip = True 19 | 20 | @Global_Audio_Bot.handlers.register(AudioBotStartEvent,"addons.handler.registerskiptimereach") 21 | def register_skiplong(event:AudioBotStartEvent): 22 | Global_Audio_Bot.mpv_player.registerPropertyHandler("addon.handler.skip_when_time_reach", 23 | MPVProperty.TIME_POS, 24 | skip_when_time_reach) 25 | Global_Audio_Bot.handlers._register(AudioBotPlayEvent, "addon.skip_when_time_reach_reset", 26 | check_play_next) 27 | -------------------------------------------------------------------------------- /addons/handler/skiplong.py: -------------------------------------------------------------------------------- 1 | from audiobot import Global_Audio_Bot 2 | from audiobot.event import AudioBotPlayEvent 3 | from audiobot.event.audiobot import AudioBotStartEvent 4 | from player.mpv import MPVProperty 5 | 6 | max_sec = 300 7 | has_skip = True 8 | 9 | def skip_when_time_too_long(property,val,*args): 10 | global has_skip 11 | time_pos = 0 if val is None else int(val) 12 | if time_pos >= max_sec and has_skip: 13 | has_skip = False 14 | Global_Audio_Bot.playNext() 15 | 16 | def check_play_next(event:AudioBotPlayEvent): 17 | global has_skip 18 | has_skip = True 19 | 20 | @Global_Audio_Bot.handlers.register(AudioBotStartEvent,"addons.handler.registerskiplong") 21 | def register_skiplong(event:AudioBotStartEvent): 22 | Global_Audio_Bot.mpv_player.registerPropertyHandler("addon.handler.skip_when_time_too_long", 23 | MPVProperty.DURATION, 24 | skip_when_time_too_long) 25 | Global_Audio_Bot.handlers._register(AudioBotPlayEvent, "addon.handler.skip_when_time_too_long_reset_skip", 26 | check_play_next) 27 | -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "gui_title": "ヽ(°∀°)卡西米尔唱片机(°∀°)ノ", 3 | "commonHeaders": { 4 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:51.0) Gecko/20100101 Firefox/51.0" 5 | }, 6 | "output_channel": { 7 | "web": { 8 | "enable": true, 9 | "port": 5000 10 | }, 11 | "file": { 12 | "enable": true, 13 | "template": "config/audiobot_template.txt", 14 | "path": "audiobot_info.txt" 15 | } 16 | }, 17 | "player_volume": 0.32, 18 | "commonCookies": {}, 19 | "default_room": "", 20 | "system_playlist": { 21 | "playlist": { 22 | "netease": [ 23 | "467824586" 24 | ], 25 | "bilibili": [] 26 | }, 27 | "song": { 28 | "netease": [], 29 | "bilibili": [] 30 | }, 31 | "random": true, 32 | "autoskip": true 33 | }, 34 | "commands": { 35 | "diange": { 36 | "cooldown": 0, 37 | "limit": 128, 38 | "visitor": true, 39 | "guard": true, 40 | "admin": true, 41 | "fan": null 42 | }, 43 | "qiege": { 44 | "self": true, 45 | "guard": false, 46 | "admin": false 47 | } 48 | }, 49 | "audio_device": { 50 | "id": "auto" 51 | } 52 | } -------------------------------------------------------------------------------- /liveroom/danmaku/base.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | # from liveroom import Global_Room_Manager 4 | 5 | class LiveRoom(): 6 | def __init__(self): 7 | self.loop = asyncio.get_event_loop() 8 | self.msg_handler = {} 9 | 10 | @classmethod 11 | def create(cls,room_id:str): 12 | from liveroom.danmaku.bilibili import BilibiliLiveRoom 13 | from liveroom.danmaku.huya import HuyaLiveRoom 14 | if room_id.startswith("bili"): 15 | return BilibiliLiveRoom(room_id[4:]) 16 | if room_id.startswith("huya"): 17 | return HuyaLiveRoom(room_id[4:]) 18 | return BilibiliLiveRoom(room_id) 19 | 20 | @property 21 | def title(self): 22 | return "" 23 | 24 | @property 25 | def room_id(self): 26 | return "" 27 | 28 | @property 29 | def unique_room_id(self): 30 | return "" 31 | 32 | @property 33 | def is_running(self) -> bool: 34 | return False 35 | 36 | def start(self): 37 | pass 38 | 39 | def stop(self): 40 | pass 41 | 42 | def register_msg_handler(self, handler_id, handler): 43 | self.msg_handler[handler_id] = handler 44 | 45 | def clear_msg_handler(self): 46 | self.msg_handler.clear() -------------------------------------------------------------------------------- /frontend/build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, function (err, stats) { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write(stats.toString({ 23 | colors: true, 24 | modules: false, 25 | children: false, 26 | chunks: false, 27 | chunkModules: false 28 | }) + '\n\n') 29 | 30 | if (stats.hasErrors()) { 31 | console.log(chalk.red(' Build failed with errors.\n')) 32 | process.exit(1) 33 | } 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /audiobot/handler.py: -------------------------------------------------------------------------------- 1 | from typing import Type, Union 2 | 3 | from audiobot.event.base import BaseAudioBotEvent 4 | 5 | class AudioBotHandlers(): 6 | def __init__(self): 7 | self._event_handlers = {} 8 | 9 | def _register(self, event:Union[str,Type[BaseAudioBotEvent]], id, func): 10 | event_name = event if isinstance(event,str) else event.__event_name__ 11 | if self._event_handlers.get(event_name) is None: 12 | self._event_handlers[event_name] = {} 13 | self._event_handlers[event_name][id] = func 14 | 15 | def register(self,event:Union[str,Type[BaseAudioBotEvent]],id): 16 | def add(func): 17 | self._register(event,id,func) 18 | return func 19 | return add 20 | 21 | def unregister(self,event:Union[str,Type[BaseAudioBotEvent]],id): 22 | event_name = event if isinstance(event, str) else event.__event_name__ 23 | try: 24 | self._event_handlers.get(event_name).pop(id) 25 | except: 26 | pass 27 | 28 | def call(self, event: BaseAudioBotEvent): 29 | event_name: str = event.__event_name__ 30 | if self._event_handlers.get(event_name) is None: 31 | return 32 | for func in self._event_handlers.get(event_name).values(): 33 | func(event) -------------------------------------------------------------------------------- /apis/bilibili/audiolist.py: -------------------------------------------------------------------------------- 1 | from apis import CommonRequestWrapper, SETTING 2 | 3 | 4 | class API: 5 | 6 | @staticmethod 7 | def info_api(id,page,pagesize): 8 | return "https://www.bilibili.com/audio/music-service-c/web/song/of-menu?" \ 9 | "ps={pagesize}&sid={id}&pn={page}".format(id = id, 10 | page = page, 11 | pagesize = pagesize) 12 | 13 | # @staticmethod 14 | # def search_api(keyword,page,pagesize): 15 | # return "https://api.bilibili.com/audio/music-service-c/s?search_type=music&" \ 16 | # "keyword={keyword}&page={page}&pagesize={pagesize}".format(keyword=keyword, 17 | # page=page, 18 | # pagesize= pagesize) 19 | 20 | 21 | @CommonRequestWrapper 22 | def getAudioListInfo(songlist_id:str,page:int=1,pagesize:int=100): 23 | """ 24 | get audiolist information 25 | 26 | :param songlist_id: amxxxx 27 | :param page: page default1 28 | :param pagesize: default 100 29 | :return: 30 | """ 31 | return ("get", 32 | API.info_api(songlist_id,page,pagesize), 33 | {"headers":SETTING.common_header} 34 | ) 35 | -------------------------------------------------------------------------------- /gui/factory/ToolTip.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | 3 | 4 | class ToolTip(object): 5 | def __init__(self, widget, tip_text=None): 6 | self.widget = widget 7 | self.tip_text = tip_text 8 | widget.bind('', self.mouse_enter) # bind mouse events 9 | widget.bind('', self.mouse_leave) 10 | 11 | def mouse_enter(self, _event): 12 | self.show_tooltip() 13 | 14 | def mouse_leave(self, _event): 15 | self.hide_tooltip() 16 | 17 | def show_tooltip(self): 18 | if self.tip_text: 19 | x_left = self.widget.winfo_rootx() # get widget top-left coordinates 20 | y_top = self.widget.winfo_rooty() - 18 # place tooltip above widget or it flickers 21 | 22 | self.tip_window = tk.Toplevel(self.widget) # create Toplevel window; parent=widget 23 | self.tip_window.overrideredirect(True) # remove surrounding toolbar window 24 | self.tip_window.geometry("+%d+%d" % (x_left, y_top)) # position tooltip 25 | 26 | label = tk.Label(self.tip_window, text=self.tip_text, justify=tk.CENTER, 27 | background="#ffffff", relief=tk.SOLID, borderwidth=1, 28 | font=("tahoma", "8", "normal")) 29 | label.pack(ipadx=1) 30 | 31 | def hide_tooltip(self): 32 | if self.tip_window: 33 | self.tip_window.destroy() 34 | -------------------------------------------------------------------------------- /frontend/build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const baseWebpackConfig = require('./webpack.base.conf') 7 | const HtmlWebpackPlugin = require('html-webpack-plugin') 8 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 9 | 10 | // add hot-reload related code to entry chunks 11 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 12 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 13 | }) 14 | 15 | module.exports = merge(baseWebpackConfig, { 16 | module: { 17 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 18 | }, 19 | // cheap-module-eval-source-map is faster for development 20 | devtool: '#cheap-module-eval-source-map', 21 | plugins: [ 22 | new webpack.DefinePlugin({ 23 | 'process.env': config.dev.env 24 | }), 25 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 26 | new webpack.HotModuleReplacementPlugin(), 27 | new webpack.NoEmitOnErrorsPlugin(), 28 | // https://github.com/ampedandwired/html-webpack-plugin 29 | new HtmlWebpackPlugin({ 30 | filename: 'index.html', 31 | template: 'index.html', 32 | inject: true 33 | }), 34 | new FriendlyErrorsPlugin() 35 | ] 36 | }) 37 | -------------------------------------------------------------------------------- /addons/cmd/adminvolume.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from audiobot.command import CommandExecutor 4 | from audiobot import Global_Command_Manager 5 | from liveroom.message import DanmakuMessage 6 | 7 | @Global_Command_Manager.register("changevolume") 8 | class ForceDiangeCommand(CommandExecutor): 9 | def __init__(self, audiobot): 10 | super().__init__(audiobot, ["调整音量"]) 11 | self.cooldowns = {} 12 | 13 | 14 | def process(self, command, dmkMsg: DanmakuMessage): 15 | msg: List[str] = dmkMsg.message.split(" ") 16 | if len(msg) < 2: 17 | return 18 | val = " ".join(msg[1::]) 19 | if not (self.__hasPermission(dmkMsg)): 20 | return 21 | print(val,self.__getpercent(val,self.audiobot.mpv_player.getVolumePercent())) 22 | if (command == "调整音量"): 23 | self.audiobot.mpv_player.setVolumePercent(self.__getpercent(val,self.audiobot.mpv_player.getVolumePercent())) 24 | 25 | def __getpercent(self,val,now): 26 | try: 27 | val = now+int(val)/100 28 | if val <=0 or val > 1: 29 | return now 30 | return val 31 | except: 32 | return now 33 | 34 | def __hasPermission(self, dmkMsg: DanmakuMessage): 35 | try: 36 | if bool(dmkMsg.admin): 37 | return True 38 | return False 39 | except: 40 | return False -------------------------------------------------------------------------------- /frontend/build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | function exec (cmd) { 7 | return require('child_process').execSync(cmd).toString().trim() 8 | } 9 | 10 | const versionRequirements = [ 11 | { 12 | name: 'node', 13 | currentVersion: semver.clean(process.version), 14 | versionRequirement: packageConfig.engines.node 15 | } 16 | ] 17 | 18 | if (shell.which('npm')) { 19 | versionRequirements.push({ 20 | name: 'npm', 21 | currentVersion: exec('npm --version'), 22 | versionRequirement: packageConfig.engines.npm 23 | }) 24 | } 25 | 26 | module.exports = function () { 27 | const warnings = [] 28 | for (let i = 0; i < versionRequirements.length; i++) { 29 | const mod = versionRequirements[i] 30 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 31 | warnings.push(mod.name + ': ' + 32 | chalk.red(mod.currentVersion) + ' should be ' + 33 | chalk.green(mod.versionRequirement) 34 | ) 35 | } 36 | } 37 | 38 | if (warnings.length) { 39 | console.log('') 40 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 41 | console.log() 42 | for (let i = 0; i < warnings.length; i++) { 43 | const warning = warnings[i] 44 | console.log(' ' + warning) 45 | } 46 | console.log() 47 | process.exit(1) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /addons/cmd/forcediange.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from audiobot.command import CommandExecutor 4 | from audiobot import Global_Command_Manager 5 | from liveroom.message import DanmakuMessage 6 | from sources.audio import BiliAudioSource, NeteaseMusicSource,KuwoMusicSource 7 | 8 | @Global_Command_Manager.register("forcediange") 9 | class ForceDiangeCommand(CommandExecutor): 10 | def __init__(self, audiobot): 11 | super().__init__(audiobot, ["播放", "播放b歌", "播放w歌", "播放k歌"]) 12 | self.cooldowns = {} 13 | 14 | def process(self, command, dmkMsg: DanmakuMessage): 15 | msg: List[str] = dmkMsg.message.split(" ") 16 | if len(msg) < 2: 17 | return 18 | val = " ".join(msg[1::]) 19 | if not (self.__hasPermission(dmkMsg)): 20 | return 21 | if (command == "播放"): 22 | self.audiobot.playAudioByUrl(val,dmkMsg.user) 23 | elif command == "播放b歌": 24 | self.audiobot.playAudioByUrl(val, dmkMsg.user, source_class=BiliAudioSource) 25 | elif command == "播放w歌": 26 | self.audiobot.playAudioByUrl(val, dmkMsg.user, source_class=NeteaseMusicSource) 27 | elif command == "播放k歌": 28 | self.audiobot.playAudioByUrl(val,dmkMsg.user, source_class=KuwoMusicSource) 29 | 30 | def __hasPermission(self, dmkMsg: DanmakuMessage): 31 | try: 32 | if bool(dmkMsg.admin): 33 | return True 34 | if int(dmkMsg.privilege_level) > 0: 35 | return True 36 | return False 37 | except: 38 | return False -------------------------------------------------------------------------------- /plugins/danmaku/paramgen/arcparam.py: -------------------------------------------------------------------------------- 1 | from . import enc 2 | from base64 import urlsafe_b64encode as b64enc 3 | from urllib.parse import quote 4 | 5 | 6 | def _header(video_id, channel_id) -> str: 7 | S1_3 = enc.rs(1, video_id) 8 | S1_5 = enc.rs(1, channel_id) + enc.rs(2, video_id) 9 | S1 = enc.rs(3, S1_3) + enc.rs(5, S1_5) 10 | S3 = enc.rs(48687757, enc.rs(1, video_id)) 11 | header_replay = enc.rs(1, S1) + enc.rs(3, S3) + enc.nm(4, 1) 12 | return b64enc(header_replay) 13 | 14 | 15 | def _build(video_id, seektime, topchat_only, channel_id) -> str: 16 | chattype = 4 if topchat_only else 1 17 | if seektime < 0: 18 | seektime = 0 19 | timestamp = int(seektime * 1000000) 20 | header = enc.rs(3, _header(video_id, channel_id)) 21 | timestamp = enc.nm(5, timestamp) 22 | s6 = enc.nm(6, 0) 23 | s7 = enc.nm(7, 0) 24 | s8 = enc.nm(8, 0) 25 | s9 = enc.nm(9, 4) 26 | s10 = enc.rs(10, enc.nm(4, 0)) 27 | chattype = enc.rs(14, enc.nm(1, 4)) 28 | s15 = enc.nm(15, 0) 29 | entity = b"".join((header, timestamp, s6, s7, s8, s9, s10, chattype, s15)) 30 | continuation = enc.rs(156074452, entity) 31 | return quote(b64enc(continuation).decode()) 32 | 33 | 34 | def getparam(video_id, seektime=0, topchat_only=False, channel_id="") -> str: 35 | """ 36 | Parameter 37 | --------- 38 | seektime : int 39 | unit:seconds 40 | start position of fetching chat data. 41 | topchat_only : bool 42 | if True, fetch only 'top chat' 43 | """ 44 | return _build(video_id, seektime, topchat_only, channel_id) 45 | -------------------------------------------------------------------------------- /sources/base/SearchResult.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from sources.base import BaseSource, CommonSource 4 | 5 | 6 | class SearchResult(BaseSource): 7 | __source_name__ = "search-result" 8 | 9 | def __init__(self, url, headers, filename,source,source_name,base_source_name): 10 | self.url = url 11 | self.headers = headers 12 | self.filename = filename 13 | self.source:CommonSource = source 14 | self.source_name = source_name 15 | self.base_source_name = base_source_name 16 | 17 | class SearchResults(BaseSource): 18 | name = "search-results" 19 | 20 | def __init__(self, results:List[SearchResult],current_page,total_page): 21 | self.results = results 22 | self.current_page = current_page 23 | self.total_page = total_page 24 | 25 | def isEmpty(self) -> bool: 26 | return len(self.results) == 0 27 | 28 | def getResultsBy(self,source_name=None,base_source_name=None): 29 | rs = self.results 30 | if source_name != None: 31 | if isinstance(source_name,List): 32 | rs = filter(lambda x:x.source_name in source_name,rs) 33 | elif isinstance(source_name,str): 34 | rs = filter(lambda x:x.source_name == source_name,rs) 35 | if base_source_name != None: 36 | if isinstance(base_source_name,List): 37 | rs = filter(lambda x:x.base_source_name in base_source_name,rs) 38 | elif isinstance(base_source_name,str): 39 | rs = filter(lambda x:x.base_source_name == base_source_name,rs) 40 | return list(rs) -------------------------------------------------------------------------------- /addons/cmd/chaduidiange.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from audiobot.command import CommandExecutor 4 | from audiobot import Global_Command_Manager 5 | from liveroom.message import DanmakuMessage 6 | from sources.audio import BiliAudioSource, NeteaseMusicSource,KuwoMusicSource 7 | 8 | @Global_Command_Manager.register("chaduidiange") 9 | class ChaduiDiangeCommand(CommandExecutor): 10 | def __init__(self, audiobot): 11 | super().__init__(audiobot, ["插队点歌", "插队点b歌", "插队点w歌", "插队点k歌"]) 12 | self.cooldowns = {} 13 | 14 | def process(self, command, dmkMsg: DanmakuMessage): 15 | msg: List[str] = dmkMsg.message.split(" ") 16 | if len(msg) < 2: 17 | return 18 | val = " ".join(msg[1::]) 19 | if not (self.__hasPermission(dmkMsg)): 20 | return 21 | if (command == "插队点歌"): 22 | self.audiobot.addAudioByUrl(val,dmkMsg.user,index=0) 23 | elif command == "插队点b歌": 24 | self.audiobot.addAudioByUrl(val,dmkMsg.user,index=0, source_class=BiliAudioSource) 25 | elif command == "插队点w歌": 26 | self.audiobot.addAudioByUrl(val,dmkMsg.user,index=0, source_class=NeteaseMusicSource) 27 | elif command == "插队点k歌": 28 | self.audiobot.addAudioByUrl(val,dmkMsg.user,index=0, source_class=KuwoMusicSource) 29 | 30 | def __hasPermission(self, dmkMsg: DanmakuMessage): 31 | try: 32 | if bool(dmkMsg.admin): 33 | return True 34 | if int(dmkMsg.privilege_level) > 0: 35 | return True 36 | return False 37 | except: 38 | return False -------------------------------------------------------------------------------- /plugins/danmaku/twitch.py: -------------------------------------------------------------------------------- 1 | import json, re, select, random, traceback 2 | from struct import pack, unpack 3 | 4 | import asyncio, aiohttp, zlib 5 | 6 | 7 | class Twitch: 8 | heartbeat = "PING" 9 | 10 | async def get_ws_info(url): 11 | reg_datas = [] 12 | room_id = re.search(r"/([^/?]+)[^/]*$", url).group(1) 13 | 14 | reg_datas.append("CAP REQ :twitch.tv/tags twitch.tv/commands twitch.tv/membership") 15 | reg_datas.append("PASS SCHMOOPIIE") 16 | nick = f"justinfan{int(8e4 * random.random() + 1e3)}" 17 | reg_datas.append(f"NICK {nick}") 18 | reg_datas.append(f"USER {nick} 8 * :{nick}") 19 | reg_datas.append(f"JOIN #{room_id}") 20 | 21 | return "wss://irc-ws.chat.twitch.tv", reg_datas 22 | 23 | def decode_msg(data): 24 | # print(data) 25 | # print('----------------') 26 | msgs = [] 27 | for d in data.splitlines(): 28 | try: 29 | msg = {} 30 | msg["name"] = re.search(r"display-name=([^;]+);", d).group(1) 31 | msg["content"] = re.search(r"PRIVMSG [^:]+:(.+)", d).group(1) 32 | # msg['msg_type'] = {'dgb': 'gift', 'chatmsg': 'danmaku', 33 | # 'uenter': 'enter'}.get(msg['type'], 'other') 34 | msg["msg_type"] = "danmaku" 35 | c = re.search(r"color=#([a-zA-Z0-9]{6});", d) 36 | msg["color"] = "ffffff" if c == None else c.group(1).lower() 37 | msgs.append(msg) 38 | except Exception as e: 39 | # traceback.print_exc() 40 | pass 41 | return msgs 42 | -------------------------------------------------------------------------------- /gui/factory/PlayerProgressBar.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | 3 | from tkinter import ttk, PhotoImage 4 | 5 | from PIL import Image,ImageTk 6 | 7 | from utils import vfile 8 | 9 | 10 | class PlayerProgressBar(ttk.Scale): 11 | style = None 12 | def __init__(self, master=None, **kw): 13 | self.slider_img = ImageTk.PhotoImage(Image.open(vfile.getResourcePath('resource/favicon.ico')).resize((16, 16), Image.ANTIALIAS)) 14 | self.style or self.__createStyle(master) 15 | self.variable = kw.pop('variable', tk.DoubleVar(master)) 16 | ttk.Scale.__init__(self, master, variable=self.variable, **kw) 17 | self._style_name = '{}.custom.Horizontal.TScale'.format(self) 18 | self['style'] = self._style_name 19 | 20 | def __createStyle(self, master): 21 | style = ttk.Style(master) 22 | style.element_create('custom.Horizontal.Scale.slider', 'image', self.slider_img) 23 | style.layout('custom.Horizontal.TScale', 24 | [('Scale.focus', 25 | {'expand': '1', 26 | 'sticky': 'nswe', 27 | 'children': [('Horizontal.Scale.trough', 28 | {'expand': '1', 'sticky': 'nswe', 29 | 'children': [( 30 | 'Horizontal.Scale.track', 31 | {'sticky': 'we'}), ( 32 | 'custom.Horizontal.Scale.slider', 33 | {'side': 'left', 34 | 'sticky': ''})]})]})]) 35 | PlayerProgressBar.style = style -------------------------------------------------------------------------------- /AudioBot.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from utils import vfile 4 | import os 5 | # os.environ['no_proxy'] = '*' 6 | vfile.registerEnvironmentPath() 7 | import nest_asyncio 8 | nest_asyncio.apply() 9 | 10 | from utils.etc import patchPyncm 11 | patchPyncm() 12 | 13 | from aiohttp import web 14 | from backend.localfileserver import LocalFileWriterServer 15 | from config import Config 16 | 17 | from gui import MainWindow 18 | from backend.aioserver import app as aioserver_app 19 | import asyncio 20 | 21 | async def mainloop(loop): 22 | a = MainWindow(loop=loop) 23 | task = [loop.create_task(a.start())] 24 | if Config.output_channel["web"]["enable"]: 25 | app = aioserver_app 26 | task.append(loop.create_task(run_backend(app))) 27 | if Config.output_channel["file"]["enable"]: 28 | lfs = LocalFileWriterServer() 29 | task.append(loop.create_task(lfs.start())) 30 | await asyncio.wait(task, return_when=asyncio.FIRST_COMPLETED) 31 | try: 32 | asyncio.get_event_loop().stop() 33 | except: 34 | pass 35 | 36 | async def mainloop_gui_only(loop): 37 | a = MainWindow(loop=loop) 38 | task = [loop.create_task(a.start())] 39 | await asyncio.wait(task, return_when=asyncio.FIRST_COMPLETED) 40 | try: 41 | asyncio.get_event_loop().stop() 42 | except: 43 | pass 44 | 45 | 46 | async def run_backend(app): 47 | web.run_app(app, host='127.0.0.1', port=Config.output_channel["web"]["port"]) 48 | 49 | 50 | if __name__ == "__main__": 51 | loop = asyncio.get_event_loop() 52 | loop.run_until_complete(mainloop(loop)) 53 | try: 54 | Config.saveConfig() 55 | Config.saveCookie() 56 | except: 57 | pass 58 | sys.exit() 59 | -------------------------------------------------------------------------------- /frontend/src/views/TextInfo.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 58 | 59 | 69 | -------------------------------------------------------------------------------- /utils/vfile.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | 5 | from utils import content_types 6 | 7 | 8 | def parseFilename(filename): 9 | pattern = r'[\\/:*?"<>|\r\n]+' 10 | return re.sub(pattern,"-",filename) 11 | 12 | 13 | def writeToFile(content,route,name,binary=False): 14 | route = os.getcwd() if route == "" else route 15 | path = os.path.join(route, parseFilename(name)) 16 | if not os.path.exists(route): 17 | os.mkdir(route) 18 | if binary: 19 | with open(path, "wb+") as f: 20 | f.write(content) 21 | else: 22 | with open(path, "w",encoding="utf-8") as f: 23 | f.write(content) 24 | 25 | def removeUrlPara(url): 26 | return url.split("?")[0] 27 | 28 | def getSuffixByUrl(url): 29 | return removeUrlPara(url).split(".")[-1] 30 | 31 | def getFileNameByUrl(url): 32 | return removeUrlPara(url).split("/")[-1] 33 | 34 | def getFileContentType(path): 35 | suffix = ".{}".format(getSuffixByUrl(path)) 36 | if content_types.CONTENT_TYPES.get(suffix) == None: 37 | return "text/html" 38 | return content_types.CONTENT_TYPES.get(suffix) 39 | 40 | 41 | def getResourcePath(relative_path): 42 | """ Get absolute path to resource, works for dev and for PyInstaller """ 43 | try: 44 | # PyInstaller creates a temp folder and stores path in _MEIPASS 45 | base_path = sys._MEIPASS 46 | except Exception: 47 | base_path = os.path.abspath(".") 48 | return os.path.join(base_path, relative_path) 49 | 50 | 51 | def registerEnvironmentPath(): 52 | os.environ["PATH"] = os.getcwd() + os.pathsep + os.environ["PATH"] 53 | try: 54 | os.environ["PATH"] = sys._MEIPASS + os.pathsep + os.environ["PATH"] 55 | print(os.environ["PATH"]) 56 | except Exception: 57 | pass -------------------------------------------------------------------------------- /frontend/config/index.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | // Template version: 1.1.3 4 | // see http://vuejs-templates.github.io/webpack for documentation. 5 | 6 | const path = require('path') 7 | 8 | module.exports = { 9 | build: { 10 | env: require('./prod.env'), 11 | // index: path.resolve(__dirname, '../../dist/index.html'), 12 | // assetsRoot: path.resolve(__dirname, '../../dist'), 13 | index: path.resolve(__dirname, '../../frontend/dist/index.html'), 14 | assetsRoot: path.resolve(__dirname, '../../frontend/dist'), 15 | assetsSubDirectory: 'static', 16 | assetsPublicPath: '/', 17 | productionSourceMap: true, 18 | // Gzip off by default as many popular static hosts such as 19 | // Surge or Netlify already gzip all static assets for you. 20 | // Before setting to `true`, make sure to: 21 | // npm install --save-dev compression-webpack-plugin 22 | productionGzip: false, 23 | productionGzipExtensions: ['js', 'css'], 24 | // Run the build command with an extra argument to 25 | // View the bundle analyzer report after build finishes: 26 | // `npm run build --report` 27 | // Set to `true` or `false` to always turn it on or off 28 | bundleAnalyzerReport: process.env.npm_config_report 29 | }, 30 | dev: { 31 | env: require('./dev.env'), 32 | port: process.env.PORT || 8080, 33 | autoOpenBrowser: true, 34 | assetsSubDirectory: 'static', 35 | assetsPublicPath: '/', 36 | proxyTable: {}, 37 | // CSS Sourcemaps off by default because relative paths are "buggy" 38 | // with this option, according to the CSS-Loader README 39 | // (https://github.com/webpack/css-loader#sourcemaps) 40 | // In our experience, they generally work as expected, 41 | // just be aware of this issue when enabling this option. 42 | cssSourceMap: false 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /plugins/danmaku/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | class LogSystem(object): 5 | handlerList = [] 6 | showOnCmd = True 7 | loggingLevel = logging.INFO 8 | loggingFile = None 9 | 10 | def __init__(self): 11 | self.cmdHandler = None 12 | for handler in logging.getLogger().handlers: 13 | if "StreamHandler" in str(handler): 14 | self.cmdHandler = handler 15 | if self.cmdHandler is None: 16 | self.cmdHandler = logging.StreamHandler() 17 | logging.getLogger().addHandler(self.cmdHandler) 18 | self.logger = logging.getLogger("danmu") 19 | self.logger.addHandler(logging.NullHandler()) 20 | self.logger.setLevel(self.loggingLevel) 21 | self.fileHandler = None 22 | 23 | def set_logging(self, showOnCmd=True, loggingFile=None, loggingLevel=logging.INFO): 24 | if showOnCmd != self.showOnCmd: 25 | if showOnCmd: 26 | logging.getLogger().addHandler(self.cmdHandler) 27 | else: 28 | logging.getLogger().removeHandler(self.cmdHandler) 29 | self.showOnCmd = showOnCmd 30 | if loggingFile != self.loggingFile: 31 | if self.loggingFile is not None: # clear old fileHandler 32 | self.logger.removeHandler(self.fileHandler) 33 | self.fileHandler.close() 34 | if loggingFile is not None: # add new fileHandler 35 | self.fileHandler = logging.FileHandler(loggingFile) 36 | self.logger.addHandler(self.fileHandler) 37 | self.loggingFile = loggingFile 38 | if loggingLevel != self.loggingLevel: 39 | self.logger.setLevel(loggingLevel) 40 | self.loggingLevel = loggingLevel 41 | 42 | 43 | ls = LogSystem() 44 | set_logging = ls.set_logging 45 | -------------------------------------------------------------------------------- /plugins/danmaku/douyu.py: -------------------------------------------------------------------------------- 1 | import json, re, select, random 2 | from struct import pack, unpack 3 | import asyncio, aiohttp 4 | 5 | color_tab = { 6 | "2": "1e87f0", 7 | "3": "7ac84b", 8 | "4": "ff7f00", 9 | "6": "ff69b4", 10 | "5": "9b39f4", 11 | "1": "ff0000", 12 | } 13 | 14 | 15 | class Douyu: 16 | heartbeat = b"\x14\x00\x00\x00\x14\x00\x00\x00\xb1\x02\x00\x00\x74\x79\x70\x65\x40\x3d\x6d\x72\x6b\x6c\x2f\x00" 17 | 18 | async def get_ws_info(url): 19 | reg_datas = [] 20 | room_id = url.split("/")[-1] 21 | data = f"type@=loginreq/roomid@={room_id}/" 22 | s = pack("i", 9 + len(data)) * 2 23 | s += b"\xb1\x02\x00\x00" # 689 24 | s += data.encode("ascii") + b"\x00" 25 | reg_datas.append(s) 26 | data = f"type@=joingroup/rid@={room_id}/gid@=-9999/" 27 | s = pack("i", 9 + len(data)) * 2 28 | s += b"\xb1\x02\x00\x00" # 689 29 | s += data.encode("ascii") + b"\x00" 30 | reg_datas.append(s) 31 | return "wss://danmuproxy.douyu.com:8506/", reg_datas 32 | 33 | def decode_msg(data): 34 | msgs = [] 35 | for msg in re.findall(b"(type@=.*?)\x00", data): 36 | try: 37 | msg = msg.replace(b"@=", b'":"').replace(b"/", b'","') 38 | msg = msg.replace(b"@A", b"@").replace(b"@S", b"/") 39 | msg = json.loads((b'{"' + msg[:-2] + b"}").decode("utf8", "ignore")) 40 | msg["name"] = msg.get("nn", "") 41 | msg["content"] = msg.get("txt", "") 42 | msg["msg_type"] = {"dgb": "gift", "chatmsg": "danmaku", "uenter": "enter"}.get( 43 | msg["type"], "other" 44 | ) 45 | msg["color"] = color_tab.get(msg.get("col", "-1"), "ffffff") 46 | msgs.append(msg) 47 | except Exception as e: 48 | pass 49 | return msgs 50 | -------------------------------------------------------------------------------- /liveroom/manager.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Dict 3 | 4 | from liveroom.danmaku import LiveRoom 5 | 6 | 7 | def room_starter(liveroom: LiveRoom): 8 | async def wrapper(): 9 | future = liveroom.start() 10 | try: 11 | await future 12 | finally: 13 | await liveroom.stop() 14 | return wrapper 15 | 16 | 17 | class RoomManager(): 18 | def __init__(self, loop=None): 19 | self.loop = asyncio.get_event_loop() if loop == None else loop 20 | self.live_rooms: Dict[str, LiveRoom] = {} 21 | 22 | def count(self): 23 | return len(self.live_rooms.keys()) 24 | 25 | def get_live_room_by_id(self, id): 26 | return self.live_rooms.get(id) 27 | 28 | def start_room(self, room_id): 29 | liveroom = self.live_rooms.get(room_id) 30 | if liveroom == None: 31 | return 32 | liveroom.start() 33 | 34 | def get_running_room_ids(self): 35 | running = [] 36 | for key, val in self.live_rooms.items(): 37 | if val.is_running: 38 | running.append(key) 39 | return running 40 | 41 | def get_running_rooms(self): 42 | running = [] 43 | for val in self.live_rooms.values(): 44 | if val.is_running: 45 | running.append(val) 46 | return running 47 | 48 | def stop_room(self, room_id): 49 | return self.live_rooms.get(room_id) and self.live_rooms.get(room_id).stop() 50 | 51 | def stop_all(self): 52 | for val in self.live_rooms.values(): 53 | if val.is_running: 54 | val.stop() 55 | 56 | def add_live_room(self, room_id) -> LiveRoom: 57 | if str(room_id) in self.live_rooms.keys(): 58 | return self.live_rooms[str(room_id)] 59 | liveroom = LiveRoom.create(room_id) 60 | self.live_rooms[str(room_id)] = liveroom 61 | return liveroom 62 | -------------------------------------------------------------------------------- /liveroom/danmaku/huya.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from audiobot.user import DanmakuUser 4 | from liveroom.message import DanmakuMessage 5 | from liveroom.platform import LivePlatform 6 | from plugins import danmaku as dmk 7 | 8 | from liveroom.danmaku import base 9 | 10 | 11 | class HuyaLiveRoom(base.LiveRoom): 12 | 13 | def __init__(self, room_id): 14 | super().__init__() 15 | self._room_id = room_id 16 | self.q = asyncio.Queue() 17 | self.dmc = dmk.DanmakuClient("https://www.huya.com/{}".format(room_id), self.q) 18 | self.running = False 19 | 20 | @property 21 | def is_running(self) -> bool: 22 | return self.running 23 | 24 | def start(self): 25 | self.running = True 26 | self.loop.create_task(self.dmc.start()) 27 | self.loop.create_task(self.__message_loop()) 28 | 29 | 30 | def stop(self): 31 | self.running = False 32 | self.loop.create_task(self.dmc.stop()) 33 | 34 | async def __message_loop(self): 35 | while self.running: 36 | m = await self.q.get() 37 | for handler in self.msg_handler.values(): 38 | handler(HuyaDanmuMessage(m)) 39 | 40 | @property 41 | def title(self): 42 | return "" 43 | 44 | @property 45 | def room_id(self): 46 | return self._room_id 47 | 48 | @property 49 | def unique_room_id(self): 50 | return "huya{}".format(self._room_id) 51 | 52 | class HuyaDanmuMessage(DanmakuMessage): 53 | def __init__(self, message:dict): 54 | self.raw_message = message 55 | self._user = DanmakuUser(message["name"],"huya{}".format(message["name"]),LivePlatform.Huya) 56 | 57 | @property 58 | def user(self) -> DanmakuUser: 59 | return self._user 60 | 61 | @property 62 | def message(self) -> str: 63 | return self.raw_message["content"] 64 | 65 | @property 66 | def admin(self) -> bool: 67 | return False 68 | 69 | @property 70 | def privilege_level(self) -> int: 71 | return 0 -------------------------------------------------------------------------------- /apis/bilibili/live.py: -------------------------------------------------------------------------------- 1 | from apis import CommonRequestWrapper 2 | from urllib import parse 3 | 4 | class FORMAT: 5 | HLS = ("hls","h5") 6 | FLV = ("flv","web") 7 | 8 | values = [HLS,FLV] 9 | 10 | @staticmethod 11 | def format(fmt): 12 | return fmt[0] 13 | 14 | @staticmethod 15 | def platform(fmt): 16 | return fmt[1] 17 | 18 | @staticmethod 19 | def getPlatformByFormat(fmt): 20 | for v in FORMAT.values: 21 | if fmt == FORMAT.format(v): 22 | return FORMAT.platform(v) 23 | return FORMAT.platform(FORMAT.HLS) 24 | 25 | class API: 26 | 27 | @staticmethod 28 | def info_api(id): 29 | return "https://api.live.bilibili.com/room/v1/Room/room_init?id={}".format(id) 30 | 31 | @staticmethod 32 | def playurl_api(real_room_id,platform): 33 | params = { 34 | 'cid': real_room_id, 35 | 'qn': 10000, 36 | 'platform': platform, 37 | 'https_url_req': 1, 38 | 'ptype': 16 39 | } 40 | return "https://api.live.bilibili.com/xlive/web-room/v1/playUrl/playUrl?{}".format(parse.urlencode(params)) 41 | 42 | 43 | @CommonRequestWrapper 44 | def getLiveInfo(room_id:str): 45 | """ 46 | get live room info including real room id 47 | 48 | :param room_id: the room id display in the url or web 49 | :return: 50 | """ 51 | return ("get", 52 | API.info_api(room_id)) 53 | 54 | @CommonRequestWrapper 55 | def getRealUrlByPlatform(real_room_id,platform): 56 | """ 57 | 58 | :param real_room_id: the real room id 59 | :param platform: see in bilibili.live.FORMAT 60 | :return: 61 | """ 62 | return ("get", 63 | API.playurl_api(real_room_id,platform)) 64 | 65 | @CommonRequestWrapper 66 | def getRealUrlByFormat(real_room_id,format): 67 | """ 68 | 69 | :param real_room_id: the real room id 70 | :param platform: see in bilibili.live.FORMAT 71 | :return: 72 | """ 73 | return ("get", 74 | API.playurl_api(real_room_id,FORMAT.getPlatformByFormat(format))) -------------------------------------------------------------------------------- /frontend/build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const config = require('../config') 5 | const vueLoaderConfig = require('./vue-loader.conf') 6 | 7 | function resolve (dir) { 8 | return path.join(__dirname, '..', dir) 9 | } 10 | 11 | module.exports = { 12 | entry: { 13 | app: './src/main.js' 14 | }, 15 | output: { 16 | path: config.build.assetsRoot, 17 | filename: '[name].js', 18 | publicPath: process.env.NODE_ENV === 'production' 19 | ? config.build.assetsPublicPath 20 | : config.dev.assetsPublicPath 21 | }, 22 | resolve: { 23 | extensions: ['.js', '.vue', '.json'], 24 | alias: { 25 | '@': resolve('src'), 26 | } 27 | }, 28 | module: { 29 | rules: [ 30 | // { 31 | // test: /\.(js|vue)$/, 32 | // loader: 'eslint-loader', 33 | // enforce: 'pre', 34 | // include: [resolve('src'), resolve('test')], 35 | // options: { 36 | // formatter: require('eslint-friendly-formatter') 37 | // } 38 | // }, 39 | { 40 | test: /\.vue$/, 41 | loader: 'vue-loader', 42 | options: vueLoaderConfig 43 | }, 44 | { 45 | test: /\.js$/, 46 | loader: 'babel-loader', 47 | include: [resolve('src'), resolve('test')] 48 | }, 49 | { 50 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 51 | loader: 'url-loader', 52 | options: { 53 | limit: 10000, 54 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 55 | } 56 | }, 57 | { 58 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 59 | loader: 'url-loader', 60 | options: { 61 | limit: 10000, 62 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 63 | } 64 | }, 65 | { 66 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 67 | loader: 'url-loader', 68 | options: { 69 | limit: 10000, 70 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 71 | } 72 | } 73 | ] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /plugins/danmaku/tars/exception.py: -------------------------------------------------------------------------------- 1 | # Tencent is pleased to support the open source community by making Tars available. 2 | # 3 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 4 | # 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this file except 6 | # in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 12 | # CONDITIONS OF ANY KIND, either express or implied. See the License for the 13 | # specific language governing permissions and limitations under the License. 14 | # 15 | 16 | 17 | class TarsException(Exception): 18 | pass 19 | 20 | 21 | class TarsTarsDecodeRequireNotExist(TarsException): 22 | pass 23 | 24 | 25 | class TarsTarsDecodeMismatch(TarsException): 26 | pass 27 | 28 | 29 | class TarsTarsDecodeInvalidValue(TarsException): 30 | pass 31 | 32 | 33 | class TarsTarsUnsupportType(TarsException): 34 | pass 35 | 36 | 37 | class TarsNetConnectException(TarsException): 38 | pass 39 | 40 | 41 | class TarsNetConnectLostException(TarsException): 42 | pass 43 | 44 | 45 | class TarsNetSocketException(TarsException): 46 | pass 47 | 48 | 49 | class TarsProxyDecodeException(TarsException): 50 | pass 51 | 52 | 53 | class TarsProxyEncodeException(TarsException): 54 | pass 55 | 56 | 57 | class TarsServerEncodeException(TarsException): 58 | pass 59 | 60 | 61 | class TarsServerDecodeException(TarsException): 62 | pass 63 | 64 | 65 | class TarsServerNoFuncException(TarsException): 66 | pass 67 | 68 | 69 | class TarsServerNoServantException(TarsException): 70 | pass 71 | 72 | 73 | class TarsServerQueueTimeoutException(TarsException): 74 | pass 75 | 76 | 77 | class TarsServerUnknownException(TarsException): 78 | pass 79 | 80 | 81 | class TarsSyncCallTimeoutException(TarsException): 82 | pass 83 | 84 | 85 | class TarsRegistryException(TarsException): 86 | pass 87 | 88 | 89 | class TarsServerResetGridException(TarsException): 90 | pass 91 | -------------------------------------------------------------------------------- /sources/base/__init__.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | from typing import List 3 | from functools import wraps 4 | 5 | class CommonSourceWrapper(): 6 | @staticmethod 7 | def handleException(func): 8 | @wraps(func) 9 | def wrapper(*args, **kwargs): 10 | try: 11 | return func(*args, **kwargs) 12 | except Exception as e: 13 | traceback.print_exc() 14 | return None 15 | return wrapper 16 | 17 | class CommonSource(): 18 | __source_name__ = None 19 | wrapper = CommonSourceWrapper 20 | 21 | @classmethod 22 | def getSourceName(cls): 23 | return cls.__source_name__ 24 | 25 | @classmethod 26 | def initFromUrl(cls,url): 27 | pass 28 | 29 | @property 30 | def info(self): 31 | return {} 32 | 33 | def getBaseSources(self,*args,**kwargs): 34 | return {} 35 | 36 | @CommonSourceWrapper.handleException 37 | def load(self,*args,**kwargs): 38 | pass 39 | 40 | @classmethod 41 | def applicable(cls, url): 42 | return False 43 | 44 | def isValid(self): 45 | return False 46 | 47 | class BaseSource(): 48 | __source_name__ = None 49 | 50 | def __init__(self): 51 | self.url = "" 52 | self.headers = {} 53 | self.filename = "" 54 | 55 | @classmethod 56 | def getSourceName(cls): 57 | return "base.%s" % cls.__source_name__ 58 | 59 | class SourceSelector(): 60 | def __init__(self,*args): 61 | self.sources = args 62 | self.sources: List[CommonSource] 63 | 64 | def select(self,url): 65 | for source in self.sources: 66 | if source.applicable(url): 67 | return source 68 | return None 69 | 70 | def getSourceNames(self): 71 | return [s.getSourceName() for s in self.sources] 72 | 73 | def getByName(self,name): 74 | for source in self.sources: 75 | source:CommonSource 76 | if source.getSourceName() == name: 77 | return source 78 | return None 79 | 80 | from .Media import MediaSource 81 | from .Picture import PictureSource 82 | from .Text import TextSource 83 | from .SearchResult import SearchResult,SearchResults -------------------------------------------------------------------------------- /frontend/src/api/AudioBotWS.js: -------------------------------------------------------------------------------- 1 | const PlaylistUpdateEvent = 'playlist_update' 2 | const AudiobotPlayEvent = 'audiobot_play' 3 | const LyricUpdateEvent = 'lyric_update' 4 | 5 | export default class AudioBotWs { 6 | constructor () { 7 | this.onPlaylistUpdate = null 8 | this.onAudiobotPlay = null 9 | this.onLyricUpdate = null 10 | 11 | this.websocket = null 12 | this.retryCount = 0 13 | this.isDestroying = false 14 | } 15 | 16 | start () { 17 | this.wsConnect() 18 | } 19 | 20 | stop () { 21 | this.isDestroying = true 22 | if (this.websocket) { 23 | this.websocket.close() 24 | } 25 | } 26 | 27 | wsConnect () { 28 | if (this.isDestroying) { 29 | return 30 | } 31 | const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws' 32 | // 开发时使用localhost:5000 33 | const host = process.env.NODE_ENV === 'development' ? 'localhost:5000' : window.location.host 34 | const url = `${protocol}://${host}/ws/audiobot` 35 | this.websocket = new WebSocket(url) 36 | this.websocket.onopen = this.onWsOpen.bind(this) 37 | this.websocket.onclose = this.onWsClose.bind(this) 38 | this.websocket.onmessage = this.onWsMessage.bind(this) 39 | } 40 | 41 | onWsOpen () { 42 | this.retryCount = 0 43 | } 44 | 45 | onWsClose () { 46 | this.websocket = null 47 | if (this.isDestroying) { 48 | return 49 | } 50 | window.console.log(`掉线重连中${++this.retryCount}`) 51 | window.setTimeout(this.wsConnect.bind(this), 1000) 52 | } 53 | 54 | onWsMessage (event) { 55 | let jsonData = JSON.parse(event.data) 56 | for (var eventName in jsonData) { 57 | switch (eventName) { 58 | case PlaylistUpdateEvent: { 59 | if (this.onPlaylistUpdate) { 60 | this.onPlaylistUpdate(jsonData[eventName]) 61 | } 62 | break 63 | } 64 | case AudiobotPlayEvent: { 65 | if (this.onAudiobotPlay) { 66 | this.onAudiobotPlay(jsonData[eventName]) 67 | } 68 | break 69 | } 70 | case LyricUpdateEvent: { 71 | if (this.onLyricUpdate) { 72 | this.onLyricUpdate(jsonData[eventName]) 73 | } 74 | break 75 | } 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /frontend/build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 | 6 | exports.assetsPath = function (_path) { 7 | const assetsSubDirectory = process.env.NODE_ENV === 'production' 8 | ? config.build.assetsSubDirectory 9 | : config.dev.assetsSubDirectory 10 | return path.posix.join(assetsSubDirectory, _path) 11 | } 12 | 13 | exports.cssLoaders = function (options) { 14 | options = options || {} 15 | 16 | const cssLoader = { 17 | loader: 'css-loader', 18 | options: { 19 | minimize: process.env.NODE_ENV === 'production', 20 | sourceMap: options.sourceMap 21 | } 22 | } 23 | 24 | // generate loader string to be used with extract text plugin 25 | function generateLoaders (loader, loaderOptions) { 26 | const loaders = [cssLoader] 27 | if (loader) { 28 | loaders.push({ 29 | loader: loader + '-loader', 30 | options: Object.assign({}, loaderOptions, { 31 | sourceMap: options.sourceMap 32 | }) 33 | }) 34 | } 35 | 36 | // Extract CSS when that option is specified 37 | // (which is the case during production build) 38 | if (options.extract) { 39 | return ExtractTextPlugin.extract({ 40 | use: loaders, 41 | fallback: 'vue-style-loader' 42 | }) 43 | } else { 44 | return ['vue-style-loader'].concat(loaders) 45 | } 46 | } 47 | 48 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 49 | return { 50 | css: generateLoaders(), 51 | postcss: generateLoaders(), 52 | less: generateLoaders('less'), 53 | sass: generateLoaders('sass', { indentedSyntax: true }), 54 | scss: generateLoaders('sass'), 55 | stylus: generateLoaders('stylus'), 56 | styl: generateLoaders('stylus') 57 | } 58 | } 59 | 60 | // Generate loaders for standalone style files (outside of .vue) 61 | exports.styleLoaders = function (options) { 62 | const output = [] 63 | const loaders = exports.cssLoaders(options) 64 | for (const extension in loaders) { 65 | const loader = loaders[extension] 66 | output.push({ 67 | test: new RegExp('\\.' + extension + '$'), 68 | use: loader 69 | }) 70 | } 71 | return output 72 | } 73 | -------------------------------------------------------------------------------- /plugins/danmaku/tars/__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | # Tencent is pleased to support the open source community by making Tars available. 6 | # 7 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 8 | # 9 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this file except 10 | # in compliance with the License. You may obtain a copy of the License at 11 | # 12 | # https://opensource.org/licenses/BSD-3-Clause 13 | # 14 | # Unless required by applicable law or agreed to in writing, software distributed 15 | # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 16 | # CONDITIONS OF ANY KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations under the License. 18 | # 19 | 20 | __version__ = "0.0.1" 21 | 22 | from .__util import util 23 | from .__tars import TarsInputStream 24 | from .__tars import TarsOutputStream 25 | from .__tup import TarsUniPacket 26 | 27 | 28 | class tarscore: 29 | class TarsInputStream(TarsInputStream): 30 | pass 31 | 32 | class TarsOutputStream(TarsOutputStream): 33 | pass 34 | 35 | class TarsUniPacket(TarsUniPacket): 36 | pass 37 | 38 | class boolean(util.boolean): 39 | pass 40 | 41 | class int8(util.int8): 42 | pass 43 | 44 | class uint8(util.uint8): 45 | pass 46 | 47 | class int16(util.int16): 48 | pass 49 | 50 | class uint16(util.uint16): 51 | pass 52 | 53 | class int32(util.int32): 54 | pass 55 | 56 | class uint32(util.uint32): 57 | pass 58 | 59 | class int64(util.int64): 60 | pass 61 | 62 | class float(util.float): 63 | pass 64 | 65 | class double(util.double): 66 | pass 67 | 68 | class bytes(util.bytes): 69 | pass 70 | 71 | class string(util.string): 72 | pass 73 | 74 | class struct(util.struct): 75 | pass 76 | 77 | @staticmethod 78 | def mapclass(ktype, vtype): 79 | return util.mapclass(ktype, vtype) 80 | 81 | @staticmethod 82 | def vctclass(vtype): 83 | return util.vectorclass(vtype) 84 | 85 | @staticmethod 86 | def printHex(buff): 87 | util.printHex(buff) 88 | -------------------------------------------------------------------------------- /utils/vhttp.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from queue import Queue 3 | 4 | import requests,traceback 5 | 6 | 7 | def httpGet(url, maxReconn=5, **kwargs): 8 | trial = 0 9 | while trial < maxReconn: 10 | try: 11 | return requests.get(url, timeout=5, **kwargs) 12 | except: 13 | trial += 1 14 | continue 15 | return None 16 | 17 | def httpPost(url, maxReconn=5,**kwargs): 18 | trial = 0 19 | while trial < maxReconn: 20 | try: 21 | return requests.post(url, timeout=5, **kwargs) 22 | except: 23 | trial += 1 24 | continue 25 | return None 26 | 27 | 28 | class HttpClient: 29 | def __init__(self,maxTrial = 1): 30 | self.maxTrial = maxTrial 31 | 32 | def get(self,url,**kwargs): 33 | trial = 0 34 | while trial < self.maxTrial: 35 | try: 36 | return requests.get(url, timeout=5, **kwargs) 37 | except: 38 | traceback.print_exc() 39 | trial += 1 40 | return None 41 | 42 | def post(self,url,**kwargs): 43 | trial = 0 44 | while trial < self.maxTrial: 45 | try: 46 | return requests.post(url, timeout=5, **kwargs) 47 | except: 48 | traceback.print_exc() 49 | trial += 1 50 | return None 51 | 52 | 53 | class ThreadingHttpClient: 54 | def __init__(self,maxTrial = 5): 55 | self.maxTrial = maxTrial 56 | 57 | def _req(self, func, url, q: Queue, *args, **kwargs): 58 | trial = 0 59 | while trial < self.maxTrial: 60 | try: 61 | return q.put(func(url, timeout=5, *args, **kwargs)) 62 | except: 63 | traceback.print_exc() 64 | trial += 1 65 | q.put(None) 66 | 67 | def get(self,url,**kwargs): 68 | q = Queue() 69 | thread = threading.Thread(target=self._req,args=(requests.get,url,q,),kwargs=kwargs) 70 | thread.start() 71 | thread.join() 72 | return q.get() 73 | 74 | def post(self,url,**kwargs): 75 | q = Queue() 76 | thread = threading.Thread(target=self._req, args=(requests.post, url, q,), kwargs=kwargs) 77 | thread.start() 78 | thread.join() 79 | return q.get() -------------------------------------------------------------------------------- /utils/formats.py: -------------------------------------------------------------------------------- 1 | import m3u8, glob, os, subprocess 2 | import re 3 | 4 | 5 | def m3u8Extract(url): 6 | try: 7 | m3u8obj = m3u8.load(url) 8 | if len(m3u8obj.playlists) != 0: 9 | return m3u8Extract(m3u8obj.playlists[0].base_uri + m3u8obj.playlists[0].uri) 10 | return [seg.base_uri + seg.uri for seg in m3u8obj.segments] 11 | except: 12 | print("m3u8 extract fail") 13 | return [] 14 | 15 | 16 | def m3u8Combine(route): 17 | target_file = os.path.join(os.path.dirname(route), 18 | os.path.basename(route) + ".ts") 19 | with open(target_file, "wb") as tf: 20 | for path in glob.glob(os.path.join(route, "*.ts")): 21 | with open(path, "rb") as f: 22 | tf.write(f.read()) 23 | 24 | 25 | def m3u8FFmpegCombine(route): 26 | target_file = os.path.join(os.path.dirname(route), 27 | os.path.basename(route) + ".ts") 28 | filelist = os.path.join(route, "filelist.txt") 29 | subprocess.run("ffmpeg -f concat -safe 0 -i {filelist} -c copy {target}" 30 | .format(filelist=filelist, target=target_file)) 31 | 32 | 33 | def htmlGetCharset(bin_content: bytes): 34 | try: 35 | html_text = bin_content.decode("utf-8", "ignore") 36 | return re.search("charset=([^ ;'\">])*[ ;'\"]", html_text).group()[8:-1:] 37 | except: 38 | return None 39 | 40 | 41 | def htmlAutoDecode(bin_content: bytes): 42 | codec = ["utf-8", "gbk", "gb2312"] 43 | c = htmlGetCharset(bin_content) 44 | if c != None and c in codec: 45 | return bin_content.decode(c, "ignore") 46 | for c in codec: 47 | try: 48 | return bin_content.decode(c) 49 | except: 50 | pass 51 | return None 52 | 53 | 54 | def htmlStrip(html_text: str): 55 | return html_text.replace("\n", "").replace("\r", "") 56 | 57 | URL_REGEXP = re.compile( 58 | r'^(?:http|ftp)s?://' # http:// or https:// 59 | r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... 60 | r'localhost|' # localhost... 61 | r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip 62 | r'(?::\d+)?' # optional port 63 | r'(?:/?|[/?]\S+)$', re.IGNORECASE) 64 | 65 | def isValidUrl(url): 66 | return URL_REGEXP.match(url) is not None 67 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "biliaudiobot-frontend", 3 | "version": "1.0.0", 4 | "description": "BiliAudioBot frontend", 5 | "author": "Aynakeya", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "start": "npm run dev", 10 | "build": "node build/build.js", 11 | "lint": "eslint --ext .js,.vue src" 12 | }, 13 | "dependencies": { 14 | "axios": ">=0.19.0", 15 | "vue": "^2.5.2", 16 | "vue-router": "^3.0.1" 17 | }, 18 | "devDependencies": { 19 | "autoprefixer": "^7.1.2", 20 | "babel-core": "^6.22.1", 21 | "babel-eslint": "^7.1.1", 22 | "babel-loader": "^7.1.1", 23 | "babel-plugin-transform-runtime": "^6.22.0", 24 | "babel-preset-env": "^1.3.2", 25 | "babel-preset-stage-2": "^6.22.0", 26 | "babel-register": "^6.22.0", 27 | "chalk": "^2.0.1", 28 | "connect-history-api-fallback": "^1.3.0", 29 | "copy-webpack-plugin": "^4.0.1", 30 | "css-loader": "^0.28.0", 31 | "eslint": "^3.19.0", 32 | "eslint-config-standard": "^10.2.1", 33 | "eslint-friendly-formatter": "^3.0.0", 34 | "eslint-loader": "^1.7.1", 35 | "eslint-plugin-html": "^3.0.0", 36 | "eslint-plugin-import": "^2.7.0", 37 | "eslint-plugin-node": "^5.2.0", 38 | "eslint-plugin-promise": "^3.4.0", 39 | "eslint-plugin-standard": "^3.0.1", 40 | "eventsource-polyfill": "^0.9.6", 41 | "express": "^4.14.1", 42 | "extract-text-webpack-plugin": "^3.0.0", 43 | "file-loader": "^1.1.4", 44 | "friendly-errors-webpack-plugin": "^1.6.1", 45 | "html-webpack-plugin": "^2.30.1", 46 | "http-proxy-middleware": "^0.17.3", 47 | "opn": "^5.1.0", 48 | "optimize-css-assets-webpack-plugin": "^3.2.0", 49 | "ora": "^1.2.0", 50 | "portfinder": "^1.0.13", 51 | "rimraf": "^2.6.0", 52 | "semver": "^5.3.0", 53 | "shelljs": "^0.7.6", 54 | "url-loader": "^0.5.8", 55 | "vue-loader": "^13.3.0", 56 | "vue-style-loader": "^3.0.1", 57 | "vue-template-compiler": "^2.5.2", 58 | "webpack": "^3.6.0", 59 | "webpack-bundle-analyzer": ">=3.3.2", 60 | "webpack-dev-middleware": "^1.12.0", 61 | "webpack-hot-middleware": "^2.18.2", 62 | "webpack-merge": "^4.1.0" 63 | }, 64 | "engines": { 65 | "node": ">= 4.0.0", 66 | "npm": ">= 3.0.0" 67 | }, 68 | "browserslist": [ 69 | "> 1%", 70 | "last 2 versions", 71 | "not ie <= 8" 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /liveroom/LiveRoom.py: -------------------------------------------------------------------------------- 1 | # import asyncio 2 | # 3 | # import aiohttp 4 | # 5 | # from plugins import blivedm 6 | # from plugins.blivedm import DanmakuMessage 7 | # from sources.base import MediaSource 8 | # from utils import vfile, vasyncio 9 | # 10 | # class LiveRoomA(): 11 | # def __init__(self): 12 | # self.msg_handler = {} 13 | # 14 | # @property 15 | # def title(self): 16 | # return "" 17 | # 18 | # @property 19 | # def room_id(self): 20 | # return "" 21 | # 22 | # @property 23 | # def unique_room_id(self): 24 | # return "" 25 | # 26 | # def register_msg_handler(self, handler_id, handler): 27 | # self.msg_handler[handler_id] = handler 28 | # 29 | # def clear_msg_handler(self): 30 | # self.msg_handler.clear() 31 | # 32 | # 33 | # class LiveRoom(blivedm.BLiveClient): 34 | # def __init__(self, room_id, uid=0, session: aiohttp.ClientSession = None, heartbeat_interval=30, ssl=True, 35 | # loop=None): 36 | # super().__init__(room_id, uid, session, heartbeat_interval, ssl, loop) 37 | # self.msg_handler = {} 38 | # 39 | # @property 40 | # def title(self): 41 | # return self._title 42 | # 43 | # @property 44 | # def description(self): 45 | # return self._description 46 | # 47 | # @property 48 | # def cover_url(self): 49 | # return self._cover 50 | # 51 | # @property 52 | # def cover(self): 53 | # # todo media source 54 | # suffix = vfile.getSuffixByUrl(self._cover) 55 | # return MediaSource(self._cover, 56 | # {}, 57 | # "cover.{}".format(suffix)) 58 | # 59 | # def _parse_room_init(self, data): 60 | # super()._parse_room_init(data) 61 | # room_info = data['room_info'] 62 | # self._title = room_info["title"] 63 | # self._cover = room_info["cover"] 64 | # self._description = room_info["description"] 65 | # return True 66 | # 67 | # async def _on_receive_danmaku(self, danmaku: DanmakuMessage): 68 | # for handler in self.msg_handler.values(): 69 | # asyncio.ensure_future(vasyncio.asyncwrapper(handler)(danmaku),loop=self._loop) 70 | # 71 | # def registerMsgHandler(self, handler_id, handler): 72 | # self.msg_handler[handler_id] = handler 73 | # 74 | # def clearMsgHandler(self): 75 | # self.msg_handler.clear() -------------------------------------------------------------------------------- /resource/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "AudioBot": "点歌机", 3 | "connect": "连接", 4 | "enter room id:": "输入房间号:", 5 | "Playlist": "点歌列表", 6 | "title": "标题", 7 | "artist": "歌手", 8 | "source": "来源", 9 | "user": "用户", 10 | "Search": "搜索", 11 | "Add": "添加", 12 | "Source: {}": "来源: {}", 13 | "replay": "重放", 14 | "play/pause": "播放/暂停", 15 | "play next": "下一首", 16 | "move to top": "置顶", 17 | "move up": "上移", 18 | "move down": "下移", 19 | "move to bottom": "置末", 20 | "delete song from list": "删除选中的歌曲", 21 | "play selected": "播放选中的歌曲", 22 | "add to the playlist": "添加到播放列表", 23 | "System Playlist": "System Playlist", 24 | "Volume: ": "音量: ", 25 | "DG Config": "点歌设置", 26 | "QG Config": "切歌设置", 27 | "Visitor": "普通观众", 28 | "Guard": "舰长+", 29 | "Admin": "房管", 30 | "Self": "切自己", 31 | "DG Cooldown (s): ": "点歌冷却 (秒): ", 32 | "DG Max number: ": "最大点歌数: ", 33 | "DG Permission: ": "点歌权限: ", 34 | "Etc": "Etc", 35 | "Experimental Feature": "实验性功能,可能会导致不可预料的后果", 36 | "filter cover": "过滤翻唱", 37 | "Playlist Config": "闲置歌单设置", 38 | "Enter netease playlist id (separate by ,) (restart after modified )": "输入网易云歌单id (英文,分开) (更新后重启)", 39 | "Example: 1234565,1234564": "例子: 1234565,1234564", 40 | "Update Playlist": "更新闲置歌单", 41 | "Audio Device": "音频输出设备", 42 | "Random System Playlist": "随机播放闲置歌单", 43 | "Skip when user add a song": "用户点歌跳过闲置歌单", 44 | "Select audio device: ": "选择输出设备: ", 45 | "OBS output": "obs浏览器输出", 46 | "Application Information": "软件信息", 47 | "Current Song Info:": "当前播放歌曲:", 48 | "CSS:": "CSS:", 49 | "Current Song Lyric:": "当前歌曲歌词:", 50 | "Current Song Cover:": "当前歌曲封面:", 51 | "TextInfo:": "点歌信息:", 52 | "Author:": "作者:", 53 | "Playlist:": "点歌列表:", 54 | "Thanks to:": "感谢:", 55 | "EtcInfo": "其他信息", 56 | "Version:": "当前版本:", 57 | "ban type": "黑名单类型", 58 | "ban content": "黑名单内容", 59 | "use whole word": "全字匹配", 60 | "delete this blacklist item": "删除选中黑名单项", 61 | "add to blacklist": "添加至黑名单", 62 | "Choose ban type": "选择黑名单类型", 63 | "Specify ban content": "输入黑名单内容", 64 | "Blacklist": "黑名单", 65 | "clear the list": "清空列表", 66 | "Play History": "播放历史", 67 | "Netease Login": "网易云登录", 68 | "Get QR": "获取二维码", 69 | "Finish QR Scan": "扫描完成后按我", 70 | "Logout": "登出", 71 | "Login As ": "登录为 ", 72 | "Not Login": "Not Login" 73 | } -------------------------------------------------------------------------------- /plugins/danmaku/tars/core.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | # Tencent is pleased to support the open source community by making Tars available. 6 | # 7 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 8 | # 9 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this file except 10 | # in compliance with the License. You may obtain a copy of the License at 11 | # 12 | # https://opensource.org/licenses/BSD-3-Clause 13 | # 14 | # Unless required by applicable law or agreed to in writing, software distributed 15 | # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 16 | # CONDITIONS OF ANY KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations under the License. 18 | # 19 | 20 | __version__ = "0.0.1" 21 | 22 | from __util import util 23 | from __tars import TarsInputStream 24 | from __tars import TarsOutputStream 25 | from __tup import TarsUniPacket 26 | 27 | 28 | class tarscore: 29 | class TarsInputStream(TarsInputStream): 30 | pass 31 | 32 | class TarsOutputStream(TarsOutputStream): 33 | pass 34 | 35 | class TarsUniPacket(TarsUniPacket): 36 | pass 37 | 38 | class boolean(util.boolean): 39 | pass 40 | 41 | class int8(util.int8): 42 | pass 43 | 44 | class uint8(util.uint8): 45 | pass 46 | 47 | class int16(util.int16): 48 | pass 49 | 50 | class uint16(util.uint16): 51 | pass 52 | 53 | class int32(util.int32): 54 | pass 55 | 56 | class uint32(util.uint32): 57 | pass 58 | 59 | class int64(util.int64): 60 | pass 61 | 62 | class float(util.float): 63 | pass 64 | 65 | class double(util.double): 66 | pass 67 | 68 | class bytes(util.bytes): 69 | pass 70 | 71 | class string(util.string): 72 | pass 73 | 74 | class struct(util.struct): 75 | pass 76 | 77 | @staticmethod 78 | def mapclass(ktype, vtype): 79 | return util.mapclass(ktype, vtype) 80 | 81 | @staticmethod 82 | def vctclass(vtype): 83 | return util.vectorclass(vtype) 84 | 85 | @staticmethod 86 | def printHex(buff): 87 | util.printHex(buff) 88 | 89 | 90 | # 被用户引用 91 | from __util import configParse 92 | from __rpc import Communicator 93 | from exception import * 94 | from __logger import tarsLogger 95 | -------------------------------------------------------------------------------- /liveroom/danmaku/bilibili.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | 3 | from audiobot.user import DanmakuUser 4 | from liveroom.message import DanmakuMessage 5 | from liveroom.platform import LivePlatform 6 | from plugins import blivedm 7 | from plugins.blivedm import DanmakuMessage as BliveMsg 8 | 9 | from liveroom.danmaku import base 10 | 11 | class BilibiliLiveRoom(base.LiveRoom): 12 | 13 | def __init__(self,room_id): 14 | super().__init__() 15 | self._room_id = room_id 16 | self.room = _DanmuClient(self._room_id,loop=self.loop) 17 | self.room._on_receive_danmaku = self._on_receive_danmaku 18 | 19 | @property 20 | def is_running(self) -> bool: 21 | return self.room.is_running 22 | 23 | def start(self): 24 | self.room.start() 25 | 26 | def stop(self): 27 | self.room.stop() 28 | 29 | @property 30 | def title(self): 31 | return self.room.title 32 | 33 | @property 34 | def room_id(self): 35 | return self._room_id 36 | 37 | @property 38 | def unique_room_id(self): 39 | return "bili{}".format(self._room_id) 40 | 41 | async def _on_receive_danmaku(self, msg: BliveMsg): 42 | for handler in self.msg_handler.values(): 43 | handler(BiliDanmuMessage(msg)) 44 | 45 | class BiliDanmuMessage(DanmakuMessage): 46 | def __init__(self, message:BliveMsg): 47 | self.raw_message = message 48 | self._user = DanmakuUser(message.uname,message.uid,LivePlatform.Bilibili) 49 | 50 | @property 51 | def user(self) -> DanmakuUser: 52 | return self._user 53 | 54 | @property 55 | def message(self) -> str: 56 | return self.raw_message.msg 57 | 58 | @property 59 | def admin(self) -> bool: 60 | return bool(self.raw_message.admin) 61 | 62 | @property 63 | def privilege_level(self) -> int: 64 | # 舰队类型,0非舰队,1总督,2提督,3舰长 65 | return self.raw_message.privilege_type 66 | 67 | 68 | class _DanmuClient(blivedm.BLiveClient): 69 | def __init__(self, room_id, uid=0, session: aiohttp.ClientSession = None, heartbeat_interval=30, ssl=True,loop=None): 70 | super().__init__(room_id, uid, session, heartbeat_interval, ssl, loop) 71 | 72 | @property 73 | def title(self): 74 | return self._title 75 | 76 | def _parse_room_init(self, data): 77 | super()._parse_room_init(data) 78 | room_info = data['room_info'] 79 | self._title = room_info["title"] 80 | return True -------------------------------------------------------------------------------- /gui/RoomGUI.py: -------------------------------------------------------------------------------- 1 | from tkinter import ttk, scrolledtext 2 | from audiobot import Global_Audio_Bot 3 | from liveroom import Global_Room_Manager 4 | import tkinter as tk 5 | import gui 6 | from config import Config 7 | from utils.vtranslation import getTranslatedText as _ 8 | 9 | class RoomGUI(): 10 | def __init__(self,main_window): 11 | self.main_window: gui.MainWindow = main_window 12 | self.widget = ttk.Frame(self.main_window.getTabController()) 13 | self.room_id = tk.StringVar() 14 | self.output_label = tk.StringVar() 15 | 16 | def initialize(self): 17 | self.main_window.getTabController().add(self.widget, text=_("AudioBot")) 18 | 19 | self.room_id.set(Config.default_room) 20 | 21 | def createWidgets(self): 22 | 23 | frame_main = ttk.LabelFrame(self.widget, text="AudioBot Test") 24 | frame_main.grid(column=0, row=0, padx=8, pady=4) 25 | 26 | # ========== input frame ================ 27 | 28 | frame_input = ttk.Frame(frame_main) 29 | frame_input.grid(column=0, row=0, padx=8, pady=4,sticky=tk.W) 30 | 31 | # Creating check box for commands 32 | ttk.Label(frame_input, text=_("enter room id:")) \ 33 | .grid(column=0, row=0, sticky=tk.W, padx=8, pady=4) 34 | 35 | room_id_input = ttk.Entry(frame_input, 36 | width=16, 37 | textvariable=self.room_id) 38 | room_id_input.grid(column=1, row=0, padx=8, pady=4) 39 | 40 | play_button = ttk.Button(frame_input, width=8, text=_("connect"), command=self.__confirmroom) 41 | play_button.grid(column=2, row=0) 42 | 43 | # ========== output frame ================ 44 | 45 | frame_output = ttk.Frame(frame_main) 46 | frame_output.grid(column=0, row=1, padx=8, pady=4, sticky=tk.W) 47 | 48 | # Creating check box for commands 49 | output_label = ttk.Label(frame_output, 50 | width = 64, 51 | textvariable = self.output_label) 52 | output_label.grid(column=0, row=0, sticky=tk.W, padx=8, pady=4) 53 | 54 | def __confirmroom(self): 55 | self.output_label.set("connect") 56 | self._startRoom() 57 | 58 | def _startRoom(self): 59 | Global_Room_Manager.stop_all() 60 | lr = Global_Room_Manager.add_live_room(self.room_id.get()) 61 | Config.default_room = self.room_id.get() 62 | Global_Room_Manager.start_room(self.room_id.get()) 63 | Global_Audio_Bot.setLiveRoom(lr) -------------------------------------------------------------------------------- /apis/kuwo.py: -------------------------------------------------------------------------------- 1 | from apis import CommonRequestWrapper, HTTP_CLIENT, SETTING, RegExpResponseContainer 2 | from urllib import parse 3 | import re 4 | 5 | 6 | class API: 7 | file_headers = {'user-agent': 'okhttp/3.10.0'} 8 | 9 | @staticmethod 10 | def info_url(song_id): 11 | return "http://www.kuwo.cn/play_detail/{song_id}".format(song_id=song_id) 12 | 13 | @staticmethod 14 | def file_api(song_id): 15 | return "http://antiserver.kuwo.cn/anti.s?type=convert_url&format=mp3&response=url&" \ 16 | "rid=MUSIC_{song_id}".format(song_id=song_id) 17 | 18 | @staticmethod 19 | def search_cookie(keyword): 20 | return "http://kuwo.cn/search/list?key={keyword}".format(keyword=parse.quote(keyword)) 21 | 22 | @staticmethod 23 | def search_api(keyword, page, pagesize): 24 | return "http://www.kuwo.cn/api/www/search/searchMusicBykeyWord?" \ 25 | "key={keyword}&pn={page}&rn={pagesize}".format(keyword=parse.quote(keyword), 26 | page=page, 27 | pagesize=pagesize) 28 | @CommonRequestWrapper 29 | def getMusicInfo(song_id: str): 30 | """ 31 | get audio info url: web page raw 32 | 33 | :param song_id: song id 34 | :return: bytes 35 | """ 36 | return ("get", 37 | API.info_url(song_id) 38 | ) 39 | 40 | @CommonRequestWrapper 41 | def getMusicFile(song_id: str): 42 | """ 43 | get audio file url 44 | 45 | :param song_id: song id 46 | :return: bytes 47 | """ 48 | return ("get", 49 | API.file_api(song_id), 50 | {"headers": API.file_headers} 51 | ) 52 | 53 | @CommonRequestWrapper 54 | def getSearchResult(keyword, page: int = 1, pagesize: int = 5): 55 | """ 56 | get search result 57 | 58 | :param keyword: string keywords 59 | :param page: default value 1, should be integer larger or equal to 1 60 | :param pagesize: default value 5 61 | :return: bytes 62 | """ 63 | token = re.search(r"kw_token=([^\s]*);", 64 | HTTP_CLIENT.get(API.search_cookie(keyword)).headers["Set-Cookie"]).group().split("=")[-1][:-1:] 65 | return ("get", 66 | API.search_api(keyword, page, pagesize), 67 | {"cookies":{"kw_token":token}, 68 | "headers": {"referer": API.search_cookie(keyword), 69 | "csrf": token} 70 | } 71 | ) 72 | 73 | # import json 74 | # print(json.dumps(json.loads(getSearchResult("莫愁")))) -------------------------------------------------------------------------------- /audiobot/commands/diange.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import List 3 | 4 | from audiobot.command import CommandExecutor 5 | from audiobot import Global_Command_Manager 6 | from config import Config 7 | from liveroom.message import DanmakuMessage 8 | from sources.audio import BiliAudioSource, NeteaseMusicSource 9 | from sources.audio.kuwo import KuwoMusicSource 10 | 11 | 12 | @Global_Command_Manager.register("diange") 13 | class DiangeCommand(CommandExecutor): 14 | def __init__(self, audiobot): 15 | super().__init__(audiobot, ["点歌", "点b歌", "点w歌", "点k歌"]) 16 | self.cooldowns = {} 17 | 18 | def process(self, command, dmkMsg: DanmakuMessage): 19 | msg: List[str] = dmkMsg.message.split(" ") 20 | if len(msg) < 2: 21 | return 22 | val = " ".join(msg[1::]) 23 | if not (self.__hasPermission(dmkMsg) and self.__inCooldown(dmkMsg) and self.__notReachMax()): 24 | return 25 | if (command == "点歌"): 26 | self.audiobot.addAudioByUrl(val, dmkMsg.user) 27 | elif command == "点b歌": 28 | self.audiobot.addAudioByUrl(val, dmkMsg.user, source_class=BiliAudioSource) 29 | elif command == "点w歌": 30 | self.audiobot.addAudioByUrl(val, dmkMsg.user, source_class=NeteaseMusicSource) 31 | elif command == "点k歌": 32 | self.audiobot.addAudioByUrl(val, dmkMsg.user, source_class=KuwoMusicSource) 33 | 34 | def __hasPermission(self, dmkMsg: DanmakuMessage): 35 | config = Config.commands["diange"] 36 | try: 37 | if config["visitor"]: 38 | return True 39 | if config["admin"] and dmkMsg.admin: 40 | return True 41 | if config["guard"] and dmkMsg.privilege_level > 0: 42 | return True 43 | # if config["fan"] is not None: 44 | # if (str(config["fan"]["room_id"]) == str(dmkMsg.room_id) 45 | # and int(dmkMsg.medal_level) >= int(config["fan"]["level"])): 46 | # return True 47 | return False 48 | except: 49 | return False 50 | 51 | def __inCooldown(self, dmkMsg: DanmakuMessage): 52 | uid = str(dmkMsg.user.identifier) 53 | if self.cooldowns.get(uid) is None: 54 | self.cooldowns[uid] = int(time.time()) 55 | return True 56 | if int(time.time()) - self.cooldowns[uid] >= Config.commands["diange"]["cooldown"]: 57 | self.cooldowns[uid] = int(time.time()) 58 | return True 59 | return False 60 | 61 | def __notReachMax(self): 62 | return self.audiobot.user_playlist.size() < Config.commands["diange"]["limit"] -------------------------------------------------------------------------------- /plugins/danmaku/paramgen/liveparam.py: -------------------------------------------------------------------------------- 1 | import random 2 | import time 3 | from . import enc 4 | from base64 import urlsafe_b64encode as b64enc 5 | from urllib.parse import quote 6 | 7 | 8 | def _header(video_id, channel_id) -> str: 9 | S1_3 = enc.rs(1, video_id) 10 | S1_5 = enc.rs(1, channel_id) + enc.rs(2, video_id) 11 | S1 = enc.rs(3, S1_3) + enc.rs(5, S1_5) 12 | S3 = enc.rs(48687757, enc.rs(1, video_id)) 13 | header_replay = enc.rs(1, S1) + enc.rs(3, S3) + enc.nm(4, 1) 14 | return b64enc(header_replay) 15 | 16 | 17 | def _build(video_id, channel_id, ts1, ts2, ts3, ts4, ts5, topchat_only) -> str: 18 | chattype = 4 if topchat_only else 1 19 | 20 | b1 = enc.nm(1, 0) 21 | b2 = enc.nm(2, 0) 22 | b3 = enc.nm(3, 0) 23 | b4 = enc.nm(4, 0) 24 | b7 = enc.rs(7, "") 25 | b8 = enc.nm(8, 0) 26 | b9 = enc.rs(9, "") 27 | timestamp2 = enc.nm(10, ts2) 28 | b11 = enc.nm(11, 3) 29 | b15 = enc.nm(15, 0) 30 | 31 | header = enc.rs(3, _header(video_id, channel_id)) 32 | timestamp1 = enc.nm(5, ts1) 33 | s6 = enc.nm(6, 0) 34 | s7 = enc.nm(7, 0) 35 | s8 = enc.nm(8, 1) 36 | body = enc.rs(9, b"".join((b1, b2, b3, b4, b7, b8, b9, timestamp2, b11, b15))) 37 | timestamp3 = enc.nm(10, ts3) 38 | timestamp4 = enc.nm(11, ts4) 39 | s13 = enc.nm(13, chattype) 40 | chattype = enc.rs(16, enc.nm(1, chattype)) 41 | s17 = enc.nm(17, 0) 42 | str19 = enc.rs(19, enc.nm(1, 0)) 43 | timestamp5 = enc.nm(20, ts5) 44 | entity = b"".join( 45 | ( 46 | header, 47 | timestamp1, 48 | s6, 49 | s7, 50 | s8, 51 | body, 52 | timestamp3, 53 | timestamp4, 54 | s13, 55 | chattype, 56 | s17, 57 | str19, 58 | timestamp5, 59 | ) 60 | ) 61 | continuation = enc.rs(119693434, entity) 62 | return quote(b64enc(continuation).decode()) 63 | 64 | 65 | def _times(past_sec): 66 | n = int(time.time()) 67 | _ts1 = n - random.uniform(0, 1 * 3) 68 | _ts2 = n - random.uniform(0.01, 0.99) 69 | _ts3 = n - past_sec + random.uniform(0, 1) 70 | _ts4 = n - random.uniform(10 * 60, 60 * 60) 71 | _ts5 = n - random.uniform(0.01, 0.99) 72 | return list(map(lambda x: int(x * 1000000), [_ts1, _ts2, _ts3, _ts4, _ts5])) 73 | 74 | 75 | def getparam(video_id, channel_id, past_sec=0, topchat_only=False) -> str: 76 | """ 77 | Parameter 78 | --------- 79 | past_sec : int 80 | seconds to load past chat data 81 | topchat_only : bool 82 | if True, fetch only 'top chat' 83 | """ 84 | return _build(video_id, channel_id, *_times(past_sec), topchat_only) 85 | -------------------------------------------------------------------------------- /backend/aioserver/router_handlers.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | from aiohttp import web 3 | import os 4 | from urllib.parse import urlparse 5 | from backend.aioserver import aiosocket_server 6 | from utils import vfile, formats 7 | 8 | from backend.aioserver import routes,ENV,DIST_DIR 9 | 10 | 11 | @routes.get("/autoredirect",name="autoredirect") 12 | async def websocket_handler(request: web.Request): 13 | target = request.query.get("url") 14 | if target is None or not formats.isValidUrl(target): 15 | return web.HTTPNotFound() 16 | hostname = urlparse(target).hostname 17 | async with aiohttp.ClientSession() as session: 18 | async with session.get(target,headers = {"referer":hostname, 19 | "origin":hostname}) as resp: 20 | return web.Response(body=await resp.read(), 21 | content_type=resp.content_type) 22 | 23 | @routes.get("/ws/audiobot") 24 | async def websocket_handler(request: web.Request): 25 | ws = web.WebSocketResponse() 26 | await ws.prepare(request) 27 | aiosocket_server.websockets.append( 28 | ws 29 | ) 30 | await aiosocket_server.sendInitialData(ws) 31 | async for msg in ws: 32 | msg:aiohttp.WSMessage 33 | if msg.type == aiohttp.WSMsgType.TEXT: 34 | if msg.data == 'close': 35 | await ws.close() 36 | elif msg.type == aiohttp.WSMsgType.ERROR: 37 | print('ws connection closed with exception %s' % 38 | ws.exception()) 39 | aiosocket_server.websockets.remove(ws) 40 | print('websocket connection closed') 41 | return ws 42 | 43 | @routes.get("/") 44 | @routes.get("/{path:.*}") 45 | async def vue_redirecting(request: web.Request): 46 | try: 47 | path = request.match_info["path"] 48 | except: 49 | path = "" 50 | if ENV == "development": 51 | async with aiohttp.ClientSession() as session: 52 | async with session.get('http://localhost:8080/{}'.format(path)) as resp: 53 | return web.Response(text=await resp.text(), 54 | content_type=resp.content_type) 55 | else: 56 | if path.startswith("static") or path == "": 57 | path = os.path.join(DIST_DIR, "index.html") if path == "" else os.path.join(DIST_DIR, path) 58 | if not os.path.exists(path): 59 | return web.Response(status=404) 60 | with open(path, "r", encoding="utf-8") as f: 61 | return web.Response(text=f.read(), 62 | content_type=vfile.getFileContentType(path)) 63 | 64 | with open(os.path.join(DIST_DIR, "index.html"), "r", encoding="utf-8") as f: 65 | return web.Response(text=f.read(), 66 | content_type=vfile.getFileContentType(path)) 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BiliAudioBot - 卡西米尔唱片机 2 | 3 | !注意,此项目可能不会再有除了bug修复之外的更新了。欢迎各位前往golang的重写版本[AynaLivePlayer](https://github.com/AynaLivePlayer/AynaLivePlayer) 4 | 5 | A python gui version of audio bot. 6 | 7 | provided by Aynakeya(划水) & Nearl_Official(摸鱼) 8 | 9 | QQ group: 621035845 10 | 11 | # how to start (demo version) 12 | 13 | `git clone https://github.com/LXG-Shadow/BiliAudioBot.git` 14 | 15 | cd to the working directory 16 | 17 | `pip install -r requirements.txt` 18 | 19 | download **mpv-1.dll** into working directory 20 | 21 | `python AudioBot.py` 22 | 23 | # how to use (demo version) 24 | 25 | **连接直播间:** 26 | 27 | 输入直播间号,按connect 28 | 29 | 30 | **通用点歌:** 31 | 32 | `点歌 关键字/id` 33 | 34 | id 35 | 36 | 网易云: `点歌 wy1817437429` 37 | 38 | bilibili `点歌 au2159832` 39 | 40 | bilibili视频 `点歌 BV1Xv411Y7eW` 41 | 42 | 酷我 `点歌 kuwo142655450` 43 | 44 | 关键字 (默认使用网易云搜索) 45 | 46 | `点歌 染 reol` 47 | 48 | **切歌:** 49 | 50 | `切歌` 目前仅允许切自己的歌 51 | 52 | **来源点歌:** 53 | 54 | 网易云 `点w歌 关键字/1817437429/wy1817437429` 55 | 56 | bilibili `点b歌 关键字/au2159832` 57 | 58 | 酷我 `点k歌 关键字/kuwo142655450/142655450` 59 | 60 | **网易云vip匹配:** 61 | 62 | 匹配顺序是 bilibili -> 酷我 -> 网易云 63 | 64 | 65 | # build 66 | 67 | first build frontend 68 | 69 | `cd fronend` 70 | 71 | `npm install` 72 | 73 | `npm run build` 74 | 75 | second run python package 76 | 77 | `pyinstaller --windowed --clean --noconfirm --icon=resource/favicon.ico --add-data "frontend/dist;frontend/dist" --add-data "resource;resource" --add-data "addons;addons" --add-data "config;config" --add-binary "mpv-1.dll;." AudioBot.py` 78 | 79 | # todo list 80 | 81 | - ~~config添加默认房间~~ 82 | 83 | - ~~tooltips~~ 84 | 85 | - ~~本地文本输出~~ 86 | 87 | - ~~本地文本输出自定义格式~~ 88 | 89 | - ~~音量写入配置文件~~ 90 | 91 | - ~~ui不显示滚动条?~~ 92 | 93 | - ~~翻译~~ 94 | 95 | - ~~歌词->文本 网页~~ 96 | 97 | - ~~自动过滤翻唱~~ 98 | 99 | - ~~输出声卡选择~~ 100 | 101 | - ~~黑名单~~ 102 | 103 | - ~~点歌历史记录~~ 104 | 105 | - 时间自定义 106 | 107 | - 使用系统代理 108 | 109 | - 修改点歌格式 110 | 111 | - 本地曲库 112 | 113 | - 具体时间->文本 网页 114 | 115 | # 已知问题 116 | 117 | 先开加速器再开本软件可能会导致web无法正常工作 118 | 119 | 解决方式: 可以先开启本软件再打开加速器。 120 | 121 | 122 | win7 中打开可能会出现问题,建议使用win7 version 123 | 124 | 如果出现faild to execute script pyi_rth_multiprocessing 125 | 126 | 建议升级win7系统 或者 下载KB2533623。 127 | 128 | (或许你可以自己用python3.6重写打包,但是我懒了 129 | 130 | 131 | 132 | # change log 133 | 134 | 2021-03-30-demo0.8.0: Public Test Version Release 135 | 136 | 2021-03-30-demo0.7.9: Web output update 137 | 138 | 2021-03-11-demo0.7.4: translation update 139 | 140 | 2021-03-11-demo0.7.0: frontend&backend demo finish. 141 | 142 | 2021-03-11-demo0.6.5: rewrite event handling 143 | 144 | 2021-03-11-demo0.6.0: asynchronous loading 145 | 146 | 2021-03-11-demo0.5.5: build executable 147 | 148 | 2021-03-10-demo0.5.0: finish demo version 149 | 150 | 2021-08-08-demo0.8.3: Netease login -------------------------------------------------------------------------------- /utils/command.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | 3 | 4 | class OptionParser(): 5 | def __init__(self, raw, first=False): 6 | raw: str 7 | self.raw = raw 8 | self.remove_first = not first 9 | self.command = "" 10 | self.options = {} 11 | self.args = [] 12 | self._process() 13 | 14 | def _process(self): 15 | rawlist = [x for x in self.raw.split(" ") if x != ""] 16 | self.command = rawlist[0] 17 | if self.remove_first: 18 | rawlist = rawlist[1::] 19 | for seg in rawlist: 20 | if len(seg) < 1: 21 | continue 22 | if seg[0] == "-": 23 | opt = seg[1::].split("=") 24 | if len(opt) < 2: 25 | self.options[opt[0]] = "" 26 | self.options[opt[0]] = "=".join(opt[1:]) 27 | else: 28 | self.args.append(seg) 29 | 30 | def getOption(self, key): 31 | return self.options.get(key) 32 | 33 | def getEmptyOptionKeyList(self): 34 | return [key for key in self.options.keys() if self.options[key] == ""] 35 | 36 | def getParsedOptions(self, empty=False, **kwargs): 37 | ops = {} 38 | if empty: 39 | ops = self.options.copy() 40 | else: 41 | for key in self.options.keys(): 42 | if self.options[key] != "": 43 | ops[key] = self.options[key] 44 | for key, val in kwargs.items(): 45 | if ops.get(key) != None: 46 | ops[key] = val(ops[key]) 47 | return ops 48 | 49 | 50 | class OutputParser(): 51 | def __init__(self, output_func=print): 52 | self.output_func = output_func 53 | 54 | def print(self, msg, offset, step, prefix=""): 55 | # if not isinstance(msg, Iterable): 56 | # msg = str(msg) 57 | if isinstance(msg, str): 58 | self.output_func("{prefix}{msg}".format(prefix=prefix, 59 | msg=self._parseOffset(msg, 60 | offset=offset, 61 | step=step))) 62 | return 63 | for m in msg: 64 | self.print(m, offset=offset + step, step=step, prefix=prefix) 65 | 66 | def _parseOffset(self, msg, offset, step): 67 | if isinstance(msg, str): 68 | return "{:>{offset}s}".format(msg, offset=len(msg) + offset) 69 | return map(lambda x: self._parseOffset(x, offset=offset, step=step) 70 | if isinstance(x, str) 71 | else self._parseOffset(x, offset=offset + step, step=step), 72 | msg) 73 | 74 | # a = OptionParser("aa -fdf -page=1") 75 | # print(a.options) 76 | # print(a.getEmptyOptionKeyList()) 77 | # print(a.getParsedOptions(page=int)) 78 | -------------------------------------------------------------------------------- /plugins/danmaku/tars/EndpointF.py: -------------------------------------------------------------------------------- 1 | # Tencent is pleased to support the open source community by making Tars available. 2 | # 3 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 4 | # 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this file except 6 | # in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 12 | # CONDITIONS OF ANY KIND, either express or implied. See the License for the 13 | # specific language governing permissions and limitations under the License. 14 | # 15 | 16 | from core import tarscore 17 | 18 | 19 | class EndpointF(tarscore.struct): 20 | __tars_class__ = "register.EndpointF" 21 | 22 | def __init__(self): 23 | self.host = "" 24 | self.port = 0 25 | self.timeout = 0 26 | self.istcp = 0 27 | self.grid = 0 28 | self.groupworkid = 0 29 | self.grouprealid = 0 30 | self.setId = "" 31 | self.qos = 0 32 | self.bakFlag = 0 33 | self.weight = 0 34 | self.weightType = 0 35 | 36 | @staticmethod 37 | def writeTo(oos, value): 38 | oos.write(tarscore.string, 0, value.host) 39 | oos.write(tarscore.int32, 1, value.port) 40 | oos.write(tarscore.int32, 2, value.timeout) 41 | oos.write(tarscore.int32, 3, value.istcp) 42 | oos.write(tarscore.int32, 4, value.grid) 43 | oos.write(tarscore.int32, 5, value.groupworkid) 44 | oos.write(tarscore.int32, 6, value.grouprealid) 45 | oos.write(tarscore.string, 7, value.setId) 46 | oos.write(tarscore.int32, 8, value.qos) 47 | oos.write(tarscore.int32, 9, value.bakFlag) 48 | oos.write(tarscore.int32, 11, value.weight) 49 | oos.write(tarscore.int32, 12, value.weightType) 50 | 51 | @staticmethod 52 | def readFrom(ios): 53 | value = EndpointF() 54 | value.host = ios.read(tarscore.string, 0, True, value.host) 55 | value.port = ios.read(tarscore.int32, 1, True, value.port) 56 | value.timeout = ios.read(tarscore.int32, 2, True, value.timeout) 57 | value.istcp = ios.read(tarscore.int32, 3, True, value.istcp) 58 | value.grid = ios.read(tarscore.int32, 4, True, value.grid) 59 | value.groupworkid = ios.read(tarscore.int32, 5, False, value.groupworkid) 60 | value.grouprealid = ios.read(tarscore.int32, 6, False, value.grouprealid) 61 | value.setId = ios.read(tarscore.string, 7, False, value.setId) 62 | value.qos = ios.read(tarscore.int32, 8, False, value.qos) 63 | value.bakFlag = ios.read(tarscore.int32, 9, False, value.bakFlag) 64 | value.weight = ios.read(tarscore.int32, 11, False, value.weight) 65 | value.weightType = ios.read(tarscore.int32, 12, False, value.weightType) 66 | return value 67 | -------------------------------------------------------------------------------- /backend/aioserver/aiosocket_server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import traceback 3 | 4 | from audiobot import Global_Audio_Bot 5 | from audiobot.audio import AudioItem 6 | from audiobot.event import AudioBotPlayEvent 7 | from audiobot.event.lyric import LyricUpdateEvent 8 | from audiobot.event.playlist import PlaylistUpdateEvent 9 | from backend.aioserver import app 10 | loop = asyncio.get_event_loop() 11 | websockets = [] 12 | 13 | 14 | def getImgRedirectedUrl(url): 15 | try: 16 | return str(app.router["autoredirect"].url_for().with_query({"url": url})) 17 | except: 18 | traceback.print_exc() 19 | return "" 20 | 21 | def sendJsonData(data): 22 | for ws in websockets: 23 | asyncio.ensure_future(ws.send_json(data), loop=loop) 24 | 25 | 26 | @Global_Audio_Bot.user_playlist.handlers.register(PlaylistUpdateEvent.__event_name__, 27 | "websocket.updateplaylist") 28 | def listenPlaylistUpdate(event: PlaylistUpdateEvent): 29 | sendJsonData({event.__event_name__: parsePlaylistUpdate(event.playlist.playlist)}) 30 | 31 | 32 | def parsePlaylistUpdate(playlist): 33 | playlist_data = [] 34 | for item in playlist: 35 | item: AudioItem 36 | cover_url = item.source.getCover().url if item.source.getCover() != None else "" 37 | playlist_data.append({"title": item.source.getTitle(), 38 | "artist": item.source.getArtist(), 39 | "cover": getImgRedirectedUrl(cover_url), 40 | "username": item.username}) 41 | return playlist_data 42 | 43 | 44 | @Global_Audio_Bot.handlers.register(AudioBotPlayEvent.__event_name__, 45 | "websocket.updateplaying") 46 | def listenAudioBotPlay(event: AudioBotPlayEvent): 47 | item: AudioItem = event.item 48 | sendJsonData({event.__event_name__: parseAudioBotPlayData(item)}) 49 | 50 | 51 | def parseAudioBotPlayData(item: AudioItem): 52 | if item == None: 53 | return {"title": "", 54 | "artist": "", 55 | "cover": "", 56 | "username": ""} 57 | cover_url = item.source.getCover().url if item.source.getCover() != None else "" 58 | return {"title": item.source.getTitle(), 59 | "artist": item.source.getArtist(), 60 | "cover": getImgRedirectedUrl(cover_url), 61 | "username": item.username} 62 | 63 | 64 | @Global_Audio_Bot.lyrics.handlers.register(LyricUpdateEvent, 65 | "websocket.updatelyric") 66 | def listenLyricUpdate(event: LyricUpdateEvent): 67 | sendJsonData({event.__event_name__: {"lyric": event.lyric.lyric}}) 68 | 69 | 70 | async def sendInitialData(ws): 71 | await ws.send_json({PlaylistUpdateEvent.__event_name__: 72 | parsePlaylistUpdate(Global_Audio_Bot.user_playlist.playlist)}) 73 | await ws.send_json({AudioBotPlayEvent.__event_name__: 74 | parseAudioBotPlayData(Global_Audio_Bot.current)}) 75 | -------------------------------------------------------------------------------- /apis/bilibili/audio.py: -------------------------------------------------------------------------------- 1 | from apis import CommonRequestWrapper, RegExpResponseContainer 2 | 3 | class QUALITY: 4 | HIGH = ("2","320k","高品质") 5 | MEDIAN = ("1","196k","标准") 6 | NORMAL = ("0","128k","流畅") 7 | 8 | values = [HIGH,MEDIAN,NORMAL] 9 | 10 | @staticmethod 11 | def id(quality): 12 | return quality[0] 13 | 14 | @staticmethod 15 | def bitrate(quality): 16 | return quality[1] 17 | 18 | @staticmethod 19 | def description(quality): 20 | return quality[2] 21 | 22 | class API: 23 | headers = {"user-agent": "BilibiliClient/2.33.3", 24 | 'Accept': "*/*", 25 | 'Connection': "keep-alive"} 26 | 27 | @staticmethod 28 | def info_api(song_id): 29 | return "https://www.bilibili.com/audio/music-service-c/web/song/info?sid={song_id}".format(song_id = song_id) 30 | 31 | @staticmethod 32 | def file_api(song_id,quality): 33 | return "http://api.bilibili.com/audio/music-service-c/url?" \ 34 | "mid=8047632&mobi_app=iphone&platform=ios&privilege=2" \ 35 | "&quality={quality}&songid={song_id}".format(song_id = song_id, 36 | quality = quality) 37 | 38 | @staticmethod 39 | def search_api(keyword,page,pagesize): 40 | return "https://api.bilibili.com/audio/music-service-c/s?search_type=music&" \ 41 | "keyword={keyword}&page={page}&pagesize={pagesize}".format(keyword=keyword, 42 | page=page, 43 | pagesize= pagesize) 44 | 45 | 46 | @CommonRequestWrapper 47 | def getAudioInfo(song_id:str): 48 | """ 49 | get audio info 50 | 51 | :param song_id: 52 | :return: 53 | """ 54 | return ("get", 55 | API.info_api(song_id), 56 | {"headers":API.headers} 57 | ) 58 | 59 | @CommonRequestWrapper 60 | def getAudioFile(song_id:str,quality = QUALITY.id(QUALITY.HIGH)): 61 | """ 62 | get audio file url 63 | 64 | :param song_id: song id 65 | :param quality: quality id, see bilibili.audio.QUALITY 66 | :return: bytes 67 | """ 68 | return ("get", 69 | API.file_api(song_id,quality), 70 | {"headers": API.headers} 71 | ) 72 | 73 | @CommonRequestWrapper 74 | def getSearchResult(keyword,page:int = 1,pagesize:int = 5): 75 | """ 76 | get search result 77 | 78 | :param keyword: string keywords 79 | :param page: default value 1, should be integer larger or equal to 1 80 | :param pagesize: default value 5 81 | :return: bytes 82 | """ 83 | return ("get", 84 | API.search_api(keyword,page,pagesize) 85 | ) 86 | 87 | # print(getSearchResult("向轮椅奔去").decode()) 88 | 89 | # from apis import JsonResponseContainer 90 | # container = JsonResponseContainer(getAudioFile("18439063333333",quality=2), 91 | # cdns = "data.cdns") 92 | # 93 | # print(container.data) -------------------------------------------------------------------------------- /audiobot/MatchEngine.py: -------------------------------------------------------------------------------- 1 | from typing import List, Type 2 | 3 | from audiobot.handler import AudioBotHandlers 4 | from audiobot.audio import AudioItem 5 | from audiobot.event.audiobot import FindSearchResultEvent 6 | from sources.audio import BiliAudioSource, NeteaseMusicSource 7 | from sources.audio.kuwo import KuwoMusicSource 8 | from sources.base import CommonSource 9 | from sources.base.interface import SearchableSource 10 | 11 | SEARCH_ENGINE_LIST:List[SearchableSource.__class__] = [BiliAudioSource,KuwoMusicSource,NeteaseMusicSource] 12 | DEFAULT_SEARCH_ENGINE = NeteaseMusicSource 13 | 14 | HANDLERS = AudioBotHandlers() 15 | 16 | def check(item:AudioItem): 17 | source = item.source 18 | if source == None: 19 | return item 20 | if isinstance(source,NeteaseMusicSource): 21 | item.source = matchNetease(source,keyword=item.keyword) 22 | return item 23 | return item 24 | 25 | def search(url, source_class:CommonSource.__class__): 26 | if source_class == None: 27 | source_class = DEFAULT_SEARCH_ENGINE 28 | source = source_class.initFromUrl(url) 29 | if source == None: 30 | if issubclass(source_class,SearchableSource): 31 | return searchFirst(url, engine=source_class), url 32 | else: 33 | return searchFirst(url), url 34 | else: 35 | return source,"" 36 | 37 | def searchFirst(url, engine:SearchableSource.__class__=None): 38 | engine = engine if engine else DEFAULT_SEARCH_ENGINE 39 | search_results = engine.search(url) 40 | if search_results == None or search_results.isEmpty(): 41 | return None 42 | for result in search_results.results: 43 | event = FindSearchResultEvent(result) 44 | HANDLERS.call(event) 45 | if event.isCancelled(): 46 | continue 47 | return result.source 48 | return search_results.results[0].source 49 | 50 | def matchNetease(netease:NeteaseMusicSource,keyword=""): 51 | if not netease.vip and netease.available: 52 | return netease 53 | else: 54 | for engine in SEARCH_ENGINE_LIST: 55 | if keyword != "": 56 | result = searchFirst(keyword, engine=engine) 57 | if result != None: 58 | netease.getBaseSources = result.getBaseSources 59 | netease.getSourceName = result.getSourceName 60 | # result.getTitle = netease.getTitle 61 | # result.getArtist = netease.getArtist 62 | # result.getCover = netease.getCover 63 | return netease 64 | result = searchFirst(" ".join([netease.title] + netease.artists), engine=engine) 65 | if result != None: 66 | netease.getBaseSources = result.getBaseSources 67 | netease.getSourceName = result.getSourceName 68 | # result.getTitle = netease.getTitle 69 | # result.getArtist = netease.getArtist 70 | # result.getCover = netease.getCover 71 | return netease 72 | return netease 73 | -------------------------------------------------------------------------------- /audiobot/lyric.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import re 3 | import traceback 4 | import aiohttp 5 | from typing import List 6 | 7 | from audiobot.handler import AudioBotHandlers 8 | from audiobot.audio import AudioItem 9 | from audiobot.event.lyric import LyricUpdateEvent 10 | from sources.audio import AudioSource 11 | 12 | 13 | class LyricItem(): 14 | @staticmethod 15 | def convert_to_sec(raw): 16 | raw = raw[1:-1:].split(":") 17 | minutes = raw[0] 18 | seconds = raw[1] 19 | return float(minutes) * 60 + float(seconds) 20 | 21 | def __init__(self, time, lyric): 22 | self.time = time 23 | self.lyric = lyric 24 | 25 | @classmethod 26 | def init_from_text(cls, text): 27 | time_pattern = re.compile(r"\[[0-9]+:[0-9]+\.[0-9]+\]") 28 | tt = time_pattern.match(text) 29 | if tt is None: 30 | return None 31 | try: 32 | return cls(cls.convert_to_sec(tt.group()), time_pattern.sub("", text)) 33 | except: 34 | traceback.print_exc() 35 | return None 36 | 37 | 38 | class Lyrics(): 39 | def __init__(self, audiobot): 40 | self.audiobot = audiobot 41 | self.lyrics: List[LyricItem] = [] 42 | self.previous = None 43 | self.handlers = AudioBotHandlers() 44 | 45 | def load(self, item: AudioItem): 46 | self.clear() 47 | source = item.source 48 | if not isinstance(source, AudioSource): 49 | return 50 | else: 51 | lrc_source = source.lyric 52 | if lrc_source is None: 53 | return 54 | if lrc_source.filecontent != "": 55 | self.loadContent(lrc_source.filecontent) 56 | else: 57 | asyncio.ensure_future(self._async_load(lrc_source), loop=self.audiobot.loop) 58 | 59 | async def _async_load(self, lrc_source): 60 | async with aiohttp.ClientSession() as session: 61 | async with session.get(lrc_source.url, headers=lrc_source.headers) as response: 62 | self.loadContent(await response.text()) 63 | 64 | def loadContent(self, raw): 65 | for line in raw.split("\n"): 66 | li = LyricItem.init_from_text(line) 67 | if li is not None: 68 | self.lyrics.append(li) 69 | self.lyrics.append(LyricItem(2147483647, "")) 70 | 71 | def findLyricByTime(self, time): 72 | for i in range(len(self.lyrics) - 1): 73 | if (self.lyrics[i].time <= time < self.lyrics[i + 1].time): 74 | return self.lyrics[i] 75 | else: 76 | continue 77 | 78 | def clear(self): 79 | self.handlers.call(LyricUpdateEvent(self, 80 | LyricItem(0, ""))) 81 | self.lyrics.clear() 82 | 83 | def _raiseEvent(self, property, val, *args): 84 | if val is None: 85 | return 86 | lrc = self.findLyricByTime(float(val)) 87 | if lrc is None: 88 | return 89 | if self.previous and lrc.lyric == self.previous.lyric: 90 | return 91 | self.previous = lrc 92 | self.handlers.call(LyricUpdateEvent(self, 93 | lrc)) 94 | -------------------------------------------------------------------------------- /apis/__init__.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from typing import List 3 | 4 | from utils import vhttp, formats 5 | import json,re 6 | 7 | HTTP_CLIENT = vhttp.HttpClient() 8 | 9 | class SETTING: 10 | common_header = { 11 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:51.0) Gecko/20100101 Firefox/51.0" 12 | } 13 | 14 | class RequestMethod: 15 | GET = "get" 16 | POST = "post" 17 | 18 | 19 | def BaseApiWrapper(api_wrapper): 20 | @wraps(api_wrapper) 21 | def ApiWrapper(api_func): 22 | @wraps(api_func) 23 | def process(*args, **kwargs): 24 | retval = api_func(*args, **kwargs) 25 | method, url = retval[0], retval[1] 26 | req_kwargs = retval[2] if len(retval) >= 3 else {} 27 | return api_wrapper(method, url, **req_kwargs) 28 | return process 29 | return ApiWrapper 30 | 31 | 32 | @BaseApiWrapper 33 | def CommonRequestWrapper(method, url, **request_kwargs) -> bytes: 34 | try: 35 | if method == RequestMethod.GET: 36 | return HTTP_CLIENT.get(url, **request_kwargs).content 37 | elif method == RequestMethod.POST: 38 | return HTTP_CLIENT.post(url, **request_kwargs).content 39 | except: 40 | return b'' 41 | 42 | class RegExpResponseContainer(): 43 | def __init__(self, content:bytes, strip:[str,List]=None, **kwargs): 44 | """ 45 | :param content: the byte content of the api 46 | :param strip: list of str that need to be stripped 47 | :param kwargs: data you wants to get 48 | """ 49 | self.content = content 50 | self.kwargs = kwargs 51 | self.strip = strip 52 | self.data = {} 53 | 54 | self.__process() 55 | 56 | def __process(self): 57 | text = self.__stripHTML(formats.htmlAutoDecode(self.content)) 58 | for key,val in self.kwargs.items(): 59 | try: 60 | if isinstance(val,tuple): 61 | self.data[key] = val[1](re.search(val[0],text).group()) 62 | else: 63 | self.data[key] = re.search(val,text).group() 64 | except: 65 | self.data[key] = None 66 | 67 | def __stripHTML(self,text): 68 | if self.strip != None: 69 | if isinstance(self.strip,str): 70 | text = text.replace(self.strip, "") 71 | else: 72 | for s in self.strip: 73 | text = text.replace(s,"") 74 | return text 75 | 76 | class JsonResponseContainer(): 77 | def __init__(self, content:bytes, path_sep=".", **kwargs): 78 | self.content = content 79 | self.kwargs = kwargs 80 | self.data = {} 81 | self.path_sep = path_sep 82 | 83 | self.__process() 84 | 85 | def __process(self): 86 | jdata = json.loads(self.content) 87 | for key,val in self.kwargs.items(): 88 | if isinstance(val,tuple): 89 | self.data[key] = val[1](self.__tryFind(jdata,val[0].split(self.path_sep),0)) 90 | else: 91 | self.data[key] = self.__tryFind(jdata,val.split(self.path_sep),0) 92 | 93 | def __tryFind(self,data,paths,now): 94 | if now == len(paths) - 1: 95 | return data[paths[now]] 96 | return self.__tryFind(data[paths[now]], paths, now + 1) 97 | -------------------------------------------------------------------------------- /gui/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from mttkinter import mtTkinter as tk 4 | from PIL import Image,ImageTk 5 | from tkinter import ttk, PhotoImage 6 | from tkinter import Menu 7 | from tkinter.ttk import Notebook 8 | 9 | from audiobot import Global_Audio_Bot 10 | from config import Config 11 | from gui.BlacklistGUI import BlacklistGUI 12 | from gui.ConfigGUI import ConfigGUI 13 | from gui.HistoryPlaylistGUI import HistoryPlaylistGUI 14 | from gui.InfoGUI import InfoGUI 15 | from gui.PlaylistGUI import PlaylistGUI 16 | from gui.MPVGUI import MPVGUI 17 | from gui.RoomGUI import RoomGUI 18 | from gui.SearchGUI import SearchGUI 19 | from gui.WYLoginGUI import WYLoginGUI 20 | from utils import vasyncio, vfile 21 | 22 | class MainWindow(): 23 | def __init__(self, loop=None, interval=1 / 20): 24 | self.window = tk.Tk() 25 | self.interval = interval 26 | self.window.title(Config.gui_title) 27 | self.tab_controller: Notebook = ttk.Notebook(self.window) 28 | self.menu_controller = Menu(self.window) 29 | self._loop = asyncio.get_event_loop() if loop == None else loop 30 | self._running = True 31 | self._initialize() 32 | 33 | def _initialize(self): 34 | self.window.iconphoto(True, 35 | ImageTk.PhotoImage(Image.open(vfile.getResourcePath('resource/favicon.ico')))) 36 | self.window.resizable(False, False) 37 | self.window.geometry("720x480") 38 | self.tab_controller.pack(expand=1, fill="both") 39 | 40 | def getTabController(self) -> Notebook: 41 | return self.tab_controller 42 | 43 | # def async_update(self, func, *args, **kwargs): 44 | # asyncio.ensure_future(vasyncio.asyncwrapper(func)(*args, **kwargs), loop=self._loop) 45 | 46 | def threading_update(self, func, *args, **kwargs): 47 | self._loop.run_in_executor(None, 48 | lambda :func(*args, **kwargs)) 49 | 50 | async def _async_update(self): 51 | while self._running: 52 | try: 53 | self.window.update() 54 | await asyncio.sleep(self.interval) 55 | except: 56 | break 57 | 58 | def start(self): 59 | self.createWidgets() 60 | self.window.quit() 61 | return self._async_update() 62 | 63 | def createWidgets(self): 64 | mpv = MPVGUI(self) 65 | room = RoomGUI(self) 66 | playlist = PlaylistGUI(self) 67 | search = SearchGUI(self) 68 | configgui = ConfigGUI(self) 69 | infogui = InfoGUI(self) 70 | blacklistgui = BlacklistGUI(self) 71 | hitorylistgui = HistoryPlaylistGUI(self) 72 | wylogingui = WYLoginGUI(self) 73 | 74 | mpv.createWidgets() 75 | room.createWidgets() 76 | playlist.createWidgets() 77 | configgui.createWidgets() 78 | search.createWidgets() 79 | infogui.createWidgets() 80 | blacklistgui.createWidgets() 81 | hitorylistgui.createWidgets() 82 | wylogingui.createWidgets() 83 | 84 | room.initialize() 85 | playlist.initialize() 86 | search.initialize() 87 | hitorylistgui.initialize() 88 | configgui.initialize() 89 | blacklistgui.initialize() 90 | infogui.initialize() 91 | mpv.initialize() 92 | wylogingui.initialize() 93 | 94 | Global_Audio_Bot.start() 95 | -------------------------------------------------------------------------------- /plugins/danmaku/__init__.py: -------------------------------------------------------------------------------- 1 | import re, asyncio, aiohttp 2 | 3 | from .youtube import Youtube 4 | from .twitch import Twitch 5 | from .bilibili import Bilibili 6 | from .douyu import Douyu 7 | from .huya import Huya 8 | 9 | __all__ = ["DanmakuClient"] 10 | 11 | 12 | class DanmakuClient: 13 | def __init__(self, url, q, **kargs): 14 | self.__url = "" 15 | self.__site = None 16 | self.__usite = None 17 | self.__hs = None 18 | self.__ws = None 19 | self.__stop = False 20 | self.__dm_queue = q 21 | self.__link_status = True 22 | self.__extra_data = kargs 23 | if "http://" == url[:7] or "https://" == url[:8]: 24 | self.__url = url 25 | else: 26 | self.__url = "http://" + url 27 | for u, s in { 28 | "douyu.com": Douyu, 29 | "live.bilibili.com": Bilibili, 30 | "twitch.tv": Twitch, 31 | "huya.com": Huya, 32 | }.items(): 33 | if re.match(r"^(?:http[s]?://)?.*?%s/(.+?)$" % u, url): 34 | self.__site = s 35 | break 36 | if self.__site == None: 37 | for u, s in {"youtube.com/channel": Youtube, "youtube.com/watch": Youtube}.items(): 38 | if re.match(r"^(?:http[s]?://)?.*?%s(.+?)$" % u, url): 39 | self.__usite = s 40 | if self.__usite == None: 41 | raise Exception("Invalid link!") 42 | self.__hs = aiohttp.ClientSession() 43 | 44 | async def init_ws(self): 45 | ws_url, reg_datas = await self.__site.get_ws_info(self.__url) 46 | self.__ws = await self.__hs.ws_connect(ws_url) 47 | for reg_data in reg_datas: 48 | if type(reg_data) == str: 49 | await self.__ws.send_str(reg_data) 50 | else: 51 | await self.__ws.send_bytes(reg_data) 52 | 53 | async def heartbeats(self): 54 | while self.__stop != True: 55 | # print('heartbeat') 56 | await asyncio.sleep(20) 57 | try: 58 | if type(self.__site.heartbeat) == str: 59 | await self.__ws.send_str(self.__site.heartbeat) 60 | else: 61 | await self.__ws.send_bytes(self.__site.heartbeat) 62 | except: 63 | pass 64 | 65 | async def fetch_danmaku(self): 66 | while self.__stop != True: 67 | async for msg in self.__ws: 68 | # self.__link_status = True 69 | ms = self.__site.decode_msg(msg.data) 70 | for m in ms: 71 | await self.__dm_queue.put(m) 72 | if self.__stop != True: 73 | await asyncio.sleep(1) 74 | await self.init_ws() 75 | await asyncio.sleep(1) 76 | 77 | async def start(self): 78 | if self.__site != None: 79 | await self.init_ws() 80 | await asyncio.gather( 81 | self.heartbeats(), 82 | self.fetch_danmaku(), 83 | ) 84 | else: 85 | await self.__usite.run(self.__url, self.__dm_queue, self.__hs, **self.__extra_data) 86 | 87 | async def stop(self): 88 | self.__stop = True 89 | if self.__site != None: 90 | await self.__hs.close() 91 | else: 92 | await self.__usite.stop() 93 | await self.__hs.close() 94 | -------------------------------------------------------------------------------- /frontend/build/dev-server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | const config = require('../config') 5 | if (!process.env.NODE_ENV) { 6 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 7 | } 8 | 9 | const opn = require('opn') 10 | const path = require('path') 11 | const express = require('express') 12 | const webpack = require('webpack') 13 | const proxyMiddleware = require('http-proxy-middleware') 14 | const webpackConfig = require('./webpack.dev.conf') 15 | 16 | // default port where dev server listens for incoming traffic 17 | const port = process.env.PORT || config.dev.port 18 | // automatically open browser, if not set will be false 19 | const autoOpenBrowser = !!config.dev.autoOpenBrowser 20 | // Define HTTP proxies to your custom API backend 21 | // https://github.com/chimurai/http-proxy-middleware 22 | const proxyTable = config.dev.proxyTable 23 | 24 | const app = express() 25 | const compiler = webpack(webpackConfig) 26 | 27 | const devMiddleware = require('webpack-dev-middleware')(compiler, { 28 | publicPath: webpackConfig.output.publicPath, 29 | quiet: true 30 | }) 31 | 32 | const hotMiddleware = require('webpack-hot-middleware')(compiler, { 33 | log: false, 34 | heartbeat: 2000 35 | }) 36 | // force page reload when html-webpack-plugin template changes 37 | // currently disabled until this is resolved: 38 | // https://github.com/jantimon/html-webpack-plugin/issues/680 39 | // compiler.plugin('compilation', function (compilation) { 40 | // compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 41 | // hotMiddleware.publish({ action: 'reload' }) 42 | // cb() 43 | // }) 44 | // }) 45 | 46 | // enable hot-reload and state-preserving 47 | // compilation error display 48 | app.use(hotMiddleware) 49 | 50 | // proxy api requests 51 | Object.keys(proxyTable).forEach(function (context) { 52 | let options = proxyTable[context] 53 | if (typeof options === 'string') { 54 | options = { target: options } 55 | } 56 | app.use(proxyMiddleware(options.filter || context, options)) 57 | }) 58 | 59 | // handle fallback for HTML5 history API 60 | app.use(require('connect-history-api-fallback')()) 61 | 62 | // serve webpack bundle output 63 | app.use(devMiddleware) 64 | 65 | // serve pure static assets 66 | const staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 67 | app.use(staticPath, express.static('./static')) 68 | 69 | const uri = 'http://localhost:' + port 70 | 71 | var _resolve 72 | var _reject 73 | var readyPromise = new Promise((resolve, reject) => { 74 | _resolve = resolve 75 | _reject = reject 76 | }) 77 | 78 | var server 79 | var portfinder = require('portfinder') 80 | portfinder.basePort = port 81 | 82 | console.log('> Starting dev server...') 83 | devMiddleware.waitUntilValid(() => { 84 | portfinder.getPort((err, port) => { 85 | if (err) { 86 | _reject(err) 87 | } 88 | process.env.PORT = port 89 | var uri = 'http://localhost:' + port 90 | console.log('> Listening at ' + uri + '\n') 91 | // when env is testing, don't need open it 92 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { 93 | opn(uri) 94 | } 95 | server = app.listen(port) 96 | _resolve() 97 | }) 98 | }) 99 | 100 | module.exports = { 101 | ready: readyPromise, 102 | close: () => { 103 | server.close() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /gui/factory/ConfigGUIFactory.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | import traceback 3 | from tkinter import ttk 4 | 5 | 6 | def ConvertWithDefault(func, default): 7 | def inner(n): 8 | try: 9 | return func(n) 10 | except: 11 | traceback.print_exc() 12 | return default 13 | return inner 14 | 15 | 16 | def getConfigWriter(config, key, vari, func, funv): 17 | def inner(*args): 18 | config[key] = func(vari.get()) 19 | vari.set(funv(config[key])) 20 | return inner 21 | 22 | 23 | def getInput(master, config, key, func_c, func_v, 24 | **kwargs): 25 | variable = tk.StringVar() 26 | variable.trace_variable("w", getConfigWriter(config, 27 | key, 28 | variable, 29 | func_c, 30 | func_v)) 31 | input_entry = tk.Entry(master, 32 | textvariable=variable, 33 | **kwargs) 34 | variable.set(func_v(config[key])) 35 | return input_entry 36 | 37 | 38 | def getInputWithButton(master, config, key, func_c, func_v, 39 | input_kwargs=None, button_kwargs=None): 40 | input_kwargs = input_kwargs or {} 41 | button_kwargs = button_kwargs or {} 42 | variable = tk.StringVar() 43 | input_entry = tk.Entry(master, 44 | textvariable=variable, 45 | **input_kwargs) 46 | update_button = ttk.Button(master, 47 | command=getConfigWriter(config, 48 | key, 49 | variable, 50 | func_c, 51 | func_v), 52 | **button_kwargs) 53 | variable.set(func_v(config[key])) 54 | return input_entry, update_button 55 | 56 | 57 | def getCheckButton(master, text, config, key, func_c, func_v, 58 | **kwargs): 59 | variable = tk.IntVar() 60 | check_button = tk.Checkbutton(master, 61 | text=text, 62 | variable=variable, 63 | command=getConfigWriter(config, 64 | key, 65 | variable, 66 | func_c, 67 | func_v), 68 | **kwargs) 69 | variable.set(func_v(config[key])) 70 | return check_button 71 | 72 | 73 | def getBoxSelector(master, config, key, values, func_c, func_v, 74 | **kwargs): 75 | variable = tk.StringVar() 76 | variable.trace_variable("w", getConfigWriter(config, 77 | key, 78 | variable, 79 | func_c, 80 | func_v)) 81 | box_selector = ttk.Combobox(master, 82 | textvariable=variable, 83 | **kwargs) 84 | box_selector['values'] = tuple(values) 85 | variable.set(func_v(config[key])) 86 | return box_selector 87 | -------------------------------------------------------------------------------- /plugins/danmaku/tars/__logger.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # filename: __logger.py 5 | 6 | # Tencent is pleased to support the open source community by making Tars available. 7 | # 8 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 9 | # 10 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this file except 11 | # in compliance with the License. You may obtain a copy of the License at 12 | # 13 | # https://opensource.org/licenses/BSD-3-Clause 14 | # 15 | # Unless required by applicable law or agreed to in writing, software distributed 16 | # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 17 | # CONDITIONS OF ANY KIND, either express or implied. See the License for the 18 | # specific language governing permissions and limitations under the License. 19 | # 20 | 21 | """ 22 | @version: 0.01 23 | @brief: 日志模块 24 | """ 25 | 26 | # 仅用于调试 27 | 28 | import logging 29 | from logging.handlers import RotatingFileHandler 30 | import os 31 | import re 32 | 33 | tarsLogger = logging.getLogger("TARS client") 34 | strToLoggingLevel = { 35 | "critical": logging.CRITICAL, 36 | "error": logging.ERROR, 37 | "warn": logging.WARNING, 38 | "info": logging.INFO, 39 | "debug": logging.DEBUG, 40 | "none": logging.NOTSET, 41 | } 42 | # console = logging.StreamHandler() 43 | # console.setLevel(logging.DEBUG) 44 | # filelog = logging.FileHandler('tars.log') 45 | # filelog.setLevel(logging.DEBUG) 46 | # formatter = logging.Formatter('%(asctime)s | %(levelname)8s | [%(name)s] %(message)s', '%Y-%m-%d %H:%M:%S') 47 | # console.setFormatter(formatter) 48 | # filelog.setFormatter(formatter) 49 | # tarsLogger.addHandler(console) 50 | # tarsLogger.addHandler(filelog) 51 | # tarsLogger.setLevel(logging.DEBUG) 52 | # tarsLogger.setLevel(logging.INFO) 53 | # tarsLogger.setLevel(logging.ERROR) 54 | 55 | 56 | def createLogFile(filename): 57 | if filename.endswith("/"): 58 | raise ValueError("The logfile is a dir not a file") 59 | if os.path.exists(filename) and os.path.isfile(filename): 60 | pass 61 | else: 62 | fileComposition = str.split(filename, "/") 63 | print(fileComposition) 64 | currentFile = "" 65 | for item in fileComposition: 66 | if item == fileComposition[-1]: 67 | currentFile += item 68 | if not os.path.exists(currentFile) or not os.path.isfile(currentFile): 69 | while True: 70 | try: 71 | os.mknod(currentFile) 72 | break 73 | except OSError as msg: 74 | errno = re.findall(r"\d+", str(msg)) 75 | if len(errno) > 0 and errno[0] == "17": 76 | currentFile += ".log" 77 | continue 78 | break 79 | currentFile += item + "/" 80 | if not os.path.exists(currentFile): 81 | os.mkdir(currentFile) 82 | 83 | 84 | def initLog(logpath, logsize, lognum, loglevel): 85 | createLogFile(logpath) 86 | handler = RotatingFileHandler(filename=logpath, maxBytes=logsize, backupCount=lognum) 87 | formatter = logging.Formatter( 88 | "%(asctime)s | %(levelname)6s | [%(filename)18s:%(lineno)4d] | [%(thread)d] %(message)s", 89 | "%Y-%m-%d %H:%M:%S", 90 | ) 91 | handler.setFormatter(formatter) 92 | tarsLogger.addHandler(handler) 93 | if loglevel in strToLoggingLevel: 94 | tarsLogger.setLevel(strToLoggingLevel[loglevel]) 95 | else: 96 | tarsLogger.setLevel(strToLoggingLevel["error"]) 97 | 98 | 99 | if __name__ == "__main__": 100 | tarsLogger.debug("debug log") 101 | tarsLogger.info("info log") 102 | tarsLogger.warning("warning log") 103 | tarsLogger.error("error log") 104 | tarsLogger.critical("critical log") 105 | -------------------------------------------------------------------------------- /plugins/danmaku/tars/__tup.py: -------------------------------------------------------------------------------- 1 | # Tencent is pleased to support the open source community by making Tars available. 2 | # 3 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 4 | # 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this file except 6 | # in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 12 | # CONDITIONS OF ANY KIND, either express or implied. See the License for the 13 | # specific language governing permissions and limitations under the License. 14 | # 15 | 16 | import struct 17 | import string 18 | from .__util import util 19 | from .__tars import TarsOutputStream 20 | from .__tars import TarsInputStream 21 | from .__packet import RequestPacket 22 | 23 | 24 | class TarsUniPacket(object): 25 | def __init__(self): 26 | self.__mapa = util.mapclass(util.string, util.bytes) 27 | self.__mapv = util.mapclass(util.string, self.__mapa) 28 | self.__buffer = self.__mapv() 29 | self.__code = RequestPacket() 30 | 31 | # @property 32 | # def version(self): 33 | # return self.__code.iVersion 34 | 35 | # @version.setter 36 | # def version(self, value): 37 | # self.__code.iVersion = value 38 | 39 | @property 40 | def servant(self): 41 | return self.__code.sServantName 42 | 43 | @servant.setter 44 | def servant(self, value): 45 | self.__code.sServantName = value 46 | 47 | @property 48 | def func(self): 49 | return self.__code.sFuncName 50 | 51 | @func.setter 52 | def func(self, value): 53 | self.__code.sFuncName = value 54 | 55 | @property 56 | def requestid(self): 57 | return self.__code.iRequestId 58 | 59 | @requestid.setter 60 | def requestid(self, value): 61 | self.__code.iRequestId = value 62 | 63 | @property 64 | def result_code(self): 65 | if ("STATUS_RESULT_CODE" in self.__code.status) == False: 66 | return 0 67 | 68 | return string.atoi(self.__code.status["STATUS_RESULT_CODE"]) 69 | 70 | @property 71 | def result_desc(self): 72 | if ("STATUS_RESULT_DESC" in self.__code.status) == False: 73 | return "" 74 | 75 | return self.__code.status["STATUS_RESULT_DESC"] 76 | 77 | def put(self, vtype, name, value): 78 | oos = TarsOutputStream() 79 | oos.write(vtype, 0, value) 80 | self.__buffer[name] = {vtype.__tars_class__: oos.getBuffer()} 81 | 82 | def get(self, vtype, name): 83 | if (name in self.__buffer) == False: 84 | raise Exception("UniAttribute not found key:%s,type:%s" % (name, vtype.__tars_class__)) 85 | 86 | t = self.__buffer[name] 87 | if (vtype.__tars_class__ in t) == False: 88 | raise Exception("UniAttribute not found type:" + vtype.__tars_class__) 89 | 90 | o = TarsInputStream(t[vtype.__tars_class__]) 91 | return o.read(vtype, 0, True) 92 | 93 | def encode(self): 94 | oos = TarsOutputStream() 95 | oos.write(self.__mapv, 0, self.__buffer) 96 | 97 | self.__code.iVersion = 2 98 | self.__code.sBuffer = oos.getBuffer() 99 | 100 | sos = TarsOutputStream() 101 | RequestPacket.writeTo(sos, self.__code) 102 | 103 | return struct.pack("!i", 4 + len(sos.getBuffer())) + sos.getBuffer() 104 | 105 | def decode(self, buf): 106 | ois = TarsInputStream(buf[4:]) 107 | self.__code = RequestPacket.readFrom(ois) 108 | 109 | sis = TarsInputStream(self.__code.sBuffer) 110 | self.__buffer = sis.read(self.__mapv, 0, True) 111 | 112 | def clear(self): 113 | self.__code.__init__() 114 | 115 | def haskey(self, name): 116 | return name in self.__buffer 117 | -------------------------------------------------------------------------------- /plugins/danmaku/huya.py: -------------------------------------------------------------------------------- 1 | import json, re, select, random 2 | from struct import pack, unpack 3 | 4 | import asyncio, aiohttp 5 | 6 | from .tars import tarscore 7 | 8 | 9 | class Huya: 10 | heartbeat = b"\x00\x03\x1d\x00\x00\x69\x00\x00\x00\x69\x10\x03\x2c\x3c\x4c\x56\x08\x6f\x6e\x6c\x69\x6e\x65\x75\x69\x66\x0f\x4f\x6e\x55\x73\x65\x72\x48\x65\x61\x72\x74\x42\x65\x61\x74\x7d\x00\x00\x3c\x08\x00\x01\x06\x04\x74\x52\x65\x71\x1d\x00\x00\x2f\x0a\x0a\x0c\x16\x00\x26\x00\x36\x07\x61\x64\x72\x5f\x77\x61\x70\x46\x00\x0b\x12\x03\xae\xf0\x0f\x22\x03\xae\xf0\x0f\x3c\x42\x6d\x52\x02\x60\x5c\x60\x01\x7c\x82\x00\x0b\xb0\x1f\x9c\xac\x0b\x8c\x98\x0c\xa8\x0c" 11 | 12 | async def get_ws_info(url): 13 | reg_datas = [] 14 | url = "https://m.huya.com/" + url.split("/")[-1] 15 | headers = { 16 | "user-agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Mobile Safari/537.36" 17 | } 18 | async with aiohttp.ClientSession() as session: 19 | async with session.get(url, headers=headers) as resp: 20 | room_page = await resp.text() 21 | # print(room_page) 22 | m = re.search(r"window.HNF_GLOBAL_INIT *= *(\{.+?\})\s*", room_page, re.MULTILINE) 23 | j = json.loads(m.group(1)) 24 | ayyuid = j["roomInfo"]["tProfileInfo"]["lUid"] 25 | # tid = j["roomInfo"]["tLiveInfo"]["tLiveStreamInfo"]["vStreamInfo"]["value"][0]["lChannelId"] 26 | # sid = j["roomInfo"]["tLiveInfo"]["tLiveStreamInfo"]["vStreamInfo"]["value"][0]["lSubChannelId"] 27 | 28 | # print(ayyuid) 29 | # print(tid) 30 | # print(sid) 31 | 32 | # a = tarscore.string 33 | 34 | l = tarscore.vctclass(tarscore.string)() 35 | l.append(f"live:{ayyuid}") 36 | l.append(f"chat:{ayyuid}") 37 | oos = tarscore.TarsOutputStream() 38 | oos.write(tarscore.vctclass(tarscore.string), 0, l) 39 | oos.write(tarscore.string, 1, "") 40 | 41 | # oos.write(tarscore.int64, 0, int(ayyuid)) 42 | # oos.write(tarscore.boolean, 1, True) # Anonymous 43 | # oos.write(tarscore.string, 2, "") # sGuid 44 | # oos.write(tarscore.string, 3, "") 45 | # oos.write(tarscore.int64, 4, int(tid)) 46 | # oos.write(tarscore.int64, 5, int(sid)) 47 | # oos.write(tarscore.int64, 6, 0) 48 | # oos.write(tarscore.int64, 7, 0) 49 | 50 | 51 | wscmd = tarscore.TarsOutputStream() 52 | wscmd.write(tarscore.int32, 0, 16) 53 | # wscmd.write(tarscore.int32, 0, 1) 54 | wscmd.write(tarscore.bytes, 1, oos.getBuffer()) 55 | 56 | reg_datas.append(wscmd.getBuffer()) 57 | return "wss://cdnws.api.huya.com/", reg_datas 58 | 59 | def decode_msg(data): 60 | class user(tarscore.struct): 61 | def readFrom(ios): 62 | return ios.read(tarscore.string, 2, False).decode("utf8") 63 | 64 | class dcolor(tarscore.struct): 65 | def readFrom(ios): 66 | return ios.read(tarscore.int32, 0, False) 67 | 68 | name = "" 69 | content = "" 70 | msgs = [] 71 | ios = tarscore.TarsInputStream(data) 72 | 73 | if ios.read(tarscore.int32, 0, False) == 7: 74 | ios = tarscore.TarsInputStream(ios.read(tarscore.bytes, 1, False)) 75 | if ios.read(tarscore.int64, 1, False) == 1400: 76 | ios = tarscore.TarsInputStream(ios.read(tarscore.bytes, 2, False)) 77 | name = ios.read(user, 0, False) # username 78 | content = ios.read(tarscore.string, 3, False).decode("utf8") # content 79 | color = ios.read(dcolor, 6, False) # danmaku color 80 | if color == -1: 81 | color = 16777215 82 | 83 | if name != "": 84 | msg = {"name": name, "color": f"{color:06x}", "content": content, "msg_type": "danmaku"} 85 | else: 86 | msg = {"name": "", "content": "", "msg_type": "other"} 87 | msgs.append(msg) 88 | return msgs 89 | -------------------------------------------------------------------------------- /sources/audio/kuwo.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from apis import RegExpResponseContainer, JsonResponseContainer 4 | from sources.audio import AudioSource 5 | from sources.base import SearchResults, CommonSourceWrapper, MediaSource, SearchResult, PictureSource 6 | from sources.base.interface import SearchableSource, AudioBotInfoSource 7 | from apis import kuwo as kuwoApi 8 | from utils import vfile 9 | 10 | 11 | class KuwoMusicSource(AudioSource, 12 | AudioBotInfoSource, 13 | SearchableSource): 14 | 15 | __source_name__ = "kuwo" 16 | pattern = "kuwo[0-9]+" 17 | 18 | def __init__(self,id): 19 | self.id = id 20 | self.artist = "" 21 | self.title = "" 22 | 23 | @classmethod 24 | def search(cls, keyword, page=1, pagesize=20, *args, **kwargs) -> SearchResults: 25 | try: 26 | container = JsonResponseContainer(kuwoApi.getSearchResult(keyword, 27 | page=page, 28 | pagesize=pagesize), 29 | total = ("data.total",int), 30 | songlist="data.list") 31 | rs = [] 32 | for song in container.data["songlist"]: 33 | id = song["musicrid"].replace("MUSIC_", "") 34 | source = cls(id) 35 | source.title = song["name"] 36 | source.artist = song["artist"] 37 | rs.append(SearchResult(kuwoApi.API.info_url(id), 38 | {}, 39 | "{} - {}".format(song["name"], 40 | song["artist"] 41 | ), 42 | source, 43 | cls.getSourceName(), 44 | "audio")) 45 | return SearchResults(rs,1,container.data["total"] // pagesize) 46 | except: 47 | return SearchResults([], 0, 0) 48 | 49 | @property 50 | def audio(self): 51 | return self.getAudio() 52 | 53 | @CommonSourceWrapper.handleException 54 | def getAudio(self): 55 | url = kuwoApi.getMusicFile(self.id).decode() 56 | return MediaSource(url, 57 | kuwoApi.API.file_headers, 58 | "{} - {}.{}".format(self.title, 59 | self.artist, 60 | vfile.getSuffixByUrl(url))) 61 | 62 | def getUniqueId(self): 63 | return "kuwo{}".format(self.id) 64 | 65 | def getTitle(self): 66 | return self.title 67 | 68 | def getArtist(self): 69 | return self.artist 70 | 71 | def getCover(self) -> PictureSource: 72 | return None 73 | 74 | @classmethod 75 | def initFromUrl(cls, url:str): 76 | if url.isdigit(): 77 | return cls(url) 78 | r = re.search(cls.pattern,url) 79 | return cls(r.group()[4::]) if r != None else None 80 | 81 | @property 82 | def info(self): 83 | return {"Type": self.getBaseSources(), 84 | "ID": self.id, 85 | "Title": self.title, 86 | "Artist": self.artist} 87 | 88 | def getBaseSources(self, *args, **kwargs): 89 | return {"audio": self.getAudio()} 90 | 91 | @CommonSourceWrapper.handleException 92 | def load(self, *args, **kwargs): 93 | if self.isValid(): 94 | return 95 | title = RegExpResponseContainer(kuwoApi.getMusicInfo(self.id), 96 | title=(r"(.*)", 97 | lambda x: x[7:-8:].split("_"))) 98 | self.title = title.data["title"][0] 99 | self.artist = title.data["title"][1] 100 | 101 | @classmethod 102 | def applicable(cls, url): 103 | return re.search(cls.pattern,url) != None 104 | 105 | def isValid(self): 106 | return self.artist != "" and self.title != "" 107 | -------------------------------------------------------------------------------- /plugins/danmaku/bilibili.py: -------------------------------------------------------------------------------- 1 | import json, re, select, random, traceback 2 | from struct import pack, unpack 3 | 4 | import asyncio, aiohttp, zlib 5 | 6 | 7 | class Bilibili: 8 | heartbeat = b"\x00\x00\x00\x1f\x00\x10\x00\x01\x00\x00\x00\x02\x00\x00\x00\x01\x5b\x6f\x62\x6a\x65\x63\x74\x20\x4f\x62\x6a\x65\x63\x74\x5d" 9 | 10 | async def get_ws_info(url): 11 | url = "https://api.live.bilibili.com/room/v1/Room/room_init?id=" + url.split("/")[-1] 12 | reg_datas = [] 13 | async with aiohttp.ClientSession() as session: 14 | async with session.get(url) as resp: 15 | room_json = json.loads(await resp.text()) 16 | room_id = room_json["data"]["room_id"] 17 | data = json.dumps( 18 | {"roomid": room_id, "uid": int(1e14 + 2e14 * random.random()), "protover": 2}, 19 | separators=(",", ":"), 20 | ).encode("ascii") 21 | data = ( 22 | pack(">i", len(data) + 16) 23 | + b"\x00\x10\x00\x01" 24 | + pack(">i", 7) 25 | + pack(">i", 1) 26 | + data 27 | ) 28 | reg_datas.append(data) 29 | 30 | return "wss://broadcastlv.chat.bilibili.com/sub", reg_datas 31 | 32 | def decode_msg(data): 33 | dm_list_compressed = [] 34 | dm_list = [] 35 | ops = [] 36 | msgs = [] 37 | # print(data) 38 | while True: 39 | try: 40 | packetLen, headerLen, ver, op, seq = unpack("!IHHII", data[0:16]) 41 | except Exception as e: 42 | break 43 | if len(data) < packetLen: 44 | break 45 | if ver == 1 or ver == 0: 46 | ops.append(op) 47 | dm_list.append(data[16:packetLen]) 48 | elif ver == 2: 49 | dm_list_compressed.append(data[16:packetLen]) 50 | if len(data) == packetLen: 51 | data = b"" 52 | break 53 | else: 54 | data = data[packetLen:] 55 | 56 | for dm in dm_list_compressed: 57 | d = zlib.decompress(dm) 58 | while True: 59 | try: 60 | packetLen, headerLen, ver, op, seq = unpack("!IHHII", d[0:16]) 61 | except Exception as e: 62 | break 63 | if len(d) < packetLen: 64 | break 65 | ops.append(op) 66 | dm_list.append(d[16:packetLen]) 67 | if len(d) == packetLen: 68 | d = b"" 69 | break 70 | else: 71 | d = d[packetLen:] 72 | 73 | for i, d in enumerate(dm_list): 74 | try: 75 | msg = {} 76 | if ops[i] == 5: 77 | j = json.loads(d) 78 | msg["msg_type"] = { 79 | "SEND_GIFT": "gift", 80 | "DANMU_MSG": "danmaku", 81 | "WELCOME": "enter", 82 | "NOTICE_MSG": "broadcast", 83 | }.get(j.get("cmd"), "other") 84 | if msg["msg_type"] == "danmaku": 85 | msg["name"] = j.get("info", ["", "", ["", ""]])[2][1] or j.get( 86 | "data", {} 87 | ).get("uname", "") 88 | msg["content"] = j.get("info", ["", ""])[1] 89 | msg["color"] = f"{j.get('info', [[0, 0, 0, 16777215]])[0][3]:06x}" 90 | elif msg["msg_type"] == "broadcast": 91 | msg["type"] = j.get("msg_type", 0) 92 | msg["roomid"] = j.get("real_roomid", 0) 93 | msg["content"] = j.get("msg_common", "none") 94 | msg["raw"] = j 95 | else: 96 | msg["content"] = j 97 | else: 98 | msg = {"name": "", "content": d, "msg_type": "other"} 99 | msgs.append(msg) 100 | except Exception as e: 101 | # traceback.print_exc() 102 | # print(e) 103 | pass 104 | 105 | return msgs 106 | -------------------------------------------------------------------------------- /utils/bilibili.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import time 4 | 5 | import qrcode 6 | 7 | from config import Config 8 | from utils.vhttp import httpGet, httpPost 9 | 10 | 11 | class videoIdConvertor(): 12 | videoUrl = "https://www.bilibili.com/video/%s" 13 | patternAv = r"av[0-9]+" 14 | patternBv = r"BV[0-9,A-Z,a-z]+" 15 | table = 'fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF' 16 | tr = dict(("fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF"[i], i) for i in range(58)) 17 | s = [11, 10, 3, 8, 4, 6] 18 | xor = 177451812 19 | add = 8728348608 20 | 21 | @classmethod 22 | def bv2av(cls, x): 23 | r = 0 24 | for i in range(6): 25 | r += cls.tr[x[cls.s[i]]] * 58 ** i 26 | return (r - cls.add) ^ cls.xor 27 | 28 | @classmethod 29 | def av2bv(cls, x): 30 | x = (int(x) ^ cls.xor) + cls.add 31 | r = list('BV1 4 1 7 ') 32 | for i in range(6): 33 | r[cls.s[i]] = cls.table[x // 58 ** i % 58] 34 | return ''.join(r) 35 | 36 | @classmethod 37 | def urlConvert(cls, url): 38 | if re.search(cls.patternBv, url): 39 | return cls.videoUrl % ("av%s" % cls.bv2av(re.search(cls.patternBv, url).group())) 40 | if re.search(cls.patternAv, url): 41 | return cls.videoUrl % cls.av2bv(int(re.search(cls.patternAv, url).group()[2::])) 42 | return "" 43 | 44 | class QrLogin(): 45 | infoApi = "https://account.bilibili.com/home/userInfo" 46 | qrApi = "https://passport.bilibili.com/qrcode/getLoginUrl" 47 | checkQrApi = "https://passport.bilibili.com/qrcode/getLoginInfo" 48 | 49 | def __init__(self,invert=False,console=False): 50 | self.invert = invert 51 | self.console = console 52 | self.oauthKey = "" 53 | self.urldata = "" 54 | 55 | @staticmethod 56 | def manuallylogin(): 57 | ql = QrLogin.newlogin() 58 | print("getting qrcode") 59 | ql.getQrcode() 60 | print("Please scan qrcode using your application") 61 | if ql.getResult(): 62 | print("login success, you Sessdata is %s" % ql.getSessdata()) 63 | Config.getCookie("bilibili")["SESSDATA"] = ql.getSessdata() 64 | if input("write to the config? y/n ") == "y": 65 | Config.saveCookie() 66 | else: 67 | print("fail, please try again") 68 | 69 | @classmethod 70 | def newlogin(cls): 71 | a= input("invert color? y/n ") == "y" 72 | b = input("Console output? y/n ") == "y" 73 | return cls(a,b) 74 | 75 | @classmethod 76 | def isLogin(cls): 77 | return Config.getCookie("bilibili").get("SESSDATA") != None 78 | # resp = httpGet(cls.infoApi,cookies=Config.commonCookies) 79 | # try: 80 | # print(resp.json()) 81 | # return resp.json()["code"] == 0 82 | # except: 83 | # return False 84 | 85 | def getQrcode(self): 86 | data = httpGet(self.qrApi).json() 87 | 88 | qrurl = data["data"]["url"] 89 | self.oauthKey = data["data"]["oauthKey"] 90 | qc = qrcode.QRCode() 91 | qc.add_data(qrurl) 92 | if self.console: 93 | qc.print_ascii(invert=self.invert) 94 | else: 95 | qc.make_image().save("./qrcode.png") 96 | os.system("qrcode.png") 97 | 98 | def getResult(self,interval=1): 99 | data = httpPost(self.checkQrApi,data={'oauthKey':self.oauthKey,'gourl': 'https://passport.bilibili.com/account/security'}) 100 | if data == None: 101 | return False 102 | while not data.json()["status"]: 103 | data = httpPost(self.checkQrApi, 104 | data={'oauthKey': self.oauthKey, 'gourl': 'https://passport.bilibili.com/account/security'}) 105 | if data.json()['data'] == -2: 106 | print('二维码已过期') 107 | return False 108 | time.sleep(interval) 109 | self.urldata = data.json()["data"]["url"] 110 | return True 111 | def getSessdata(self): 112 | pattern = r"SESSDATA=(.*?)&" 113 | return "" if re.search(pattern,self.urldata) == None else re.search(pattern,self.urldata).group()[9:-1:] 114 | 115 | def isValid(self): 116 | return self.oauthKey != "" 117 | 118 | 119 | def danmuass(path): 120 | pass -------------------------------------------------------------------------------- /gui/WYLoginGUI.py: -------------------------------------------------------------------------------- 1 | import time 2 | from tkinter import ttk 3 | import tkinter as tk 4 | 5 | import qrcode 6 | from PIL import Image, ImageTk 7 | 8 | from audiobot import Global_Audio_Bot 9 | from config import Config 10 | from gui.factory.PlayerProgressBar import PlayerProgressBar 11 | from utils import vwrappers 12 | from utils.vtranslation import getTranslatedText as _ 13 | from pyncm import GetCurrentSession, SetCurrentSession, LoadSessionFromString, DumpSessionAsString 14 | from pyncm.apis import login #GetCurrentLoginStatus, WriteLoginInfo,LoginQrcodeUnikey,LoginQrcodeCheck 15 | import gui 16 | 17 | 18 | class WYLoginGUI(): 19 | def __init__(self, main_window): 20 | self.main_window: gui.MainWindow = main_window 21 | self.widget = ttk.Frame(self.main_window.getTabController()) 22 | 23 | self.login_qr = None 24 | self.empty_image = ImageTk.PhotoImage(Image.new("RGB", (250, 250), (255, 255, 255))) 25 | self.current_status = tk.StringVar() 26 | self.uuid = "" 27 | 28 | def initialize(self): 29 | self.main_window.getTabController().add(self.widget, text=_("Netease Login")) 30 | 31 | 32 | def createWidgets(self): 33 | frame_main = ttk.LabelFrame(self.widget, 34 | text=_("Netease Login")) 35 | frame_main.grid_columnconfigure(0, weight=1) 36 | frame_main.grid_columnconfigure(2, weight=1) 37 | frame_main.pack(fill="both", expand="yes", padx=8, pady=4) 38 | # ==== Row 1 ==== 39 | 40 | frame_row_1 = ttk.Frame(frame_main) 41 | frame_row_1.grid(column=1, row=0, padx=8, pady=4) 42 | 43 | self.login_qr = ttk.Label(frame_row_1, width=250) 44 | self.login_qr.grid(column=0, row=0, sticky="news") 45 | self.__setQrImg(self.empty_image) 46 | 47 | # ==== Row 2 ==== 48 | 49 | frame_row_2 = ttk.Frame(frame_main) 50 | frame_row_2.grid(column=1, row=1, padx=8, pady=4) 51 | 52 | getqr_button = ttk.Button(frame_row_2, width=16, text=_("Get QR"), command=self.__tryLogin) 53 | getqr_button.grid(column=0, row=0) 54 | finishqr_button = ttk.Button(frame_row_2, width=16, text=_("Finish QR Scan"), command=self.__checkLogin) 55 | finishqr_button.grid(column=1, row=0) 56 | logout_button = ttk.Button(frame_row_2, width=16, text=_("Logout"), command=self.__tryLogout) 57 | logout_button.grid(column=2, row=0) 58 | 59 | # ==== Row 3 ==== 60 | 61 | frame_row_3 = ttk.Frame(frame_main) 62 | frame_row_3.grid(column=1, row=2, padx=8, pady=4) 63 | 64 | playing_title_label = ttk.Label(frame_row_3, textvariable=self.current_status) 65 | playing_title_label.grid(column=0, row=0, pady=2, padx=2) 66 | self.current_status.set("Waiting") 67 | 68 | 69 | self.__setLoginInfo() 70 | 71 | @vwrappers.TryExceptRetNone 72 | def __tryLogin(self): 73 | self.__setQrImg(self.empty_image) 74 | self.uuid = login.LoginQrcodeUnikey()['unikey'] 75 | url = f'https://music.163.com/login?codekey={self.uuid}' 76 | img = qrcode.make(url) 77 | tkimg = ImageTk.PhotoImage(img.resize((250, 250))) 78 | self.__setQrImg(tkimg) 79 | 80 | def __checkLogin(self): 81 | rsp = login.LoginQrcodeCheck(self.uuid) 82 | if rsp['code'] == 803 or rsp['code'] == 800: 83 | login.WriteLoginInfo(login.GetCurrentLoginStatus()) 84 | Config.getCookie("netease", "pyncm")["session"] = DumpSessionAsString(GetCurrentSession()) 85 | self.__setQrImg(self.empty_image) 86 | self.__setLoginInfo() 87 | 88 | 89 | @vwrappers.TryExceptRetNone 90 | def __tryLogout(self): 91 | self.__setQrImg(self.empty_image) 92 | login.LoginLogout() 93 | Config.getCookie("netease", "pyncm")["session"] = "" 94 | self.__setLoginInfo() 95 | 96 | @vwrappers.TryExceptRetNone 97 | def __setQrImg(self, img): 98 | self.login_qr.configure(image=img) 99 | self.login_qr.image = img 100 | 101 | def __isLogin(self): 102 | return login.GetCurrentSession().login_info["success"] 103 | 104 | def __setLoginInfo(self): 105 | if self.__isLogin(): 106 | self.current_status.set(_("Login As ")+GetCurrentSession().login_info['content']['profile']['nickname']) 107 | else: 108 | self.current_status.set(_("Not Login")) -------------------------------------------------------------------------------- /player/mpv.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Union 3 | 4 | from plugins import mpv_lib 5 | 6 | class MPVEvent(Enum): 7 | FILE_START = "START_FILE" 8 | FILE_LOADED = "FILE_LOADED" 9 | FILE_END = "END_FILE" 10 | 11 | class MPVProperty(Enum): 12 | VOLUME = "volume" 13 | PAUSE = "pause" 14 | IDLE = "idle-active" 15 | 16 | TIME_POS = "time-pos" 17 | PERCENT_POS = "percent-pos" 18 | DURATION = "duration" 19 | 20 | AUDIO_DEVICE = "audio-device" 21 | AUDIO_DEVICE_LIST = "audio-device-list" 22 | 23 | class MPVPlayer: 24 | MAX_VOLUME = 120 25 | 26 | def __init__(self,window_id:str): 27 | self.window_id = window_id 28 | self.mpv_core = mpv_lib.MPV(wid=self.window_id) 29 | self.property_handlers = {} 30 | self.event_handlers = {} 31 | 32 | def isPaused(self): 33 | return self.mpv_core._get_property("pause") 34 | 35 | def isPlaying(self): 36 | return not self.isPaused() 37 | 38 | def isLoaded(self): 39 | return not self.mpv_core._get_property("idle-active") 40 | 41 | def play(self): 42 | self.mpv_core._set_property("pause", False) 43 | 44 | def pause(self): 45 | self.mpv_core._set_property("pause", True) 46 | 47 | def toggle(self): 48 | self.mpv_core._set_property("pause",not self.isPaused()) 49 | 50 | def goto(self,time): 51 | self.mpv_core.seek(time,reference="absolute") 52 | 53 | def stop(self): 54 | self.mpv_core.stop() 55 | 56 | def getVolume(self): 57 | return self.mpv_core._get_property("volume") 58 | 59 | def getVolumePercent(self): 60 | return self.mpv_core._get_property("volume") / self.MAX_VOLUME 61 | 62 | def setVolume(self,volume): 63 | self.mpv_core._set_property("volume", volume) 64 | 65 | def setVolumePercent(self,percent): 66 | self.mpv_core._set_property("volume", self.MAX_VOLUME * percent) 67 | 68 | def playByUrl(self, url, **options): 69 | if options.get("headers") != None: 70 | headers = options.pop("headers") 71 | if headers.get("user-agent") != None: 72 | self.mpv_core._set_property("user-agent", headers.get("user-agent")) 73 | if headers.get("referer") != None: 74 | self.mpv_core._set_property("referrer", headers.get("referer")) 75 | self.mpv_core._set_property("http-header-fields", 76 | self.__parseHeader(headers)) 77 | self.mpv_core.play(url) 78 | self.play() 79 | 80 | def __parseHeader(self,header): 81 | headerlist = [] 82 | for key,val in header.items(): 83 | if key == "referer": 84 | headerlist.append("referrer:{}".format(val)) 85 | continue 86 | headerlist.append("{}:{}".format(key,val)) 87 | return headerlist 88 | 89 | def setProperty(self,property:Union[MPVProperty,str],val): 90 | p = property if isinstance(property,str) else property.value 91 | self.mpv_core._set_property(p,val) 92 | 93 | def getProperty(self,property:Union[MPVProperty,str]): 94 | p = property if isinstance(property, str) else property.value 95 | return self.mpv_core._get_property(p) 96 | 97 | def registerPropertyHandler(self,id,property:MPVProperty,func): 98 | self.unregisterPropertyHandler(id) 99 | self.property_handlers[id] = (property.value,func) 100 | self.mpv_core.observe_property(property.value,func) 101 | 102 | def unregisterPropertyHandler(self,id): 103 | currrent = self.property_handlers.get(id) 104 | if currrent != None: 105 | self.mpv_core.unobserve_property(currrent[0],currrent[1]) 106 | self.property_handlers.pop(id) 107 | 108 | def clearPropertyHandler(self): 109 | for key in self.property_handlers.keys(): 110 | self.unregisterPropertyHandler(key) 111 | 112 | def registerEventHandler(self,id,property:MPVEvent,func): 113 | self.unregisterEventHandler(id) 114 | self.event_handlers[id] = self.mpv_core.event_callback(property.value)(func) 115 | 116 | def unregisterEventHandler(self,id): 117 | currrent = self.event_handlers.get(id) 118 | if currrent != None: 119 | self.mpv_core.unregister_event_callback(currrent) 120 | self.event_handlers.pop(id) 121 | 122 | def clearEventHandler(self): 123 | self.mpv_core._event_callbacks.clear() -------------------------------------------------------------------------------- /plugins/danmaku/tars/__packet.py: -------------------------------------------------------------------------------- 1 | # Tencent is pleased to support the open source community by making Tars available. 2 | # 3 | # Copyright (C) 2016THL A29 Limited, a Tencent company. All rights reserved. 4 | # 5 | # Licensed under the BSD 3-Clause License (the "License"); you may not use this file except 6 | # in compliance with the License. You may obtain a copy of the License at 7 | # 8 | # https://opensource.org/licenses/BSD-3-Clause 9 | # 10 | # Unless required by applicable law or agreed to in writing, software distributed 11 | # under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 12 | # CONDITIONS OF ANY KIND, either express or implied. See the License for the 13 | # specific language governing permissions and limitations under the License. 14 | # 15 | 16 | 17 | from .__util import util 18 | 19 | 20 | class RequestPacket(util.struct): 21 | mapcls_context = util.mapclass(util.string, util.string) 22 | mapcls_status = util.mapclass(util.string, util.string) 23 | 24 | def __init__(self): 25 | self.iVersion = 0 26 | self.cPacketType = 0 27 | self.iMessageType = 0 28 | self.iRequestId = 0 29 | self.sServantName = "" 30 | self.sFuncName = "" 31 | self.sBuffer = bytes() 32 | self.iTimeout = 0 33 | self.context = RequestPacket.mapcls_context() 34 | self.status = RequestPacket.mapcls_status() 35 | 36 | @staticmethod 37 | def writeTo(oos, value): 38 | oos.write(util.int16, 1, value.iVersion) 39 | oos.write(util.int8, 2, value.cPacketType) 40 | oos.write(util.int32, 3, value.iMessageType) 41 | oos.write(util.int32, 4, value.iRequestId) 42 | oos.write(util.string, 5, value.sServantName) 43 | oos.write(util.string, 6, value.sFuncName) 44 | oos.write(util.bytes, 7, value.sBuffer) 45 | oos.write(util.int32, 8, value.iTimeout) 46 | oos.write(RequestPacket.mapcls_context, 9, value.context) 47 | oos.write(RequestPacket.mapcls_status, 10, value.status) 48 | 49 | @staticmethod 50 | def readFrom(ios): 51 | value = RequestPacket() 52 | value.iVersion = ios.read(util.int16, 1, True, 0) 53 | print(("iVersion = %d" % value.iVersion)) 54 | value.cPacketType = ios.read(util.int8, 2, True, 0) 55 | print(("cPackerType = %d" % value.cPacketType)) 56 | value.iMessageType = ios.read(util.int32, 3, True, 0) 57 | print(("iMessageType = %d" % value.iMessageType)) 58 | value.iRequestId = ios.read(util.int32, 4, True, 0) 59 | print(("iRequestId = %d" % value.iRequestId)) 60 | value.sServantName = ios.read(util.string, 5, True, "22222222") 61 | value.sFuncName = ios.read(util.string, 6, True, "") 62 | value.sBuffer = ios.read(util.bytes, 7, True, value.sBuffer) 63 | value.iTimeout = ios.read(util.int32, 8, True, 0) 64 | value.context = ios.read(RequestPacket.mapcls_context, 9, True, value.context) 65 | value.status = ios.read(RequestPacket.mapcls_status, 10, True, value.status) 66 | return value 67 | 68 | 69 | class ResponsePacket(util.struct): 70 | __tars_class__ = "tars.RpcMessage.ResponsePacket" 71 | mapcls_status = util.mapclass(util.string, util.string) 72 | 73 | def __init__(self): 74 | self.iVersion = 0 75 | self.cPacketType = 0 76 | self.iRequestId = 0 77 | self.iMessageType = 0 78 | self.iRet = 0 79 | self.sBuffer = bytes() 80 | self.status = RequestPacket.mapcls_status() 81 | 82 | @staticmethod 83 | def writeTo(oos, value): 84 | oos.write(util.int16, 1, value.iVersion) 85 | oos.write(util.int8, 2, value.cPacketType) 86 | oos.write(util.int32, 3, value.iRequestId) 87 | oos.write(util.int32, 4, value.iMessageType) 88 | oos.write(util.int32, 5, value.iRet) 89 | oos.write(util.bytes, 6, value.sBuffer) 90 | oos.write(value.mapcls_status, 7, value.status) 91 | 92 | @staticmethod 93 | def readFrom(ios): 94 | value = ResponsePacket() 95 | value.iVersion = ios.read(util.int16, 1, True) 96 | value.cPacketType = ios.read(util.int8, 2, True) 97 | value.iRequestId = ios.read(util.int32, 3, True) 98 | value.iMessageType = ios.read(util.int32, 4, True) 99 | value.iRet = ios.read(util.int32, 5, True) 100 | value.sBuffer = ios.read(util.bytes, 6, True) 101 | value.status = ios.read(value.mapcls_status, 7, True) 102 | return value 103 | -------------------------------------------------------------------------------- /frontend/build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const merge = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const HtmlWebpackPlugin = require('html-webpack-plugin') 10 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 11 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 12 | 13 | const env = config.build.env 14 | 15 | const webpackConfig = merge(baseWebpackConfig, { 16 | module: { 17 | rules: utils.styleLoaders({ 18 | sourceMap: config.build.productionSourceMap, 19 | extract: true 20 | }) 21 | }, 22 | devtool: config.build.productionSourceMap ? '#source-map' : false, 23 | output: { 24 | path: config.build.assetsRoot, 25 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 26 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 27 | }, 28 | plugins: [ 29 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 30 | new webpack.DefinePlugin({ 31 | 'process.env': env 32 | }), 33 | // UglifyJs do not support ES6+, you can also use babel-minify for better treeshaking: https://github.com/babel/minify 34 | new webpack.optimize.UglifyJsPlugin({ 35 | compress: { 36 | warnings: false 37 | }, 38 | sourceMap: true 39 | }), 40 | // extract css into its own file 41 | new ExtractTextPlugin({ 42 | filename: utils.assetsPath('css/[name].[contenthash].css') 43 | }), 44 | // Compress extracted CSS. We are using this plugin so that possible 45 | // duplicated CSS from different components can be deduped. 46 | new OptimizeCSSPlugin({ 47 | cssProcessorOptions: { 48 | safe: true 49 | } 50 | }), 51 | // generate dist index.html with correct asset hash for caching. 52 | // you can customize output by editing /index.html 53 | // see https://github.com/ampedandwired/html-webpack-plugin 54 | new HtmlWebpackPlugin({ 55 | filename: config.build.index, 56 | template: 'index.html', 57 | inject: true, 58 | minify: { 59 | removeComments: true, 60 | collapseWhitespace: true, 61 | removeAttributeQuotes: true 62 | // more options: 63 | // https://github.com/kangax/html-minifier#options-quick-reference 64 | }, 65 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 66 | chunksSortMode: 'dependency' 67 | }), 68 | // keep module.id stable when vender modules does not change 69 | new webpack.HashedModuleIdsPlugin(), 70 | // split vendor js into its own file 71 | new webpack.optimize.CommonsChunkPlugin({ 72 | name: 'vendor', 73 | minChunks: function (module) { 74 | // any required modules inside node_modules are extracted to vendor 75 | return ( 76 | module.resource && 77 | /\.js$/.test(module.resource) && 78 | module.resource.indexOf( 79 | path.join(__dirname, '../node_modules') 80 | ) === 0 81 | ) 82 | } 83 | }), 84 | // extract webpack runtime and module manifest to its own file in order to 85 | // prevent vendor hash from being updated whenever app bundle is updated 86 | new webpack.optimize.CommonsChunkPlugin({ 87 | name: 'manifest', 88 | chunks: ['vendor'] 89 | }), 90 | // copy custom static assets 91 | new CopyWebpackPlugin([ 92 | { 93 | from: path.resolve(__dirname, '../static'), 94 | to: config.build.assetsSubDirectory, 95 | ignore: ['.*'] 96 | } 97 | ]) 98 | ] 99 | }) 100 | 101 | if (config.build.productionGzip) { 102 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 103 | 104 | webpackConfig.plugins.push( 105 | new CompressionWebpackPlugin({ 106 | asset: '[path].gz[query]', 107 | algorithm: 'gzip', 108 | test: new RegExp( 109 | '\\.(' + 110 | config.build.productionGzipExtensions.join('|') + 111 | ')$' 112 | ), 113 | threshold: 10240, 114 | minRatio: 0.8 115 | }) 116 | ) 117 | } 118 | 119 | if (config.build.bundleAnalyzerReport) { 120 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 121 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 122 | } 123 | 124 | module.exports = webpackConfig 125 | -------------------------------------------------------------------------------- /audiobot/playlist.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from audiobot.audio import AudioItem 4 | from audiobot.handler import AudioBotHandlers 5 | from audiobot.event.base import BaseAudioBotEvent 6 | from audiobot.event.playlist import PlaylistUpdateEvent, PlaylistAppendEvent 7 | from functools import wraps 8 | 9 | import random 10 | from audiobot.user import User, PlaylistUser 11 | 12 | 13 | def _trigger_event_handler(func): 14 | @wraps(func) 15 | def execute_handler(self, *args, **kwargs): 16 | ret = func(self, *args, **kwargs) 17 | if ret is None: 18 | return 19 | if isinstance(ret, BaseAudioBotEvent): 20 | retval, event = None, ret 21 | else: 22 | retval, event = ret[0], ret[1] 23 | self.handlers.call(event) 24 | return retval 25 | return execute_handler 26 | 27 | 28 | class Playlist(): 29 | def __init__(self, audio_bot, name, random_next=False): 30 | self.audio_bot = audio_bot 31 | self.name = name 32 | self.playlist: List[AudioItem] = [] 33 | self.current_index = 0 34 | self.handlers = AudioBotHandlers() 35 | self.random_next = random_next 36 | 37 | def __len__(self): 38 | return len(self.playlist) 39 | 40 | def size(self): 41 | return len(self.playlist) 42 | 43 | def insert_raw(self, cm, index, user: User = None, keyword="") -> PlaylistAppendEvent: 44 | event = self.append_raw(cm, user=user, keyword=keyword) 45 | if event.isCancelled(): 46 | return event 47 | self.move(event.index, index) 48 | return event 49 | 50 | def append_raw(self, cm, user: User = None, keyword="") -> PlaylistAppendEvent: 51 | if user is None: 52 | return self.append(AudioItem(cm, PlaylistUser, keyword)) 53 | else: 54 | return self.append(AudioItem(cm, user, keyword)) 55 | 56 | def insert(self, index: int, item: AudioItem) -> PlaylistAppendEvent: 57 | event = self.append(item) 58 | if event.isCancelled(): 59 | return event 60 | self.move(event.index, index) 61 | return event 62 | 63 | def append(self, item: AudioItem) -> PlaylistAppendEvent: 64 | event = PlaylistAppendEvent(self, item, self.size()) 65 | self.handlers.call(event) 66 | if event.isCancelled(): 67 | return event 68 | self.playlist.append(item) 69 | self.handlers.call(PlaylistUpdateEvent(self)) 70 | return event 71 | 72 | def get(self, index) -> AudioItem: 73 | if index >= len(self.playlist) or index < 0: 74 | return 75 | return self.playlist[index] 76 | 77 | def clear(self): 78 | self.playlist.clear() 79 | self.handlers.call(PlaylistUpdateEvent(self)) 80 | 81 | @_trigger_event_handler 82 | def pop_first(self): 83 | if len(self.playlist) == 0: 84 | return None 85 | return self.playlist.pop(0), PlaylistUpdateEvent(self) 86 | 87 | @_trigger_event_handler 88 | def remove(self, index): 89 | if index >= len(self.playlist) or index < 0: 90 | return 91 | return self.playlist.pop(index), PlaylistUpdateEvent(self) 92 | 93 | @_trigger_event_handler 94 | def move(self, index, target_index): 95 | if index >= len(self.playlist) or index < 0: 96 | return 97 | if target_index < 0: 98 | target_index = 0 99 | if target_index >= len(self.playlist): 100 | target_index = len(self.playlist) - 1 101 | if index == target_index: 102 | return 103 | step = int((target_index - index) / abs(target_index - index)) 104 | tmp = self.playlist[index] 105 | for i in range(index, target_index, step): 106 | self.playlist[i] = self.playlist[i + step] 107 | self.playlist[target_index] = tmp 108 | return PlaylistUpdateEvent(self) 109 | 110 | def get_next(self) -> AudioItem: 111 | ''' 112 | get next AudioItem, if not exists return None 113 | 114 | :return: AudioItem 115 | ''' 116 | if len(self.playlist) == 0: 117 | return 118 | index = 0 119 | if self.random_next: 120 | index = random.randint(0, len(self.playlist) - 1) 121 | self.current_index = index 122 | else: 123 | index = self.current_index 124 | self.current_index += 1 125 | if self.current_index >= self.size(): 126 | self.current_index = 0 127 | return self.playlist[index] 128 | -------------------------------------------------------------------------------- /gui/MPVGUI.py: -------------------------------------------------------------------------------- 1 | from tkinter import ttk 2 | import tkinter as tk 3 | 4 | from audiobot import Global_Audio_Bot 5 | from gui.factory.PlayerProgressBar import PlayerProgressBar 6 | from player.mpv import MPVPlayer, MPVProperty 7 | import gui 8 | 9 | 10 | class MPVGUI(): 11 | MAX_VOLUME = 128 12 | instance = None 13 | 14 | @staticmethod 15 | def getInstance(): 16 | return MPVGUI.instance 17 | 18 | def __init__(self, main_window): 19 | self.main_window: gui.MainWindow = main_window 20 | self.widget = ttk.Frame(self.main_window.getTabController()) 21 | 22 | # self.volume = tk.DoubleVar() 23 | self.progress = tk.IntVar() 24 | self.mpv_player: MPVPlayer = None 25 | self.mpv_window_id = "" 26 | 27 | MPVGUI.instance = self 28 | 29 | @property 30 | def progressPercent(self): 31 | return self.progress.get() / 100 32 | 33 | def initialize(self): 34 | self.main_window.getTabController().add(self.widget, text="MPV") 35 | self.mpv_player.registerPropertyHandler("mpvgui.syncprogress", 36 | MPVProperty.PERCENT_POS, 37 | self._syncProgress) 38 | self._pause() 39 | 40 | 41 | def createWidgets(self): 42 | 43 | frame_main = ttk.LabelFrame(self.widget, 44 | text="MPV Player") 45 | frame_main.grid_columnconfigure(0, weight=1) 46 | frame_main.grid_columnconfigure(2, weight=1) 47 | frame_main.pack(fill="both", expand="yes", padx=8, pady=4) 48 | 49 | # ==== Row 0 ==== 50 | 51 | frame_row_1 = ttk.Frame(frame_main) 52 | frame_row_1.grid(column=1, row=0, padx=8, pady=4) 53 | 54 | frame_player = ttk.Frame(frame_row_1, 55 | width=510, height=340) 56 | frame_player.grid(column=0, row=0, sticky="news") 57 | 58 | self.mpv_window_id = str(int(frame_player.winfo_id())) 59 | self.mpv_player = MPVPlayer(self.mpv_window_id) 60 | Global_Audio_Bot.setPlayer(self.mpv_player) 61 | 62 | # ==== Row 2 ==== 63 | frame_row_2 = ttk.Frame(frame_main) 64 | frame_row_2.grid(column=1, row=1, padx=8, pady=4) 65 | 66 | # add progress scale 67 | progress_scale = PlayerProgressBar(frame_row_2, 68 | orient=tk.HORIZONTAL, 69 | variable=self.progress, 70 | from_=0, 71 | to=100, 72 | length = 510, 73 | command = self._setProgress) 74 | progress_scale.grid(column=0, row=0) 75 | 76 | # ==== Row 3 ==== 77 | 78 | frame_row_3 = ttk.Frame(frame_main) 79 | frame_row_3.grid(column=1, row=2, padx=8, pady=4) 80 | 81 | # Adding pause Button 82 | pause_button = ttk.Button(frame_row_3, width=8, text="pause", command=self._pause) 83 | pause_button.grid(column=2, row=0) 84 | 85 | # Adding play Button 86 | play_button = ttk.Button(frame_row_3, width=8, text="play", command=self._play) 87 | play_button.grid(column=1, row=0) 88 | 89 | # Adding stop Button 90 | stop_button = ttk.Button(frame_row_3, width=8, text="stop", command=self._stop) 91 | stop_button.grid(column=0, row=0) 92 | 93 | def _parseHeader(self,header): 94 | headerlist = [] 95 | for key,val in header.items(): 96 | if key == "referer": 97 | headerlist.append("referrer:{}".format(val)) 98 | continue 99 | headerlist.append("{}:{}".format(key,val)) 100 | return headerlist 101 | 102 | def _play(self): 103 | self.mpv_player.play() 104 | self._syncProgress() 105 | 106 | def _pause(self): 107 | self.mpv_player.pause() 108 | 109 | def _stop(self): 110 | self.mpv_player.stop() 111 | 112 | def _syncProgress(self,*args): 113 | if self.mpv_player.getProperty(MPVProperty.PERCENT_POS) == None: 114 | self.progress.set(0) 115 | self.progress.set(self.mpv_player.getProperty(MPVProperty.PERCENT_POS)) 116 | 117 | def _setProgress(self,*args): 118 | tmp = self.mpv_player.getVolume() 119 | self.mpv_player.setVolume(0) 120 | if self.mpv_player.getProperty(MPVProperty.PERCENT_POS) == None: 121 | self.progress.set(0) 122 | return 123 | self.mpv_player.setProperty(MPVProperty.PERCENT_POS, self.progress.get()) 124 | self.mpv_player.setVolume(tmp) -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import json 2 | from utils import vwrappers 3 | 4 | 5 | class ConfigFile: 6 | gui_title = "ヽ(°∀°)点歌姬(°∀°)ノ" 7 | 8 | environment = "production" 9 | # environment = "development" 10 | version = "Demo0.8.91" 11 | 12 | commonHeaders = { 13 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:51.0) Gecko/20100101 Firefox/51.0" 14 | } 15 | 16 | commonCookies = {} 17 | 18 | system_playlist = { 19 | "playlist":{"netease":[], 20 | "bilibili":[]}, 21 | "song":{"netease":[], 22 | "bilibili":[]}, 23 | "random":True, 24 | "autoskip":True 25 | } 26 | 27 | output_channel = { 28 | "web":{"enable":True, 29 | "port":5000}, 30 | "file":{"enable":True, 31 | "template":"config/audiobot_template.txt", 32 | "path":"audiobot_info.txt"} 33 | } 34 | 35 | commands = { 36 | "diange": { 37 | "cooldown":0, 38 | "limit":128, 39 | "visitor":True, 40 | "guard":True, 41 | "admin":True, 42 | "fan":None 43 | }, 44 | "qiege": { 45 | "self":True, 46 | "guard":False, 47 | "admin":False 48 | }, 49 | } 50 | 51 | audio_device = {"id":"auto"} 52 | 53 | player_volume = 0.32 54 | 55 | default_room = "" 56 | 57 | config_path = "config/config.json" 58 | 59 | cookie_path = "config/cookies.json" 60 | 61 | blacklist_path = "config/blacklist.json" 62 | 63 | addon_cmd_path = "addons/cmd" 64 | addon_handler_path = "addons/handler" 65 | 66 | translation = {"enable":True, 67 | "path":"resource/translation.json"} 68 | 69 | 70 | def __init__(self): 71 | print("Loading config") 72 | self._loadConfig() 73 | print("Cookie initialized") 74 | # structure: {"site":{"identifier":{"cookie1":"value1"}}} 75 | self.cookies = {} 76 | self.loadCookie() 77 | self.blacklist = {} 78 | self._loadBlacklist() 79 | 80 | @vwrappers.TryExceptRetNone 81 | def loadCookie(self): 82 | with open(self.cookie_path,"r") as f: 83 | jdata = json.loads(f.read().replace(" ", "")) 84 | for key,val in jdata.items(): 85 | for id, content in val.items(): 86 | cookie = self.getCookie(key,id) 87 | cookie.update(dict(x.split("=") for x in content.split(";") if x != "")) 88 | 89 | @vwrappers.TryExceptRetNone 90 | def saveCookie(self): 91 | tmp = self.cookies.copy() 92 | for host, val in self.cookies.items(): 93 | for id,content in val.items(): 94 | tmp[host][id] = ";".join("{}={}".format(key,val) for key,val in self.cookies[host][id].items()) 95 | with open(self.cookie_path,"w",encoding="utf-8") as f: 96 | f.write(json.dumps(tmp,indent=2,ensure_ascii=False)) 97 | 98 | def getCookiesByHost(self, host): 99 | if host == "": 100 | return self.commonCookies 101 | if self.cookies.get(host) == None: 102 | self.cookies[host] = {} 103 | return self.cookies[host] 104 | return self.cookies.get(host) 105 | 106 | def getCookie(self,host,identifier): 107 | cookies = self.getCookiesByHost(host) 108 | if cookies.get(identifier) == None: 109 | cookies[identifier] = {} 110 | return self.cookies[host][identifier] 111 | return cookies[identifier] 112 | 113 | @vwrappers.TryExceptRetNone 114 | def _loadConfig(self): 115 | with open(self.config_path, "r", encoding="utf-8") as f: 116 | data = json.loads(f.read()) 117 | for key,val in data.items(): 118 | if hasattr(self,key): 119 | if isinstance(self.__getattribute__(key),dict): 120 | self.__getattribute__(key).update(val) 121 | else: 122 | self.__setattr__(key,val) 123 | 124 | def saveConfig(self): 125 | with open(self.config_path, "r", encoding="utf-8") as f: 126 | data = json.loads(f.read()) 127 | with open(self.config_path, "w", encoding="utf-8") as f: 128 | for key,val in data.items(): 129 | if hasattr(self,key): 130 | data[key] = self.__getattribute__(key) 131 | f.write(json.dumps(data,indent=2,ensure_ascii=False)) 132 | 133 | self._saveBlacklist() 134 | 135 | @vwrappers.TryExceptRetNone 136 | def _loadBlacklist(self): 137 | with open(self.blacklist_path, "r", encoding="utf-8") as f: 138 | self.blacklist = json.loads(f.read()) 139 | 140 | 141 | def _saveBlacklist(self): 142 | with open(self.blacklist_path, "w", encoding="utf-8") as f: 143 | f.write(json.dumps(self.blacklist, indent=2, ensure_ascii=False)) 144 | 145 | 146 | Config = ConfigFile() --------------------------------------------------------------------------------