├── 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 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/frontend/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AynaLivePlayer/BiliAudioBot/HEAD/frontend/src/assets/logo.png
--------------------------------------------------------------------------------
/frontend/src/components/NotFound.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
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 |
2 | {{lyric}}
3 |
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 |
2 |
3 |
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 |
2 | {{ title }} - {{ artist }} - {{ username }}
3 |
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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 | -
4 |
#{{ playlist.indexOf(item) }} - {{ item.title }} - {{ item.artist }} - {{ item.username }}
5 |
6 |
7 |
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 |
2 |
3 |
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 |
2 |
3 |
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 |
2 |
3 |
4 |
5 |
37 |
38 |
44 |
--------------------------------------------------------------------------------
/frontend/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
42 |
--------------------------------------------------------------------------------
/frontend/src/components/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
42 |
--------------------------------------------------------------------------------
/frontend/src/views/PlaylistView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
2 |
11 |
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()
--------------------------------------------------------------------------------